Skip to content

pm-stack(aggregator): epic-graph rankedBlocked traversal is exponential on diamond blockedBy graphs — and contributingItems lists duplicates #6371

Open

Description

@dezren39

pm-stack(aggregator): epic-graph rankedBlocked traversal is exponential on diamond blockedBy graphs — and contributingItems lists duplicates

Evidence

pm-stack/aggregator/src/epic-graph.ts:25-86 defines two recursive helpers,
transitiveOpenEpicDescendants and contributingDownstreamItems. They are
functionally identical (both return the set of open Epic items in the transitive
blockedBy ancestry of a ref), and both pass new Set(visited) — a fresh copy
of the visited set — to each recursive call (epic-graph.ts:45 and :76).
A visited set copied per branch only prunes cycles along the current path; it
never prunes a subgraph reached by a second path. On a diamond-shaped
blockedBy graph (two items in one layer both blocked by the same two items
in the next layer — the natural shape when parallel work shares blockers),
the number of root-to-leaf paths doubles per layer, and every path is walked
in full. epicGraph() (epic-graph.ts:350-353) then runs both helpers for
every blocker of every open member, over the whole pool (byRef is
the uncapped pool; the 120-node display cap at epic-graph.ts:237 does not
bound this traversal).

render.ts:6567 calls epicGraph() once per epic section on every project
page render (epicGraphSections → epicGraphOneHtml), so one diamond
anywhere in the pool's blockedBy graph, reachable from any member's blocker,
stalls the whole page render.

Verified this run on the laptop against origin/main @359996097f, probe
~/git/fleet/probe-epic-diamond (bun, real epicGraph from the tree; pool
items are otherwise minimal — one member blocked by both layer-0 nodes, each
layer's nodes blocked by both nodes of the next layer, last layer blocked by
one open Epic):

diamond layers pool nodes epicGraph time
4 11 1.3 ms
8 19 0.9 ms
12 27 5.8 ms
16 35 84.7 ms
20 43 1757.1 ms

Growth is ~2× per added layer (12→16 layers: ~15×; 16→20: ~21×), so ~24
layers ≈ tens of seconds and ~28 layers ≈ many minutes — from a 43-node
pool, far below any pool-size limit. Control: a plain 40-node chain through
the same code takes 0.3 ms, so the cost is the diamond path multiplicity,
not pool size or depth alone.

Second, simpler defect in the same computation: rankedBlocked's
contributingItems is built by flat-mapping the per-blocker contributing
sets with no deduplication (epic-graph.ts:396, r.sortedBlockers.flatMap(b => b.contributingItems)),
while downstreamCount counts the blockers' sets distinctly per blocker.
In the probe above (member with two blockers that share the same downstream
epic), the output was:

{"ref":"developing-today/code#2","downstreamCount":1,
 "contributingItems":["developing-today/code#9000","developing-today/code#9000"]}

render.ts:6592-6594 renders that list verbatim, so the project page lists
the same "Contributing downstream epics" link twice.

Why it matters

  • The aggregator is a live dashboard service; its project page render path
    can be stalled for seconds-to-minutes by ordinary issue-graph data (two
    shared blockers per layer is a normal shape for epic work), with no cap,
    timeout, or memoization anywhere in the traversal. Nothing about the input
    is exotic or attacker-crafted — it is the fleet's own native blockedBy
    relations (impl(aggregator+collector) part 1 of 2: Project pool graph edges contract (split of #1122) #2399 contract).
  • The duplicate contributingItems entries are a visible correctness bug on
    the same page, and the duplicated helper (two identical traversals per
    blocker) doubles the exponential cost for no benefit.

Acceptance criteria

  • The transitive blockedBy traversal visits each reachable pool item at
    most once per query (shared visited set / memoization, not a per-branch
    copy), so a 20-layer diamond pool of ~43 nodes scores in milliseconds,
    comparable to the chain control — with a regression test (named oracle:
    the same diamond fixture, asserting both the result set and a
    node-visit bound or a generous wall-clock bound).
  • Only one traversal implementation remains (the duplicate helper is
    removed or one calls the other), with identical results to today's
    sets on the existing epic-graph tests.
  • rankedBlocked[].contributingItems contains each ref at most once
    (distinct, in a deterministic order), pinned by a test with two
    blockers sharing a downstream epic.
  • Existing epic-graph test suite passes unchanged in behaviour for
    chains, cycles (a blockedBy cycle still terminates and returns the
    correct set), and out-of-pool blockers.

Filed by new-issue-creator-code-b (Lane B), verified this run; probe and
timings above are reproducible from ~/git/fleet/probe-epic-diamond.

Activity

  1. developing-today commented on Nov 5, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 5, 2026
  3. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (new-issue-creator-code-b)
    • Session / ID: cron new-issue-creator-code-b, run 2026-11-05T20:28Z
    • Found via: filed this run
    • Task: Filed after verifying on the laptop @359996097f (probe ~/git/fleet/probe-epic-diamond, bun, real epicGraph): the rankedBlocked transitive blockedBy traversal is exponential on diamond graphs (43-node pool, 20 layers → 1757ms; chain control 0.3ms) and contributingItems lists duplicate refs.
    • Expectation: effort m; done = memoized/single traversal with diamond regression test, deduped contributingItems, existing epic-graph suite green.
    • Links: probe ~/git/fleet/probe-epic-diamond (laptop); body evidence in the issue.
  4. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 5, 2026
  5. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (triage)

  6. 101 remaining items

  7. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    on Nov 6, 2026
  8. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    Handed off to opencode claimant per execution-lane assignment (task 10756, issue #6371).

  9. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  10. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 6, 2026
  11. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 · repo-worker-code (OpenChamber scheduled lane 335a5d1f) · picked up task 10756 · implementing shared-visited traversal + contributingItems dedupe in pm-stack/aggregator/src/epic-graph.ts. Next: PR.

  12. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 · repo-worker-code (OpenChamber scheduled lane 335a5d1f) · task 10756 done · PR opened: #6551 (shared-visited traversal, single helper, deduped contributingItems, diamond/shared-downstream/cycle regression tests). CI pending; the issue closes on merge.

  13. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · bound

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/pm-stackpm-stack (Pad/Grist) in developing-today/codekind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus:in-reviewtask/triageQueue packet kind: verify, reproduce, and route this item

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions