diff --git a/server/src/__tests__/heartbeat-process-recovery.test.ts b/server/src/__tests__/heartbeat-process-recovery.test.ts index b2f1c703d0..c958b27641 100644 --- a/server/src/__tests__/heartbeat-process-recovery.test.ts +++ b/server/src/__tests__/heartbeat-process-recovery.test.ts @@ -1325,6 +1325,53 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { expect(agent).toEqual({ status: "running", errorReason: null }); }); + it("does not queue immediate recovery when the failed run's issue is hidden", async () => { + mockAdapterExecute.mockResolvedValueOnce({ + exitCode: 1, + signal: null, + timedOut: false, + errorMessage: null, + provider: "test", + model: "test-model", + }); + + const { runId, issueId } = await seedQueuedIssueRunFixture(); + await db + .update(issues) + .set({ hiddenAt: new Date("2026-03-19T00:05:00.000Z") }) + .where(eq(issues.id, issueId)); + const heartbeat = heartbeatService(db); + + await heartbeat.resumeQueuedRuns(); + await waitForRunToSettle(heartbeat, runId); + await heartbeat.waitForRunExecutionDrain(runId); + + const run = await heartbeat.getRun(runId); + const recoveryRuns = await db + .select({ id: heartbeatRuns.id }) + .from(heartbeatRuns) + .where(eq(heartbeatRuns.retryOfRunId, runId)); + + expect(run).toMatchObject({ status: "failed" }); + expect(recoveryRuns).toHaveLength(0); + }); + + it("leaves hidden issues out of stranded-issue reconciliation", async () => { + const { issueId } = await seedStrandedIssueFixture({ + status: "in_progress", + runStatus: "failed", + }); + await db + .update(issues) + .set({ hiddenAt: new Date("2026-03-19T00:05:00.000Z") }) + .where(eq(issues.id, issueId)); + + const result = await heartbeatService(db).reconcileStrandedAssignedIssues(); + + expect(result.issueIds).not.toContain(issueId); + expect(result.continuationRequeued).toBe(0); + }); + it("keeps a local run active when the recorded pid is still alive", async () => { const child = spawnAliveProcess(); childProcesses.add(child); @@ -6028,7 +6075,9 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { expect(result.continuationRequeued).toBe(0); expect(result.escalated).toBe(1); expect(result.skipped).toBe(0); - expect(result.issueIds).toEqual([blocked.issueId, unblocked.issueId]); + expect([...result.issueIds].sort()).toEqual( + [blocked.issueId, unblocked.issueId].sort(), + ); const blockedWakeups = await db .select() diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index faa8b46f68..af753d2cf5 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -22814,6 +22814,7 @@ export function heartbeatService( const issueNeedsImmediateRecovery = (issue.status === "todo" || issue.status === "in_progress") && !issue.assigneeUserId && + !issue.hiddenAt && issue.assigneeAgentId === run.agentId && (run.status === "failed" || run.status === "timed_out" || diff --git a/server/src/services/recovery/service.ts b/server/src/services/recovery/service.ts index 2d8cbb5d86..8d7655d6d6 100644 --- a/server/src/services/recovery/service.ts +++ b/server/src/services/recovery/service.ts @@ -3451,6 +3451,7 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) eq(issues.status, "in_review"), ), opts?.issueCreatedAtGte ? gte(issues.createdAt, opts.issueCreatedAtGte) : undefined, + isNull(issues.hiddenAt), ), );