fix(issues): ignore cancelled child blockers in attention (#7577)
Docker / build-and-push (push) Failing after 3m33s
Refresh Lockfile / refresh (push) Successful in 10m42s
Release / verify_canary (push) Failing after 16m21s
Release / verify_stable (push) Has been skipped
Release / publish_canary (push) Has been skipped
Release / preview_stable (push) Has been skipped
Release / publish_stable (push) Has been skipped

Fixes #7578

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - Blocked issue health is surfaced through blockerAttention so
operators can see whether blocked work has a live path.
> - The blockerAttention graph uses both explicit blockedBy edges and
direct child issue edges.
> - Done children were already ignored, but cancelled direct children
still appeared as unresolved blockers.
> - Explicit cancelled dependencies should remain visible as dependency
problems, but terminal direct children should not inflate a parent
blocker count.
> - This pull request narrows child-edge traversal to non-terminal
children and adds a regression test for the observed case.
> - The benefit is that cancelled child issues no longer make blocked
parents look like they have extra unresolved blocker attention.

## What Changed

- Added terminal child status filtering for blockerAttention
parent-child traversal so cancelled direct children are ignored with
done children.
- Added a server regression test where a blocked parent has active
explicit blockers plus a cancelled direct child; the cancelled child no
longer increases unresolved counts or becomes the sample blocker.

## Verification

- `perl -e 'alarm shift; exec @ARGV' 300 pnpm exec vitest run
server/src/__tests__/issue-blocker-attention.test.ts` -> 19 tests
passed.

## Risks

- Low risk: the change only affects implicit direct-child
blockerAttention edges.
- Explicit blockedBy edges to cancelled issues are intentionally
unchanged and remain represented as attention-required dependency
problems.

## Model Used

- OpenAI GPT-5, Codex coding agent in tool-enabled CLI environment;
reasoning and code execution used for repository inspection, patching,
git operations, and targeted test verification.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots (not applicable; no UI change)
- [x] I have updated relevant documentation to reflect my changes (not
applicable; bugfix covered by regression test)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
This commit is contained in:
Svetlana Zolotenkova
2026-06-21 10:29:44 +03:00
committed by GitHub
parent d1e6662ed8
commit 9ac24317e6
2 changed files with 45 additions and 1 deletions
@@ -255,6 +255,49 @@ describeEmbeddedPostgres("issue blocker attention", () => {
});
});
it("ignores cancelled direct children when counting unresolved blocker attention", async () => {
const { companyId, agentId } = await createCompany("PBD");
const parentId = await insertIssue({ companyId, identifier: "PBD-1", title: "Parent", status: "blocked" });
const activeBlockerOneId = await insertIssue({
companyId,
identifier: "PBD-2",
title: "Running dependency one",
status: "todo",
assigneeAgentId: agentId,
});
const activeBlockerTwoId = await insertIssue({
companyId,
identifier: "PBD-3",
title: "Running dependency two",
status: "todo",
assigneeAgentId: agentId,
});
await insertIssue({
companyId,
identifier: "PBD-4",
title: "Cancelled child",
status: "cancelled",
parentId,
assigneeAgentId: agentId,
});
await block({ companyId, blockerIssueId: activeBlockerOneId, blockedIssueId: parentId });
await block({ companyId, blockerIssueId: activeBlockerTwoId, blockedIssueId: parentId });
await activeRun({ companyId, agentId, issueId: activeBlockerOneId });
await activeRun({ companyId, agentId, issueId: activeBlockerTwoId });
const parent = (await svc.list(companyId, { status: "blocked" })).find((issue) => issue.id === parentId);
expect(parent?.blockerAttention).toMatchObject({
state: "covered",
reason: "active_dependency",
unresolvedBlockerCount: 2,
coveredBlockerCount: 2,
stalledBlockerCount: 0,
attentionBlockerCount: 0,
});
expect(parent?.blockerAttention?.sampleBlockerIdentifier).not.toBe("PBD-4");
});
it("covers recursive blocker chains when the downstream leaf has active work", async () => {
const { companyId, agentId } = await createCompany("PBR");
const parentId = await insertIssue({ companyId, identifier: "PBR-1", title: "Parent", status: "blocked" });
+2 -1
View File
@@ -1204,6 +1204,7 @@ const BLOCKER_ATTENTION_ACTIVE_WAKE_STATUSES = ["queued", "deferred_issue_execut
const BLOCKER_ATTENTION_PENDING_INTERACTION_STATUSES = ["pending"];
const BLOCKER_ATTENTION_PENDING_APPROVAL_STATUSES = ["pending", "revision_requested"];
const BLOCKER_ATTENTION_OPEN_RECOVERY_ORIGIN_KIND = "harness_liveness_escalation";
const BLOCKER_ATTENTION_CHILD_TERMINAL_STATUSES = ["done", "cancelled"];
const PRODUCTIVITY_REVIEW_ORIGIN_KIND = "issue_productivity_review";
const PRODUCTIVITY_REVIEW_TERMINAL_STATUSES = ["done", "cancelled"];
const PRODUCTIVITY_REVIEW_ACTIVITY_ACTIONS = [
@@ -1636,7 +1637,7 @@ async function listIssueBlockerAttentionMap(
and(
eq(issues.companyId, companyId),
inArray(issues.parentId, chunk),
ne(issues.status, "done"),
notInArray(issues.status, BLOCKER_ATTENTION_CHILD_TERMINAL_STATUSES),
),
);
const [explicitBlockerRows, childRows] = await Promise.all([