Skip to content

Reset quota view on every dispatch, not just before a wait-and-retry - #32

Merged
dbisina merged 1 commit into
mainfrom
fix/quota-view-poisoned-across-handoffs
Jul 30, 2026
Merged

dbisina merged 1 commit into
mainfrom
fix/quota-view-poisoned-across-handoffs

Conversation

@dbisina

@dbisina dbisina commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

Live bug, reproduced directly from a real user's daemon.log: a single-account session hit an unbroken chain of 14+ handoffs in a row, each one "quota breach on claude" immediately after the previous dispatch had just started, with no other account to fail over to.

Root cause

ClaudeQuotaAdapter's Layer 2 fallback (declared cap, used when no real anthropic-ratelimit-* header has been observed yet) accumulates tokensLogged for the entire lifetime of the orchestrator session, and only resets on an explicit account switch. With one account, that switch never happens, so the counter just keeps growing across every handoff. Once cumulative usage crosses the fallback's effective cap (declaredCap/1.5, ~26,666 tokens by default, trivial for a long session), every future dispatch reports an instant breach regardless of that account's real remaining quota, forever. doHandoff never called resetQuotaView(); the wait-and-retry path already did before its own retry, but a handoff got no such reset.

Fix

Call resetQuotaView() at the end of doHandoff, after the new provider is set active, mirroring the existing wait-and-retry behaviour. A fresh dispatch now gets a fresh Layer-2 baseline: a real proxy header observed during that dispatch supersedes it immediately if quota genuinely remains; if it does not, the account breaches again honestly instead of on a stale accumulated total inherited from every prior handoff this session.

Tests

internal/quota/claude_test.go (new, package had no tests) proves the mechanism directly: cumulative RecordRequest calls breach Layer 2 and stay breached without an explicit Reset, and Reset() returns both the declared-cap fallback and a stale observed header to a fresh, non-breached baseline. doHandoff itself is too heavy to unit test cheaply (needs adapters, graph store, FSM machine) — same proportionate-testing call made elsewhere this session.

Full daemon suite green (145 tests, 22 packages), go vet clean.

Test plan

  • New unit tests pass, full suite green
  • Manual: reproduce the original scenario (single account, long session) and confirm handoffs stop looping

Live bug, reproduced from a real user's daemon.log: a single-account
session hit an unbroken chain of 14+ handoffs in a row, each one
"quota breach on claude" immediately after the previous dispatch had
just started running, with no other account to fail over to.

Root cause: ClaudeQuotaAdapter's Layer 2 fallback (declared cap, used
when no real anthropic-ratelimit-* header has been observed yet) logs
tokensLogged for the entire lifetime of the orchestrator session, and
only resets on an explicit account switch (Reset()'s own doc comment).
With one account, that switch never happens, so tokensLogged just
keeps accumulating across every handoff. Once the cumulative total
crosses the fallback's effective cap (declaredCap/1.5, the default
works out to ~26,666 tokens, trivial for a long session), every future
dispatch reports an instant breach regardless of that account's real
remaining quota, forever. doHandoff never called resetQuotaView(); the
wait-and-retry path already did before its own retry, but a same- or
cross-provider handoff got no such reset.

Fixed by calling resetQuotaView() at the end of doHandoff, after the
new provider is set active, mirroring the existing wait-and-retry
behaviour. A fresh dispatch now gets a fresh Layer-2 baseline: a real
proxy header observed during that dispatch supersedes it immediately
if quota genuinely remains, and if it does not, the account breaches
again honestly instead of on a stale accumulated total inherited from
every prior handoff this session.

Tests: internal/quota/claude_test.go (new file, package had none)
proves the mechanism directly — cumulative RecordRequest calls breach
Layer 2 and stay breached without an explicit Reset, and Reset()
returns both the declared-cap fallback and a stale observed header to
a fresh, non-breached baseline. doHandoff itself is too heavy to unit
test cheaply (needs adapters, graph store, FSM machine); this is the
same proportionate-testing call made elsewhere in this session, e.g.
worktree.go's capture/apply tests rather than a full orchestrator.New()
integration test.

Full daemon suite green (145 tests, 22 packages), go vet clean.
@dbisina
dbisina merged commit 764b383 into main Jul 30, 2026
@dbisina
dbisina deleted the fix/quota-view-poisoned-across-handoffs branch July 30, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant