diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 8db1c30782..22306fa55e 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -1468,7 +1468,7 @@ async function refreshOpenPullRequestsForScheduledSweep( // head SHA, which resets the count naturally (the target key is scoped to repo+PR+SHA). const REGATE_REPAIR_ATTEMPT_EVENT_TYPE = "agent.sweep.regate.repair_attempt"; const REGATE_REPAIR_EXHAUSTED_EVENT_TYPE = "agent.sweep.regate.repair_exhausted"; -const REGATE_REPAIR_MAX_ATTEMPTS_PER_SHA = 2; +const REGATE_REPAIR_MAX_ATTEMPTS_PER_SHA = 5; const REGATE_REPAIR_ATTEMPT_LOOKBACK_MS = 24 * 60 * 60 * 1000; function regateRepairTargetKey(repoFullName: string, prNumber: number, headSha: string): string { diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 8e895a2ad9..9ab01ebac8 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -6861,12 +6861,13 @@ describe("queue processors", () => { await upsertRepositoryFromGitHub(env, { name: "agent-repo", full_name: "owner/agent-repo", private: false, owner: { login: "owner" } }, 9407); await upsertRepositorySettings(env, { repoFullName: "owner/agent-repo", autonomy: { merge: "auto" }, gateCheckMode: "enabled", checkRunMode: "off", commentMode: "off", publicSurface: "off" }); // PR 1: missing its current Gate check for its current head -- would ordinarily be flagged outage-repair - // priority on every tick. Pre-seed 3 prior repair-attempt audit events for this EXACT head SHA to simulate a - // review that keeps failing (e.g. a timeout) and never publishes a completed gate check. + // priority on every tick. Pre-seed REGATE_REPAIR_MAX_ATTEMPTS_PER_SHA=5 (#3998) prior repair-attempt audit + // events for this EXACT head SHA to simulate a review that keeps failing (e.g. a timeout) and never + // publishes a completed gate check. await upsertPullRequestFromGitHub(env, "owner/agent-repo", { number: 1, title: "Stuck repair", state: "open", user: { login: "c" }, head: { sha: "stuck-sha" }, labels: [], body: "" }); await repositoriesModule.markPullRequestSurfacePublished(env, "owner/agent-repo", 1, "stuck-sha"); const targetKey = "owner/agent-repo#1#stuck-sha"; - for (let i = 0; i < 3; i += 1) { + for (let i = 0; i < 5; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "agent.sweep.regate.repair_attempt", actor: "gittensory", @@ -6894,12 +6895,12 @@ describe("queue processors", () => { const attempts = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") .bind("agent.sweep.regate.repair_attempt", targetKey) .first<{ n: number }>(); - expect(attempts?.n).toBe(3); + expect(attempts?.n).toBe(5); // Sentry-visible signal (via the structured-log forwarder) fires exactly once alongside the audit event. const exhaustedLogs = errors.mock.calls.filter(([line]) => typeof line === "string" && line.includes("regate_repair_exhausted")); expect(exhaustedLogs).toHaveLength(1); const logged = JSON.parse(exhaustedLogs[0]![0] as string) as Record; - expect(logged).toMatchObject({ level: "error", event: "regate_repair_exhausted", repo: "owner/agent-repo", pullNumber: 1, headSha: "stuck-sha", attempts: 3 }); + expect(logged).toMatchObject({ level: "error", event: "regate_repair_exhausted", repo: "owner/agent-repo", pullNumber: 1, headSha: "stuck-sha", attempts: 5 }); } finally { errors.mockRestore(); } @@ -6920,14 +6921,18 @@ describe("queue processors", () => { const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); expect(fanned.map((job) => job.deliveryId)).toContain("regate-repair:owner/agent-repo#2"); + // #orb-retry-storm (#3998): repair_attempt is now recorded at EXECUTION time (inside regatePullRequest, + // after rate-limit admission), not at dispatch time -- a deferred/dropped fan-out no longer counts against + // the cap. The sweep only dispatches the per-PR job above; it must actually run for the attempt to land. + await processJob(env, fanned.find((job) => job.deliveryId === "regate-repair:owner/agent-repo#2")!); const attemptsAfterFirst = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") .bind("agent.sweep.regate.repair_attempt", targetKey) .first<{ n: number }>(); expect(attemptsAfterFirst?.n).toBe(1); - // Manually push this SHA over the cap, then run the sweep twice more -- the exhausted event must be recorded - // only once even though the PR is (re-)evaluated on every tick. - for (let i = 0; i < 2; i += 1) { + // Manually push this SHA over the REGATE_REPAIR_MAX_ATTEMPTS_PER_SHA=5 cap (#3998), then run the sweep twice + // more -- the exhausted event must be recorded only once even though the PR is (re-)evaluated on every tick. + for (let i = 0; i < 4; i += 1) { await repositoriesModule.recordAuditEvent(env, { eventType: "agent.sweep.regate.repair_attempt", actor: "gittensory",