diff --git a/server/src/__tests__/heartbeat-process-recovery.test.ts b/server/src/__tests__/heartbeat-process-recovery.test.ts index b9564bbe8b0..45842b428fb 100644 --- a/server/src/__tests__/heartbeat-process-recovery.test.ts +++ b/server/src/__tests__/heartbeat-process-recovery.test.ts @@ -2151,12 +2151,10 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, issueId)); expect(comments).toHaveLength(1); - expect(comments[0]?.body).toContain("retried dispatch"); - expect(comments[0]?.body).toContain("Nested recovery: suppressed"); - expect(comments[0]?.body).toContain("already a `stranded_issue_recovery` issue"); - expect(comments[0]?.body).toContain(`Original source issue: [${issuePrefix}-2]`); + expect(comments[0]?.body).toContain("stopped automatic stranded-work recovery"); + expect(comments[0]?.body).toContain("recovery issues do not create nested `stranded_issue_recovery` issues"); + expect(comments[0]?.body).toContain(`Recovery issue: [${recoveryIssues[0]?.identifier}]`); expect(comments[0]?.body).toContain("Next action:"); - expect(comments[0]?.body).not.toContain("Recovery issue: ["); }); it("assigns open unassigned blockers back to their creator agent", async () => { diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 29cdda9e1c5..9dbce604cdd 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -8363,8 +8363,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return { kind: "released" as const }; } + if (issue.originKind === RECOVERY_ORIGIN_KINDS.strandedIssueRecovery) { + return { + kind: "blocked_recovery_in_place" as const, + issue, + previousStatus: issue.status, + }; + } + const shouldBlockImmediately = - issue.originKind === RECOVERY_ORIGIN_KINDS.strandedIssueRecovery || !recoveryAgentInvokable || !recoveryAgent || didAutomaticRecoveryFail(run, issue.status === "todo" ? "assignment_recovery" : "issue_continuation_needed"); @@ -8464,6 +8471,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return; } + if (promotionResult?.kind === "blocked_recovery_in_place") { + await recovery.escalateStrandedRecoveryIssueInPlace({ + issue: promotionResult.issue, + previousStatus: promotionResult.previousStatus as "todo" | "in_progress", + latestRun: run, + }); + return; + } + const promotedRun = promotionResult?.run ?? null; if (!promotedRun) return; diff --git a/server/src/services/recovery/service.ts b/server/src/services/recovery/service.ts index 28ff8ecaada..3aee7d2b3f1 100644 --- a/server/src/services/recovery/service.ts +++ b/server/src/services/recovery/service.ts @@ -2797,6 +2797,7 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup }) return { buildRunOutputSilence, + escalateStrandedRecoveryIssueInPlace, escalateStrandedAssignedIssue, recordWatchdogDecision, scanSilentActiveRuns,