You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(backfill): #4529's freshness gate reads a clock unrelated jobs keep bumping — the open-data crawl has been skipped since 2026-07-09 and recent_merged_pull_requests is dead #10193
recent_merged_pull_requests has had no new rows since 2026-07-09T23:26Z. The writer was never removed, never flag-gated, and never throws — the entire 4-segment open-data backfill fan-out (labels, open_issues, open_pull_requests, recent_merged_pull_requests) has been skipped on every 30-minute cron tick since that timestamp, because the freshness gate added in #4529 reads a clock that unrelated, high-frequency jobs keep rewriting.
Three of the four segments hide the damage (their tables have webhook or sweep writers that keep them current). recent_merged_pull_requests is the only one whose sole writer is the backfill, so it is the only one that visibly froze.
repo_sync_state for all three: status = success/partial, last_completed_at = minutes ago.
The remaining 15 repos in the table are is_installed = 0 and froze separately on 2026-07-13 when the cron fan-out (which filters on isInstalled) stopped selecting them. That part is expected, not a bug.
Root cause
Commit a6fb46ab — fix(backfill): scheduled per-repo backfill respects freshness/error-backoff (#4529), closing #4497 — landed 2026-07-09T23:27:09Z. The last open-data crawl completed 2026-07-09T23:26:01Z, sixty-eight seconds earlier. Nothing has run since.
That commit added syncFreshnessSkipReason to enqueueRepositoryOpenDataBackfill (src/github/backfill.ts:338, called at :485), which is the function the real scheduled cron routes through. It skips the fan-out when:
The problem is repo_sync_state.lastCompletedAt. It is a repo-wide clock written by refreshRepoSyncStateFromSegments (src/github/backfill.ts:1984), which is called at the end of every segment write path — including two that run far more often than every six hours and have nothing to do with open-data:
refreshOpenPullRequestsForScheduledSweep (src/queue/processors.ts:1094) calls backfillRepositorySegment("open_pull_requests", force: true) on the ~2-minute re-gate sweep cadence. That call ends in refreshRepoSyncStateFromSegments (backfill.ts:589).
On completion it enqueues backfill-pr-details (backfill.ts:587), and backfillOpenPullRequestDetails ends in the same call (backfill.ts:724).
So lastCompletedAt is refreshed every few minutes, permanently inside the 6-hour window, and the gate #4529 introduced never opens. labels, open_issues, and recent_merged_pull_requests are never dispatched again. open_pull_requests survives only because it has its own independent refresher that bypasses the gate with force: true.
The skip is also silent: enqueueRepositoryOpenDataBackfill returns the reason as a warnings[] string that its cron caller (src/queue/job-dispatch.ts:202) discards. Nothing is logged, counted, or audited — which is why three weeks of a dead crawl produced no signal anywhere.
This is the same class as #377 (closed): a freshness rollup that quietly swallowed the merged-history crawl. #377 fixed the rollup's inputs; this is the rollup's consumers reading a clock too coarse to mean what they need.
What degraded
listRecentMergedPullRequests (200-row cap, mergedAt desc) has been returning a window that ends 2026-07-09 for three weeks. Consumers:
src/queue/copycat-detection.ts:99 — the copycat containment engine wired into the live gate (feat(review): wire the copycat containment engine into the gate #5999). Its candidate set is merged PRs; nothing merged after 07-09 can be matched against, so recent copy-of-a-recent-merge cases are invisible to it.
src/review/repo-culture-profile.ts:238,310 — worse than stale. The cache-invalidation signal is countRecentMergedPullRequests; with the count frozen the profile never regenerates, so it is pinned to a three-week-old snapshot by design of the invalidation check.
src/review/maintainer-recap-wire.ts:201 — the maintainer recap reports merged-PR activity that stops on 07-09.
src/services/{issue-quality,maintainer-lane,maintainer-noise,repo-outcome-patterns,contributor-issue-draft,agent-orchestrator}.ts — every one of these derives stats/history from the frozen window.
src/api/routes.ts (7 call sites) and src/mcp/server.ts (6 call sites) — public API and MCP surfaces serving a three-week-old picture as current.
Separately: listContributorRecentMergedPullRequests (src/db/repositories.ts:5411) has zero production callers — only test/unit/data-spine.test.ts references it. It is dead code independent of this bug.
Is the table superseded by pull_requests?
No. pull_requests only holds PRs the review pipeline has actually observed; recent_merged_pull_requests holds backfilled merged history, including PRs that predate installation and repos the pipeline never processed:
repo
recent_merged_pull_requests
pull_requests merged
of which merged ≤ 07-09
JSONbored/loopover
2185
3655
1327
JSONbored/metagraphed
1767
2703
910
JSONbored/awesome-claude
1193
979
327
we-promise/sure
755
0
0
gittensor-vanguard/vanguarstew
347
0
0
(7 more uninstalled repos)
3–237 each
0
0
Nine of twelve repos have zero merged rows in pull_requests. For the three installed repos the pre-07-09 history in recent_merged_pull_requests is ~2.5x what pull_requests retains. 5,383 of 6,927 rows also carry hydrated changed_files_json, which is what the copycat engine matches on.
Restore the writer. Do not drop the table.
Proposed fix
Gate the open-data fan-out on an open-data-specific freshness anchor instead of the repo-wide lastCompletedAt — the per-segment timestamps already on repo_sync_state (labelsSyncedAt, issuesSyncedAt, pullRequestsSyncedAt, mergedPullRequestsSyncedAt), taking the oldest of them. This preserves exactly what fix(backfill): scheduled per-repo backfill ignores freshness/error-backoff, re-syncs every repo every 30 min forever #4497 asked for (a repo that genuinely completed an open-data crawl still backs off for 6h) while making a segment that has not run in three weeks read as stale. The error-backoff branch keeps using status/lastCompletedAt unchanged.
Make the skip observable: emit a counter/log on the skip path in enqueueRepositoryOpenDataBackfill so a permanently-skipped repo is visible instead of silent.
Delete listContributorRecentMergedPullRequests and its test (dead code).
Backfilling the missed 2026-07-09 → now window needs no special work: the segment crawls /pulls?state=closed&sort=updated&direction=desc from page 1 and upserts, so the first unblocked run picks the gap up.
Summary
recent_merged_pull_requestshas had no new rows since 2026-07-09T23:26Z. The writer was never removed, never flag-gated, and never throws — the entire 4-segment open-data backfill fan-out (labels,open_issues,open_pull_requests,recent_merged_pull_requests) has been skipped on every 30-minute cron tick since that timestamp, because the freshness gate added in #4529 reads a clock that unrelated, high-frequency jobs keep rewriting.Three of the four segments hide the damage (their tables have webhook or sweep writers that keep them current).
recent_merged_pull_requestsis the only one whose sole writer is the backfill, so it is the only one that visibly froze.Evidence (edge-nl-01 Postgres, 2026-07-31)
repo_sync_segments, the three installed repos:open_pull_requestspull_request_files/pull_request_reviews/check_summarieslabelsopen_issuesrecent_merged_pull_requestsrepo_sync_statefor all three:status= success/partial,last_completed_at= minutes ago.The remaining 15 repos in the table are
is_installed = 0and froze separately on 2026-07-13 when the cron fan-out (which filters onisInstalled) stopped selecting them. That part is expected, not a bug.Root cause
Commit
a6fb46ab—fix(backfill): scheduled per-repo backfill respects freshness/error-backoff (#4529), closing #4497 — landed 2026-07-09T23:27:09Z. The last open-data crawl completed 2026-07-09T23:26:01Z, sixty-eight seconds earlier. Nothing has run since.That commit added
syncFreshnessSkipReasontoenqueueRepositoryOpenDataBackfill(src/github/backfill.ts:338, called at:485), which is the function the real scheduled cron routes through. It skips the fan-out when:The problem is
repo_sync_state.lastCompletedAt. It is a repo-wide clock written byrefreshRepoSyncStateFromSegments(src/github/backfill.ts:1984), which is called at the end of every segment write path — including two that run far more often than every six hours and have nothing to do with open-data:refreshOpenPullRequestsForScheduledSweep(src/queue/processors.ts:1094) callsbackfillRepositorySegment("open_pull_requests", force: true)on the ~2-minute re-gate sweep cadence. That call ends inrefreshRepoSyncStateFromSegments(backfill.ts:589).backfill-pr-details(backfill.ts:587), andbackfillOpenPullRequestDetailsends in the same call (backfill.ts:724).So
lastCompletedAtis refreshed every few minutes, permanently inside the 6-hour window, and the gate #4529 introduced never opens.labels,open_issues, andrecent_merged_pull_requestsare never dispatched again.open_pull_requestssurvives only because it has its own independent refresher that bypasses the gate withforce: true.The skip is also silent:
enqueueRepositoryOpenDataBackfillreturns the reason as awarnings[]string that its cron caller (src/queue/job-dispatch.ts:202) discards. Nothing is logged, counted, or audited — which is why three weeks of a dead crawl produced no signal anywhere.This is the same class as #377 (closed): a freshness rollup that quietly swallowed the merged-history crawl. #377 fixed the rollup's inputs; this is the rollup's consumers reading a clock too coarse to mean what they need.
What degraded
listRecentMergedPullRequests(200-row cap,mergedAtdesc) has been returning a window that ends 2026-07-09 for three weeks. Consumers:src/queue/copycat-detection.ts:99— the copycat containment engine wired into the live gate (feat(review): wire the copycat containment engine into the gate #5999). Its candidate set is merged PRs; nothing merged after 07-09 can be matched against, so recent copy-of-a-recent-merge cases are invisible to it.src/review/repo-culture-profile.ts:238,310— worse than stale. The cache-invalidation signal iscountRecentMergedPullRequests; with the count frozen the profile never regenerates, so it is pinned to a three-week-old snapshot by design of the invalidation check.src/review/maintainer-recap-wire.ts:201— the maintainer recap reports merged-PR activity that stops on 07-09.src/services/{issue-quality,maintainer-lane,maintainer-noise,repo-outcome-patterns,contributor-issue-draft,agent-orchestrator}.ts— every one of these derives stats/history from the frozen window.src/queue/signal-snapshot.ts:114,src/queue/processors.ts:6401,16495.src/api/routes.ts(7 call sites) andsrc/mcp/server.ts(6 call sites) — public API and MCP surfaces serving a three-week-old picture as current.Separately:
listContributorRecentMergedPullRequests(src/db/repositories.ts:5411) has zero production callers — onlytest/unit/data-spine.test.tsreferences it. It is dead code independent of this bug.Is the table superseded by
pull_requests?No.
pull_requestsonly holds PRs the review pipeline has actually observed;recent_merged_pull_requestsholds backfilled merged history, including PRs that predate installation and repos the pipeline never processed:recent_merged_pull_requestspull_requestsmergedNine of twelve repos have zero merged rows in
pull_requests. For the three installed repos the pre-07-09 history inrecent_merged_pull_requestsis ~2.5x whatpull_requestsretains. 5,383 of 6,927 rows also carry hydratedchanged_files_json, which is what the copycat engine matches on.Restore the writer. Do not drop the table.
Proposed fix
lastCompletedAt— the per-segment timestamps already onrepo_sync_state(labelsSyncedAt,issuesSyncedAt,pullRequestsSyncedAt,mergedPullRequestsSyncedAt), taking the oldest of them. This preserves exactly what fix(backfill): scheduled per-repo backfill ignores freshness/error-backoff, re-syncs every repo every 30 min forever #4497 asked for (a repo that genuinely completed an open-data crawl still backs off for 6h) while making a segment that has not run in three weeks read as stale. The error-backoff branch keeps usingstatus/lastCompletedAtunchanged.enqueueRepositoryOpenDataBackfillso a permanently-skipped repo is visible instead of silent.listContributorRecentMergedPullRequestsand its test (dead code).Backfilling the missed 2026-07-09 → now window needs no special work: the segment crawls
/pulls?state=closed&sort=updated&direction=descfrom page 1 and upserts, so the first unblocked run picks the gap up.