Harden graph liveness legacy retirement

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-05-16 21:06:32 -05:00
1 parent aa0d8b9a31
commit 06a7177d4d
2 files changed
+116 -21

No files matched your search

@@ -426,6 +426,83 @@ describeEmbeddedPostgres("heartbeat issue graph liveness escalation", () => {
expect(events.filter((event) => event.action === "issue.blockers.updated")).toHaveLength(0);
});
it("migrates an existing legacy liveness escalation to source-scoped recovery before removing its blocker", async () => {
await enableAutoRecovery();
const { companyId, managerId, blockedIssueId, blockerIssueId } = await seedBlockedChain();
const legacyRecoveryIssueId = randomUUID();
const incidentKey = [
"harness_liveness",
companyId,
blockedIssueId,
"blocked_by_unassigned_issue",
blockerIssueId,
].join(":");
const leafFingerprint = [
"harness_liveness_leaf",
companyId,
"blocked_by_unassigned_issue",
blockerIssueId,
].join(":");
await db.insert(issues).values({
id: legacyRecoveryIssueId,
companyId,
title: "Legacy liveness escalation",
status: "todo",
priority: "high",
assigneeAgentId: managerId,
issueNumber: 3,
identifier: `LEGACY-${legacyRecoveryIssueId.slice(0, 8)}`,
originKind: "harness_liveness_escalation",
originId: incidentKey,
originFingerprint: leafFingerprint,
});
await db.insert(issueRelations).values({
companyId,
issueId: legacyRecoveryIssueId,
relatedIssueId: blockedIssueId,
type: "blocks",
});
const heartbeat = heartbeatService(db);
const first = await heartbeat.reconcileIssueGraphLiveness();
const second = await heartbeat.reconcileIssueGraphLiveness();
expect(first.findings).toBe(1);
expect(first.sourceRecoveryActionsOpened).toBe(1);
expect(first.obsoleteRecoveriesRetired).toBe(1);
expect(first.obsoleteRecoveryBlockerRelationsRemoved).toBe(1);
expect(second.findings).toBe(0);
expect(second.sourceRecoveryActionsOpened).toBe(0);
expect(second.existingSourceRecoveryActions).toBe(0);
const legacyRecoveries = await db
.select()
.from(issues)
.where(and(eq(issues.companyId, companyId), eq(issues.originKind, "harness_liveness_escalation")));
expect(legacyRecoveries).toHaveLength(1);
expect(legacyRecoveries[0]?.id).toBe(legacyRecoveryIssueId);
expect(legacyRecoveries[0]?.status).toBe("cancelled");
const actions = await db
.select()
.from(issueRecoveryActions)
.where(and(eq(issueRecoveryActions.companyId, companyId), eq(issueRecoveryActions.sourceIssueId, blockerIssueId)));
expect(actions).toHaveLength(1);
expect(actions[0]).toMatchObject({
kind: "issue_graph_liveness",
cause: "issue_graph_liveness",
status: "active",
ownerAgentId: managerId,
fingerprint: leafFingerprint,
});
const blockers = await db
.select({ blockerIssueId: issueRelations.issueId })
.from(issueRelations)
.where(eq(issueRelations.relatedIssueId, blockedIssueId));
expect(blockers.map((row) => row.blockerIssueId).sort()).toEqual([blockerIssueId].sort());
});
it("skips budget-blocked direct owners and assigns recovery to the manager fallback", async () => {
await enableAutoRecovery();
const { companyId, managerId, coderId, blockedIssueId, blockerIssueId } = await seedBlockedChain();
+39 -21
View File
@@ -2380,21 +2380,7 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup })
const openRecoveryIssues = recoveryIssueRows.flatMap((row) => {
if (row.originKind === RECOVERY_ORIGIN_KINDS.issueGraphLivenessEscalation) {
const parsed = parseIssueGraphLivenessIncidentKey(row.originId);
if (!parsed || parsed.companyId !== row.companyId) return [];
if (parsed.state !== "blocked_by_assigned_backlog_issue") return [];
return [
{
companyId: row.companyId,
issueId: parsed.issueId,
status: row.status,
},
{
companyId: row.companyId,
issueId: parsed.leafIssueId,
status: row.status,
},
];
return [];
}
const issueId = readNonEmptyString(row.originId);
@@ -2574,6 +2560,32 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup })
return Boolean(contextRun || issueRun);
}
async function hasActiveIssueGraphLivenessActionForLegacyRecovery(recovery: typeof issues.$inferSelect) {
const parsed = parseLivenessIncidentKey(recovery.originId);
if (!parsed || parsed.companyId !== recovery.companyId) return false;
const expectedFingerprint = recovery.originFingerprint && recovery.originFingerprint !== "default"
? recovery.originFingerprint
: livenessRecoveryLeafKey(parsed.companyId, parsed.state, parsed.leafIssueId);
const row = await db
.select({ id: issueRecoveryActions.id })
.from(issueRecoveryActions)
.where(
and(
eq(issueRecoveryActions.companyId, recovery.companyId),
eq(issueRecoveryActions.sourceIssueId, parsed.leafIssueId),
eq(issueRecoveryActions.kind, "issue_graph_liveness"),
eq(issueRecoveryActions.cause, "issue_graph_liveness"),
eq(issueRecoveryActions.fingerprint, expectedFingerprint),
inArray(issueRecoveryActions.status, ["active", "escalated"]),
),
)
.limit(1)
.then((rows) => rows[0] ?? null);
return Boolean(row);
}
async function retireObsoleteLivenessRecoveryIssues(findings: IssueLivenessFinding[]) {
const currentIncidentKeys = new Set(findings.map((finding) => finding.incidentKey));
const currentLeafKeys = new Set(
@@ -2603,14 +2615,14 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup })
};
for (const recovery of openRecoveries) {
if (recovery.originId && currentIncidentKeys.has(recovery.originId)) continue;
const parsed = parseLivenessIncidentKey(recovery.originId);
if (!parsed) continue;
if (
currentLeafKeys.has(
livenessRecoveryLeafKey(parsed.companyId, parsed.state, parsed.leafIssueId),
)
) {
const hasCurrentIncidentFinding = Boolean(recovery.originId && currentIncidentKeys.has(recovery.originId));
const hasCurrentLeafFinding = currentLeafKeys.has(
livenessRecoveryLeafKey(parsed.companyId, parsed.state, parsed.leafIssueId),
);
const sourceScopedActionTookOver = await hasActiveIssueGraphLivenessActionForLegacyRecovery(recovery);
if ((hasCurrentIncidentFinding || hasCurrentLeafFinding) && !sourceScopedActionTookOver) {
continue;
}
if (await removeRecoveryBlockerFromSource(recovery)) {
@@ -3309,6 +3321,12 @@ export function recoveryService(db: Db, deps: { enqueueWakeup: RecoveryWakeup })
}
}
const postRecoveryCleanup = await retireObsoleteLivenessRecoveryIssues(findings);
result.obsoleteRecoveriesRetired += postRecoveryCleanup.retired;
result.obsoleteRecoveriesActiveSkipped += postRecoveryCleanup.activeSkipped;
result.obsoleteRecoveryBlockerRelationsRemoved += postRecoveryCleanup.blockerRelationsRemoved;
result.retiredRecoveryIssueIds.push(...postRecoveryCleanup.retiredIssueIds);
return result;
}