Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 13 additions & 8 deletions test/unit/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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<string, unknown>;
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();
}
Expand All @@ -6920,14 +6921,18 @@ describe("queue processors", () => {

const fanned = sent.filter((job): job is Extract<import("../../src/types").JobMessage, { type: "agent-regate-pr" }> => 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",
Expand Down
Loading