[Bug]: T3 overestimates how full a conversation is when the provider doesn't report token usage#22637

opened6d agobyKKainoa Newton

Summary

Some providers never tell T3 how much of their context window they've used. In my case that's Factory Droid (via the ACP registry) and Cursor. For these providers T3 guesses, and the guess runs high:

  • It counts every byte of stored history as a token, including command output. Real text is roughly 3–4 bytes per token.
  • If it doesn't know the model's window size, it assumes 128k, even for models with bigger windows.

The result is that switching providers gets blocked with "Insufficient context allowance" while the model still has plenty of room.

What T3 assumesCloser to reality
Window size128k (fallback)The model's real window, often 200k+
Used so far1 token per byte of stored historyRoughly 1 token per 3–4 bytes
Room for a switch0 → blocked after about 96 KB of historyPlenty

Area

apps/server

Steps to reproduce

  1. Use a provider that reports no token usage, such as Factory Droid from the ACP registry. Run a few turns that produce large command output.
  2. Switch the thread to another provider and send a message.
  3. Switch back and send a message.

Expected behavior

T3's estimate of how full the conversation is stays reasonably close to real usage, so the switch goes through when the model has room.

Actual behavior

The switch back fails with "Insufficient context allowance", even though the real conversation is far below the model's limit.

In my local statev2.sqlite, 0 of 7 Factory Droid provider turns and 0 of 23 Cursor provider turns have tokenUsage. For comparison, Codex has 24 of 25 and Claude has 23 of 23. So every budget decision for Factory and Cursor runs on the estimate.

Impact

Major degradation or frequent failure

Version or commit

T3 Code Nightly 0.0.46-nightly.20261014.3132. Code references are to main @ e64766c.

Environment

macOS arm64, Factory Droid 0.228.0 via the ACP registry, Cursor

Workaround

Fork the thread with the provider selected so that it starts a fresh session.


Details

How the budget is computed (packages/provider-core/src/server/handoffBudget.ts, handoffBudget()):

1window = modelContextWindow ?? usage.maxTokens ?? 128_000
2used = usage.usedTokens ?? nativeContextEstimate
3room = window − used − currentMessage − max(16_000, window / 4) // then capped at 16k

With the 128k fallback, the reserve is 32k. Once the estimate passes about 96k, room is 0 and ContextHandoffBudgetError is thrown.

How the estimate is made (ProviderTurnStartService.ts, nativeContextEstimate): it sums Buffer.byteLength(historicalMessage(item).text) over the stored history of that native session, plus an allowance for attachments. historicalMessage includes command_execution items. In #19875, 274 KB of a 354 KB estimate was command output.

Where the window comes from. getModelContextWindow exists for Claude, Cursor (since #17462), Grok, OpenCode and Pi. The ACP registry flavor has none, so registry agents always get 128k unless they send an ACP usage_update with a size. T3 already reads usage_update generically, but Factory doesn't send one.

Cursor note. #17462 fixed Cursor's window size, but Cursor still reports no usage, so the byte estimate still applies there.

Related: #19875, #16147 and #20166 (now closed; that fix covered the window size, not usage) have the same root cause through other providers. #22636 covers what happens once the budget hits 0.

Possible directions. These are ideas, not a spec:

  • Convert bytes to an approximate token count. Even a conservative ratio would be much closer than 1:1.
  • Weight or cap tool and command output in the estimate. Agents may not pass all of it to the model, so counting it in full is likely to overshoot.
  • For ACP registry agents, use a window size the agent exposes (usage_update.size, session config), or let users set a context window per provider instance.
  • When usage is only an estimate, it may be better not to hard-block the turn. A warning, or deferring the handoff, would be gentler.
  • Mark the context meter as "estimated" when no measurement exists, so users understand why it looks full.

Note

🤖 Claude Opus 5.5 responding on behalf of Theo

Thanks for the report. One part of this is fixed on main in #17462. Please test on the nightly built from main on 2026-10-21, or on the next stable release. One part is still open: stored history is still counted one token per byte, and ACP registry agents still use the 128k fallback. If that part still matters to you, please open a new issue for that part only.

The remaining byte-based context estimate and ACP 128k fallback are now tracked in #23144, filed by the same reporter after the earlier closure. That report covers the same insufficient-context failure when switching back to Factory Droid or Cursor. Keeping this issue closed and consolidating further investigation there; no new reproduction or code changes in this duplicate triage.