mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
d977bcecb1
Opening a plan review from Pi on Windows could exit the entire Pi host with an uncaught `write EPIPE` raised inside `PiProcessNode.send()`. The provider checked `stdin.destroyed` and then wrote, which cannot close the race: the nested `pi --mode rpc` child can close the pipe between the check and the write. Node then reports EPIPE either as a synchronous throw or as an `error` event on the stream, and because no stream had an `error` listener that became an `uncaughtException` and terminated the host agent process. Add a shared guard (`packages/ai/providers/child-io.ts`) and apply it to both JSONL/JSON-RPC providers: - `guardChildStreams` attaches `error` listeners to the child and every pipe immediately after spawn, so a stream error can never escalate. The previous one-shot spawn listener was removed on success, leaving the child with no `error` listener for the rest of its life. - `writeChildLine` reports a synchronous failure through its return value and an asynchronous one through the write callback, so both paths converge. - A failure now resolves as a provider failure: in-flight requests reject, the process end is broadcast to listeners so a streaming query terminates, the child is reaped, and `alive` flips false so the next query re-spawns. Previously a failed write also left `sendAndWait` pending forever, because `send()` was fire-and-forget and the Pi provider has no RPC timeout. Also guards the Bun variant's FileSink write/flush symmetrically, and switches Pi's Node stderr from an un-drained "pipe" to "ignore", matching the deadlock reasoning already documented in codex-app-server.ts. Regression test runs the provider in a real `node` child against a fake Pi that closes its own stdin; the child installs no `uncaughtException` handler, so surviving to print its results is the proof. Against the unfixed provider that child dies with `Error: write EPIPE`, exit 1. Reported by @Kaelenx.