diff --git a/server/src/__tests__/heartbeat-process-recovery.test.ts b/server/src/__tests__/heartbeat-process-recovery.test.ts index f90e890b119..e3469dc7988 100644 --- a/server/src/__tests__/heartbeat-process-recovery.test.ts +++ b/server/src/__tests__/heartbeat-process-recovery.test.ts @@ -1954,6 +1954,70 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { expect(lease?.releasedAt).toBeTruthy(); }); + it("does not synthesize a finalize barrier when the dead run is retried", async () => { + const { companyId, runId, issueId } = await seedRunFixture({ + agentStatus: "idle", + processPid: 999_999_999, + }); + const projectId = randomUUID(); + const executionWorkspaceId = randomUUID(); + + await db.insert(projects).values({ + id: projectId, + companyId, + name: "Retried dead-run project", + status: "in_progress", + }); + await db.insert(executionWorkspaces).values({ + id: executionWorkspaceId, + companyId, + projectId, + mode: "shared_workspace", + strategyType: "local_path", + name: "Retried dead-run workspace", + status: "active", + providerType: "local_fs", + }); + await db + .update(issues) + .set({ + projectId, + status: "done", + executionWorkspaceId, + completedAt: new Date("2026-03-19T00:01:00.000Z"), + }) + .where(eq(issues.id, issueId)); + await db.insert(workspaceOperations).values({ + companyId, + executionWorkspaceId, + heartbeatRunId: runId, + issueId, + phase: "adapter_execute", + status: "succeeded", + metadata: { qaFixture: "retried-dead-run-finalize" }, + startedAt: new Date("2026-03-19T00:00:30.000Z"), + finishedAt: new Date("2026-03-19T00:00:45.000Z"), + }); + + await heartbeatService(db).reapOrphanedRuns(); + + const retry = await db + .select({ id: heartbeatRuns.id }) + .from(heartbeatRuns) + .where(eq(heartbeatRuns.retryOfRunId, runId)) + .then((rows) => rows[0] ?? null); + expect(retry?.id).toBeTruthy(); + + const finalizeRows = await db + .select({ id: workspaceOperations.id }) + .from(workspaceOperations) + .where(and( + eq(workspaceOperations.heartbeatRunId, runId), + eq(workspaceOperations.phase, "workspace_finalize"), + )); + expect(finalizeRows).toEqual([]); + }); + it("synthesizes one finalize barrier for a dead done blocker and wakes its dependent", async () => { const { companyId, agentId, runId, issueId: blockerIssueId } = await seedRunFixture({ agentStatus: "idle", diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 4392d59b560..48643bd1a6d 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -12439,7 +12439,6 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) if (!finalizedRun) finalizedRun = await getRun(run.id); if (!finalizedRun) continue; finalizedRun = await classifyAndPersistRunLiveness(finalizedRun, parseObject(finalizedRun.resultJson)) ?? finalizedRun; - await reconcileSyntheticWorkspaceFinalizeForDeadRun(finalizedRun); await releaseEnvironmentLeasesForRun({ runId: finalizedRun.id, companyId: finalizedRun.companyId, @@ -12460,6 +12459,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) } if (!retriedRun) { + await reconcileSyntheticWorkspaceFinalizeForDeadRun(finalizedRun); await releaseIssueExecutionAndPromote(finalizedRun); }