fix: BA-17 — native (claude-mcp) turn is one assistant message, not one stream event (v0.33.1)#25
Merged
Merged
Conversation
… one stream event The claude CLI emits a separate `assistant` stream event per content BLOCK, each repeating that message's `usage`. `createSessionStream` fired one `onTurn` per event, which corrupted both metering axes: - Turn axis: a caller whose attempt bound is an LLM-turn count saw 14 "turns" for 2 real ones (7x; 4.4x on the adopter's failing job — 35 events for 8 turns). Its net guillotined the session at ~half the advertised allowance, and on native that routed to humanChannel -> terminate, discarding the worker's output. - Token axis: the same message's usage was summed once per block — 5.04x inflated vs the CLI's own session total, so a budget cap fired on tokens never spent. A run of consecutive events sharing message.id is now ONE turn (adjacent-run dedup — not a Set, so a recurring id still counts; no id degrades to per-event, never collapses the session into one turn). `--max-turns` itself was never the defect. The ask filed it as "does not enforce, counts tool-calling turns" — both measured false on the wire: it enforces (12-step task under --max-turns 4 stopped at 4, named error_max_turns) and counts assistant turns (12 tool calls across 2 turns inside --max-turns 3). Also in this fix: - The turn bound is now bare-agent's guarantee. --max-turns still maps through (it stops cleanly and emits the cost-bearing result event); a parent-side counter kills the session only on an OVERRUN (> not >=), because --max-turns is undocumented in `claude --help` and a rename would silently unbound it. - BA-5 on native: a bounded session returns the last turn's text (the CLI reports result:null on a bound), and the imposed terminal reports stopReason:'max_turns' via an own-property lookup (proto-key guard), not null. - The closing kind:'session' event reconciles the TOKEN axis: a turn's usage is an unrevised first-block snapshot, so the streamed sum is short of the total. The closing event carries the per-tier residual (floored at 0), so a wired gate's tokens sum to exactly the CLI's own total. Verified live (streamed + residual = 821 = the CLI's output total). - GenerateResult.model on native is read from result.modelUsage via mapClaudeMeta (the result event has no `model` key), so model/costUsd stop being null. No new public surface (patch). Adopters bounding by LLM turns get fewer onTurn events and smaller, correct token numbers than 0.33.0 reported. Evidence: poc/ba17-turn-unit.mjs, poc/ba17-unit-parallel.mjs (the flag's real unit), poc/ba17-verify-shipped.mjs (shipped code on a live session, both cases green incl. token reconciliation). +34 tests; every new guard mutation-proved in both directions (19 mutations, all red). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QxUb57y7xtJcaviVP9avJ6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BA-17 — the native turn-unit fix (bareloop upstream ask)
The claude CLI emits one
assistantstream event per content BLOCK, each repeating that message'susage.createSessionStreamfired oneonTurnper event, corrupting both metering axes:humanChannel → terminate, discarding the worker's output.A run of consecutive events sharing
message.idis now ONE turn (adjacent-run dedup; no id ⇒ per-event, never a collapse).--max-turnsitself was never the defect — the ask filed it as "does not enforce, counts tool-calling turns"; both measured false on the wire (enforces at 4; 12 tool calls across 2 turns inside--max-turns 3). The symptom was the mis-count.Also in this fix
--max-turnsstill maps through (clean stop + cost-bearing result event), plus a parent-side counter that kills only on an overrun (>not>=) — the flag is undocumented inclaude --help.result:null) and reportsstopReason:'max_turns'via an own-property lookup (proto-key guard).kind:'session'event reconciles the token residual — a turn'susageis an unrevised first-block snapshot, so the streamed sum is short; residual (floored at 0) makes a gate's tiers sum to the CLI's own total.GenerateResult.model/costUsdon native read fromresult.modelUsageviamapClaudeMeta.No new public surface (patch). Adopters bounding by LLM turns get fewer
onTurnevents and smaller, correct token numbers.Verification
poc/ba17-turn-unit.mjs,poc/ba17-unit-parallel.mjs,poc/ba17-verify-shipped.mjs.🤖 Generated with Claude Code
https://claude.ai/code/session_01QxUb57y7xtJcaviVP9avJ6