fix: recover standalone bridge from closed stdin
Summary
- treat a live child with non-writable stdin as an unusable standalone transport
- retire the stale child before respawn so
BridgePoolcannot keep returning a permanently wedged bridge - capture the child used for each write and invalidate it only when it is still current, preventing a late callback from killing a replacement
Outcome semantics
The two failure windows intentionally differ:
- If stdin is already non-writable when
ensureSpawned()runs, no write has been attempted. The stale child is invalidated, a replacement is spawned, and thatsend()continues on the replacement. - If
stdin.write()reports an asynchronous error after initiation, the current request rejects withBridgeTransportUnavailableErrorbecause mutation outcome may be unknown. The child is invalidated so the next request respawns; the current request is never blindly replayed.
This removes the permanent-wedge class while preserving conservative unknown-outcome handling.
Verification
- regression first reproduced the old permanent
stdin not writablewedge - targeted closed-stdin and async-write tests: 2 passed
- complete
bridge-transport.test.ts: 22 passed @cortexkit/aft-bridgetypecheck: passed- Biome on changed files: passed
- live driver: one
BinaryBridgereplaced child PID 77753 with PID 77767 and served the next ping successfully
Closes #231
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.
Summary by cubic
Fixes a permanent wedge in aft-bridge when a child stays alive but its stdin closes. Previously the bridge stayed stuck; now non-writable stdin is treated as transport failure, the child is invalidated, and a fresh child is spawned. In-flight writes that fail asynchronously reject and are not replayed.
BinaryBridge.ensureSpawnednow invalidates a live child with non-writablestdinand respawns, soBridgePoolno longer returns a wedged bridge.send()captures the child used for the write and only invalidates it if it’s still current, avoiding late-callback races that could kill a replacement.- On async
stdin.writeerrors, the request rejects withBridgeTransportUnavailableError; the child is invalidated so the next request respawns. - Adds tests for closed-stdin-before-exit and async write failure.
Greptile Summary
The PR recovers standalone bridges when a live child has unusable stdin and makes asynchronous write-error invalidation child-specific.
- Adds transport invalidation that detaches and kills unusable children, clears process-scoped state, and rejects pending requests.
- Respawns before sending when the current child is alive but stdin is not writable.
- Captures the child used for each write so delayed callbacks cannot invalidate a newer replacement.
- Adds regressions for closed stdin and asynchronous write failures.
Confidence Score: 3/5
The PR should not merge until replacement processes are isolated from the retired child's stream handlers and configuration lifecycle.
Immediate respawn allows delayed stdout closure from the retired child to consume replacement framing, while an in-flight configure continuation can incorrectly transfer configured state to the new process.
Files Needing Attention: packages/aft-bridge/src/bridge.ts
Important Files Changed
| Filename | Overview |
|---|---|
| packages/aft-bridge/src/bridge.ts | Adds closed-stdin recovery and generation-aware write callbacks, but immediate replacement remains exposed to stale stream listeners and configuration continuations. |
| packages/aft-bridge/src/tests/bridge-transport.test.ts | Adds useful recovery regressions, but they do not exercise retired-child stream events or configuration completion racing with replacement. |
Sequence Diagram
sequenceDiagram
participant A as Request A
participant B as BinaryBridge
participant Old as Retired child
participant New as Replacement child
A->>B: send while configure is active
Old-->>B: stdin becomes non-writable
B->>Old: SIGKILL
B->>New: spawn immediately
New-->>B: partial stdout response
Old-->>B: delayed stdout end
B->>B: flush shared stdout buffer
Note over B: Replacement frame may be discarded
B->>B: stale configure continuation marks configured
Note over B,New: New child may never receive configure
Reviews (1): Last reviewed commit: "fix: recover standalone bridge from clos..." | Re-trigger Greptile
Greptile also left 2 inline comments on this PR.
Context used:
- Knowledge Base — TypeScript AFT bridge: native and daemon-backed transport