anomalyco / anomalyco/opencode

keep.tokens is not honoured: the compaction split walk-back is unbounded and skips synthetic messages, so agent-driven sessions carry 234K past a 15K setting

Open
#43,250 0 comments 0 reactions 1 assignee View on GitHub

@neriousy is already working on this.

Since Aug 18, 2026.

2.0
Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Description

The docs describe keep.tokens (default 15000) as "Tokens from newest context to retain alongside
summary". In agent-driven sessions the actual figure is routinely 5–15× that.

select() accumulates from the tail until it exceeds keep.tokens, then walks backwards to the
nearest user message:

https://github.com/anomalyco/opencode/blob/39be1595993de57a5a667c86e21ff788a80ebbbe/packages/core/src/session/compaction.ts#L178-L212

while (split > 0 && conversation[split].message.type !== "user") split--

keep.tokens only decides where that walk starts. The walk itself has no bound, so everything back
to the last user turn ends up in recent and is carried forward verbatim. Interactive sessions have a
user turn every few messages; agent-driven ones — subagents, long tool loops, autonomous runs — do
not, and the walk crosses hundreds of messages.

Synthetic messages are already user messages everywhere else. to-llm-message.ts lowers them to
role: "user" on the wire:

https://github.com/anomalyco/opencode/blob/39be1595993de57a5a667c86e21ff788a80ebbbe/packages/core/src/session/runner/to-llm-message.ts#L246-L247

but select() runs against the pre-lowering SessionMessage.Info, where the type is "synthetic",
so the walk-back steps straight over every one. Two views of the same message disagree about what it
is, and that disagreement is what makes the walk long. shell messages have the same mismatch — also
lowered to role: "user", also skipped.

Measured impact

169 completed compactions from a local opencode.db, bucketed by tokens carried forward in recent
(chars/4):

carried forward compactions
<15K (the configured target) 46
15–40K 56
40–80K 39
80–150K 17
>150K 11

Worst case: 234,632 tokens carried forward, with 243 assistant messages between the last user
message and the compaction. That session holds 2,985 assistant messages against 45 user messages
(66:1) and 495 synthetics — roughly 11 usable anchors per user turn, none of them used.

The excess is re-read on every request until the next compaction. Over the seven sessions with ≥5
compactions, tokens carried above the 15K target × requests in the window comes to ~0.71 Gtok of
avoidable re-read:

session windows avg carried reqs/window excess re-read
A 18 91,024 160 0.20 Gtok
B 20 84,446 136 0.15 Gtok
C 19 64,832 161 0.15 Gtok
D 26 45,848 140 0.12 Gtok

It compounds: planContent feeds the previous recent back into the next compaction prompt, so an
overshoot inflates the next compaction's cold input too.

Expected behaviour

keep.tokens is a hard upper bound on the estimated serialized recent context:

Token.estimate(recent) <= keep.tokens

If the newest serialized message or logical unit alone exceeds the budget, retaining no recent tail is valid. The implementation must never walk backwards without a bound simply to find a role anchor.

Suggested fix

The fix should be expressed in terms of the retained-budget invariant:

  1. Select the newest serialized messages or logical units without exceeding keep.tokens.
  2. If role-aware boundaries are preserved, classify every message that lowers to a user-role provider message consistently, including synthetic and shell.
  3. Do not use an unbounded walk-back. If no valid boundary fits, split at the budget or retain no recent tail.
Plugins

No response

OpenCode version

0.0.0-beta-17498 (source verified at 39be1595993de57a5a667c86e21ff788a80ebbbe)

Steps to reproduce
  1. Leave compaction.keep.tokens at its default of 15000.
  2. Run an agent session that executes many tool calls without intervening user messages — a subagent
    fan-out or an autonomous loop.
  3. Let auto-compaction fire.
  4. Inspect the resulting compaction message's recent field. It contains everything since the last
    user turn, which can be an order of magnitude over keep.tokens.
Screenshot and/or share link

No response

Operating System

No response

Terminal

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.