Skip to content

fix(locks): stop contention killing jobs, and stop three paths freeing a live holder's lock (#9465, #9468) - #9506

Merged
JSONbored merged 2 commits into
mainfrom
fix/lock-lifecycle-integrity
Jul 28, 2026
Merged

fix(locks): stop contention killing jobs, and stop three paths freeing a live holder's lock (#9465, #9468)#9506
JSONbored merged 2 commits into
mainfrom
fix/lock-lifecycle-integrity

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

The per-PR lock is mis-handled on both sides at once: waiters are killed for waiting, and three separate paths free a lock whose owner is still running. This PR fixes the contender side and the lifecycle edges.

Closes #9465
Closes #9468

The holder side (#9467 — the 600s TTL underrunning the AI review it wraps, with no renewal) is deliberately not here. It changes the primitive every actuation path depends on, and getting it wrong causes double-merges rather than preventing them, so it gets its own PR with invariant + regression coverage.

#9465 — contention burned the retry budget and killed the job

PrActuationLockContendedError retries on a flat 5s delay, but every retry incremented attempts and the job died at maxRetries (default 5). So a waiter died after ~25 seconds of contention — against a lock whose TTL is 600s and which spans an entire publish → AI-review → maintain pass.

Production evidence (7 days to 2026-07-27): the only dead-lettered jobs on the deployment were three actuation-lock contentions:

pr actuation lock contended for JSONbored/loopover#9450 during reopen-reclose
pr actuation lock contended for JSONbored/loopover#9447 during public-surface-publish
pr actuation lock contended for JSONbored/loopover#9457 during public-surface-publish

The public-surface-publish deaths are recovered later by the stale-surface sweep. The reopen-reclose death is not: maybeRecloseDisallowedReopen has exactly one call site, gated on payload.action === "reopened", and no sweep, watchdog or reconciler re-derives it. That one-shot close enforcement was lost outright on #9450.

The queue already models this correctly for the other "not our turn yet" condition — a GitHub rate-limit failure re-pends without incrementing attempts (deferred_by='rate_limit'). Lock contention now gets the same treatment in both backends, bounded by job age (ATTEMPT_FREE_RETRY_DEADLINE_MS, 15 min — comfortably past the 600s TTL) so a genuinely wedged lock still converges to a dead job rather than deferring forever. It lands under a distinct job_lock_contended audit event and its own counter, so "waiting its turn" is never read as a failure in the audit trail or its dashboards.

#9468 — three paths freed a live holder's lock

Boot flush. flushOrphanedLocksAtBoot deletes every key matching the orphaned-lock patterns. Its justification — "on this single-instance deployment any lock present at boot is provably orphaned" — was never enforced: the same Redis is explicitly shared across replicas (the token cache says so), and the pg queue backend exists precisely to support more than one instance. A restarting replica therefore freed a live sibling's in-flight ai-review-lock and pr-actuation-lock, and the next pass claimed them fresh and duplicated the review and the actuation — with no TTL expiry needed at all. A crash-looping replica becomes a periodic fleet-wide lock wipe.

Now opt-in via LOOPOVER_SINGLE_INSTANCE, defaulting to skip: the cost of skipping is a genuinely orphaned lock riding out its TTL (which #9008's steal path already recovers on demand), while the cost of flushing wrongly is a double close or merge.

Shutdown ordering. #8998 released every held lock first, before the drain, so a SIGKILL mid-drain could not strand an ai-review-lock for its full 1800s TTL. But backend.shutdown() deliberately lets in-flight work finish (#9007) — so in the case where the drain completes, jobs kept running with their locks already freed, and a sibling (or the new container in an overlapped deploy) could claim one and duplicate the actuation. The token-checked release protects the new claim from corruption; it does not stop the double-run.

The drain now runs first and each job releases its own lock through its own finally — correct and precise. The proactive bulk release stays for the short-grace case #8998 was written for, but only fires when the drain has not finished within LOOPOVER_SHUTDOWN_LOCK_RELEASE_AFTER_MS (unset = wait for the drain, which is right wherever the grace period comfortably exceeds a review — this deployment's stop_grace_period is 300s).

Registry keying. held-lock-registry was keyed by lock key alone, so an earlier holder's cleanup could evict a later one's live entry: a maintainer's forced re-run steals a lock in-process (#9008), overwriting the entry with the stealer's release; when the original pass finished, its store-side release correctly no-oped (wrong token) but its unregisterHeldLock deleted the stealer's entry. A SIGTERM during the stealer's still-running review then skipped that key, stranding it for the full TTL after a hard kill — exactly the #8998 shape the registry exists to prevent. Register and unregister are now token-scoped, mirroring the store-side compare-and-delete.

Validation

  • npx tsc --noEmit -p tsconfig.json — clean
  • npm run selfhost:env-reference — regenerated and committed (two new env.* reads)
  • 392 passed across held-lock-registry, retryable, selfhost-pg-queue, selfhost-sqlite-queue, selfhost-redis-cache, selfhost-metrics

New tests:

The existing registry tests were updated to thread a token rather than being deleted, and two of my new assertions were made relative to a baseline count after the shared registry leaked state between tests.

Note on the new env vars

Both default to the safe behaviour, so this deploys as a pure improvement with no configuration change: no boot flush (locks recover via TTL/steal), and no premature lock release (wait for the drain). An operator on a short SIGKILL grace period sets LOOPOVER_SHUTDOWN_LOCK_RELEASE_AFTER_MS; a genuinely single-instance operator sets LOOPOVER_SINGLE_INSTANCE=true to keep the faster orphan recovery.

…g a live holder's lock (#9465, #9468)

Lock contention was charged an attempt like any other error, so with a flat 5s retry and
maxRetries 5 a waiter died after roughly 25 seconds -- against a lock designed to be held for
minutes (PR_ACTUATION_LOCK_TTL_SECONDS is 600s, spanning a whole publish -> AI review ->
maintain pass). Production confirmed it: the only dead-lettered jobs in a 7-day window were
three actuation-lock contentions, and one was a reopen-reclose, whose single webhook-gated
trigger and absent reconciler meant that one-shot enforcement was lost outright (PR #9450).
Both backends now re-pend without consuming the budget -- the same treatment the sibling
GitHub rate-limit path already gets -- bounded by job age so a genuinely wedged lock still
converges to a dead job an operator can see, under a distinct job_lock_contended audit event so
"waiting its turn" is never read as a failure.

Three paths freed locks whose owners were still running. The boot flush deletes every key
matching the orphaned-lock patterns, which is only sound on a single instance -- but nothing
enforced that, while the same Redis is explicitly shared across replicas and the pg backend
exists to support more than one; a restarting sibling therefore freed a live replica's
in-flight ai-review and pr-actuation locks, letting the next pass duplicate that review and its
actuation with no TTL expiry involved. It is now opt-in via LOOPOVER_SINGLE_INSTANCE, defaulting
to skip because the cost of skipping is a lock riding out its TTL (which #9008's steal path
already recovers) while the cost of flushing wrongly is a double close or merge.

Shutdown released every held lock BEFORE the drain, but the drain deliberately lets in-flight
work finish -- so a completed drain left jobs running with their locks already freed, and a
sibling or the new container in an overlapped deploy could claim one and duplicate the
actuation. The drain now runs first and each job releases its own lock through its own finally;
the proactive bulk release remains for the short-grace case it was written for, but only fires
when the drain has not finished within LOOPOVER_SHUTDOWN_LOCK_RELEASE_AFTER_MS.

The held-lock registry was keyed by lock key alone, so an earlier holder's cleanup could evict a
later one's live entry: a forced re-run steals a lock in-process, and the original pass's
unregister then deleted the stealer's entry, so a SIGTERM skipped that key and it stranded for
the full TTL after a hard kill -- exactly the shape the registry exists to prevent. Register and
unregister are now token-scoped, mirroring the store-side compare-and-delete.
@loopover-orb

loopover-orb Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Warning

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-28 01:46:10 UTC

17 files · 1 AI reviewer · no blockers · CI green · clean

⏸️ Suggested Action - Manual Review

Review summary
This PR closes #9465 and #9468 by (1) making per-PR actuation-lock contention re-pend jobs without consuming their retry budget in both queue backends, bounded by a 15-minute age deadline that comfortably exceeds the lock's 600s TTL, and (2) fixing three related lock-lifecycle bugs: gating the boot-time orphaned-lock flush behind an explicit LOOPOVER_SINGLE_INSTANCE opt-in (a shared-Redis flush was deleting live sibling locks), reordering shutdown to drain in-flight jobs before force-releasing locks (previously freed locks of still-running jobs), and scoping held-lock-registry register/unregister by ownerToken so a stolen lock's cleanup can't evict the stealer's live entry. I traced isAttemptFreeRetry's retryKind ('pr_actuation_lock_contended') back to its sole producer in transient-locks.ts and it matches; the shutdown race logic (Number('') = 0, so forceReleaseAfterMs defaults to the 'wait for drain' branch) is correct; and the token-scoped registry change is exercised by solid regression tests for the exact steal scenario described. Green validate/build CI confirms all call sites of the changed registerHeldLock/unregisterHeldLock signatures were updated consistently.

Nits — 6 non-blocking
  • codecov/patch failed at 58.82% vs the 99% target — the new attempt-free-retry branches in src/selfhost/pg-queue.ts and src/selfhost/sqlite-queue.ts, plus the new shutdown-race logic in src/server.ts (forceReleaseAfterMs / drainedInTime), aren't visibly covered by any test in this diff.
  • src/selfhost/queue-common.ts:3-8 places an `export { ATTEMPT_FREE_RETRY_DEADLINE_MS, isAttemptFreeRetry }` statement between two `import` statements — it compiles, but group exports together for readability.
  • The boot-time orphaned-lock flush now defaults to OFF unless LOOPOVER_SINGLE_INSTANCE is explicitly set (src/selfhost/redis-cache.ts:136) — this silently changes behavior for existing single-instance self-host operators upgrading into this PR; worth a callout in release/upgrade notes.
  • apps/loopover-ui/src/lib/selfhost-env-reference.ts is a generated file per its own header comment — confirm it was produced via `scripts/gen-selfhost-env-reference.ts` rather than hand-edited.
  • Add a direct unit test for the pg-queue.ts/sqlite-queue.ts lock-contention UPDATE (status='pending', deferred_by='lock_contended', attempts unchanged) to close the codecov gap and lock in the attempt-preserving behavior at the query level.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9465, #9468
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 (2 linked issues).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 334 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 334 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Partially addressed
The PR fully delivers attempt-free lock-contention deferral in both queue backends with a bounded deadline, jitter via existing backoff/delay helpers, a distinct metric, and audit event distinguishing contention from failure/dead jobs, but it explicitly punts the reopen-reclose reconciler (a core requirement to make the enforcement survive a lost job) and doesn't add a specific alert on dead jobs

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 14 PR(s), 334 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
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.
🧪 Chat with LoopOver

Ask 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.

  • @loopover ask <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: merge · clause: success
  • config: 1f76891833dbb5c63b6b58335d38c9c1425f9efd98d9caac63cd8d496bae2c6a · pack: oss-anti-slop · ci: passed
  • record: 7c7577ef6862c13a3e08028bce847dd410298e6fddcdf523f9cb4de9b920db85 (schema v5, head 5e08877)

🟩 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.

  • Re-run LoopOver review

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui 3eacc8c Commit Preview URL

Branch Preview URL
Jul 28 2026, 01:00 AM

@JSONbored JSONbored self-assigned this Jul 28, 2026
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 127 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.64MB 127 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-DUQb7lQc.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-sUd4-iZs.js (New) 862.21kB 862.21kB 100.0% 🚀
assets/docs.fumadocs-spike-api-reference-C8dKd-fh.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-66cSI5DN.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-DiWKRGp8.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-B15ImyKI.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/self-hosting-configuration-BLECzfnN.js (New) 101.89kB 101.89kB 100.0% 🚀
assets/maintainer-panel-L5CmhE3s.js (New) 78.99kB 78.99kB 100.0% 🚀
assets/routes-DpL6IZHW.js (New) 35.96kB 35.96kB 100.0% 🚀
assets/owner-panel-BcPJ2s-l.js (New) 27.92kB 27.92kB 100.0% 🚀
assets/app-ssLhsjEQ.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/ui-vendor-B6uo62Xa.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/miner-panel-CHkigePk.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-DgDqQIiV.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-Dhyx4jJi.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-DJJYNi6h.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-DK7uHo3Z.js (New) 15.52kB 15.52kB 100.0% 🚀
assets/playground-panel-CNGuSTYF.js (New) 14.42kB 14.42kB 100.0% 🚀
assets/fairness-BP-jqELb.js (New) 10.73kB 10.73kB 100.0% 🚀
assets/app.audit-Cj3Wa49F.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-BIHynSxb.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-CyuheKjv.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-g3S0DziV.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-DZAEolQ-.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-Dc8E7qzz.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-Bp1botcD.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-iJ70-HcL.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-BNZ02dQd.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-Cy97kWvg.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/docs.index-zbb0LSvV.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-DCPwDChq.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-D1foIej0.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-9U9yD_ue.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-D4sOmPJr.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-CbwZT2g3.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-R1l9c0VC.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-VkzrsDbD.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-D-rF86CY.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-_Y89XwQE.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-D9Rm7Faq.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-CBojz_cq.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-n0s_W2UQ.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-BHZULm0o.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-DnV52G5B.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-BmiKkSg0.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-De2IGhch.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-DM9IXLT5.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-JCRnvBKg.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-zjwqfLjI.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-Da7Oy3zg.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-DPt7kVCf.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-CTGBB9vf.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-D6xt49Y9.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-Cf9j1V8x.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-NrHQka4m.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-al7sk1Zn.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-D6f-PYjd.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-Bg3aGHXa.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-D8URfKp_.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-BMh-2Bzy.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-BrZCp9vq.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-BqH2vsyU.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-DNtkAK_q.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-CTuOZuP-.js (Deleted) -862.21kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-CVUdHqJ3.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-bior5jpB.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-Dlw1ekda.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-BHHKRY7F.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/self-hosting-configuration-CYuVfPdX.js (Deleted) -101.76kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-quMjDWvU.js (Deleted) -78.99kB 0 bytes -100.0% 🗑️
assets/routes-Brw1qzrG.js (Deleted) -35.96kB 0 bytes -100.0% 🗑️
assets/owner-panel-D6njTkLo.js (Deleted) -27.92kB 0 bytes -100.0% 🗑️
assets/app-AvEQXG2r.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/ui-vendor-EqijVGEG.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/miner-panel-C0J6xeb3.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-BhceeUCs.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-AuN1xNHZ.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-Dh3vu_9z.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-Dn1WAoh0.js (Deleted) -15.52kB 0 bytes -100.0% 🗑️
assets/playground-panel-CJwG6uQ7.js (Deleted) -14.42kB 0 bytes -100.0% 🗑️
assets/fairness-DGY5wdLs.js (Deleted) -10.73kB 0 bytes -100.0% 🗑️
assets/app.audit-C1rAnEVV.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-DORDUrGx.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-8wfgBX5W.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-TTZZW_Nn.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-ZBCRw84Z.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-C441hY3v.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-6FCdaw61.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-00KfKyhx.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-BWWFzFiI.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-BOtF6yn5.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/docs.index-mH3qUt3L.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/api.index-DdaS_Ope.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-CgxFifLP.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-4NW7ftU1.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-DJ9FLVGl.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-C4iIvcQo.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-CJW4VGS5.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-Owt_iEtM.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-D7N-3-6f.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-BCwtDQGS.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-DjXzuQpt.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-o-uXKAee.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-CE3KyZZt.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-CfLmK4KM.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-Dnb2zKPK.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-COod3NwY.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-BudsaZbB.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-Byl0qEn1.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-DuZq6HMM.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-BuLectJQ.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-CiuVAqUC.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-SZeDlMQ9.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-BDW3zLYI.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-DSXgNNVq.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-DmS5ada6.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-D0fM0Vau.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-qPiewFc7.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-Dd4aINnB.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-BfWwsWL5.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-CFlnGk-w.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-CpTr4NiZ.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-E2h_a0sC.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-DdM6vRkb.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.65%. Comparing base (0f6c6b8) to head (5e08877).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9506      +/-   ##
==========================================
- Coverage   89.55%   88.65%   -0.90%     
==========================================
  Files         843      843              
  Lines      110073   110083      +10     
  Branches    26194    26199       +5     
==========================================
- Hits        98573    97592     -981     
- Misses      10238    11520    +1282     
+ Partials     1262      971     -291     
Flag Coverage Δ
backend 93.62% <100.00%> (-1.64%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/queue/ai-review-orchestration.ts 98.78% <100.00%> (ø)
src/queue/held-lock-registry.ts 100.00% <100.00%> (ø)
src/queue/retryable.ts 100.00% <100.00%> (ø)
src/selfhost/audit.ts 100.00% <ø> (ø)
src/selfhost/metrics.ts 100.00% <ø> (ø)
src/selfhost/queue-common.ts 98.65% <ø> (ø)
src/selfhost/redis-cache.ts 97.29% <100.00%> (+0.07%) ⬆️
src/selfhost/sqlite-queue.ts 99.63% <100.00%> (+<0.01%) ⬆️

... and 3 files with indirect coverage changes

@loopover-orb loopover-orb Bot added gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. manual-review Gittensor contributor context labels Jul 28, 2026
… lifecycle arms

The attempt-free deferral block -- the core of #9465 -- had no coverage in either backend.
Adds a regression per backend (re-pends without consuming an attempt, tagged
deferred_by='lock_contended') plus two invariants: a genuine failure still consumes its budget
and converges to dead, and past the age deadline a wedged lock does too rather than deferring
forever. Also covers the single-instance boot-flush gate (defaulting to false is the fail-safe
direction) and the null-token release arm, which must never evict a live holder.

makePool's enqueueJob gained an optional createdAt so the age-based deadline is testable; it
defaults to now, so every existing caller is unaffected.
@JSONbored
JSONbored merged commit 0822ca3 into main Jul 28, 2026
10 checks passed
@JSONbored
JSONbored deleted the fix/lock-lifecycle-integrity branch July 28, 2026 02:14
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. manual-review Gittensor contributor context

Projects

None yet

1 participant