fix(quota): add cooldown + 2-poll confirmation to prevent swap ping-pong

Anthropic HTTP 500 errors surface in the TUI with payloads containing
"rate limit" text, which the monitor was matching against quotaPatterns
and treating as a real 429 quota hit. With no cooldown and no
confirmation, a burst of 500s produced sub-minute ping-pong swaps that
tore down user sessions.

Two-layer fix:
- quota.reactivate_cooldown (already in config, 5m) now gates the
  monitor too — not just the dispatcher. A completed swap suppresses
  further detection for the cooldown window.
- A hit with no parseable reset time is treated as suspected only on
  the first poll; a second consecutive poll is required before
  emitting SwapRequested. Legitimate 429s with "resets in ..." still
  swap instantly on the first detection.

Adds state.RecordSwap / LastSwapInfo for the cooldown, and a
forensic log line on every detection: trigger_session, matched
pattern, 120-char pane snippet.

Tests cover: instant swap with reset, 2-poll confirmation without
reset, and suspected-state reset on recovery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ubuntu 2026-04-15 19:18:27 +00:00
parent 75b5110748
commit 7c5f8384fa
5 changed files with 246 additions and 25 deletions

View file

@ -109,8 +109,10 @@ func (a *AccountSwitcher) executeSwitch(req quota.SwitchRequest) {
a.killAllPoolSessions()
a.recreatePoolSessions()
// Update active account.
// Update active account and record the swap timestamp so the quota
// monitor can enforce a cooldown before requesting another one.
a.state.SetActiveAccount(target.Name)
a.state.RecordSwap(req.From, target.Name)
// 3. RESUMING — sessions are alive, dispatcher will fill them.
a.currentState = StateResuming