What happened
The claude-cli one-shot output path has diverged from the live-session path; four related defects, all in the same classification seam:
is_error results delivered as successful replies. parseClaudeCliJsonlResult (src/agents/cli-output.ts:408-432) and parseCliJson (:378-400) never check is_error/error subtypes; only the live session does (src/agents/cli-runner/claude-live-session.ts:938). The repo's own fixture (src/agents/test-helpers/claude-api-error-fixture.ts:34-40) shows Claude emits {"type":"result","subtype":"success","is_error":true,"result":"API Error: 400 ..."} — on the one-shot path that raw API error string is delivered to the user as the assistant reply, the session binding is persisted, and no failover happens.
- Raw NDJSON transcript delivered as the reply. When no result envelope is recognized,
parseCliOutput falls back to { text: params.raw.trim() } (cli-output.ts:1078-1084) — up to 1 MiB (one-shot) / 8 MB (live) of raw stream-json sent to the user. The streamed-assistant-text fallback in getOutput() (:962-971) exists only for the Gemini dialect.
- Unbounded line buffer.
createCliJsonlStreamingParser grows lineBuffer without bound (cli-output.ts:948-955) and execute.ts:1123-1133 keeps pushing even after stdoutParseExceeded. The live path caps pending lines at 8 MB and aborts (claude-live-session.ts:955-967); the one-shot path can allocate GBs from a babbling child before the overall timeout.
- UTF-8 corruption at chunk boundaries on POSIX.
src/process/supervisor/adapters/child.ts:175-196 decodes each stdout Buffer independently; the incremental decoder in src/infra/windows-encoding.ts:158-172 is only constructed on win32. A multibyte character split across a 64 KB pipe read decodes to U+FFFD on both sides, inside a valid JSON string, so corrupted text flows silently to the user.
Expected behavior
One classification seam in cli-output.ts shared by live and one-shot paths: is_error/error subtypes classify to errorText, stream-json dialects never fall back to raw transcript text, line buffering is capped like the live path, and stdout decoding is incremental on all platforms.
What happened
The claude-cli one-shot output path has diverged from the live-session path; four related defects, all in the same classification seam:
is_errorresults delivered as successful replies.parseClaudeCliJsonlResult(src/agents/cli-output.ts:408-432) andparseCliJson(:378-400) never checkis_error/error subtypes; only the live session does (src/agents/cli-runner/claude-live-session.ts:938). The repo's own fixture (src/agents/test-helpers/claude-api-error-fixture.ts:34-40) shows Claude emits{"type":"result","subtype":"success","is_error":true,"result":"API Error: 400 ..."}— on the one-shot path that raw API error string is delivered to the user as the assistant reply, the session binding is persisted, and no failover happens.parseCliOutputfalls back to{ text: params.raw.trim() }(cli-output.ts:1078-1084) — up to 1 MiB (one-shot) / 8 MB (live) of raw stream-json sent to the user. The streamed-assistant-text fallback ingetOutput()(:962-971) exists only for the Gemini dialect.createCliJsonlStreamingParsergrowslineBufferwithout bound (cli-output.ts:948-955) andexecute.ts:1123-1133keeps pushing even afterstdoutParseExceeded. The live path caps pending lines at 8 MB and aborts (claude-live-session.ts:955-967); the one-shot path can allocate GBs from a babbling child before the overall timeout.src/process/supervisor/adapters/child.ts:175-196decodes each stdout Buffer independently; the incremental decoder insrc/infra/windows-encoding.ts:158-172is only constructed on win32. A multibyte character split across a 64 KB pipe read decodes to U+FFFD on both sides, inside a valid JSON string, so corrupted text flows silently to the user.Expected behavior
One classification seam in
cli-output.tsshared by live and one-shot paths:is_error/error subtypes classify toerrorText, stream-json dialects never fall back to raw transcript text, line buffering is capped like the live path, and stdout decoding is incremental on all platforms.