fix(daemon): stop retrying hard quota failures (#4439)

This commit is contained in:
hhang
2026-07-03 02:58:11 +00:00
committed by GitHub
parent 06426ca029
commit cf4b662aad
5 changed files with 43 additions and 12 deletions
@@ -671,17 +671,19 @@ export function classifyRunFailure(
}
if (errorCode === 'RATE_LIMITED' || serviceFailure === 'RATE_LIMITED' || isHardQuotaText(text) || isRateLimitText(text)) {
const retryable = retryableHint ?? !isHardQuotaText(text);
const hardQuota = isHardQuotaText(text);
const workspaceCredits = isWorkspaceCreditsText(text);
const retryable = hardQuota ? false : (retryableHint ?? true);
return classification(
'rate_limit',
isWorkspaceCreditsText(text)
workspaceCredits
? 'workspace_credits_exhausted'
: isHardQuotaText(text)
: hardQuota
? 'hard_quota'
: 'rate_limit_429',
'session_init',
retryable,
retryable ? 'retry' : isWorkspaceCreditsText(text) ? 'recharge' : 'none',
retryable ? 'retry' : workspaceCredits ? 'recharge' : 'none',
);
}
@@ -784,4 +786,4 @@ export function classifyRunFailure(
retryableHint ?? false,
retryableHint ? 'retry' : 'none',
);
}
}
+9 -2
View File
@@ -13,6 +13,7 @@ import { classifyAgentServiceFailure, type AgentServiceFailureCode } from './aut
export interface OpenCodeServiceFailure {
code: AgentServiceFailureCode;
message: string;
retryable: boolean;
statusCode: number | null;
}
@@ -84,6 +85,9 @@ export function readLatestOpenCodeLogTail(
const SERVICE_ERROR_MESSAGE_RE =
/usage limit|rate[ _-]?limit|quota|limit reached|insufficient|credit|balance|overloaded|unavailable|unauthor|authenticat|invalid[ _-]?(?:api[ _-]?)?key|api key|\/login|exhaust|too many requests/i;
const HARD_QUOTA_MESSAGE_RE =
/\b(session limit|usage limit|limit reached|quota|billing (?:hard )?limit|insufficient[ _-]?(?:quota|credit|funds)|exceeded your current quota)\b/i;
function pickServiceErrorMessage(line: string): string | null {
const re = /"message":"((?:[^"\\]|\\.)*)"/g;
let fallback: string | null = null;
@@ -151,8 +155,11 @@ export function extractOpenCodeServiceFailure(
statusCode != null ? codeFromStatus(statusCode) : null;
if (!code && message) code = classifyAgentServiceFailure(message);
if (!code) return null;
const retryable = code === 'UPSTREAM_UNAVAILABLE' || (
code === 'RATE_LIMITED' && !HARD_QUOTA_MESSAGE_RE.test(message ?? '')
);
return { code, message: message || defaultMessageForCode(code), statusCode };
return { code, message: message || defaultMessageForCode(code), retryable, statusCode };
}
// Convenience for the run close handler / inactivity watchdog: resolve the
@@ -167,4 +174,4 @@ export function readOpenCodeServiceFailure(
const tail = readLatestOpenCodeLogTail(logDir, options);
if (!tail) return null;
return extractOpenCodeServiceFailure(tail);
}
}
+3 -3
View File
@@ -7099,7 +7099,7 @@ export async function startServer({
stallPayload = createSseErrorPayload(
logFailure.code,
logFailure.message,
{ retryable: true },
{ retryable: logFailure.retryable },
);
}
}
@@ -8608,7 +8608,7 @@ export async function startServer({
send('error', createSseErrorPayload(
openCodeFailure.code,
openCodeFailure.message,
{ retryable: true },
{ retryable: openCodeFailure.retryable },
));
} else {
const rewritten = rewriteKnownAgentStreamError(
@@ -9511,4 +9511,4 @@ function sanitizeSlug(text) {
.replace(/[\s_]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 64);
}
}
@@ -268,6 +268,27 @@ describe('classifyRunFailure', () => {
});
});
it('does not let retryable hints override session-limit hard quota text', () => {
expect(
classify(
'RATE_LIMITED',
"You've hit your session limit; resets at 3:10am.",
[
errorEvent(
'RATE_LIMITED',
"You've hit your session limit; resets at 3:10am.",
true,
),
],
),
).toMatchObject({
failure_category: 'rate_limit',
failure_detail: 'hard_quota',
retryable: false,
user_action: 'none',
});
});
it('maps upstream failures to retry guidance', () => {
expect(classify('UPSTREAM_UNAVAILABLE', 'HTTP 503 upstream unavailable')).toMatchObject({
failure_category: 'upstream_unavailable',
@@ -1189,4 +1210,4 @@ describe('classifyRunFailure — batch A reclassification out of execution_faile
expect(result?.failure_category).toBe('process_exit');
expect(result?.failure_detail).toBe('session_resume_expired');
});
});
});
@@ -30,6 +30,7 @@ describe('extractOpenCodeServiceFailure', () => {
const failure = extractOpenCodeServiceFailure(USAGE_LIMIT_LINE);
expect(failure).not.toBeNull();
expect(failure!.code).toBe('RATE_LIMITED');
expect(failure!.retryable).toBe(false);
expect(failure!.statusCode).toBe(429);
expect(failure!.message).toContain('Monthly usage limit reached');
expect(failure!.message).toContain('Resets in 6 days');
@@ -161,4 +162,4 @@ describe('readOpenCodeServiceFailure (end to end from env)', () => {
readOpenCodeServiceFailure({ HOME: home }, { since: runStart }),
).toBeNull();
});
});
});