Skip to content

Session token usage grows unbounded via cache.read and causes unrecoverable context-window errors #30649

Closed as not planned

Description

@wang1970

Description

Long-running sessions can grow their recorded token usage without a practical upper bound, especially through tokens.cache.read, until most models/providers fail with a context-window error and the session becomes unrecoverable.

In my real session, OpenCode reported approximately 56,128,248 total tokens while the persisted conversation data was small:

session tokens:
  input:        14,774,671
  output:           77,881
  reasoning:        20,400
  cache.read:   41,255,296
  cache.write:           0
  total:        56,128,248

stored rows for that session:
  message: 224 rows, ~0.52 MB JSON data
  part:   1042 rows, ~2.33 MB JSON data

After this, continuing the same session fails with:

Your input exceeds the context window of this model. Please adjust your input and try again.

The issue is not that the saved message JSON itself is tens of millions of tokens. The large number appears to come from cumulative session usage accounting. In particular, tokens_cache_read can grow very large over repeated turns.

Relevant code path observed in source:

packages/core/src/session/projector.ts
  tokens_input:       SessionTable.tokens_input + value.tokens.input
  tokens_output:      SessionTable.tokens_output + value.tokens.output
  tokens_reasoning:   SessionTable.tokens_reasoning + value.tokens.reasoning
  tokens_cache_read:  SessionTable.tokens_cache_read + value.tokens.cache.read
  tokens_cache_write: SessionTable.tokens_cache_write + value.tokens.cache.write

This makes session-level usage an ever-growing lifetime counter. That is fine for statistics, but it becomes dangerous if any compaction / routing / provider request / context-window logic treats cumulative session usage, especially cache.read, as if it represented the current prompt/context size.

Impact:

  • Long sessions eventually hit provider context-window errors.
  • Auto compaction does not reliably save an already-overflowed session.
  • Once overflowed, the same session can repeatedly fail and effectively becomes unusable.
  • opencode stats can become slow/hang on large databases/sessions.
  • This affects providers/models with smaller context windows especially badly when using an auto model/router.

Expected behavior:

  • Current context-window checks should be based on the actual messages that will be sent to the provider, not lifetime cumulative usage counters.
  • cache.read should not be counted as current prompt/context tokens for overflow decisions.
  • Auto compaction should trigger before the provider rejects the request.
  • If a provider returns a known context-window error, OpenCode should attempt a safe compact/prune/retry or suggest/fork a new session instead of leaving the session dead.
  • There should be a hard cap / safety guard for long-running sessions, e.g. archive/fork/reset usage or force compaction before sending.

Plugins

Only a local/custom provider/router was used (providerID: 9090, modelID: auto). No plugin-specific behavior is required to reproduce the core issue: repeated turns can accumulate session usage far beyond any real model context window.

OpenCode version

1.15.13

Steps to reproduce

  1. Use OpenCode with a provider/model that reports token usage including cache read tokens.
  2. Keep a long-running session active for many turns, especially with repeated cache hits or provider-side prompt caching.
  3. Inspect the SQLite session row, for example:
select
  tokens_input,
  tokens_output,
  tokens_reasoning,
  tokens_cache_read,
  tokens_cache_write
from session
where id = '<session id>';
  1. Observe that cumulative tokens_cache_read and total session tokens can grow to tens of millions even when the stored message/part JSON is only a few MB.
  2. Continue the same session until the provider rejects the next request with a context-window error:
Your input exceeds the context window of this model. Please adjust your input and try again.
  1. Try enabling aggressive compaction settings such as:
{
  "compaction": {
    "auto": true,
    "prune": true,
    "tail_turns": 1,
    "preserve_recent_tokens": 4000,
    "reserved": 12000
  }
}

The already-overflowed session still remains difficult/impossible to continue.

Screenshot and/or share link

No public share link, because the affected session contains private project data. Sanitized measurements are included above.

Related existing issues that look adjacent but do not mention the cache.read/cumulative-session-usage aspect:

Operating System

Windows 11

Terminal

Windows Terminal / PowerShell

