fix(heartbeat): defer dead-run finalize until retries settle

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-31 23:30:38 +00:00
1 parent 276e0d83c6
commit 44a36400ad
2 files changed
+65 -1

No files matched your search

@@ -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",
+1 -1
View File
@@ -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);
}