pingdotgg / pingdotgg/t3code

[Bug]: Claude context meter jumps to 100% at end of turn - completeTurn falls back to cumulative session usage from result.usage

Open
#8,594 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
23k
Forks
5.9k
Avg merge
11h 14m
Merged PRs (30d)
357

Description

Before submitting
  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.
Area

apps/server

Steps to reproduce
  1. Run a Claude thread (1M context window) and let a turn do real work, so it makes several tool calls.
  2. Let the turn finish normally, without interrupting it.
  3. Watch the context meter in the composer at the moment the turn ends.

The turn has to complete on its own. Interrupted turns take a different branch in completeTurn and are not affected.

This reproduces reliably when query.getContextUsage() is unavailable or slow. On my machines that call takes 780-1152 ms (n=51, median 880) on a Mac mini and 1069-1542 ms (n=7, all over budget) on a MacBook, against the 1 second budget at ClaudeAdapter.ts:2150. On the MacBook it therefore misses on essentially every turn.

Expected behavior

The meter keeps showing the active context, roughly the value it displayed during the turn.

Actual behavior

At the end of the turn the meter jumps to exactly maxTokens and shows 100% - 1m/1m, together with the "Resume with less context" banner, on a thread whose real context is around 113k. It stays wrong until a later turn happens to get an authoritative reading.

The chain, all in apps/server/src/provider/Layers/ClaudeAdapter.ts:

  1. completeTurn asks the authoritative source first: queryCurrentContextUsage -> context.query.getContextUsage(), wrapped in Effect.timeoutOption("1 second") (~:2150). Failure is silent: the catch at ~:2147 is empty and timeoutOption swallows the timeout, so nothing is logged.
  2. On failure it falls back to result.usage through normalizeClaudeActiveTokenUsage (~:2277).
  3. result.usage is not the active context. The CLI builds it by summing the per-model accumulators in modelUsage, which are never reset, so it accumulates across the whole session. claudeUsageInputTokens (~:505) then adds input + cache_creation + cache_read.
  4. makeClaudeTokenUsageSnapshot clamps with usedTokens = Math.min(activeTokens, maxTokens) (~:557), producing exactly maxTokens.

Two stored context-window.updated payloads from the same thread, three seconds apart:

{"usedTokens":112994,"inputTokens":111443,"maxTokens":1000000}
{"usedTokens":1000000,"totalProcessedTokens":2202960,"inputTokens":2184473,"outputTokens":18487,"maxTokens":1000000}

2184473 + 18487 = 2202960, which is exactly the cumulative total. The number rendered as "context used" is that total, clamped.

In one week on a single install, 93 stored events carry usedTokens of exactly 1,000,000. The most inflated had a cumulative inputTokens of 19,767,501.

A note on the iterations guard

resultHasActiveUsage (~:2269) is widened by hasResultUsageIteration so that per-iteration snapshots keep updating the meter. The bundled CLI never emits them: its result usage template is {..., iterations: [], speed: "standard"} and the aggregator only fills the token counters, so lastClaudeUsageIteration always returns undefined for result.usage. The guard protects a shape that does not occur, while the cumulative path it lets through is the one that breaks the meter.

Relationship to existing issues

This is a different write path from #4650, which is the task_progress ratchet through Math.max. This one is the end-of-turn fallback in completeTurn, and it happens on turns with no task_progress involvement at all. #5942 was closed as a duplicate of #4650 and describes the subagent case, which is also distinct.

#6586 targets the same meter from the subagent angle and has been inactive since 2026-08-14 with a conflicting branch.

Impact

Major degradation or frequent failure

Version or commit

0.0.36-nightly.20260828.1210, source read at main @ ac3b2adf9

Environment

macOS 15 (Apple Silicon). Provider: Claude Agent SDK 0.3.221 with the bundled Claude Code 2.1.221. Models with a 1M context window. Reproduced on both a headless t3 serve on a Mac mini and the desktop app's embedded server on a MacBook.

Logs or stack traces
# Nothing is logged when the authoritative read fails. That is the point:
# apps/server/src/provider/Layers/ClaudeAdapter.ts:2143-2150
#   const usage = yield* Effect.promise(async () => {
#     try { return await context.query.getContextUsage?.(); }
#     catch { return undefined; }
#   }).pipe(Effect.timeoutOption("1 second"));
#
# The empty catch also swallows the remote case; the CLI carries the string
# "Context usage isn't available over this remote connection".
Workaround

None from the UI. Patching the bundle locally to raise the budget restores correct readings (7 of 7 turns on a live server emitted the authoritative snapshot), which confirms the mechanism, but raising that budget is not a good fix on its own: that timeout also sits on the Stop path via interruptTurn -> stopSessionInternal -> completeTurn, so a larger value delays teardown after the user presses Stop.

A better direction, if useful: prefer the last per-request reading over the cumulative one. With includePartialMessages: true, every parent message_delta already updates context.lastKnownTokenUsage through normalizeClaudeActiveTokenUsage (~:2473), and BetaMessageDeltaUsage is cumulative per API request, so input_tokens + cache_read_input_tokens there is the real context at the last request of the turn. Keeping result.usage for totalProcessedTokens only would leave that field correct while the meter stops lying.

Happy to open a PR along those lines if it would be welcome.

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.

Research direction

Start in apps/server/src/provider/Layers/ClaudeAdapter.ts at completeTurn around lines 2147-2150 and trace normalizeClaudeActiveTokenUsage, resultHasActiveUsage, and makeClaudeTokenUsageSnapshot. Compare the authoritative context read with the result.usage fallback and the per-request context.lastKnownTokenUsage path. Done means a slow or unavailable authoritative read no longer makes the meter report cumulative session usage as 100%, while total processed tokens remain correct.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.