Skip to content

runtime: async catch's mutation of a shared object not visible at state-commit → pi one-shot exits 0 not 1 (GATE 2a rc divergence) — pi wall #10 #6672

Description

@proggeramlug

Wall #10 of the pi bring-up (tracker #6564). GATE 2a is stdout+stderr byte-identical to node but exit code diverges: node rc=1, perry rc=0. The natural-exit process.exitCode consultation (#6666, PR #6671) is a necessary prerequisite but NOT the cause — a runtime trace shows process.exitCode is never assigned and process.exit() never called, because the value that would set it stays 0.

Confirmed symptom (fresh run, request_id masked)

cd secret-tests/pi-target/dist
HOME=$(mktemp -d) ./pi-native -p --provider anthropic --model claude-3-5-haiku-latest --api-key sk-ant-bogus-diff "hi"
  • node: stdout empty, stderr = model-warning + deprecation + 401 {...authentication_error...}, rc=1
  • perry: stdout + stderr byte-identical (req_id aside), rc=0

The chain (bundle line refs in pi-target/dist/pi-bundle.mjs)

  1. Streaming error handler (async, ~51092): on the 401 the try throws, the catch (error40) runs and sets output.stopReason = "error", output.errorMessage = ..., then stream12.push({ type: "error", error: output }). That pushed error is what prints the 401 to stderr (via the runPrintMode subscription) — and it prints identically under perry, so the catch DID run and DID push.
  2. runPrintMode (~292838, print-mode.js): after the prompt resolves, it reads session.state.messages' last message; if (assistantMsg.stopReason === "error" || ... "aborted") { console.error(...); exitCode = 1 }. Returns exitCode. Caller does if (exitCode !== 0) process.exitCode = exitCode.

The paradox that localizes the bug

stderr is byte-identical (the streamed error surfaced fine), yet runPrintMode returns 0 under perry — which means the assistant message committed to session.state.messages does not carry stopReason === "error", even though the catch that mutates output.stopReason = "error" provably executed (it produced the printed error). So: the catch's mutation of output is not visible on the object that lands in session.state under perry, or the message is snapshotted/committed to state at the wrong point relative to the async catch.

Leads (perry side, not pi)

Reproduction & assets

Binary pi-target/dist/pi-native (built on the #6666 branch — includes that fix; pi-native-dbg = debug symbols). Bundle+assets under pi-target/dist. Minimal-fixture target: an async function that awaits a stream/iterator, whose catch mutates a shared object then that object is read after the await — assert the mutation is visible, byte-identical to node.

Blocks GATE 2a full pass (rc parity). Does NOT block gate 2b/TUI investigation structurally, but exit-code correctness is core parity.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regressionparityCompatibility gap with Node.js, ECMAScript, or the supported ecosystem

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions