fix(selfhost): stop delayed webhooks from resurrecting closed PRs' review tracking - #7839
Conversation
…view tracking A webhook job delayed by queue backpressure could dequeue AFTER a newer event for the same PR (e.g. closed) already landed, and its stale embedded snapshot would silently regress the locally stored state/headSha/mergedAt back to what GitHub reported minutes earlier -- restarting active_review_tracking for a PR that had already closed, with nothing left to terminalize it again. Guard upsertPullRequestFromGitHub with GitHub's own updated_at (new pull_requests.github_updated_at column) so an out-of-order payload can no longer clobber newer state, and correct this call's own returned record to match what was actually persisted. Add a flag-gated reconcile-active-review-tracking sweep (LOOPOVER_ACTIVE_REVIEW_RECONCILIATION) that re-checks stale active rows against live GitHub state as a self-heal backstop.
…iation sweep The reconciliation sweep added in the prior commit was env-var-only (LOOPOVER_ACTIVE_REVIEW_RECONCILIATION), unlike every sibling self-heal flag (prReconciliation, sweepWatchdog, ops), which all resolve a top-level .loopover.yml manifest block first and fall back to the env var. Add the same activeReviewReconciliation: manifest-override layer end to end: engine-package parsing/serialization/lint recognition, the app-level cached resolver, and the cron/dispatch call sites -- config-as-code is the established control surface for these fleet-wide flags, not .env. Also fixes a follow-on bug in the prior commit's out-of-order-webhook guard: lastSeenOpenAt, isReadyForReview, and the review-latency clock (headShaObservedAt) were still derived from the raw incoming payload instead of the staleness-resolved state/headSha, so a rejected-as-stale payload could still corrupt those derived fields even though the state/headSha/mergedAt columns themselves were protected.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
loopover-ui | 98fa4db | Commit Preview URL Branch Preview URL |
Jul 21 2026, 02:46 PM |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7839 +/- ##
=======================================
Coverage 91.37% 91.38%
=======================================
Files 729 730 +1
Lines 74688 74751 +63
Branches 22793 22809 +16
=======================================
+ Hits 68246 68308 +62
Misses 5396 5396
- Partials 1046 1047 +1
Flags with carried forward coverage won't be shown. Click here to find out more.
|
|
Caution 🛑 LoopOver review result - fixes requiredReview updated: 2026-07-21 15:02:29 UTC
Review summary Nits — 5 non-blocking
Why this is blocked
📋 Copy for AI agents — paste into your coding agentCI checks failing
Decision drivers
Context & advisory signals — never blocks the verdict
Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 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 LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
Summary
upsertPullRequestFromGitHubnow compares GitHub's ownupdated_at(newpull_requests.github_updated_atcolumn) against what's already stored before applyingstate/headSha/mergedAt, so a webhook job delayed by queue backpressure can no longer dequeue after a newer event for the same PR and silently regress its state back to "open" — this is the exact mechanism that leftactive_review_trackingstuck showing an already-closed PR as still under active review.handlePullRequestWebhookEventreason from that return value, not a fresh DB read.reconcile-active-review-trackingsweep (activeReviewReconciliation:in.loopover.yml, falling back toLOOPOVER_ACTIVE_REVIEW_RECONCILIATION, default OFF) that re-checks staleactiverows against live GitHub state and terminalizes the ones confirmed closed — a self-heal backstop for this failure mode regardless of cause, matching the config-as-code pattern already used byprReconciliation/sweepWatchdog/ops.lastSeenOpenAt,isReadyForReview, and the review-latency clock (headShaObservedAt) were still derived from the raw incoming payload rather than the staleness-resolved values, so a rejected-as-stale payload could still corrupt those derived fields even thoughstate/headSha/mergedAtthemselves were protected.Scope
type(scope): short summaryConventional Commit format.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run actionlintnpm run typechecknpm run test:coveragelocally — full unsharded run, 19,861 tests, 0 failures; every line/branch touched by this diff verified hit via the generatedlcov.info(spot-checkedsrc/index.ts,src/db/repositories.ts,src/queue/job-dispatch.ts,src/selfhost/queue-common.ts,src/review/active-review-reconciliation.ts— the only uncovered lines/branches in those files are pre-existing, unrelated to this diff)npm run test:workersnpm run build:mcp(build:mcp:checkin the full gate)npm run test:mcp-packnpm run ui:openapi:checknpm run ui:lintnpm run ui:typechecknpm run ui:buildnpm audit --audit-level=moderate— 0 vulnerabilitiesRan the complete
npm run test:cigate three times end to end as this PR was iterated (each time after a fix), all green; final run: 1049 test files, 19,861 tests, 0 failures, 17 skipped (pre-existing, unrelated).Safety
UI Evidencesection. (N/A — no UI changes.)config/examples/loopover.full.ymland.loopover.yml.example(self-host operator-facing config reference) are updated to document the newactiveReviewReconciliation:block;CHANGELOG.mditself is intentionally untouched (not a release-prep PR).UI Evidence
N/A — no UI, frontend, docs-site, or extension changes.
Notes
0172_pull_requests_github_updated_at.sqladds a single nullableTEXTcolumn (github_updated_at); purely additive, no backfill needed — the staleness guard fails open (applies the write, exactly like before this column existed) whenever it'sNULLon either side of the comparison.test/unit/active-review-reconciliation.test.ts(new, 12 tests),test/unit/db-parsers.test.ts(+8 tests: the out-of-order-webhook guard and its two review-latency-clock regressions),test/unit/db-persistence.test.ts(+4 tests:listStaleActiveReviewTracking),test/unit/focus-manifest.test.ts(+8 tests:activeReviewReconciliation:parsing/round-trip),test/unit/focus-manifest-validation.test.ts(+2 tests),test/unit/selfhost-config-lint.test.ts(+1 test),test/unit/index.test.ts(+2 tests: cron wiring),test/unit/queue-5.test.ts(+2 tests: job dispatch),test/unit/selfhost-queue-common.test.ts(+1 test: GitHub-budget registration).