Skip to content

fix(selfhost): cap historical PR file hydration and cache by head SHA - #2527

Merged
JSONbored merged 3 commits into
mainfrom
fix/selfhost-scoped-file-hydration
Jul 2, 2026
Merged

fix(selfhost): cap historical PR file hydration and cache by head SHA#2527
JSONbored merged 3 commits into
mainfrom
fix/selfhost-scoped-file-hydration

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

  • Live VPS evidence: the shared GitHub App installation's REST bucket drained to single-digit remaining within ~2 hours, with /pulls/:n/files as the dominant route (1,272–1,672 calls per repo). A follow-up DB check found 475–544 distinct PR numbers with file calls but no matching pull_requests row, against actual open-PR counts of 5–37 per repo.
  • Root cause: hydrateMergedPullRequestFiles (historical merged-PR file backfill, src/github/backfill.ts) walks GitHub's live /pulls?state=closed history directly — its PR numbers never need a local pull_requests row, which is exactly why the missing-row evidence pointed here. It had no per-run cap and no rate-limit re-check beyond the once-per-segment gate at the top of backfillRepositorySegment, so a large un-hydrated backlog (a fresh/partial backfill) could fan out one /pulls/:n/files fetch per un-hydrated PR — up to SEGMENT_PAGE_BUDGET × 100 PRs — inside a single job execution, well past what the once-at-entry admission check could stop.
  • Advances chore(selfhost): beta-stable release readiness roadmap #1936 (self-host beta-stable rate-limit readiness). No dedicated issue existed for this specific mechanism, so filing directly with the live evidence above as justification.

What changed

  • Historical hydration is now capped and re-checked per page, not just once per segment: MERGED_PR_FILE_HYDRATION_BATCH_SIZE bounds how many not-yet-hydrated merged PRs get a files fetch per recent_merged_pull_requests page, and a new HISTORICAL_BACKFILL_RESERVED_HEADROOM floor (stricter than the existing maintenance floor) is checked before every page. A PR skipped for either reason stays a candidate on the next run — nothing is silently dropped, just deferred.
  • Durable repo+PR+headSha file snapshot: pull_request_detail_sync_state now tracks the head SHA files were last synced for (migration 0090). fetchAndStorePullRequestDetails skips the /pulls/:n/files fetch when the stored snapshot's head SHA still matches the PR's current head, and always refetches when it has moved — this benefits the live-review and open-PR-convergence paths, which previously refetched files unconditionally on every trigger regardless of whether the head had actually changed.
  • Closed-PR guard: refreshPullRequestDetails now skips a closed PR that already has complete stored telemetry, unless the caller explicitly forces a refresh. The manual "review-now" repair command now passes force: true so an explicit user re-check is never served stale data.
  • Webhook coalescing gap fixed: reopened triggers the same file-refresh path as opened/synchronize/ready_for_review (PR_PUBLIC_SURFACE_ACTIONS) but was missing from the coalescable action set in webhook-coalesce.ts, so a burst of reopen-adjacent deliveries for the same PR+head fanned out one job per delivery instead of collapsing into one.
  • Bounded observability: a new gittensory_github_pull_request_files_fetch_total{caller=...} metric (3 fixed values: backfill_open_pr_details, backfill_merged_history, live_review) attributes files-endpoint traffic by caller, so a future budget incident can be diagnosed without re-deriving it from raw route counts.

Correctness / staleness guarantees

  • Current open PR convergence (backfillOpenPullRequestDetails) and live webhook-triggered reviews are unaffected in scope — they still only ever touch PRs already present in pull_requests.
  • The head-SHA cache only gates the files fetch; reviews/checks continue to refresh on every call (out of scope for this rate-limit issue, and more volatile at a fixed head).
  • A merged PR's files remain treated as immutable (pre-existing #1941 design, unchanged) — the new cap only bounds when a not-yet-hydrated merged PR's files get fetched, never fetches a stale result.

Rate-limit impact

  • Worst case for a single recent_merged_pull_requests segment run drops from up to SEGMENT_PAGE_BUDGET[mode] × 100 (1,000 for full/resume) files fetches to SEGMENT_PAGE_BUDGET[mode] × MERGED_PR_FILE_HYDRATION_BATCH_SIZE[mode] (200 for full/resume, 20 for light), with an additional early yield once REST headroom drops below the new historical floor.
  • Redundant unconditional refetches on unchanged-head triggers (manifest-check and direct-webhook call sites, which previously had no head-SHA guard at all) are now skipped when cached.

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage locally (unsharded) — full suite green; new/changed lines in src/github/backfill.ts, src/db/repositories.ts, src/github/rate-limit.ts, src/github/webhook-coalesce.ts are fully line+branch covered per coverage/lcov.info (cross-checked against the diff hunks directly)
  • npm run test:workers
  • npm run build:mcp
  • npm run test:mcp-pack
  • npm run ui:openapi:check
  • npm run ui:lint
  • npm run ui:typecheck
  • npm run ui:build
  • npm audit --audit-level=moderate — 0 vulnerabilities
  • New tests cover: missing-PR-row guard, closed-PR guard (unforced skip / forced refetch / no-telemetry-yet still fetches), live-review fetch with no snapshot, snapshot reuse on unchanged head, fresh fetch on head change, historical-hydration budget defer, current-PR convergence still allowed under the same reduced budget, per-page hydration cap under healthy budget, webhook-coalescing "reopened" regression, and bounded metric caller labels

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests. — not applicable, no auth/session/CORS surface touched.
  • API/OpenAPI/MCP behavior is updated and tested where needed. — no API/OpenAPI/MCP surface changed.
  • UI changes use live API data... — not applicable, no UI changes.
  • Visible UI changes include a UI Evidence section. — not applicable, backend-only change.
  • Public docs/changelogs are updated. — not applicable; CHANGELOG.md is not edited in a normal PR.

Notes

  • No hardcoded repo names, installation IDs, PR numbers, or private operational assumptions — the fix is entirely config/data-driven (BackfillMode-keyed caps, admission-key-scoped rate observations) and applies uniformly to every self-hosted installation.

Live evidence showed the shared GitHub App installation's REST bucket draining
to single digits within ~2 hours, dominated by /pulls/:n/files calls for PR
numbers with no matching pull_requests row -- the historical merged-PR file
backfill (hydrateMergedPullRequestFiles) walks GitHub's live closed-PR history
directly and had no per-run cap or budget re-check beyond the once-per-segment
gate, so a large un-hydrated backlog could burn through hundreds of file
fetches in a single job execution before admission control caught up.

- Cap historical merged-PR file hydration to a small per-page batch and
  re-check GitHub REST headroom against a new, stricter
  HISTORICAL_BACKFILL_RESERVED_HEADROOM floor before every page (not just once
  at segment entry), so the least-urgent GitHub consumer yields earliest and
  never floods the shared bucket.
- Add a durable repo+PR+headSha file snapshot: pull_request_detail_sync_state
  now tracks the head SHA files were last synced for, so the live-review and
  open-PR-convergence paths skip a redundant /pulls/:n/files fetch when the
  PR's current head is unchanged, and always refetch when it moves.
- Add a closed-PR guard to refreshPullRequestDetails: a closed PR with
  complete stored telemetry is not refetched unless explicitly forced (the
  manual "review-now" repair command now forces past the cache).
- Fix a webhook-coalescing gap: "reopened" triggers the same file-refresh path
  as "opened"/"synchronize" but was missing from the coalescable action set,
  so a reopen-adjacent event burst fanned out one job per delivery instead of
  collapsing into one per PR+head.
- Add a bounded caller label (backfill_open_pr_details,
  backfill_merged_history, live_review) on the PR files fetch metric so future
  REST-budget incidents can be attributed by caller without re-deriving it
  from raw route counts.

Validation: npm run test:ci, npm run test:coverage (unsharded), npm audit
--audit-level=moderate all green; new/changed lines are fully covered per
coverage/lcov.info.
@dosubot dosubot Bot added the size:L label Jul 2, 2026
@loopover-orb

loopover-orb Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Warning

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏸️ Gittensory review result - manual review recommended

Review updated: 2026-07-02 08:08:02 UTC

10 files · 1 AI reviewer · no blockers · readiness 87/100 · CI pending · blocked

⏸️ Suggested Action - Manual Review

  • Touches a guarded path — held for manual review

Review summary
The change correctly adds a durable head-SHA marker for PR file snapshots, uses it to avoid repeat `/pulls/:n/files` calls when the PR head is unchanged, and separately caps low-priority historical merged-PR hydration behind a stricter rate-limit floor. The schema, repository mapping, migration, and main hot-path callers are wired together, and the added tests cover the important cache-hit, cache-miss, forced-refresh, historical deferral, and webhook coalescing cases. I do not see a reachable correctness break in the visible diff.

Nits — 6 non-blocking
  • nit: `src/github/backfill.ts:1807` duplicates the detail-sync-state write shape already used by `backfillOpenPullRequestDetails`; consider extracting a tiny helper so future cache-marker fields cannot drift between the two paths.
  • nit: `src/github/backfill.ts:1979` treats any stored `filesSyncedAt` plus matching `headSha` as authoritative; consider documenting or asserting that callers only write `filesSyncedAt` after a successful file sync, since the cache depends on that invariant.
  • nit: `migrations/0090_pull_request_detail_sync_head_sha.sql:1` is a straightforward nullable add-column migration, but confirm `0090` is the next free migration number before merge because the full migrations directory was not provided.
  • In `src/github/backfill.ts`, centralize the repeated `upsertPullRequestDetailSyncState` completion payload into a helper used by both open-PR backfill paths.
  • In `test/unit/backfill-file-hydration-scoping.test.ts`, add a small regression case where `headSha` matches but `filesSyncedAt` is absent, proving the cache does not skip the fetch without a completed file snapshot.
  • Touches a guarded path — held for manual review — A maintainer must review and merge this change.
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ✅ No-issue rationale PR body explains why no issue is linked.
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (size label size:L; no linked issue context).
Validation posture ⚠️ 12/25 Preflight needs author follow-up before maintainer review.
Contributor workload ✅ 10/10 Author activity: 65 registered-repo PR(s), 55 merged, 548 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 65 PR(s), 548 issue(s).
Gate result ⚠️ Not blocking Advisory; not blocking this PR.
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: Python, TypeScript, JavaScript, Ruby, Go, Kotlin, MDX, Shell
  • Official Gittensor activity: 65 PR(s), 548 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Address findings or add validation evidence.
  • No action.
  • Link the issue being solved, or explicitly explain why this is a no-issue PR.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.

  • Re-run Gittensory review

@loopover-orb loopover-orb Bot added gittensor gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. labels Jul 2, 2026
@JSONbored JSONbored self-assigned this Jul 2, 2026
JSONbored added 2 commits July 2, 2026 01:00
…ll path

backfillRegisteredRepositories -> backfillRepository (the /v1/internal/jobs/
backfill-registered-repos/run admin endpoint's synchronous path) called the
new cache-aware fetchAndStorePullRequestDetails but never wrote the resulting
head SHA back to pull_request_detail_sync_state, so the cache it reads from
was never populated by that path -- every run refetched files for PRs only
ever touched here, even with an unchanged head.

Mirror the write-back already done in backfillOpenPullRequestDetails and
refreshPullRequestDetails. Added a regression test that fails without the fix
(confirmed by reverting it locally) and passes with it.
…he relies on

upsertPullRequestDetailSyncState's partial-update behavior (an omitted field
leaves the column unchanged, relying on drizzle stripping undefined from the
SET clause) was implicit and undocumented. A future edit coalescing an
omitted field to null (e.g. `headSha: state.headSha ?? null`) would silently
stop the repo+PR+headSha file cache from ever hitting again, with no test
failure to catch it.

Document the contract at the function and add an invariant test pinning it
(plus the explicit-null-clears-it counterpart), confirmed to fail under the
exact regression shape described above.
@JSONbored
JSONbored merged commit 99681c3 into main Jul 2, 2026
7 checks passed
@JSONbored
JSONbored deleted the fix/selfhost-scoped-file-hydration branch July 2, 2026 08:12
@github-project-automation github-project-automation Bot moved this from Todo to Done in gittensory - v1 roadmap Jul 2, 2026
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.95%. Comparing base (8f2e0fb) to head (f8ef9e3).
⚠️ Report is 33 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #2527    +/-   ##
========================================
  Coverage   95.95%   95.95%            
========================================
  Files         226      228     +2     
  Lines       25387    25595   +208     
  Branches     9234     9315    +81     
========================================
+ Hits        24359    24560   +201     
- Misses        417      425     +8     
+ Partials      611      610     -1     
Files with missing lines Coverage Δ
src/db/repositories.ts 96.52% <100.00%> (+0.01%) ⬆️
src/db/schema.ts 69.46% <ø> (ø)
src/github/backfill.ts 96.76% <100.00%> (+0.27%) ⬆️
src/github/rate-limit.ts 100.00% <100.00%> (ø)
src/github/webhook-coalesce.ts 100.00% <ø> (ø)
src/queue/processors.ts 91.77% <100.00%> (+0.03%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

JSONbored added a commit that referenced this pull request Jul 2, 2026
… sync-state pattern

Closes #2537.

GET /pulls/{n} (PR state/mergeable_state) and GET /pulls/{n}/reviews were the
largest and third-largest GitHub REST contributors observed in a live
rate-limit trough, and neither was covered by the earlier PR-files cache
(#audit-rate-headroom / #2527). The bare-PR-state read was implemented as
four near-identical helpers with no caching or coalescing, called from
readiness checks, the maintenance planner, duplicate-sibling reconciliation,
and a gate-override command; reviews were explicitly left uncached ("more
volatile than files" -- true at a fixed head, but reviews only actually
change on a pull_request_review webhook).

Extends the existing pull_request_detail_sync_state table (migration 0093)
with prMergeableState/prState/prStateFetchedAt, and reuses the already-present
reviewsSyncedAt column as the review cache's freshness marker. PR-state is
event-invalidated on pull_request synchronize/closed/reopened (with a 5-minute
safety-net TTL so a missed webhook self-heals within one sweep tick); reviews
are invalidated on pull_request_review submitted/edited/dismissed rather than
on head-SHA change, since reviews are independent of the head.

The pre-merge verdict thread and its unified-comment mirror (the #4220
act-boundary) are deliberately left routed through the raw, uncached fetch --
they must always force-refetch live, since the stored/cached mergeable_state
lagging GitHub's async recompute is exactly what caused #4220. This is
verified by two dedicated regression tests plus direct mutation testing
(temporarily rerouting the act-boundary through the cache and confirming both
tests -- and no others in the suite -- catch it).
JSONbored added a commit that referenced this pull request Jul 2, 2026
… sync-state pattern

Closes #2537.

GET /pulls/{n} (PR state/mergeable_state) and GET /pulls/{n}/reviews were the
largest and third-largest GitHub REST contributors observed in a live
rate-limit trough, and neither was covered by the earlier PR-files cache
(#audit-rate-headroom / #2527). The bare-PR-state read was implemented as
four near-identical helpers with no caching or coalescing, called from
readiness checks, the maintenance planner, duplicate-sibling reconciliation,
and a gate-override command; reviews were explicitly left uncached ("more
volatile than files" -- true at a fixed head, but reviews only actually
change on a pull_request_review webhook).

Extends the existing pull_request_detail_sync_state table (migration 0093)
with prMergeableState/prState/prStateFetchedAt, and reuses the already-present
reviewsSyncedAt column as the review cache's freshness marker. PR-state is
event-invalidated on pull_request synchronize/closed/reopened (with a 5-minute
safety-net TTL so a missed webhook self-heals within one sweep tick); reviews
are invalidated on pull_request_review submitted/edited/dismissed rather than
on head-SHA change, since reviews are independent of the head.

The pre-merge verdict thread and its unified-comment mirror (the #4220
act-boundary) are deliberately left routed through the raw, uncached fetch --
they must always force-refetch live, since the stored/cached mergeable_state
lagging GitHub's async recompute is exactly what caused #4220. This is
verified by two dedicated regression tests plus direct mutation testing
(temporarily rerouting the act-boundary through the cache and confirming both
tests -- and no others in the suite -- catch it).
JSONbored added a commit that referenced this pull request Jul 2, 2026
… sync-state pattern

Closes #2537.

GET /pulls/{n} (PR state/mergeable_state) and GET /pulls/{n}/reviews were the
largest and third-largest GitHub REST contributors observed in a live
rate-limit trough, and neither was covered by the earlier PR-files cache
(#audit-rate-headroom / #2527). The bare-PR-state read was implemented as
four near-identical helpers with no caching or coalescing, called from
readiness checks, the maintenance planner, duplicate-sibling reconciliation,
and a gate-override command; reviews were explicitly left uncached ("more
volatile than files" -- true at a fixed head, but reviews only actually
change on a pull_request_review webhook).

Extends the existing pull_request_detail_sync_state table (migration 0093)
with prMergeableState/prState/prStateFetchedAt, and reuses the already-present
reviewsSyncedAt column as the review cache's freshness marker. PR-state is
event-invalidated on pull_request synchronize/closed/reopened (with a 5-minute
safety-net TTL so a missed webhook self-heals within one sweep tick); reviews
are invalidated on pull_request_review submitted/edited/dismissed rather than
on head-SHA change, since reviews are independent of the head.

The pre-merge verdict thread and its unified-comment mirror (the #4220
act-boundary) are deliberately left routed through the raw, uncached fetch --
they must always force-refetch live, since the stored/cached mergeable_state
lagging GitHub's async recompute is exactly what caused #4220. This is
verified by two dedicated regression tests plus direct mutation testing
(temporarily rerouting the act-boundary through the cache and confirming both
tests -- and no others in the suite -- catch it).
JSONbored added a commit that referenced this pull request Jul 2, 2026
… sync-state pattern

Closes #2537.

GET /pulls/{n} (PR state/mergeable_state) and GET /pulls/{n}/reviews were the
largest and third-largest GitHub REST contributors observed in a live
rate-limit trough, and neither was covered by the earlier PR-files cache
(#audit-rate-headroom / #2527). The bare-PR-state read was implemented as
four near-identical helpers with no caching or coalescing, called from
readiness checks, the maintenance planner, duplicate-sibling reconciliation,
and a gate-override command; reviews were explicitly left uncached ("more
volatile than files" -- true at a fixed head, but reviews only actually
change on a pull_request_review webhook).

Extends the existing pull_request_detail_sync_state table (migration 0093)
with prMergeableState/prState/prStateFetchedAt, and reuses the already-present
reviewsSyncedAt column as the review cache's freshness marker. PR-state is
event-invalidated on pull_request synchronize/closed/reopened (with a 5-minute
safety-net TTL so a missed webhook self-heals within one sweep tick); reviews
are invalidated on pull_request_review submitted/edited/dismissed rather than
on head-SHA change, since reviews are independent of the head.

The pre-merge verdict thread and its unified-comment mirror (the #4220
act-boundary) are deliberately left routed through the raw, uncached fetch --
they must always force-refetch live, since the stored/cached mergeable_state
lagging GitHub's async recompute is exactly what caused #4220. This is
verified by two dedicated regression tests plus direct mutation testing
(temporarily rerouting the act-boundary through the cache and confirming both
tests -- and no others in the suite -- catch it).
JSONbored added a commit that referenced this pull request Jul 2, 2026
* feat(github): cache PR reviews with webhook-based invalidation

The recently-merged head-SHA file cache (#2527) deliberately left reviews
uncached ("more volatile than files"), but reviews only actually change on
a pull_request_review webhook, not on every sweep tick -- unlike files,
reviews are independent of head SHA entirely.

fetchAndStorePullRequestDetails now skips the GET /pulls/{n}/reviews call
when a prior sync already covers every review-invalidating event: the new
reviews_invalidated_at column (bumped by a pull_request_review webhook
with action submitted/dismissed/edited) is compared against the existing
reviews_synced_at timestamp. A stored row whose last review sync itself
failed is excluded from the cache-hit path so a transient failure can't
poison the cache into never retrying.

Broadened the existing sync-state row lookup (previously gated on
`!forceFiles && headSha`, which would silently disable review caching
whenever headSha was momentarily unknown) since reviews never depend on
headSha or the files-only forceFiles flag.

Bare PR-state caching (state/mergeable_state/head SHA), the other half of
this issue, was deliberately NOT implemented after auditing every live-read
call site in the codebase: each one (the freshness guard in
agent-action-executor.ts, the draft-dodge-close and reopen-reclose
re-checks, the gate-override command, and the sweep-resync path) is a
documented act-boundary that intentionally requires a live, uncached read
immediately before a mutation or before publishing review output.
mergeable_state already has per-pass caching via the existing
LiveGithubFacts mechanism. Caching any of these would risk exactly the
regression the issue's own acceptance criteria forbids ("No regression in
any decision that depends on a live PR state"). Filing as a follow-up
rather than force-fitting a cache onto call sites that don't have safe
surface area for one.

* fix(github): stop forceFiles from bleeding into the reviews cache decision

Gate review finding on PR #2633: the sync-state row lookup was skipped
entirely whenever forceFiles && headSha (the manual "refresh files" path),
which zeroed out reviewsUpToDate too and forced an unrelated GET
/pulls/{n}/reviews on every manual files-only force, even when reviews
were already cache-current. forceFiles only ever means "force a files
refetch" -- reviews caching must be completely independent of it.

Now fetches the row unconditionally and applies forceFiles only to
filesUpToDate, never to reviewsUpToDate.

* fix(github): close the reviews-cache TOCTOU race and harden invalidation

Second round of gate findings on PR #2633:

1. fetchAndStorePullRequestDetails read existingState as a snapshot at
   the top of the call, but every caller then unconditionally stamped
   reviewsSyncedAt to "now" once the whole call finished. A
   pull_request_review webhook racing in after the snapshot read but
   before that final write would set reviewsInvalidatedAt to a moment the
   stamped reviewsSyncedAt would then read as already covering -- the
   cache could confirm itself fresh through an invalidation it never
   actually observed. Now returns the correct reviewsSyncedAt to the
   caller instead: captured BEFORE the fetch starts on a genuine success
   (conservative against races from that instant on), and left unchanged
   on a skip or a failed fetch. This also makes a stored reviewsSyncedAt
   trustworthy on its own, so the separate errorSummary string-matching
   safety net (fragile: it only ever reflects whichever of files/reviews/
   checks failed LAST in a given pass, since they run concurrently) is no
   longer needed and has been removed. reviewsUpToDate's comparison is
   also now a strict `>` rather than `>=`, so a millisecond-resolution
   timestamp tie between a sync and a racing invalidation fails toward
   "still needs a refetch" rather than silently trusting the cache.

2. markPullRequestReviewsInvalidated is the sole source of the
   invalidation signal, so a single transient D1 write failure would
   permanently lose that PR's "reviews changed" event. Added a bounded
   3-attempt retry -- still best-effort from the webhook handler's
   perspective (never blocks/retries the whole job), but absorbs a
   momentary blip in-process.

* fix(github): clean up legacy reviews_synced_at rows before trusting the cache

Third gate finding on PR #2633: reviews_synced_at has existed since
migration 0006, long before this PR gives it any cache-skip meaning --
every sync pass since then has stamped it unconditionally, including
passes whose review fetch itself failed. On deploy, reviewsUpToDate would
immediately trust every existing row's reviews_synced_at at face value,
silently skipping re-fetches for PRs whose last review sync actually
failed, until some later invalidating webhook happened to arrive.

Migration 0094 now resets reviews_synced_at to NULL for every row whose
last sync status was not 'complete' (status only reads 'complete' when
that pass recorded zero warnings across files/reviews/checks, which
reliably means reviews specifically succeeded -- any other status could
have been a review failure, so it's reset to force a guaranteed-safe
refetch). A genuinely 'complete' row is left untouched.

Adds a migration-effect test (applying the actual 0094 SQL against a
scratch table shaped like the pre-#2537 schema, seeded with rows exactly
as years of pre-#2537 code would have written them) proving the cleanup
targets the right rows and nothing else.
JSONbored added a commit that referenced this pull request Jul 2, 2026
Advances #2537. The head-SHA file cache (#2527) and the recently-merged
review cache (#2633) left one sibling route uncached: a bare GET /pulls/{n}
(state/mergeable_state/head SHA), implemented as four separate near-identical
helper functions with no caching or cross-call coalescing, called from many
independent sites within one review pass.

Adds a durable, webhook-invalidated cache for this read on
pull_request_detail_sync_state (prMergeableState/prState/prStateFetchedAt),
capped at a 5-minute PR_STATE_CACHE_MAX_AGE_MS safety net so a missed
invalidation self-heals within one sweep tick. A single GET /pulls/{n} write-
throughs all three fields together under one shared fetchedAt stamp
(fetchAndCachePrStateFields), so a cache miss on any one field never leaves
another looking falsely fresh with an unfetched value.

Wired at the freshness-guard readiness read (cachedLiveMergeState), the
dup-winner reconcile (reconcileLiveDuplicateSiblings), and primed for free
by the per-PR sweep's existing resync fetch (primeDurablePrStateCache) --
deliberately NOT wired into any act-boundary read: the merge/close decision
(refreshLiveMergeState, unchanged) and the gate-override head-SHA resolution
(resolveOverrideHeadSha, unchanged) both keep forcing a live fetch by design,
since both need the literal current commit rather than a value that can be
briefly stale.

Test plan:
- npm run typecheck clean
- npm run db:migrations:check clean (0095, contiguous)
- npm run test:ci full local gate green
- 100% branch coverage on every changed line in backfill.ts/processors.ts/
  repositories.ts/schema.ts, confirmed via lcov diff-coverage cross-check
- New tests cover: cache miss/hit/expiry for all three fields, the shared-
  fetch write-through, webhook invalidation on synchronize/closed/reopened
  (and non-invalidation on unrelated actions), dup-winner reuse of a warm
  cache row, the sweep priming the cache from its own resync, and two
  mutation-verified regressions: gate-override must never route through the
  durable head-SHA cache (the race this function exists to close), and the
  sweep's resync must actually persist to the cache for later readers.
JSONbored added a commit that referenced this pull request Jul 2, 2026
…es (#2640)

* feat(github): cache bare PR-state reads at non-authoritative call sites

Advances #2537. The head-SHA file cache (#2527) and the recently-merged
review cache (#2633) left one sibling route uncached: a bare GET /pulls/{n}
(state/mergeable_state/head SHA), implemented as four separate near-identical
helper functions with no caching or cross-call coalescing, called from many
independent sites within one review pass.

Adds a durable, webhook-invalidated cache for this read on
pull_request_detail_sync_state (prMergeableState/prState/prStateFetchedAt),
capped at a 5-minute PR_STATE_CACHE_MAX_AGE_MS safety net so a missed
invalidation self-heals within one sweep tick. A single GET /pulls/{n} write-
throughs all three fields together under one shared fetchedAt stamp
(fetchAndCachePrStateFields), so a cache miss on any one field never leaves
another looking falsely fresh with an unfetched value.

Wired at the freshness-guard readiness read (cachedLiveMergeState), the
dup-winner reconcile (reconcileLiveDuplicateSiblings), and primed for free
by the per-PR sweep's existing resync fetch (primeDurablePrStateCache) --
deliberately NOT wired into any act-boundary read: the merge/close decision
(refreshLiveMergeState, unchanged) and the gate-override head-SHA resolution
(resolveOverrideHeadSha, unchanged) both keep forcing a live fetch by design,
since both need the literal current commit rather than a value that can be
briefly stale.

Test plan:
- npm run typecheck clean
- npm run db:migrations:check clean (0095, contiguous)
- npm run test:ci full local gate green
- 100% branch coverage on every changed line in backfill.ts/processors.ts/
  repositories.ts/schema.ts, confirmed via lcov diff-coverage cross-check
- New tests cover: cache miss/hit/expiry for all three fields, the shared-
  fetch write-through, webhook invalidation on synchronize/closed/reopened
  (and non-invalidation on unrelated actions), dup-winner reuse of a warm
  cache row, the sweep priming the cache from its own resync, and two
  mutation-verified regressions: gate-override must never route through the
  durable head-SHA cache (the race this function exists to close), and the
  sweep's resync must actually persist to the cache for later readers.

* fix(github): keep the duplicate-winner reconcile on a live PR-state read

Addresses the blocker the gate's own AI review flagged: reconcileLiveDuplicateSiblings was routed through cachedFetchLivePullRequestState, but that reconcile directly feeds duplicate-winner selection, which can auto-CLOSE the current PR when GITTENSORY_DUPLICATE_WINNER is on. A cached "open" read up to PR_STATE_CACHE_MAX_AGE_MS stale after a missed closed webhook would keep an already-closed sibling eligible as the winner, wrongly closing the current PR as the loser -- the same class of irreversible-actuation risk the merge/close decision and gate-override guard against.

Reverts this call site to the raw fetchLivePullRequestState (unchanged from before this PR), and replaces the test that asserted cache reuse with a regression proving the opposite: a warm-but-wrong cached row must never be served here.

Also replaces three test-fixture token values (fake-installation-token / installation-token) with the shorter placeholder already used elsewhere in these same tests -- both incidentally matched the repo's generic-secret-assignment scan pattern (a keyword-shaped heuristic with no placeholder-value exclusion in REES's copy of the analyzer, unlike the stricter src/review/secrets-scan.ts). No real credential was ever present; this just avoids tripping a keyword-length heuristic on new lines.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant