You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
child_process: a child's data/exit/close are delivered BEFORE the continuation that awaited its 'spawn' — cc's MCP stdio client says 'Not connected' where node says 'Connection closed' #9535
Found through the MCP debug logs that #9491 unlocked (#9500 / #9531): with the log tree finally written, cc's stdio mcp cases still differ from node in what they log, and this is the reason for ~9 of the 15 harness cases (every stdio server that exits at once — the /bin/echo, /bin/true fixtures):
node : Connection failed after 1ms: MCP error -32000: Connection closed
perry: Connection failed after 1ms: Not connected
Mechanism (MCP SDK 1.30.0, client/stdio.js)
start() resolves its promise from the child's 'spawn' listener; the 'close' listener sets this._process = undefined; send() throws Error('Not connected') when this._process?.stdin is gone (stdio.js:189). Under node the continuation that awaited start() runs before the child's data/exit/close — the initialize request is written, the child is gone, and the pending request rejects with Connection closed when 'close' lands later. Under perry the child's whole lifecycle is delivered before that continuation resumes, so _process is already cleared when send() runs.
node 26.5.1 (linux): spawn resumed-after-spawn microtask data exit immediate close
perry main 0b24670dd9 : spawn data exit close resumed-after-spawn microtask immediate
Node runs a microtask checkpoint after every emitted callback, so a promise resolved from 'spawn' resumes before the loop delivers the next event. Perry's reactor pump drains the child's queued events (Data/Eof/Exited → data/end/exit/close) in one go without a microtask checkpoint between emits — crates/perry-runtime/src/child_process/reactor.rs, the main-thread pump. A child that finishes inside one turn therefore has its entire lifecycle emitted before any promise continuation that one of those events resolved.
What matters
This is the remaining divergence for the stdio mcp add/list/get family once #9499 (the self-server hang) is in: the same ordering rule reaches every consumer that awaits 'spawn' before writing (execa's spawned promise, cross-spawn users, node-pty wrappers). Expected fix shape: run the microtask checkpoint after each emitted child-process event (or at minimum after 'spawn'), the way the timer and fs callback paths already do; the fixture above should print node's line.
Activity
fix(child-process): preserve spawn and close callback turns (#9535)