Skip to content

process.exit() right after process.stdout.write() in print mode (-p --output-format json) can truncate the final result line when stdout is piped #700

Description

@mahmudph

Title

process.exit() right after process.stdout.write() in print mode (-p --output-format json) can truncate the final result line when stdout is piped

Body

Summary

When running command-code in non-interactive print mode with JSON output (-p --output-format json) and stdout redirected to a pipe (not a TTY) — the normal case for any orchestrator/CI that spawns the CLI as a child process and reads its stdout — the final {"type":"result",...} NDJSON line is sometimes silently dropped even though the run completed successfully and every prior {"type":"event",...} line streamed correctly.

The child process exits with code 0 and no stderr output. From the caller's side, the NDJSON stream just ends without ever emitting the one line that signals "this run is done, here is the final text/usage/session id."

Environment

  • command-code version: 1.27.1 (also current latest on npm — confirmed no newer release fixes this)
  • Node.js: v22+ (as required by the CLI itself)
  • OS: Linux
  • Invocation (representative): command-code -p --output-format json --yolo --skip-onboarding --no-auto-update -m <model> [--resume <session-id>], spawned as a child process with stdout captured via a pipe (not inherited from a TTY).

What we found in dist/cli.mjs

The print-mode entrypoint builds the result payload via runPrintMode(...), then does:

t?.write(),
  o.stdoutText && process.stdout.write(o.stdoutText),
  o.stderrText && process.stderr.write(o.stderrText),
  process.exit(o.exitCode)

process.stdout.write() is called and its return value (backpressure signal) is ignored, and process.exit() is invoked immediately afterward with no wait for a flush/'drain' event or write callback.

On Linux, when stdout is a pipe (as opposed to a TTY or a regular file), writes are asynchronous at the libuv level. process.exit() does not wait for pending async writes to complete — it tears the process down immediately. If the OS pipe buffer is under backpressure (e.g. right after a burst of event lines, or when the final result payload — which embeds the full finalText — is large), this last write() can be partially or fully lost before it reaches the reader on the other end of the pipe.

This matches observed behavior exactly:

  • Every streamed event line arrives fine (each write appears to get enough of a scheduling gap to flush).
  • Only the single final result line — written once, immediately followed by exit() — is intermittently missing.
  • It reproduces on successful runs with larger output/tool-call counts, consistent with the final payload being big enough to not fit in one synchronous pipe write.
  • No stderr, no non-zero exit code — the process believes it exited cleanly.

Suggested fix

Don't call process.exit() unconditionally right after process.stdout.write(). Either:

  • Await the write completing (pass a callback to .write(), or wait for 'drain' if it returns false) before exiting, or
  • Prefer setting process.exitCode = o.exitCode and letting the process exit naturally once the event loop drains, instead of forcing process.exit().

Impact

Any tool that drives command-code non-interactively and parses its NDJSON stdout (CI pipelines, orchestrators, IDE integrations) cannot reliably distinguish "the run actually finished successfully but the terminal frame got dropped" from "the run crashed" — because from the outside, both look identical: stdout just stops, exit code 0, no result frame. Consumers are forced to either trust an unconfirmed completion (risking silently reporting failed work as succeeded) or treat every truncated stream as a failure (which then re-runs/retries a run that actually already finished).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions