What happens
A tool call cut off by the provider's output limit can be silently completed into valid JSON and executed. For a write, the user's file is overwritten with a partial body while the model was still mid-argument.
Root cause
crates/tui/src/tools/arg_repair.rs runs a five-stage repair ladder and returns Ok(value) identically regardless of which stage succeeded. Stages 1-3 (strict parse, control-char stripping, trailing commas) normalize text that was already structurally complete. Stage 4 balance_braces (arg_repair.rs:114-132) invents structure — and it counts {/} without tracking string literals, so a stream cut right after a complete string value needs exactly one appended } to parse.
The caller therefore cannot distinguish "the model finished writing this" from "we guessed the ending". input_parse_error stays None and dispatch sees a well-formed argument.
output_limit_truncated does exist (crates/tui/src/core/engine/turn_loop.rs:1553) but is first read at :1888 and :2528 — after dispatch at :2648 — and the truncation notice is appended to the session after the tool result. Nothing on the dispatch path consults it.
Reproduction
Argument text {"path": "notes.md", "content": "first line" (stream cut after a complete string value) parses to {"path": "notes.md", "content": "first line"} and dispatches. notes.md is written containing only first line.
Why this matters
AGENTS.md lists data-loss handling as never-cut. There is no test covering it — grep "output limit\|max_tokens" crates/tui/src/core/engine/tests.rs finds nothing.
Found by
Comparing against refs/piagent, whose agent loop branches on stopReason === "length" before executing anything, with a comment naming this exact defect class. Recorded in codewhale-ops/FINDINGS-20260907-REFS-STUDY-AND-DEFECTS.md §1.1.
What happens
A tool call cut off by the provider's output limit can be silently completed into valid JSON and executed. For a
write, the user's file is overwritten with a partial body while the model was still mid-argument.Root cause
crates/tui/src/tools/arg_repair.rsruns a five-stage repair ladder and returnsOk(value)identically regardless of which stage succeeded. Stages 1-3 (strict parse, control-char stripping, trailing commas) normalize text that was already structurally complete. Stage 4balance_braces(arg_repair.rs:114-132) invents structure — and it counts{/}without tracking string literals, so a stream cut right after a complete string value needs exactly one appended}to parse.The caller therefore cannot distinguish "the model finished writing this" from "we guessed the ending".
input_parse_errorstaysNoneand dispatch sees a well-formed argument.output_limit_truncateddoes exist (crates/tui/src/core/engine/turn_loop.rs:1553) but is first read at:1888and:2528— after dispatch at:2648— and the truncation notice is appended to the session after the tool result. Nothing on the dispatch path consults it.Reproduction
Argument text
{"path": "notes.md", "content": "first line"(stream cut after a complete string value) parses to{"path": "notes.md", "content": "first line"}and dispatches.notes.mdis written containing onlyfirst line.Why this matters
AGENTS.mdlists data-loss handling as never-cut. There is no test covering it —grep "output limit\|max_tokens" crates/tui/src/core/engine/tests.rsfinds nothing.Found by
Comparing against
refs/piagent, whose agent loop branches onstopReason === "length"before executing anything, with a comment naming this exact defect class. Recorded incodewhale-ops/FINDINGS-20260907-REFS-STUDY-AND-DEFECTS.md§1.1.