diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 873fadd17d3..c12a78314df 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -3487,6 +3487,12 @@ describeEmbeddedPostgres("issueService blockers and dependency wake readiness", const updated = await svc.update(issueId, { status: "blocked" }); expect(updated?.status).toBe("blocked"); + const [listed] = await svc.list(companyId, { status: "blocked" }); + expect(listed?.blockerAttention).toMatchObject({ + state: "covered", + reason: null, + unresolvedBlockerCount: 0, + }); }); it("auto-routes a blocked issue to todo when its final blocker edge is deleted", async () => { diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 9df0ff41848..301f20a6c70 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -1649,7 +1649,7 @@ type IssueBlockerAttentionInputNode = IssueBlockerAttentionNode, "id" | "companyId" | "parentId" | "identifier" | "title" | "status" | "assigneeAgentId" | "assigneeUserId" > - & { executionRunId?: string | null; description?: string | null }; + & { executionRunId?: string | null; description?: string | null; originKind?: string | null }; type IssueBlockerAttentionEdge = { issueId: string; @@ -2045,6 +2045,31 @@ async function listIssueBlockerAttentionMap( } if (roots.length === 0) return attentionMap; + const rootIds = roots.map((root) => root.id); + const recoveryOwnedIssueIds = new Set(); + for (const chunk of chunkList(rootIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) { + const recoveryRows = await dbOrTx + .select({ + sourceIssueId: issueRecoveryActions.sourceIssueId, + recoveryIssueId: issueRecoveryActions.recoveryIssueId, + }) + .from(issueRecoveryActions) + .where( + and( + eq(issueRecoveryActions.companyId, companyId), + inArray(issueRecoveryActions.status, ["active", "escalated"]), + or( + inArray(issueRecoveryActions.sourceIssueId, chunk), + inArray(issueRecoveryActions.recoveryIssueId, chunk), + ), + ), + ); + for (const row of recoveryRows) { + if (row.sourceIssueId) recoveryOwnedIssueIds.add(row.sourceIssueId); + if (row.recoveryIssueId) recoveryOwnedIssueIds.add(row.recoveryIssueId); + } + } + const nodesById = new Map(); const edgesByIssueId = new Map(); for (const root of roots) nodesById.set(root.id, { ...root }); @@ -2360,7 +2385,11 @@ async function listIssueBlockerAttentionMap( for (const root of roots) { const topLevelEdges = (edgesByIssueId.get(root.id) ?? []).filter((edge) => nodesById.get(edge.blockerIssueId)?.status !== "done"); if (topLevelEdges.length === 0) { - if (externalWaitFromDescription(root.description ?? null)) { + if ( + externalWaitFromDescription(root.description ?? null) || + recoveryOwnedIssueIds.has(root.id) || + (isStrandedIssueRecoveryOriginKind(root.originKind) && Boolean(root.assigneeAgentId)) + ) { attentionMap.set(root.id, createIssueBlockerAttention({ state: "covered" })); continue; }