You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In multi-step tool-call assistant messages, processor.ts overwrites assistantMessage.tokens on each step-finish event instead of accumulating additive fields. Only the last step's output, reasoning, and cache.write survive.
compaction.isOverflow checks use lastFinished.tokens (prompt.ts), so underestimating tokens delays auto-compaction and makes the next request more likely to hit the provider context limit
Output, reasoning, and cache.write tokens should accumulate across steps (like cost already does). Input and cache.read should keep the final step's values, since the final step's request already includes all prior steps.
This issue might be a duplicate of existing issues. Please check:
ACP PromptResponse.usage reports only the turn's final assistant message #41660: Describes the same root-cause bug — processor.ts assigns (overwrites) assistantMessage.tokens on each step-finish instead of accumulating, so only the last step's tokens survive for multi-step tool-call turns. That issue approaches it from the ACP PromptResponse.usage angle; this one focuses on the session context meter and compaction under-reporting, but both trace to the same cost += vs tokens = asymmetry.
I went looking for this in V2 at 39be1595993de57a5a667c86e21ff788a80ebbbe. It didn't survive the
port, because the step topology changed rather than the assignment being fixed.
V2 issues one request per step
llm.ts doesn't loop steps inside a call. A call returns an outcome, and continuation re-enters with
an incremented step:
The event publisher is constructed inside that per-call function, at llm.ts:284,
and that is its only call site in the tree. So each step gets its own request, its own publisher, its
own settlement, and its own assistant message. There is no object that several steps' usage could be
folded into.
Still an assignment, but a second step-finish on one publisher is a hard Effect.die. Even if the
topology above changed, this would crash rather than silently under-report.
The helper this issue asks for already exists
usage.ts:44-55
is the accumulating fold, and it's used in the two places where one logical call really does stream
several step-finish events — SessionCompaction.execute and SessionTitle both do usage = usage ? SessionUsage.add(usage, step) : step.
Worth noting it sums every field including input and cache.read, which is the opposite of what
this issue proposes for those two. That's correct for compaction and title, where each step is a
separate prompt, and would be wrong for a multi-step agentic turn where the final request already
contains the earlier ones. So if the V2 topology ever does change, SessionUsage.add isn't the right
fold to reach for as-is.
Corroboration from data
105,379 assistant messages against 4,309 user ones — a 24:1 ratio consistent with a new assistant
message per round-trip. Individual messages carry two to seven tool parts (14,349 with one, 1,473 with
two, 1,129 with three), which is parallel calls inside a single step, not sequential steps.
Thanks for the thorough dig. None of this can happen in V2: each turn gets its own publisher, a second step-finish dies, and SessionUsage.add already handles the spots that do fold steps.
This issue targets the V1 processor though (packages/opencode/src/session/processor.ts), which is still what ships. The overwrite is still there on current dev:
In V1 the prompt loop reuses one assistant message across steps, so cost += and tokens = drift apart once a turn has multiple step-finishes.
Also agreed SessionUsage.add isn't the right fold here as-is, it sums input and cache.read too. #42326 only accumulates output/reasoning/cache.write and keeps the final step's input/cache.read instead.
Can retitle with a (v1) prefix if that makes triage easier.
This is resolved in v2 by the new execution model: each model request has its own step, publisher, and assistant message, so later steps do not overwrite earlier steps’ token counts. This does not change the legacy v1 processor discussed above. To get v2, run npm i -g @opencode/cli@latest. If you still see this on v2, please open a new issue with the details.
Activity
github-actions commentedon Aug 13, 2026
This issue might be a duplicate of existing issues. Please check:
processor.tsassigns (overwrites)assistantMessage.tokenson eachstep-finishinstead of accumulating, so only the last step's tokens survive for multi-step tool-call turns. That issue approaches it from the ACPPromptResponse.usageangle; this one focuses on the session context meter and compaction under-reporting, but both trace to the samecost +=vstokens =asymmetry.Zaczero commentedon Aug 18, 2026
I went looking for this in V2 at
39be1595993de57a5a667c86e21ff788a80ebbbe. It didn't survive theport, because the step topology changed rather than the assignment being fixed.
V2 issues one request per step
llm.tsdoesn't loop steps inside a call. A call returns an outcome, and continuation re-enters withan incremented step:
opencode/packages/core/src/session/runner/llm.ts
Line 165 in 39be159
The event publisher is constructed inside that per-call function, at
llm.ts:284,and that is its only call site in the tree. So each step gets its own request, its own publisher, its
own settlement, and its own assistant message. There is no object that several steps' usage could be
folded into.
And the settlement is guarded anyway
opencode/packages/core/src/session/runner/publish-llm-event.ts
Lines 495 to 499 in 39be159
Still an assignment, but a second
step-finishon one publisher is a hardEffect.die. Even if thetopology above changed, this would crash rather than silently under-report.
The helper this issue asks for already exists
usage.ts:44-55is the accumulating fold, and it's used in the two places where one logical call really does stream
several
step-finishevents —SessionCompaction.executeandSessionTitleboth dousage = usage ? SessionUsage.add(usage, step) : step.Worth noting it sums every field including
inputandcache.read, which is the opposite of whatthis issue proposes for those two. That's correct for compaction and title, where each step is a
separate prompt, and would be wrong for a multi-step agentic turn where the final request already
contains the earlier ones. So if the V2 topology ever does change,
SessionUsage.addisn't the rightfold to reach for as-is.
Corroboration from data
105,379 assistant messages against 4,309 user ones — a 24:1 ratio consistent with a new assistant
message per round-trip. Individual messages carry two to seven tool parts (14,349 with one, 1,473 with
two, 1,129 with three), which is parallel calls inside a single step, not sequential steps.
gold-beyond commentedon Sep 13, 2026
Thanks for the thorough dig. None of this can happen in V2: each turn gets its own publisher, a second step-finish dies, and SessionUsage.add already handles the spots that do fold steps.
This issue targets the V1 processor though (packages/opencode/src/session/processor.ts), which is still what ships. The overwrite is still there on current dev:
opencode/packages/opencode/src/session/processor.ts
Lines 458 to 459 in 95daf90
In V1 the prompt loop reuses one assistant message across steps, so
cost +=andtokens =drift apart once a turn has multiple step-finishes.Also agreed SessionUsage.add isn't the right fold here as-is, it sums input and cache.read too. #42326 only accumulates output/reasoning/cache.write and keeps the final step's input/cache.read instead.
Can retitle with a (v1) prefix if that makes triage easier.
neriousy commentedon Oct 19, 2026
This is resolved in v2 by the new execution model: each model request has its own step, publisher, and assistant message, so later steps do not overwrite earlier steps’ token counts. This does not change the legacy v1 processor discussed above. To get v2, run
npm i -g @opencode/cli@latest. If you still see this on v2, please open a new issue with the details.