fix: auto-resume auto-mode after rate limit cooldown (#756) (#776)

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:
TÂCHES 2026-03-16 21:03:46 -06:00 committed by GitHub
parent 3c1a4e9109
commit 912b48adad
3 changed files with 127 additions and 3 deletions

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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",
},
]);
});