bridge: shut down + release the port when the parent dies; enforce one bridge per agent - #20
Closed
terafin wants to merge 2 commits into
Closed
bridge: shut down + release the port when the parent dies; enforce one bridge per agent#20terafin wants to merge 2 commits into
terafin wants to merge 2 commits into
Conversation
…e bridge per agent
An `a2abridge bridge` is spawned by an MCP host (Claude Code, etc.) as a stdio child that also binds an HTTP port. Two failure modes left a stale/dup bridge holding the port so the next bridge couldn't bind ("address already in use") and the agent's outbound A2A went silently dead: (1) when the host exits, the bridge reparented to init and kept running; (2) a mid-session respawn (e.g. an MCP reconnect) could leave a second bridge lingering.
Fixes, all in RunBridge:
- Linux PR_SET_PDEATHSIG(SIGTERM): the kernel signals the bridge the moment its parent dies, routed into the existing SIGTERM graceful shutdown. Set on a dedicated locked OS thread that blocks forever (PR_SET_PDEATHSIG is a per-thread attribute cleared if the setting thread exits, and Go migrates/retires threads), plus a getppid()==1 born-orphan check. No-op stub on non-Linux (pdeathsig_linux.go / pdeathsig_other.go).
- Shut down whenever ServeStdio returns (stdin closed = parent gone), including a clean EOF, not only on error.
- Bind is the singleton lock: retry briefly on EADDRINUSE (covers the port-release race on a fast restart), then if it is still held, defer to the existing bridge and exit cleanly (0) — so exactly one bridge serves an agent even if a duplicate is spawned. Non-EADDRINUSE bind errors still fail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
terafin
force-pushed
the
fix/bridge-dies-with-parent
branch
from
July 17, 2026 07:49
d2de19b to
028b4ab
Compare
…agent Behavior-level integration tests that build and run the real binary, so they exercise the shipping code as-is (ZERO production change — bridge.go and the pdeathsig files are byte-identical to the v3.0.3+1 blob) and port unchanged to both the fleet blob and the upstream a2abridge#20 branch. Tag-gated (//go:build integration) so they stay out of the default `go test ./...` / CI. - TestBridgeSecondInstanceDefersToIncumbent: a duplicate bridge on the same bind address retries then exits 0, WITHOUT killing the incumbent or running portless (exactly one bridge per agent). - TestBridgeShutsDownOnStdinEOF: closing stdin -> ServeStdio returns -> shutdown + port release, even on a clean EOF. - TestBridgeDiesOnParentDeath (Linux): killing the parent -> PR_SET_PDEATHSIG -> shutdown + port release, with the bridge's stdin held open so the death is isolated to pdeathsig (not the stdin-EOF path). Bot-author: sindri (cherry picked from commit c68674b)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
a2abridge bridgeis spawned by an MCP host (Claude Code, Codex, etc.) as a stdio child that also binds an HTTP port for inbound A2A. Two ways a stale/duplicate bridge ends up holding the port so the next bridge can't bind (listen tcp :<port>: bind: address already in use) and the agent's outbound send path goes silently dead:Observed across a multi-agent fleet: a stuck bridge held a port for hours, dropping inbound messages with no error surfaced; and a reconnect left a duplicate.
Fix — one bridge per agent, defence-in-depth (all in
RunBridge)PR_SET_PDEATHSIG(SIGTERM)(pdeathsig_linux.go): kernel signals the bridge the instant its parent dies, routed into the existingsignal.NotifyContext(SIGTERM)graceful shutdown → listener closes, port frees. Set on a dedicated locked OS thread that blocks forever (PR_SET_PDEATHSIG is a per-thread attribute cleared if the setting thread exits, and the Go runtime migrates/retires threads), plus agetppid()==1born-orphan check for the fork/exec race. No-op stub on non-Linux (pdeathsig_other.go).ServeStdioreturns: stdin closing means the parent is gone, so shut down even on a clean EOF (err == nil), not only on error.EADDRINUSE(covers the port-release race on a fast restart), then if it's still held, defer to the existing bridge and exit cleanly (0) — exactly one bridge serves an agent even if a duplicate is spawned. Non-EADDRINUSEbind errors still fail hard.(1)+(2)are redundant on purpose (kernel signal vs. stdin EOF).(3)closes both the release race and the duplicate-spawn case.Verified
go build ./...,go vet ./...,go test ./internal/cli/...pass; cross-compiles clean fordarwin/arm64+windows/amd64(platform split holds).port already held by another bridge, deferring, and exited 0, leaving A untouched.🤖 Generated with Claude Code