From 00535b9cb2ccdac155d1db638e5df62e70c07bed Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:44:44 -0700 Subject: [PATCH] fix(review): tighten the regate-repair attempt cap to 2 and surface it to Sentry Lower REGATE_REPAIR_MAX_ATTEMPTS_PER_SHA from 3 to 2 (#3747 follow-up) -- one fewer wasted attempt per stuck SHA before the sweep falls back to ordinary staleness-gated cadence. Also add a matching structured console.error at the moment a SHA exhausts its repair budget, so it flows through the existing forwardStructuredLogToSentry pipeline instead of being visible only via a direct audit_events query (level:"error" is deliberate -- the line's own existence IS the anomaly signal, same convention as selfhost_ai_provider_ failed / selfhost_ai_providers_exhausted). --- src/queue/processors.ts | 17 ++++++++++++++++- test/unit/queue.test.ts | 40 +++++++++++++++++++++++++--------------- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index ef680f25b4..b4c37b944c 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -1368,7 +1368,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 = 3; +const REGATE_REPAIR_MAX_ATTEMPTS_PER_SHA = 2; const REGATE_REPAIR_ATTEMPT_LOOKBACK_MS = 24 * 60 * 60 * 1000; function regateRepairTargetKey(repoFullName: string, prNumber: number, headSha: string): string { @@ -1435,6 +1435,21 @@ async function surfaceRepairPriorityPullNumbers( detail: `re-gate repair exhausted after ${attempts} attempt(s) for the same head SHA; falling back to ordinary staleness cadence`, metadata: { repoFullName, prNumber: pr.number, headSha: pr.headSha, attempts }, }); + // level:"error" is deliberate, not a code failure: this line only fires once the cap above already + // stopped the wasteful repair loop, so its OWN existence is the operator-visible signal (via the + // structured log → Sentry forwarder, forwardStructuredLogToSentry) that a PR kept failing repair for the + // same head SHA — the same "surface an anomaly at error level" convention selfhost_ai_provider_failed / + // selfhost_ai_providers_exhausted already use in src/selfhost/ai.ts. + console.error( + JSON.stringify({ + level: "error", + event: "regate_repair_exhausted", + repo: repoFullName, + pullNumber: pr.number, + headSha: pr.headSha, + attempts, + }), + ); }), ); return [...priorityPullNumbers]; diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index f3b00f04cc..409a4d65de 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -6284,22 +6284,32 @@ describe("queue processors", () => { }); } vi.setSystemTime(new Date("2026-05-28T02:00:00.000Z")); + const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); - await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); - - // No longer treated as priority repair -- either not fanned at all, or fanned as an ordinary "regate-sweep:" - // candidate, but never re-dispatched as "regate-repair:" once the same SHA has exhausted its attempt budget. - const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); - expect(fanned.every((job) => job.deliveryId !== "regate-repair:owner/agent-repo#1")).toBe(true); - const exhausted = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") - .bind("agent.sweep.regate.repair_exhausted", targetKey) - .first<{ n: number }>(); - expect(exhausted?.n).toBe(1); - // No further repair-attempt event was recorded for the exhausted SHA this tick. - 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); + try { + await processJob(env, { type: "agent-regate-sweep", requestedBy: "test", repoFullName: "owner/agent-repo" }); + + // No longer treated as priority repair -- either not fanned at all, or fanned as an ordinary "regate-sweep:" + // candidate, but never re-dispatched as "regate-repair:" once the same SHA has exhausted its attempt budget. + const fanned = sent.filter((job): job is Extract => job.type === "agent-regate-pr"); + expect(fanned.every((job) => job.deliveryId !== "regate-repair:owner/agent-repo#1")).toBe(true); + const exhausted = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?") + .bind("agent.sweep.regate.repair_exhausted", targetKey) + .first<{ n: number }>(); + expect(exhausted?.n).toBe(1); + // No further repair-attempt event was recorded for the exhausted SHA this tick. + 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); + // 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 }); + } finally { + errors.mockRestore(); + } }); it("REGRESSION (#orb-retry-storm): a repair dispatch under the attempt cap records a repair_attempt audit event, and a second sweep tick does not duplicate the repair_exhausted event once already flagged", async () => {