Note: this issue was filed under a wrong theory and revised several times as evidence came
in. The body below is the corrected account. Earlier comments contain retracted claims — in
particular that a corrupt file was written to disk (it was not; the file in question was
created manually by the user). See the retraction comment for what was withdrawn.
Summary
When a tool call's arguments are truncated mid-stream, Continue offers the incomplete call to
the user for acceptance. Accepting it fails with a JSON parse error, nothing is applied, and
the generated content is recoverable only by copy-pasting from the chat transcript.
Observed 3× while generating a single large Python file.
Sequence
tool-call arguments truncated mid-stream
→ call promoted to `generated` WITHOUT validating arguments
→ surfaced in the UI as a normal, acceptable pending write
→ user clicks Accept
→ JSON.parse fails: "invalid string: missing closing quote"
→ nothing applied; no file created
→ content lost from the actionable path (still visible in transcript)
The user ultimately created the file by hand from the response block.
Two triggers, one defect
Truncation arises two ways, and they are currently indistinguishable:
| Trigger |
signal.aborted |
Notes |
| Output window exhausted |
false |
Reasoning + prose + tool argument share one budget |
| User cancels generation |
true |
Reproduced at 879 bytes — nothing near any limit |
The cancellation case is what proves this is not a context-window bug. Same error, same
mid-string cut, at a fraction of the size.
The exhaustion boundary also moves, because the argument gets whatever the reasoning and
prose leave behind:
effective_limit = window − (reasoning + prose)
Observed truncation columns: 31,447 → 31,395 → 879. Any test asserting a fixed byte
threshold will be flaky.
Root causes (verified in code)
1. core/tools/parseArgs.ts:17-24 — parse failure swallowed
try {
return JSON.parse(toolCall.function?.arguments?.trim() || "{}");
} catch (e) {
//console.error(...) // diagnostic commented out
return {};
}
{} conflates "no arguments" with "arguments were corrupt". Shared by Anthropic, Gemini,
Bedrock and callTool.ts.
2. gui/src/redux/slices/sessionSlice.ts:951-973 — setToolGenerated validates nothing
if (toolCallState) {
toolCallState.status = "generated"; // unconditional
This is what makes a truncated call eligible for the approval UI. callToolById.ts:33-34
correctly refuses anything not generated — but the status claims generated, so it is admitted
and fails downstream, after the user has committed.
3. finish_reason never reaches the tool layer
Absent from core/index.d.ts; read once in openaiTypeConverters.ts:410 and dropped. So
exhaustion, cancellation, and a genuinely malformed model response are indistinguishable at the
point of decision — though only the first is resumable.
4. gui/src/redux/slices/sessionSlice.ts:581-584 — abortStream leaves calls in-flight
Aborts the controller and replaces it without marking in-flight tool calls. A cancelled call
keeps status: "generating" indefinitely. (Same shape as the reasoning-span bug fixed in #16 —
an in-flight state no path closes on cancellation.)
Not a security issue
Verified: a cancelled tool call cannot execute. callToolById.ts:33-34 hard-returns unless
status === "generated", and streamNormalInput.ts:300 returns early on abort. An interrupted
python3 << 'EOF' … payload could not have run.
Desired behaviour
Handle the two triggers differently, because the user's intent differs:
Cancellation — deliberate. Mark generating → canceled. Never parse, never execute, never
offer for acceptance. Render as interrupted, not failed.
Exhaustion — accidental. The work exists and is visibly correct. Salvage it:
- recover completed keys plus the partial trailing string value
- require a fully-parsed destination path (never guess)
- present as incomplete, with Continue / Discard / Keep — never a plain Accept that must fail
- on Continue, re-prompt with the tail of the recovered content so the model appends at the right
boundary rather than regenerating
- cap resume attempts so an unattended agent cannot loop
Fix order
setToolGenerated must refuse to promote unparseable arguments — stops the bad acceptance
offer at its source
parseArgs must surface a typed failure instead of {}
- Plumb
finish_reason to the tool layer
- Salvage + user-gated resume (highest user-facing value)
abortStream marks generating → canceled
Tests
- Truncated args → call is never offered as acceptable
- Cancel mid-argument → status
canceled, no execution, no side effects
- Exhaustion + valid path → salvage offered with the recovered tail
- Malformed JSON with
finish_reason: stop → clear error, not resumable
- Resume appends without duplicating the boundary region
- No fixed byte threshold — the boundary moves with reasoning length
Environment
Fork main @ 63bc4ef9d; observed on a local model via the orchestrator.
Summary
When a tool call's arguments are truncated mid-stream, Continue offers the incomplete call to
the user for acceptance. Accepting it fails with a JSON parse error, nothing is applied, and
the generated content is recoverable only by copy-pasting from the chat transcript.
Observed 3× while generating a single large Python file.
Sequence
The user ultimately created the file by hand from the response block.
Two triggers, one defect
Truncation arises two ways, and they are currently indistinguishable:
signal.abortedThe cancellation case is what proves this is not a context-window bug. Same error, same
mid-string cut, at a fraction of the size.
The exhaustion boundary also moves, because the argument gets whatever the reasoning and
prose leave behind:
Observed truncation columns: 31,447 → 31,395 → 879. Any test asserting a fixed byte
threshold will be flaky.
Root causes (verified in code)
1.
core/tools/parseArgs.ts:17-24— parse failure swallowed{}conflates "no arguments" with "arguments were corrupt". Shared by Anthropic, Gemini,Bedrock and
callTool.ts.2.
gui/src/redux/slices/sessionSlice.ts:951-973—setToolGeneratedvalidates nothingThis is what makes a truncated call eligible for the approval UI.
callToolById.ts:33-34correctly refuses anything not
generated— but the status claims generated, so it is admittedand fails downstream, after the user has committed.
3.
finish_reasonnever reaches the tool layerAbsent from
core/index.d.ts; read once inopenaiTypeConverters.ts:410and dropped. Soexhaustion, cancellation, and a genuinely malformed model response are indistinguishable at the
point of decision — though only the first is resumable.
4.
gui/src/redux/slices/sessionSlice.ts:581-584—abortStreamleaves calls in-flightAborts the controller and replaces it without marking in-flight tool calls. A cancelled call
keeps
status: "generating"indefinitely. (Same shape as the reasoning-span bug fixed in #16 —an in-flight state no path closes on cancellation.)
Not a security issue
Verified: a cancelled tool call cannot execute.
callToolById.ts:33-34hard-returns unlessstatus === "generated", andstreamNormalInput.ts:300returns early on abort. An interruptedpython3 << 'EOF' …payload could not have run.Desired behaviour
Handle the two triggers differently, because the user's intent differs:
Cancellation — deliberate. Mark
generating→canceled. Never parse, never execute, neveroffer for acceptance. Render as interrupted, not failed.
Exhaustion — accidental. The work exists and is visibly correct. Salvage it:
boundary rather than regenerating
Fix order
setToolGeneratedmust refuse to promote unparseable arguments — stops the bad acceptanceoffer at its source
parseArgsmust surface a typed failure instead of{}finish_reasonto the tool layerabortStreammarksgenerating→canceledTests
canceled, no execution, no side effectsfinish_reason: stop→ clear error, not resumableEnvironment
Fork
main@63bc4ef9d; observed on a local model via the orchestrator.