From 6f7121372dac2e26af83d786e67e656b1f1f40f7 Mon Sep 17 00:00:00 2001 From: cwbcheng Date: Fri, 7 Aug 2026 00:37:25 +0800 Subject: [PATCH] fix: stop queued jobs from failing in-progress babysit runs --- server/babysitter.test.ts | 80 +++++++++++++++++++++++++++++++++++++++ server/babysitter.ts | 54 ++++++++++++++++---------- 2 files changed, 114 insertions(+), 20 deletions(-) diff --git a/server/babysitter.test.ts b/server/babysitter.test.ts index 7edd886..44e223e 100644 --- a/server/babysitter.test.ts +++ b/server/babysitter.test.ts @@ -6025,6 +6025,86 @@ test("babysitPR logs metadata when skipping a duplicate in-progress run", async assert.equal(typeof skipLog?.metadata?.activeAgeMs, "number"); }); +test("runQueuedBabysitPR does not mark a live in-progress run failed when a queued job fires before replay context is persisted", async () => { + const storage = new MemStorage(); + await storage.updateConfig({ autoUpdateDocs: false }); + const pr = await storage.addPR({ + number: 107, + title: "Live run PR", + repo: "alex-morgan-o/lolodex", + branch: "feature/live", + author: "octocat", + url: "https://github.com/alex-morgan-o/lolodex/pull/107", + status: "watching", + feedbackItems: [], + accepted: 0, + rejected: 0, + flagged: 0, + testsPassed: null, + lintPassed: null, + lastChecked: null, + }); + let releaseFetch!: () => void; + let markFetchStarted!: () => void; + const fetchStarted = new Promise((resolve) => { + markFetchStarted = resolve; + }); + const fetchReleased = new Promise((resolve) => { + releaseFetch = resolve; + }); + + const babysitter = new PRBabysitter( + storage, + makeWatcherGitHubService({ + fetchPullSummary: async () => { + markFetchStarted(); + await fetchReleased; + return makePullSummary(pr); + }, + listFailingStatuses: async () => [], + }), + { + resolveAgent: async () => "codex", + ciPollIntervalMs: 0, + evaluateFixNecessityWithAgent: async () => ({ + needsFix: false, + reason: "No code change needed", + }), + applyFixesWithAgent: async () => ({ code: 0, stdout: "", stderr: "" }), + runCommand: makeGitRunCommand(), + }, + ); + + const firstRun = babysitter.babysitPR(pr.id, "codex"); + await fetchStarted; + + const runsBefore = await storage.listAgentRuns({ prId: pr.id }); + assert.equal(runsBefore.length, 1); + assert.equal(runsBefore[0]?.status, "running"); + assert.equal(runsBefore[0]?.prompt, null); + + // A watcher-triggered babysit_pr job firing while the run is still live must + // not mark the run failed just because its replay context is not persisted yet. + await babysitter.runQueuedBabysitPR(pr.id, "codex"); + + const runsAfter = await storage.listAgentRuns({ prId: pr.id }); + assert.equal(runsAfter.length, 1); + assert.equal(runsAfter[0]?.status, "running"); + assert.equal(runsAfter[0]?.lastError, null); + + const logs = await storage.getLogs(pr.id); + assert.ok(logs.some((log) => + log.phase === "run" + && log.message.includes("another run is already in progress") + )); + + releaseFetch(); + await firstRun; + + const run = await storage.getAgentRun(runsBefore[0]!.id); + assert.equal(run?.status, "completed"); +}); + test("runQueuedBabysitPR archives merged PRs instead of running or rescheduling work", async () => { const storage = new MemStorage(); await storage.updateConfig({ autoUpdateDocs: false }); diff --git a/server/babysitter.ts b/server/babysitter.ts index f0cd197..0f05406 100644 --- a/server/babysitter.ts +++ b/server/babysitter.ts @@ -2167,12 +2167,45 @@ export class PRBabysitter { } } + private async isRunAlreadyInProgress( + prId: string, + requestedPreferredAgent: CodingAgent, + ): Promise { + const activeRun = this.inProgress.get(prId); + if (!activeRun) { + return false; + } + const pr = await this.storage.getPR(prId); + if (pr) { + const activeStartedAtMs = Date.parse(activeRun.startedAt); + const activeAgeMs = Number.isNaN(activeStartedAtMs) + ? null + : Math.max(0, this.now().getTime() - activeStartedAtMs); + await this.storage.addLog(pr.id, "warn", "PR work skipped because another run is already in progress", { + phase: "run", + metadata: { + reason: "in_progress", + activeRunId: activeRun.runId, + activePreferredAgent: activeRun.preferredAgent, + activeStartedAt: activeRun.startedAt, + activeAgeMs, + requestedPreferredAgent, + }, + }); + } + return true; + } + async runQueuedBabysitPR( prId: string, preferredAgent: CodingAgent, agentSettings?: AgentRuntimeSettings, jobAttemptCount?: number, ): Promise { + if (await this.isRunAlreadyInProgress(prId, preferredAgent)) { + return; + } + const interruptedRun = (await this.storage.listAgentRuns({ status: "running", prId })) .slice() .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0]; @@ -3137,26 +3170,7 @@ export class PRBabysitter { return; } - const activeRun = this.inProgress.get(prId); - if (activeRun) { - const pr = await this.storage.getPR(prId); - if (pr) { - const activeStartedAtMs = Date.parse(activeRun.startedAt); - const activeAgeMs = Number.isNaN(activeStartedAtMs) - ? null - : Math.max(0, this.now().getTime() - activeStartedAtMs); - await this.storage.addLog(pr.id, "warn", "PR work skipped because another run is already in progress", { - phase: "run", - metadata: { - reason: "in_progress", - activeRunId: activeRun.runId, - activePreferredAgent: activeRun.preferredAgent, - activeStartedAt: activeRun.startedAt, - activeAgeMs, - requestedPreferredAgent: preferredAgent, - }, - }); - } + if (await this.isRunAlreadyInProgress(prId, preferredAgent)) { return; }