# FixV11 — over-limit tool-call drop is per-construct and chunk-invariant Base: `b3877065` (master). Worktree: `C:/Users/dwgx1/.dwgx/ops/artifacts/windsurfapi-autonomous-20261022/w16x-FixV11` (branch `fix/v11-oversize-drop-state`). Node v24.19.0. No push, no tag. Audit row: `evidence/audit/VERDICT.md` V-11 (P1) — "Over-limit parser uses a single boolean drop state, making output chunk-dependent" (`src/handlers/tool-emulation.js:1850-1930,2040-2050,2180-2200`). ## 1. Defect `ToolCallStreamParser` abandoned an over-limit construct by setting one boolean, `_oversizeDropped`, and guessing the terminator from the dialect (`gpt_native ? '}' : ''`). Consequences, all reproduced before the fix with `tmp/v11-probe.mjs` (whole-stream vs 3/7/1000/4096/65536/70000-byte and random splits): - 11/11 probe cases produced different output for the same bytes. - The `` open marker leaked at EOF (`flush()` regurgitated it because `inToolCall` was still true). - `` over-limit left `inToolResult` false with no drop state, so the matching `` leaked as prose; the body tail leaked at flush (up to ~4.4 KB of `Z` in the probe). - `{"tool_code"}` / `{"name"}` blocks under `openai_json_xml` armed the drop but the shared handler only terminated on ``, so the block never ended and `flush()` emitted its raw tail (`"}}`, `\"`). - `gpt_native` terminated on the first `}` even inside a string or a nested object, leaking `}}`; the kimi section-end marker leaked. - The non-openai sentinel path only reached the limit check on a *later* feed, so a whole-stream 70 KB construct was parsed at flush while the same bytes fed in pieces were replaced by the placeholder. - The XML/JSON caps measured the whole buffer, so a small valid call followed by 70 KB of prose was dropped in one split and parsed in another. ## 2. Fix (`src/handlers/tool-emulation.js` only) - Replaced the boolean with `_oversizeDrop`, a per-construct state: - `{ kind: 'tag', close, hold }` — swallow until this construct's literal terminator; retain only a possible split-prefix of it (`overLimitHoldTail`); - `{ kind: 'json', depth, inStr, escaped }` — swallow until the construct's own outermost brace closes (`advanceJsonBraceState`), so `}` inside strings or nested objects cannot end it early. `_oversizeDropped` remains as a getter over `_oversizeDrop` for existing tests/diagnostics. - `_armOversizeDrop(kind, close, buffered)` resolves the terminator against the bytes already consumed; when the terminator is present it returns the bytes after it so they continue through the normal path. `_consumeOversizeDrop(delta)` does the same incrementally, so the result is a function of the byte stream. - Cap decisions now measure the construct, not the buffer: `_consumeJsonBlock` (balanced-brace extent), `inToolCall` (`closeIdx`), `inToolResult` (`closeIdx`), and the non-openai sentinel branch (terminator extent). The sentinel branch now emits pre-sentinel text and reaches the verdict in the same `feed`, so a whole-stream construct cannot escape the cap until `flush()`. - Each construct arms its own terminator: `` → ``, `` → ``, kimi section → `<|tool_calls_section_end|>`, bare JSON → balanced braces. The dead `_oversizeDropped` check inside `inToolCall` was removed. - `flush()` handles EOF explicitly: an over-limit construct that never terminated is discarded whole (no placeholder duplicate, no open marker, no body, no close-tag leak) and all block flags are reset. - `test/mutations/s1-p4-prefix-tables.json` mutation 3 anchor follows the re-indented prefix-hold loop (semantics unchanged). ## 3. Red-first evidence `test/tool-emulation-overlimit.test.js` gained a property suite: every case is run whole and split at 3/97-byte, 997-byte, 4096-byte, 65536-byte, 1+65536, 65536, 65537 and a deterministic random split; aggregated `text` and `toolCalls` must be identical, and forbidden markup must not appear. Before the fix (test file with 15 property cases): ``` node --import ./test/setup-env.mjs --test test/tool-emulation-overlimit.test.js ℹ tests 27 ℹ pass 12 ℹ fail 15 # every property case; the two controls passed ``` Probe before: `11/11 cases chunk-dependent` (`tmp/probe-before.log`). Probe after: `0/11 cases chunk-dependent` (`tmp/probe-after.log`). After the fix: ``` node --import ./test/setup-env.mjs --test test/tool-emulation-overlimit.test.js ℹ tests 31 ℹ pass 31 ℹ fail 0 ``` ## 4. Controls (legitimate behavior preserved) - All 10 pre-existing over-limit tests still pass, including B1 (no zombie `inToolCall`), B3 (bounded buffer: 0 bytes while dropping), M1 (`{"name"}` sentinel) and "small complete call still parses". - B2 was updated from `"}}` to `}}`: the old tail only terminated the heuristic "first `}`" state; the JSON-aware terminator is the construct's own balanced braces, which the test name and intent already described. - A small unclosed `` is still regurgitated at EOF (control test). - A small valid call followed by 70 KB of prose is parsed for `openai_json_xml` bare JSON, ``, `{"tool_code"}` and `glm47`, for every split (control tests). - `test/s1-p4-prefix-tables.test.js` (source-level feed equivalence against the frozen baseline across 4 dialects × parse toggles × parse modes and every single-cut split) passes unchanged. ## 5. Suite evidence Targeted (`tool-emulation*`, `s1-p4-prefix-tables`, `swe-streaming-toolcall`, `tool-args-split-assembly`, `tool-call-from-thinking`, `tool-preamble*`, `wire-byte-identity`): ``` node --import ./test/setup-env.mjs --test test/tool-emulation*.test.js \ test/s1-p4-prefix-tables.test.js test/swe-streaming-toolcall.test.js \ test/tool-args-split-assembly.test.js test/tool-call-from-thinking.test.js \ test/tool-preamble-*.test.js test/wire-byte-identity.test.js ℹ tests 281 ℹ pass 280 ℹ fail 0 ℹ skipped 1 ``` Full suite (`node scripts/run-tests.mjs`, exit 0, `tmp/full2.log`): ``` TEST_DISCOVERY 574 unique files ℹ tests 7041 ℹ suites 1323 ℹ pass 6944 ℹ fail 0 ℹ skipped 97 ℹ cancelled 0 ℹ duration_ms 74441.8524 ``` `test/docs-consistency-guard.test.js` (mutation anchor + smoke matrix docs family) passes with the updated `s1-p4-prefix-tables.json` anchor. ## 6. Residual / not changed - `` over-limit bodies are still dropped silently (no placeholder), matching the pre-existing product decision; the close tag no longer leaks and the bytes after it are preserved. - An over-limit construct whose terminator never arrives is discarded at EOF; this is the defined behaviour, and it is now identical for every split. - `_oversizeDropped` remains readable as a getter; no production caller reads it.