When auto-mode pauses due to a rate limit, schedule automatic resumption after the rate limit window elapses. Shows a countdown notification so the user knows what's happening. Non-rate-limit errors still pause indefinitely for manual intervention. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3c1a4e9109
commit
912b48adad
3 changed files with 127 additions and 3 deletions
|
|
@ -716,7 +716,23 @@ export default function (pi: ExtensionAPI) {
|
|||
}
|
||||
}
|
||||
|
||||
await pauseAutoForProviderError(ctx.ui, errorDetail, () => pauseAuto(ctx, pi));
|
||||
// Detect rate-limit errors and extract retry delay for auto-resume
|
||||
const errorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
|
||||
const isRateLimit = /rate.?limit|too many requests|429/i.test(errorMsg);
|
||||
const retryAfterMs = ("retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number")
|
||||
? lastMsg.retryAfterMs
|
||||
: (() => { const m = errorMsg.match(/reset in (\d+)s/i); return m ? Number(m[1]) * 1000 : undefined; })();
|
||||
|
||||
await pauseAutoForProviderError(ctx.ui, errorDetail, () => pauseAuto(ctx, pi), {
|
||||
isRateLimit,
|
||||
retryAfterMs,
|
||||
resume: () => {
|
||||
pi.sendMessage(
|
||||
{ customType: "gsd-auto-timeout-recovery", content: "Continue execution \u2014 rate limit window elapsed.", display: false },
|
||||
{ triggerTurn: true },
|
||||
);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,38 @@ export type ProviderErrorPauseUI = {
|
|||
notify(message: string, level?: "info" | "warning" | "error" | "success"): void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pause auto-mode due to a provider error.
|
||||
*
|
||||
* For rate-limit errors with a known reset delay, schedules an automatic
|
||||
* resume after the delay and shows a countdown notification. For all other
|
||||
* errors, pauses indefinitely (user must manually resume).
|
||||
*/
|
||||
export async function pauseAutoForProviderError(
|
||||
ui: ProviderErrorPauseUI,
|
||||
errorDetail: string,
|
||||
pause: () => Promise<void>,
|
||||
options?: {
|
||||
isRateLimit?: boolean;
|
||||
retryAfterMs?: number;
|
||||
resume?: () => void;
|
||||
},
|
||||
): Promise<void> {
|
||||
ui.notify(`Auto-mode paused due to provider error${errorDetail}`, "warning");
|
||||
await pause();
|
||||
if (options?.isRateLimit && options.retryAfterMs && options.retryAfterMs > 0 && options.resume) {
|
||||
const delaySec = Math.ceil(options.retryAfterMs / 1000);
|
||||
ui.notify(
|
||||
`Rate limited${errorDetail}. Auto-resuming in ${delaySec}s...`,
|
||||
"warning",
|
||||
);
|
||||
await pause();
|
||||
|
||||
// Schedule auto-resume after the rate limit window
|
||||
setTimeout(() => {
|
||||
ui.notify("Rate limit window elapsed. Resuming auto-mode.", "info");
|
||||
options.resume!();
|
||||
}, options.retryAfterMs);
|
||||
} else {
|
||||
ui.notify(`Auto-mode paused due to provider error${errorDetail}`, "warning");
|
||||
await pause();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,3 +27,84 @@ test("pauseAutoForProviderError warns and pauses without requiring ctx.log", asy
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("pauseAutoForProviderError schedules auto-resume for rate limit errors", async () => {
|
||||
const notifications: Array<{ message: string; level: string }> = [];
|
||||
let pauseCalls = 0;
|
||||
let resumeCalled = false;
|
||||
|
||||
// Use fake timer
|
||||
const originalSetTimeout = globalThis.setTimeout;
|
||||
const timers: Array<{ fn: () => void; delay: number }> = [];
|
||||
globalThis.setTimeout = ((fn: () => void, delay: number) => {
|
||||
timers.push({ fn, delay });
|
||||
return 0 as unknown as ReturnType<typeof setTimeout>;
|
||||
}) as typeof setTimeout;
|
||||
|
||||
try {
|
||||
await pauseAutoForProviderError(
|
||||
{
|
||||
notify(message, level?) {
|
||||
notifications.push({ message, level: level ?? "info" });
|
||||
},
|
||||
},
|
||||
": rate limit exceeded",
|
||||
async () => {
|
||||
pauseCalls += 1;
|
||||
},
|
||||
{
|
||||
isRateLimit: true,
|
||||
retryAfterMs: 90000,
|
||||
resume: () => {
|
||||
resumeCalled = true;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(pauseCalls, 1, "should pause auto-mode");
|
||||
assert.equal(timers.length, 1, "should schedule one timer");
|
||||
assert.equal(timers[0].delay, 90000, "timer should match retryAfterMs");
|
||||
assert.deepEqual(notifications[0], {
|
||||
message: "Rate limited: rate limit exceeded. Auto-resuming in 90s...",
|
||||
level: "warning",
|
||||
});
|
||||
|
||||
// Fire the timer
|
||||
timers[0].fn();
|
||||
assert.equal(resumeCalled, true, "should call resume after timer fires");
|
||||
assert.deepEqual(notifications[1], {
|
||||
message: "Rate limit window elapsed. Resuming auto-mode.",
|
||||
level: "info",
|
||||
});
|
||||
} finally {
|
||||
globalThis.setTimeout = originalSetTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
test("pauseAutoForProviderError falls back to indefinite pause when not rate limit", async () => {
|
||||
const notifications: Array<{ message: string; level: string }> = [];
|
||||
let pauseCalls = 0;
|
||||
|
||||
await pauseAutoForProviderError(
|
||||
{
|
||||
notify(message, level?) {
|
||||
notifications.push({ message, level: level ?? "info" });
|
||||
},
|
||||
},
|
||||
": connection refused",
|
||||
async () => {
|
||||
pauseCalls += 1;
|
||||
},
|
||||
{
|
||||
isRateLimit: false,
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(pauseCalls, 1);
|
||||
assert.deepEqual(notifications, [
|
||||
{
|
||||
message: "Auto-mode paused due to provider error: connection refused",
|
||||
level: "warning",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue