fix(quota): veto 5xx errors + tighten patterns to stop false-positive swaps
v0.2.2's 2-poll confirmation was insufficient because Anthropic 500/503 errors are printed into Claude Code's conversation transcript and stay visible in every tmux capture until the user scrolls. A persistent server error would confirm on the second poll and still trigger a swap. Root cause: the pattern "rate limit" (bare substring) matched any 500 payload that happened to mention rate limits in its error text. Real HTTP 429s from Anthropic are typed as "rate_limit_error" in the error payload — and that's the signature we should actually key on. - Remove "rate limit" from quotaPatterns (too generic — matches transcripts). - Add "rate_limit_error" (Anthropic's typed 429 error) and "5-hour limit". - Add serverErrorPatterns veto: "api_error", "overloaded_error", "internal server error", "api error: 5". When any is present in the pane, isQuotaExhausted returns false even if a quota pattern matched. - 4 new subtests covering the veto paths + sanity that real 429s pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7c5f8384fa
commit
62e98cb9e7
3 changed files with 102 additions and 23 deletions
|
|
@ -23,13 +23,26 @@ type SwitchRequest struct {
|
|||
}
|
||||
|
||||
// quotaPatterns are substrings that indicate quota exhaustion in a pane.
|
||||
// Must be specific enough to avoid matching Anthropic server errors (500/503)
|
||||
// that happen to include generic words like "rate limit" in their payload.
|
||||
var quotaPatterns = []string{
|
||||
"you've hit your limit",
|
||||
"rate limit",
|
||||
"quota exceeded",
|
||||
"usage limit reached",
|
||||
"claude pro usage",
|
||||
"too many requests",
|
||||
"you've hit your limit", // Claude Code TUI friendly message
|
||||
"rate_limit_error", // Anthropic typed error (real HTTP 429)
|
||||
"quota exceeded", // generic, but specific enough
|
||||
"usage limit reached", // Claude Pro phrasing
|
||||
"claude pro usage", // Claude Pro dashboard phrasing
|
||||
"too many requests", // HTTP 429 status text
|
||||
"5-hour limit", // Claude Code 5h window phrasing
|
||||
}
|
||||
|
||||
// serverErrorPatterns indicate a transient upstream server error
|
||||
// (Anthropic 500/503), NOT a quota hit. When any of these is present in
|
||||
// the pane, we treat the "hit" as a false positive and do NOT swap.
|
||||
var serverErrorPatterns = []string{
|
||||
"api_error", // Anthropic typed error (HTTP 500)
|
||||
"overloaded_error", // Anthropic typed error (HTTP 503)
|
||||
"internal server error", // HTTP 500 status text
|
||||
"api error: 5", // Claude TUI rendering of 5xx
|
||||
}
|
||||
|
||||
// resetTimeRe extracts reset times like "resets 8pm", "resets at 11:30pm",
|
||||
|
|
@ -210,9 +223,14 @@ func (m *Monitor) isQuotaPaused() bool {
|
|||
return len(m.switchCh) > 0
|
||||
}
|
||||
|
||||
// isQuotaExhausted returns true if the pane content indicates quota exhaustion.
|
||||
// isQuotaExhausted returns true if the pane content indicates quota exhaustion
|
||||
// AND does not simultaneously show a transient server error (which would make
|
||||
// the apparent quota match a false positive).
|
||||
func isQuotaExhausted(paneContent string) bool {
|
||||
return firstMatchingPattern(paneContent) != ""
|
||||
if firstMatchingPattern(paneContent) == "" {
|
||||
return false
|
||||
}
|
||||
return !hasServerError(paneContent)
|
||||
}
|
||||
|
||||
// firstMatchingPattern returns the first quota pattern found in paneContent,
|
||||
|
|
@ -227,6 +245,18 @@ func firstMatchingPattern(paneContent string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// hasServerError reports whether the pane content shows an Anthropic 5xx /
|
||||
// transient server error. Used to veto a quota match: a 500 is not a quota.
|
||||
func hasServerError(paneContent string) bool {
|
||||
lower := strings.ToLower(paneContent)
|
||||
for _, p := range serverErrorPatterns {
|
||||
if strings.Contains(lower, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// snippet returns a 120-char single-line excerpt of pane content for logging.
|
||||
func snippet(s string) string {
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue