fix: escalate to model fallback after rate-limit profile rotation cap (#58707)

* fix: escalate to model fallback after rate-limit profile rotation cap

Per-model rate limits (e.g. Anthropic Sonnet-only quotas) are not
relieved by rotating auth profiles — if all profiles share the same
model quota, cycling between them loops forever without falling back
to the next model in the configured fallbacks chain.

Apply the same rotation-cap pattern introduced for overloaded_error
(#58348) to rate_limit errors:

- Add `rateLimitedProfileRotations` to auth.cooldowns config (default: 1)
- After N profile rotations on a rate_limit error, throw FailoverError
  to trigger cross-provider model fallback
- Add `resolveRateLimitProfileRotationLimit` helper following the same
  pattern as `resolveOverloadProfileRotationLimit`

Fixes #58572

* fix: cap prompt-side rate-limit failover (#58707) (thanks @Forgely3D)

* fix: restore latest-main gates for #58707

---------

Co-authored-by: Ember (Forgely3D) <ember@forgely.co>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Forgely3D
2026-04-01 02:54:10 -06:00
committed by GitHub
parent 8fce663861
commit 4fa11632b4
22 changed files with 357 additions and 45 deletions

View File

@@ -22,7 +22,10 @@ import {
resolveUsageProviderId,
} from "../../infra/provider-usage.js";
import type { MediaUnderstandingDecision } from "../../media-understanding/types.js";
import { listTasksForAgentId, listTasksForSessionKey } from "../../tasks/task-registry.js";
import {
listTasksForAgentIdForStatus,
listTasksForSessionKeyForStatus,
} from "../../tasks/task-status-access.js";
import {
buildTaskStatusSnapshot,
formatTaskStatusDetail,
@@ -61,7 +64,7 @@ function shouldLoadUsageSummary(params: {
}
function formatSessionTaskLine(sessionKey: string): string | undefined {
const snapshot = buildTaskStatusSnapshot(listTasksForSessionKey(sessionKey));
const snapshot = buildTaskStatusSnapshot(listTasksForSessionKeyForStatus(sessionKey));
const task = snapshot.focus;
if (!task) {
return undefined;
@@ -79,7 +82,7 @@ function formatSessionTaskLine(sessionKey: string): string | undefined {
}
function formatAgentTaskCountsLine(agentId: string): string | undefined {
const snapshot = buildTaskStatusSnapshot(listTasksForAgentId(agentId));
const snapshot = buildTaskStatusSnapshot(listTasksForAgentIdForStatus(agentId));
if (snapshot.totalCount === 0) {
return undefined;
}

View File

@@ -2,8 +2,11 @@ import { resolveSessionAgentId } from "../../agents/agent-scope.js";
import { logVerbose } from "../../globals.js";
import { formatDurationCompact } from "../../infra/format-time/format-duration.ts";
import { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
import { listTasksForAgentId, listTasksForSessionKey } from "../../tasks/task-registry.js";
import type { TaskRecord } from "../../tasks/task-registry.types.js";
import {
listTasksForAgentIdForStatus,
listTasksForSessionKeyForStatus,
} from "../../tasks/task-status-access.js";
import { buildTaskStatusSnapshot } from "../../tasks/task-status.js";
import type { ReplyPayload } from "../types.js";
import type { CommandHandler, HandleCommandsParams } from "./commands-types.js";
@@ -35,7 +38,7 @@ function formatTaskHeadline(snapshot: ReturnType<typeof buildTaskStatusSnapshot>
}
function formatAgentFallbackLine(agentId: string): string | undefined {
const snapshot = buildTaskStatusSnapshot(listTasksForAgentId(agentId));
const snapshot = buildTaskStatusSnapshot(listTasksForAgentIdForStatus(agentId));
if (snapshot.totalCount === 0) {
return undefined;
}
@@ -75,7 +78,9 @@ function formatVisibleTask(task: TaskRecord, index: number): string {
}
export function buildTasksText(params: { sessionKey: string; agentId: string }): string {
const sessionSnapshot = buildTaskStatusSnapshot(listTasksForSessionKey(params.sessionKey));
const sessionSnapshot = buildTaskStatusSnapshot(
listTasksForSessionKeyForStatus(params.sessionKey),
);
const lines = ["📋 Tasks", formatTaskHeadline(sessionSnapshot)];
if (sessionSnapshot.totalCount > 0) {