Skip to content

Headless --prompt stream-json: final assistant message + session.resume_hint can be lost (stdout backpressure ignored, process.exit on signal discards undrained pipe) #1897

Description

@sypherin

Environment

kimi-code 0.27.0 (Homebrew, Linux), bundled Node v26.5.0, driven headlessly by a bridge (chenhg5/cc-connect) as: kimi --output-format stream-json --prompt <text>, stdout = pipe.

Symptom

On longer multi-tool turns (e.g. 3-4 sequential Read calls with large results), the consumer receives the tool_call/tool events but the stream ends WITHOUT the final {"role":"assistant","content":"..."} line and WITHOUT the {"role":"meta","type":"session.resume_hint",...} trailer. Short turns (0-1 tools) are fine. The session transcript on disk (agents/main/wire.jsonl) always contains the final assistant message and finishReason:"end_turn" — the answer is generated and persisted, only the stdout emission is lost.

Root cause (from reading the 0.27.0 bundle, dist/main.mjs)

  1. The headless JSON writer does not honor stdout backpressure: PromptJsonWriter.writeJsonLine (~line 430351) and writeResumeHint (~430206) call stdout.write(...) and ignore the return value; the waitForDrain/drainStdout helpers that exist elsewhere in the bundle are never used on this path.
  2. The prompt-mode signal handler (installPromptTerminationCleanupexitAfterCleanup, ~431087) ends with process.exit(...), which discards any un-drained stdout buffer. Cleanup is time-bounded (PROMPT_CLEANUP_TIMEOUT_MS=8000 / CLI_SHUTDOWN_TIMEOUT_MS=3000) but never flushes the pipe.

So when the consumer is slow to drain (big tool-result lines back up the pipe) and the process is terminated by signal after the turn logically completes, exactly the two trailing writes — the final assistant text and the resume hint — are lost. The consumer then sees an "empty" reply and (because the resume hint is gone) also loses session continuity.

Suggested fix

Await drain (or use a write callback / stream.finished) in writeJsonLine/writeResumeHint, and prefer process.exitCode + natural exit (or an explicit stdout flush) over process.exit() in the prompt-mode signal path.

Repro sketch

  1. kimi --output-format stream-json --prompt "read these four files then reply DONE" with stdout piped to a slow consumer (or a consumer that SIGTERMs on its own timeout).
  2. Compare stdout capture vs the session's wire.jsonl: wire.jsonl has the final assistant message; stdout ends at the last tool event.

🤖 Generated with Claude Code

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