mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(daemon): stop retrying hard quota failures (#4439)
This commit is contained in:
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user