From 9b4976b5329f6f04a5a43219087a0abded6b7a88 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 15 Jul 2026 20:47:29 +0000 Subject: [PATCH] fix(server): dedupe recovery liveness findings Co-Authored-By: Paperclip --- .../heartbeat-process-recovery.test.ts | 2 +- server/src/__tests__/issue-liveness.test.ts | 20 +++++++++++++++++++ server/src/services/heartbeat.ts | 7 +++++-- .../services/recovery/issue-graph-liveness.ts | 2 +- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/server/src/__tests__/heartbeat-process-recovery.test.ts b/server/src/__tests__/heartbeat-process-recovery.test.ts index 40db0e77501..78ca629263e 100644 --- a/server/src/__tests__/heartbeat-process-recovery.test.ts +++ b/server/src/__tests__/heartbeat-process-recovery.test.ts @@ -1389,7 +1389,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { }); await db .update(issues) - .set({ monitorNextCheckAt: new Date("2026-03-19T00:00:00.000Z") }) + .set({ monitorNextCheckAt: new Date("2099-03-19T00:00:00.000Z") }) .where(and(eq(issues.id, issueId), eq(issues.companyId, companyId))); const heartbeat = heartbeatService(db); diff --git a/server/src/__tests__/issue-liveness.test.ts b/server/src/__tests__/issue-liveness.test.ts index 780dc5c9de5..22de86cf8fe 100644 --- a/server/src/__tests__/issue-liveness.test.ts +++ b/server/src/__tests__/issue-liveness.test.ts @@ -297,6 +297,26 @@ describe("issue graph liveness classifier", () => { }); }); + it("prefers the blocker finding for an in-review source with a cancelled blocker", () => { + const findings = classifyIssueGraphLiveness({ + issues: [ + issue({ status: "in_review" }), + issue({ + id: blockerId, + identifier: "PAP-1704", + title: "Cancelled unblock work", + status: "cancelled", + assigneeAgentId: "blocker-agent", + }), + ], + relations: blocks, + agents: [agent(), manager, agent({ id: "blocker-agent", name: "Cancelled owner" })], + }); + + expect(findings).toHaveLength(1); + expect(findings[0]?.state).toBe("blocked_by_cancelled_issue"); + }); + it("detects blocker assignees under terminated org ancestors as uninvokable", () => { const findings = classifyIssueGraphLiveness({ issues: [ diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 92da28bdd6c..a284d3a1450 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -10993,10 +10993,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const runContext = parseObject(run.contextSnapshot); const monitorIssueId = readNonEmptyString(runContext.issueId); + const monitorNextCheckAt = monitorIssueId + ? monitorNextCheckAtByIssue.get(`${run.companyId}:${monitorIssueId}`) + : undefined; const monitorDispatchLostWithoutFutureWake = readNonEmptyString(runContext.wakeReason) === "issue_monitor_due" && - Boolean(monitorIssueId) && - monitorNextCheckAtByIssue.get(`${run.companyId}:${monitorIssueId}`) === null; + monitorNextCheckAt !== undefined && + (!monitorNextCheckAt || monitorNextCheckAt.getTime() <= now.getTime()); const shouldRetry = (run.processLossRetryCount ?? 0) < 1 && ( (tracksLocalChild && (!!run.processPid || !!run.processGroupId)) || monitorDispatchLostWithoutFutureWake diff --git a/server/src/services/recovery/issue-graph-liveness.ts b/server/src/services/recovery/issue-graph-liveness.ts index 7cd8d14d7ad..11b2542a71b 100644 --- a/server/src/services/recovery/issue-graph-liveness.ts +++ b/server/src/services/recovery/issue-graph-liveness.ts @@ -608,7 +608,7 @@ export function classifyIssueGraphLiveness(input: IssueGraphLivenessInput): Issu if (chainFinding) findings.push(chainFinding); } - if (issue.status === "in_review" && !unresolvedBlockers.has(issue.id)) { + if (issue.status === "in_review" && !shouldInspectBlockedChain && !unresolvedBlockers.has(issue.id)) { const review = reviewFinding(issue, issue, [issue]); if (review) findings.push(review); }