Skip to content

projector(reconcile): the prs stream reads the issues endpoint, so reconcile can never learn draft/merged/merged_at — first-sight PRs are stored draft=0/merged=0 forever #4528

Closed

Description

@dezren39

Summary

The projector's reconcile poller can never learn a PR's draft / merged / merged_at — and for a PR it discovers first, it stores the wrong values permanently.

Both item streams in pm-stack/projector/src/reconcile.ts read the issues endpoint:

  • reconcile.ts:47-49 — the issues and prs streams share one path: `repos/${repo}/issues?${q}&filter=all`; the prs stream only filters the same list to objects with a pull_request link (:55-57).

Objects from that endpoint are issue-shaped for PRs: a pull_request link (whose nested object carries merged_at) but no top-level draft / merged / merged_at — the same shape the repo's own test documents for webhook issue objects (pm-stack/projector/test/projector.test.ts, the #4158 describe block: "it has a pull_request link but NO merged / merged_at / draft").

Since the #4158 fix (merged today as #4516), fromIssueOrPr (projector.ts:52-58) emits null for those three fields on issue-shaped payloads, and upsertItem (store.ts) preserves stored values for null. That is correct for webhooks. For reconcile it means:

  1. First sight materializes false defaults. A brand-new row INSERTs with COALESCE(?,0) (store.ts, upsertItem insert step), so a draft PR reconcile discovers is stored draft=0 (ready for review) and a merged PR is stored merged=0, merged_at=null (closed-unmerged).
  2. Reconcile can never heal it. Every later poll re-reads the same issue-shaped objects, emits null again, and the store preserves the wrong stored values. Only a later pull_request webhook with a newer updated_at can correct the row — exactly the delivery reconcile exists to not depend on ("poll GitHub … so the projection self-heals beyond webhook delivery", reconcile.ts:1-4).
  3. The truth is already in the same run, and is discarded. The reviews stream fetches repos/${repo}/pulls?per_page=100&state=all (reconcile.ts:128) — pulls-shaped objects that DO carry draft / merged / merged_at — and uses them only to enumerate PR numbers for review fetching. No item row is upserted from them.

Evidence (verified this run, laptop probe)

Probe: ~/git/fleet/probe-2026-11-02-laneB-reconcileprs/ on the laptop — verbatim copies of pm-stack/projector/src/{reconcile,projector,store,github}.ts + schema.sql at main 4c42670d0 (md5-matched against the primary clone), driven through the real reconcileRepos with a fake GitHub fetch:

Stored rows after reconcile run 1 and run 2 (identical):

PROBE run1 #7 draft/merged/merged_at: 0 0 null
PROBE run1 #8 draft/merged/merged_at: 0 0 null
PROBE run2 #7 draft/merged/merged_at: 0 0 null
PROBE run2 #8 draft/merged/merged_at: 0 0 null

Control: the repo's own reconcile.test.ts + projector.test.ts pass in the same probe directory (0 fail).

Why it matters

pmp_items.draft/merged/merged_at is what the aggregator/dashboard reads as fact about PR state. Any PR whose pull_request webhook was missed, predates the projector, or lands while webhooks are down is projected by reconcile with merge fields that are silently wrong — a merged PR reads as closed-unmerged and a draft reads as ready — and no amount of re-polling fixes it. This is the reconcile-side counterpart of #4158: that issue fixed issue-shaped payloads resetting known values; this one is reconcile never being able to learn them, because its item source is an endpoint that does not carry them.

Distinct from: #4158 (webhook reset, fixed), #4302/#4303 (the application contract and its writer-inventory audit — this is a concrete writer defect that inventory should disposition), #4210 (item deletion path).

Acceptance criteria

  • Reconcile sources PR-shaped data for the prs stream (e.g. upsert items from the /pulls list response it already fetches, or fetch pulls-shaped objects for the prs stream), so a reconcile-discovered PR is stored with its real draft / merged / merged_at.
  • A PR first projected by reconcile as a draft reads draft=1; a merged PR reads merged=1 with its merged_at; a second reconcile pass over unchanged data changes nothing.
  • A row previously materialized with the false defaults is healed by a later reconcile pass (the pulls-shaped upsert carries a non-null carrier signal, so the projector(items): an issues/issue_comment event for a merged PR resets merged to 0 and clears merged_at — issue-shaped payloads carry no merge fields #4158 preservation logic does not pin the wrong values).
  • Regression tests in pm-stack/projector/test/reconcile.test.ts (fake-GitHub style, like the existing streams tests) cover draft-first-sight, merged-first-sight, and heal-after-webhook-gap; the existing projector suite stays green.
  • Issues stream behavior is unchanged (plain issues still project from /issues).

Activity

  1. 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)
    queue/queuedHas a queued task in the Supabase agent queue
    effort/sSmall: expected under 2 hours of work
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    on Nov 2, 2026
  2. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  4. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked-up

  5. 43 remaining items

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

    @dezren39
    MemberAuthor

    agent-note:v1 — state: parked (task 6483, OpenChamber scheduled lane)

  8. 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 3, 2026
  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  10. added 2 commits that reference this issue on Nov 3, 2026
  11. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (task 6483, lane code-d)

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

Metadata

Metadata

Assignees

Labels

app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/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