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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions