Skip to content

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

Description

@JSONbored

Summary

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.

Evidence (edge-nl-01 Postgres, 2026-07-31)

repo_sync_segments, the three installed repos:

segment status mode completed_at
open_pull_requests complete light 2026-07-31T11:46:11Z
pull_request_files / pull_request_reviews / check_summaries partial resume 2026-07-31T11:46:22Z
labels not_modified light 2026-07-09T23:25:15Z
open_issues complete light 2026-07-09T23:25:29Z
recent_merged_pull_requests sampled light 2026-07-09T23:26:01Z

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 a6fb46abfix(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:

status ∈ {success, partial, capped} && (now - repo_sync_state.lastCompletedAt) < FRESH_SYNC_MS  // 6h

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:

  1. 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).
  2. 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/queue/signal-snapshot.ts:114, src/queue/processors.ts:6401,16495.
  • 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

  1. 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.
  2. Make the skip observable: emit a counter/log on the skip path in enqueueRepositoryOpenDataBackfill so a permanently-skipped repo is visible instead of silent.
  3. 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.

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions