Summary
refreshRepoSyncStateFromSegments computes a repo's overall sync status from a required segment list that omits the recent_merged_pull_requests segment, even though it loads and reads that segment:
// src/github/backfill.ts:1344-1362
const [previous, totals, metadata, labels, openIssues, openPullRequests, recentMerged, files, reviews, checks] = await Promise.all([ ... ]);
const required = [metadata, labels, openIssues, openPullRequests, files, reviews, checks].filter(Boolean) as RepoSyncSegmentRecord[]; // <- recentMerged missing
const waiting = required.some((segment) => segment.status === "waiting_rate_limit" || segment.status === "rate_limited");
const running = required.some((segment) => segment.status === "running" || segment.status === "refreshing");
const errored = required.some((segment) => segment.status === "error");
const incomplete = required.some((segment) => segment.status !== "complete" && segment.status !== "not_modified");
const status: RepoSyncStateRecord["status"] = waiting ? "rate_limited" : errored ? "error" : running ? "running" : incomplete ? "partial" : "success";
const warnings = [...new Set(required.flatMap((segment) => segment.warnings))];
const completedAt = running || waiting ? previous?.lastCompletedAt : nowIso();
recentMerged is destructured (:1351) and used for recentMergedPullRequestsCount (:1373) and mergedPullRequestsSyncedAt (:1377), but it is not in required, so its running / waiting_rate_limit / error / non-terminal status — and its warnings — are ignored when deriving the repo-level status.
Why this is wrong
recent_merged_pull_requests is a first-class tracked backfill segment everywhere else. The parallel monolithic backfill path's summarizeSegments derives partial from all segments (recent-merged included):
// src/github/backfill.ts (summarizeSegments — monolithic path)
partial: segments.some((segment) => segment.status !== "complete" && segment.status !== "not_modified") || warnings.length > 0,
So the two backfill code paths now disagree on whether the merged-history crawl's completeness affects sync status. The segment-rollup path is the outlier that drops it.
Failure mode
A registered repo is backfilled via the segment pipeline; all four base segments plus PR-detail segments are queued, and refreshRepoSyncStateFromSegments runs after each segment completes. Suppose open_issues, open_pull_requests, pull_request_files/reviews/checks, labels, and metadata all reach complete, but recent_merged_pull_requests is still running (or waiting_rate_limit, or error, or otherwise non-terminal — the merged-history crawl is the largest and most rate-limit-prone):
- Current:
required excludes recent-merged, so waiting/running/errored/incomplete are all false → status = "success", and completedAt = nowIso() → lastCompletedAt = now.
- Correct:
status should be rate_limited / error / running / partial, reflecting the unfinished merged-history crawl.
Then the freshness check skips the repo on the next run:
// src/github/backfill.ts:345-364
const freshSuccess = (syncState.status === "success" || syncState.status === "partial" || syncState.status === "capped") && Number.isFinite(ageMs) && ageMs < FRESH_SYNC_MS;
...
if (freshSuccess || recentError) {
return { ..., status: "skipped", ... }; // skips backfillRepository for FRESH_SYNC_MS (~6h)
}
So the repo is treated as freshly synced and skipped for the freshness window, leaving its recent-merged PR history silently stale/undercounted — and it also produces an internal inconsistency: status === "success" while mergedPullRequestsSyncedAt (set only when recent-merged is complete/sampled, :1377) remains stale.
Downstream impact
recentMergedPullRequestsCount and the merged-PR history feed buildRepoOutcomePatterns (merge-rate / outcome dimensions), buildCollisionReport (recent-merged collisions), and contributor merge-history. Marking an incomplete merged-history crawl as success and skipping its refresh leaves those signals computed on partial data — wrong merge-rate/closure-risk outcomes (compounding the recent-merged data issues already tracked in #312 and #352, but via a different mechanism: premature success + freshness-skip, not empty file lists).
Steps to reproduce
- Run the segment backfill for a repo where
recent_merged_pull_requests does not reach a terminal state (e.g. it returns running/waiting_rate_limit/error) while the other segments complete.
- Read the repo sync state (
getRepoSyncState).
- Observe
status: "success" with a fresh lastCompletedAt, despite the merged-history segment being unfinished — and backfillRegisteredRepositories then reports the repo as skipped for the freshness window.
Expected behavior
The repo-level sync status and warnings reflect the recent_merged_pull_requests segment's state: a running / waiting_rate_limit / error / non-terminal merged-history crawl yields running / rate_limited / error / partial (not success), so the freshness-skip does not prematurely mark it fresh.
Actual behavior
The merged-history segment is excluded from the status/warnings derivation, so an unfinished crawl is reported as success with a fresh lastCompletedAt, and the repo is then skipped as fresh.
Suggested fix
- Include
recent_merged_pull_requests in the segment set that feeds waiting / running / errored / incomplete and warnings. Adding it to required alone is not correct: recent-merged legitimately ends in the "sampled" terminal state (it uses progressiveHistory: true; status = "sampled" at backfill.ts:1164, and :1377 already treats sampled as synced), which the incomplete = status !== "complete" && status !== "not_modified" predicate would wrongly flag as incomplete, making the repo perpetually partial.
- So introduce sampled-aware terminal handling — e.g. a small helper/predicate that treats
complete, not_modified, and (for the history segment) sampled as terminal — and apply it across incomplete (and the non-terminal checks) for the full segment set including recent-merged. This aligns the segment-rollup path with summarizeSegments and keeps status consistent with mergedPullRequestsSyncedAt.
- Add fail-on-revert coverage: when every base/detail segment is
complete but recent_merged_pull_requests is running (and again error / waiting_rate_limit), the rolled-up status is running / error / rate_limited (not success) and lastCompletedAt is not advanced; and when recent-merged is sampled with the rest complete, status is success (sampled is terminal, not partial).
Summary
refreshRepoSyncStateFromSegmentscomputes a repo's overall syncstatusfrom arequiredsegment list that omits therecent_merged_pull_requestssegment, even though it loads and reads that segment:recentMergedis destructured (:1351) and used forrecentMergedPullRequestsCount(:1373) andmergedPullRequestsSyncedAt(:1377), but it is not inrequired, so itsrunning/waiting_rate_limit/error/ non-terminal status — and its warnings — are ignored when deriving the repo-levelstatus.Why this is wrong
recent_merged_pull_requestsis a first-class tracked backfill segment everywhere else. The parallel monolithic backfill path'ssummarizeSegmentsderivespartialfrom all segments (recent-merged included):So the two backfill code paths now disagree on whether the merged-history crawl's completeness affects sync status. The segment-rollup path is the outlier that drops it.
Failure mode
A registered repo is backfilled via the segment pipeline; all four base segments plus PR-detail segments are queued, and
refreshRepoSyncStateFromSegmentsruns after each segment completes. Supposeopen_issues,open_pull_requests,pull_request_files/reviews/checks,labels, andmetadataall reachcomplete, butrecent_merged_pull_requestsis stillrunning(orwaiting_rate_limit, orerror, or otherwise non-terminal — the merged-history crawl is the largest and most rate-limit-prone):requiredexcludes recent-merged, sowaiting/running/errored/incompleteare allfalse→status = "success", andcompletedAt = nowIso()→lastCompletedAt = now.statusshould berate_limited/error/running/partial, reflecting the unfinished merged-history crawl.Then the freshness check skips the repo on the next run:
So the repo is treated as freshly synced and skipped for the freshness window, leaving its recent-merged PR history silently stale/undercounted — and it also produces an internal inconsistency:
status === "success"whilemergedPullRequestsSyncedAt(set only when recent-merged iscomplete/sampled,:1377) remains stale.Downstream impact
recentMergedPullRequestsCountand the merged-PR history feedbuildRepoOutcomePatterns(merge-rate / outcome dimensions),buildCollisionReport(recent-merged collisions), and contributor merge-history. Marking an incomplete merged-history crawl assuccessand skipping its refresh leaves those signals computed on partial data — wrong merge-rate/closure-risk outcomes (compounding the recent-merged data issues already tracked in #312 and #352, but via a different mechanism: prematuresuccess+ freshness-skip, not empty file lists).Steps to reproduce
recent_merged_pull_requestsdoes not reach a terminal state (e.g. it returnsrunning/waiting_rate_limit/error) while the other segments complete.getRepoSyncState).status: "success"with a freshlastCompletedAt, despite the merged-history segment being unfinished — andbackfillRegisteredRepositoriesthen reports the repo asskippedfor the freshness window.Expected behavior
The repo-level sync
statusandwarningsreflect therecent_merged_pull_requestssegment's state: arunning/waiting_rate_limit/error/ non-terminal merged-history crawl yieldsrunning/rate_limited/error/partial(notsuccess), so the freshness-skip does not prematurely mark it fresh.Actual behavior
The merged-history segment is excluded from the status/warnings derivation, so an unfinished crawl is reported as
successwith a freshlastCompletedAt, and the repo is then skipped as fresh.Suggested fix
recent_merged_pull_requestsin the segment set that feedswaiting/running/errored/incompleteandwarnings. Adding it torequiredalone is not correct: recent-merged legitimately ends in the"sampled"terminal state (it usesprogressiveHistory: true;status = "sampled"atbackfill.ts:1164, and:1377already treatssampledas synced), which theincomplete = status !== "complete" && status !== "not_modified"predicate would wrongly flag as incomplete, making the repo perpetuallypartial.complete,not_modified, and (for the history segment)sampledas terminal — and apply it acrossincomplete(and the non-terminal checks) for the full segment set including recent-merged. This aligns the segment-rollup path withsummarizeSegmentsand keepsstatusconsistent withmergedPullRequestsSyncedAt.completebutrecent_merged_pull_requestsisrunning(and againerror/waiting_rate_limit), the rolled-upstatusisrunning/error/rate_limited(notsuccess) andlastCompletedAtis not advanced; and when recent-merged issampledwith the restcomplete,statusissuccess(sampled is terminal, notpartial).