Activity

  1. github-actions commented on Jun 4, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. wang1970 commented on Jun 4, 2026

    @wang1970
    Author

    Thanks for the pointers. I checked both suggested issues and I think this report is related but not a straight duplicate.

    Clarification:

    • Multi-step tool calls overwrite token counts instead of accumulating (output, reasoning, cache.write lost) #21913 is about per-assistant-message multi-step accounting in processor.ts: whether fields are overwritten vs accumulated within one assistant message. It explicitly treats cache.read as a per-step/session snapshot-like value that should not be summed within one multi-step message.
    • This issue is about session-level persisted counters in the projector/database growing unbounded across many turns, especially session.tokens_cache_read. In my case the stored session row reached:
    total ~= 56,128,248
    input = 14,774,671
    output = 77,881
    reasoning = 20,400
    cache.read = 41,255,296
    

    while the persisted content was only:

    message: 224 rows, ~0.52 MB
    part:    1042 rows, ~2.33 MB
    

    So the main distinction is: cache.read may be reasonable as a provider usage/cost metric, but it should not be allowed to make the session's effective context accounting / compaction threshold / context display / recovery behavior look like tens of millions of tokens when the actual stored prompt material is only a few MB.

    If this should be merged into one of those issues, the key extra signal to preserve is: session-level cumulative tokens_cache_read can dominate token totals and make context/compaction behavior diverge from real prompt size. A likely fix is to separate billing/usage totals from effective-context accounting, and exclude/handle cache-read tokens differently for context-window/compaction decisions.

  3. wang1970 commented on Jun 14, 2026

    @wang1970
    Author

    Bump — this issue has been open for 10 days without any maintainer response.

    To add urgency: the unbounded cache.read accumulation is not just a cosmetic counting bug. It directly causes massive token over-consumption in real usage. Sessions that should have only consumed a few hundred thousand tokens are being accounted as tens of millions, which triggers premature context overflow, failed compaction, and forces expensive re-sends of entire conversation histories.

    For anyone hitting the same problem: every long session is silently burning tokens at an inflated rate. This is a cost and usability regression that affects all OpenCode users with non-trivial session lengths.

    Could a maintainer take a look? The core fix seems straightforward — separate billing/usage totals from effective-context accounting, or exclude cache.read from context-window/compaction decisions. Happy to help test or provide more data if needed.

  4. Necmttn commented on Jun 21, 2026

    @Necmttn

    The fix likely needs two counters, not one corrected sum. Keep cumulative billing/usage counters separate from the active context-window counters used for eligibility and compaction decisions.

    cache.read can be meaningful for cost and provider accounting, but it should not make the next prompt look like it contains 41M active tokens. A good regression would assert that repeated cached turns increase billed/cache usage while active context stays bounded by retained messages plus current tool/schema overhead.


    Generated with ax.

  5. AdityaPagare619 commented on Jun 23, 2026

    @AdityaPagare619

    Same thing on my end. Session had 937M cache read tokens but only ~41MB of
    actual data. The cumulative counter was way out of sync with reality.

    This made the context overflow from Bug #3 worse — the session thought it
    was already over the limit when the actual context was still manageable.

  6. royruho commented on Jul 8, 2026

    @royruho

    Confirming this from the user-facing side, and adding a version boundary that may help narrow it.
    The additive accumulation described here (session token fields, especially cache.read, summed every turn rather than reflecting current context) appears to be what surfaces as #34712: in Web/Desktop the input/context count is inflated and /compact doesn't bring it down, while the TUI shows the correct current-context number for the same session. That lines up with pruning messages being unable to decrement a cumulative counter.
    Version boundary on my end:

    1.17.11 — count behaves correctly, no inflation
    1.17.12 / 1.17.14 — inflated count, /compact doesn't reduce it

  7. mcj777 commented on Aug 10, 2026

    @mcj777

    Bug #30649 confirmed: cache.read in isOverflow() — source analysis and timeline

    We performed a full analysis of bug #30649 based on anomalyco/opencode source (branch dev, HEAD: fe82a1b6, 2026-08-08) and git history. Confirmed: the bug is present and unfixed.

    Root cause

    File packages/opencode/src/session/overflow.ts:32:

    const count = tokens.total || tokens.input + tokens.output + tokens.cache.read + tokens.cache.write

    cache.read represents tokens served from the provider's prompt cache — they were not transmitted in the current request. DeepSeek and MiMo return 10-41M+ tokens in cache.read per step. With context=128K, usable≈96K: 10M ≫ 96K → isOverflow() always returns true → false compaction at ~16% actual context fill.

    Introduction timeline

    Confirmed manifestations

    Issue Version Description
    #30649 v1.15.13 56M total tokens with 0.52MB actual data
    #34712 v1.17.12 "Input tokens inconsistent with circle context"
    #37695 v1.18.3 "Conversation history Too large to compact" on DeepSeek v4 Flash
    #39798 v1.18.8 Compaction at 50% context on DeepSeek v4 Pro (NVIDIA NIM)

    Workaround (source-verified)

    Two short-circuit paths in overflow.ts:28-29 bypass the bug without code changes:

    1. compaction.auto: false in opencode.json — disables isOverflow() entirely (line 28)
    2. limit.context: 0 in model configuration — model-level short circuit (line 29)

    Both verified: false compactions stop. ContextOverflowError from the API remains active as an honest safety net.

    Fix

    Line overflow.ts:32: replace cache.read + cache.write with cache.write — a one-line fix. cache.read tokens are NOT part of the context window and should not influence compaction decisions.

  8. Zaczero commented on Aug 18, 2026

    @Zaczero

    The lifetime-counter observation still holds on 0.0.0-beta-17498 — the session totals are cumulative
    adds, exactly as quoted, at
    projector.ts:59-63.

    The part worth checking is the conditional: "dangerous if any compaction / routing / provider request /
    context-window logic treats cumulative session usage as if it represented the current prompt size." I
    grepped every reader of those columns to find out whether anything does.

    Where they get read, and what for

    Four places read tokens_input, tokens_cache_read and their siblings. Every one of them is a
    display or transport path:

    • info.ts:38-46 decodes them into Session.Info.tokens — the shape the API and TUI render.
    • projector.ts:71-95 selects them and publishes SessionEvent.UsageUpdated. That event is declared
      Event.ephemeral, so it isn't even persisted, and nothing in core subscribes to it; it exists to
      push a number at connected clients.
    • transfer.ts:113-117 carries the totals along when a session moves between instances.
    • v1-migration.bun.ts backfills them once from per-message tokens.

    Nothing in compaction, routing, provider request assembly or context-window accounting consults them.

    Compaction in particular reads the last assistant message, never the session row:

    const required = (input: RequiredInput) => {
    if (!config.auto) return false
    const context = input.model.route.defaults.limits?.context
    if (context === undefined || context <= 0) return false
    const last = input.messages.findLast(
    (message): message is SessionMessage.Assistant & { tokens: NonNullable<SessionMessage.Assistant["tokens"]> } =>
    message.type === "assistant" && message.tokens !== undefined,
    )
    if (!last) return false
    const limits = input.model.route.defaults.limits
    const output = Math.min(limits?.output ?? 0, OUTPUT_TOKEN_MAX)
    const promptCeiling = Math.min(
    limits?.input === undefined ? Number.POSITIVE_INFINITY : limits.input - config.buffer,
    context - Math.max(output, config.buffer),
    )
    const used =
    last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
    if (used <= 0) return false
    return used >= promptCeiling
    }

    So the 41M cumulative cache.read in the original report can't reach the compaction trigger. My
    database bears that out from the other direction: sessions carry lifetime cache.read in the billions
    — 6.74B on openai alone — while their per-message cache.read tracks the real prompt size and
    compaction fires normally throughout.

    Which leaves two separate things

    The counters are a presentation problem. The number in opencode stats is a lifetime sum that reads
    like a context measurement, and relabelling or documenting it would remove the alarm without changing
    any behaviour.

    The context-window failure is a different bug that this report may have misattributed. If it still
    reproduces on V2 it deserves its own issue, and the useful diagnostic is the last assistant message's
    tokens at the point of failure rather than the session total — that's the number the trigger actually
    sees. Worth checking #42448 too, which has compaction itself blowing the context window through an
    unbounded output budget; that failure looks a lot like this one from the outside.

  9. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

  10. AdityaPagare619 commented on Oct 12, 2026

    @AdityaPagare619

    22. Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.
    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

    Make sure to handle the Session growth properly as the newer version still has some issues for exporting sessions and also session growth is making the models thinking weaker somehow after reaching certain parameters!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions