Skip to content

fix(ledger,retention): stop retention manufacturing tamper signals, and make the public counter durable (#9474, #9489) - #9532

Merged
JSONbored merged 3 commits into
mainfrom
fix/retention-vs-verification
Jul 28, 2026
Merged

fix(ledger,retention): stop retention manufacturing tamper signals, and make the public counter durable (#9474, #9489)#9532
JSONbored merged 3 commits into
mainfrom
fix/retention-vs-verification

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Retention and verification were written as if the other did not exist. Both consumers that assumed permanence get fixed here, plus #9489's final defect — the short_tail false positive — which belongs with them because it is the same failure shape: a legitimate operation reading as tampering on a public endpoint.

Closes #9474
Closes #9489

#9474a — the 180-day record prune would have broken every full-chain verification

decision_records prunes at 180 days; ledger rows are kept forever; verifyDecisionLedger demands the record for every ledger row and walks from genesis. ~180 days after the rule first bit, every verification would report missing_record at the first pruned row — a false tamper signal manufactured by the published retention policy, on the one endpoint whose entire point is that a skeptic can trust it.

The verifier now tolerates a missing record only when the ledger row's createdAt is older than the published decision_records window, counting it in a new prunedRecords result field. Two properties make this legible rather than a loophole:

  • The tolerance keys on the ledger row's timestamp, which is inside the hash chain — backdating it to sneak a fresh deletion under the cutoff breaks row_hash_mismatch first. A recent out-of-band deletion is still missing_record.
  • The cutoff comes from retentionCutoffIsoForTable("decision_records"), reading RETENTION_POLICY itself — the verifier and the prune cannot drift apart again, because they now share one source of truth.

What is genuinely given up is exactly what pruning gives up: the content re-check for that row. The chain checks still run over it, and the committed digest stays published, so a challenger holding the original preimage can still prove a historical rewrite by hand. Documented in what-you-can-verify.mdx and the OpenAPI descriptions.

#9474b — the cumulative public counter would have visibly shrunk from ~2026-10-25

getOrbGlobalStats SUMs the entire orb_pr_outcomes table and public-stats folds it into the homepage's all-time merged/closed/handled totals — but #9415 gave the table a 90-day window, so the "all-time" numbers would have plateaued and then decreased.

The prune now folds every row it deletes into a durable orb_outcome_rollups table (migration 0198) in the same batch transaction as the delete — a fold and delete that could commit separately would either double-count or under-count — and the stats query adds the rollup back. Three semantics preserved exactly:

  • Only rows the live query would have counted are folded (registered installs, no published review surface); everything else is deleted without folding, so the public total does not jump on prune day either.
  • Rollups key on the lowercased account_login, so excludeAccount de-dup keeps working after the raw rows are gone.
  • orb_pr_outcomes moved to the top of RETENTION_POLICY, above audit_events: the fold's own-ledger exclusion needs audit rows that the audit prune — same 90-day window — would otherwise have deleted moments earlier in the same pass. An ordering-guard test pins this.

Neighbour audit (deliverable 3): computeFleetAnalytics is clean — every orb_signals read is explicitly windowed (received_at >= cutoff) and orb_signals has no retention rule at all, so there is no unwindowed aggregate over a pruned table there.

#9489d — short_tail was transiently false-positive and permanently blind to interior orphans

Two independent defects in the completeness reconciliation:

  • Grace. A record and its ledger append are two writes milliseconds apart, and a verify landing between them reported short_tail — "tampering" — for a state every healthy write passes through. Records younger than a 5-minute LEDGER_APPEND_GRACE_MS are now simply not yet due; a genuinely failed append still surfaces on the next verify after the grace lapses (and its own error-level alarm fired the moment it happened).
  • Interior orphans. The old check compared created_at against the verified tail only, so the moment any newer record chained cleanly, an unchained record behind it became invisible forever. The reconciliation now asks the real question — does any ledger row vouch for this record? — via a NOT EXISTS anti-join (indexed by the new decision_ledger_record_id index). The newest orphan decides the break kind: past the tail it is the truncated-tail signature short_tail always meant; behind it, a new unchained_record break carrying the record id — the failed-append signature.

Validation

  • npx tsc --noEmit, db:migrations:check (contiguous 0001..0198), selfhost:env-reference:check, ui:openapi regenerated, git diff --check — all clean
  • 260 passed across the decision-record, retention, public-stats, orb-outcomes, pg-retention and queue-4 suites
  • Patch coverage against this diff: 0 uncovered changed lines

Regressions, each verified to fail against the unfixed code: a pruned record verifies clean and is counted, not reported as tampering; a verify during an in-flight append reports ok; an interior orphan is detected as unchained_record; the cumulative public total is byte-identical before and after a prune with the aged rows gone.

Invariants: a recent record deleted out of band is still missing_record — the tolerance cannot launder fresh deletions; past the grace window an unchained record IS reported — the grace bounds the blind spot, it does not remove the check; a fully healthy chain reports prunedRecords: 0 and both new mechanisms inert; rows the live query never counted (unregistered installs, own-ledger-published PRs) are deleted without folding; a second prune run folds nothing new; excludeAccount still de-dups against folded totals; dry-run neither folds nor deletes; and the policy-ordering guard.

The pg mock pool gained connect() (the fold's batch transaction checks out a client) and the fold statements — both still route through the same query fn, so the rowid-regression guard sees batch-issued SQL too.

…pruned-record tolerance, append grace, interior orphans, and a fold-before-delete for the public counter (#9474, #9489)
@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 07:26:51 UTC

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

⏸️ Suggested Action - Manual Review

Review summary
This PR fixes two real defects: (1) verifyDecisionLedger previously treated any pruned decision_records row as tampering (missing_record), and now correctly tolerates absence only when the ledger row's hash-chained createdAt predates the published retention cutoff, plus adds a grace window and interior-orphan detection for #9489's short_tail false positive; (2) getOrbGlobalStats's cumulative counter is protected from shrinking as orb_pr_outcomes rows age out, via a fold-before-delete into a new orb_outcome_rollups table in the same D1 batch as the delete. The reasoning is sound and thoroughly tested (regression tests simulate real retention/pruning behavior rather than fabricating states), migration 0198 is additive and D1-safe, and RETENTION_POLICY reordering is explicitly justified and pinned by a test. The one notable risk is the new unbatched DELETE in pruneExpiredRecords for orb_pr_outcomes, which the PR argues is safe given current row volume but removes the batching safety net this file otherwise enforces for every other table.

Nits — 6 non-blocking
  • src/db/retention.ts: the orb_pr_outcomes DELETE is now unbatched (no LIMIT/batch loop) unlike every other table in this function — the comment justifies it on current volume, but if that volume assumption changes this table silently loses the batching protection the rest of the file relies on; consider at least a defensive batch loop mirroring the other tables.
  • src/review/decision-record.ts: LEDGER_APPEND_GRACE_MS (5 min) and the 180-day retention cutoff are both magic numbers with good doc comments, but neither is derived from RETENTION_POLICY.find for decision_records the way retentionCutoffIsoForTable already generalizes elsewhere — fine as-is but flagged per the external brief.
  • src/orb/outcomes.ts: the two DB queries (rollup and live) are no longer run in parallel (Promise.all) — previously the live query might have raced with rollup fetch; not a correctness bug but a minor latency regression worth a Promise.all if the pattern used elsewhere in this file supports it.
  • migrations/0198_orb_outcome_rollups.sql: the new decision_ledger_record_id index is added in the same migration as the unrelated orb_outcome_rollups table — grouping two independent schema changes in one migration file is a minor DRY/clarity nit, not a blocker.
  • Consider adding a batch loop to the orb_pr_outcomes DELETE in src/db/retention.ts for consistency with the rest of pruneExpiredRecords, even if current volume doesn't require it yet.
  • 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 #9474, #9489
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, 326 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 326 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Addressed
The PR directly implements both requested deliverables: the ledger verifier now tolerates missing records only when the ledger row's created_at predates the retention cutoff (keyed via a shared retentionCutoffIsoForTable source of truth), documented in prunedRecords/verification docs, and the orb_pr_outcomes prune now atomically folds pruned rows into a durable orb_outcome_rollups table before del

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), 326 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: 0f5bee0560766287732b0b5d38e3177e972711bee137cd2adedfe7bc57695763 · pack: oss-anti-slop · ci: passed
  • record: 1e819352ad3b6d3758e78083141a98cddf223bebcb5a7d0b5711e30e6b4ca015 (schema v5, head b2129bd)
Visual preview
Route Viewport Before (production) After (this PR's preview) Diff
/ desktop before /
before /
after /
after /
/ mobile before / (mobile)
before / (mobile)
after / (mobile)
after / (mobile)

Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy.

Scroll preview
Route Before (production) After (this PR's preview)
/ before / (scroll)
before / (scroll)
after / (scroll)
after / (scroll)

A short scroll-through clip (desktop) — click either thumbnail to open the full animation. Evidence for scroll-linked behavior a single screenshot can't show.

🟩 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

@JSONbored JSONbored self-assigned this Jul 28, 2026
@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 1fb265c Commit Preview URL

Branch Preview URL
Jul 28 2026, 06:24 AM

@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 95.28%. Comparing base (6f3b613) to head (ea43b28).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9532      +/-   ##
==========================================
+ Coverage   89.56%   95.28%   +5.71%     
==========================================
  Files         843      741     -102     
  Lines      110201    60572   -49629     
  Branches    26230    21342    -4888     
==========================================
- Hits        98705    57717   -40988     
+ Misses      10233     1613    -8620     
+ Partials     1263     1242      -21     
Flag Coverage Δ
backend 95.28% <100.00%> (+<0.01%) ⬆️
control-plane ?
engine ?
rees ?

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

Files with missing lines Coverage Δ
src/db/retention.ts 100.00% <100.00%> (ø)
src/openapi/spec.ts 99.63% <ø> (ø)
src/orb/outcomes.ts 95.45% <100.00%> (+6.56%) ⬆️
src/review/decision-record.ts 100.00% <100.00%> (ø)

... and 272 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
The #9474 migration creates orb_outcome_rollups, but schema-drift only accepts a
migrated table that either has a Drizzle declaration or is a declared raw-SQL
exception -- so both validate-code (db:schema-drift:check) and validate-tests
(check-schema-drift-script's real-repo regression guard) failed on it.

Raw-SQL-only is the correct classification, not an escape hatch: the table is
written by pruneExpiredRecords' fold and read by getOrbGlobalStats, both through
env.DB.prepare, with no Drizzle use anywhere. orb_pr_outcomes -- the table it
rolls up -- is already in the same list.

Refs #9474
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 4.39kB (0.06%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.65MB 4.39kB (0.06%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-BGiofTdr.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-CAmKk_82.js (New) 862.81kB 862.81kB 100.0% 🚀
openapi.json 603 bytes 623.05kB 0.1%
assets/docs.fumadocs-spike-api-reference-Dmod4yBh.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-Dj7Q2DiK.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-Doejy3VB.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-vtNzLSGX.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/maintainer-panel-BynErEAP.js (New) 78.99kB 78.99kB 100.0% 🚀
assets/what-you-can-verify-CiXBz4TS.js (New) 50.92kB 50.92kB 100.0% 🚀
assets/routes-DIvliUWU.js (New) 35.96kB 35.96kB 100.0% 🚀
assets/owner-panel-DHhGASuz.js (New) 27.92kB 27.92kB 100.0% 🚀
assets/app-Csp1kIDG.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/ui-vendor-D9njWRPf.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/miner-panel-DsAdeD5e.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-4Sqg3_i_.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-Cp33mRsF.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-Bhrv9lDK.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-D3X1GCXP.js (New) 15.52kB 15.52kB 100.0% 🚀
assets/playground-panel-BrqVOiYd.js (New) 14.42kB 14.42kB 100.0% 🚀
assets/fairness-Dpmw9k5n.js (New) 10.73kB 10.73kB 100.0% 🚀
assets/app.audit-GW9gKYAJ.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-DC8AbSuF.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-Dxk-Hwsw.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-htdj6Z3s.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-BYhzYEAB.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-E-g3In8l.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-Ct95GWYK.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-drQYUe_u.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-DPCq9qZo.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-DREoKjnw.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/docs.index-CJJ8kSy3.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-93WROv_E.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-DKJ-4vrm.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-NlQDydLq.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-CV_nBSTi.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-nVA-JpZ1.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-CS-SxAXG.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-DtSZ7CrU.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-Ds6pX53R.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-IgQiJZfC.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-gGSlcPEt.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-BAS9Puxu.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-DbjJt0jc.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-BhIX-qm4.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-CCLyDHM1.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-YIDK8-pr.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-D9cDkUi1.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-yqKuI2xr.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-Ds_lJyBq.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-GNLdbEUM.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-CwTf8nCc.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-DPlaYVKD.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-kL4mAkUc.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-BFnjx5EL.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-QvGLYCB-.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-DdFcJhYH.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-BBNJmGOS.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square--V06LDcW.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-BTMX7uPx.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-CoTDnnUW.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-4194I6xQ.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-zf9xFlpf.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-BPyJza0S.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-DfSWH_G8.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-DlE8KRRq.js (Deleted) -862.21kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-DXuUvBPj.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-BxS5NNDl.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-BPXZqCLn.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-uM4cVNuh.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-B9t-d9Zw.js (Deleted) -78.99kB 0 bytes -100.0% 🗑️
assets/what-you-can-verify-BFyFqYSi.js (Deleted) -47.74kB 0 bytes -100.0% 🗑️
assets/routes-Y_NbJAbg.js (Deleted) -35.96kB 0 bytes -100.0% 🗑️
assets/owner-panel-CzBHfctr.js (Deleted) -27.92kB 0 bytes -100.0% 🗑️
assets/app-B_6sfE-I.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/ui-vendor-BOVA_Gv_.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/miner-panel-BcE6af2I.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-jvHy0nQf.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-D_G7SOLY.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-DW6TyEtj.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-Cj53ClQh.js (Deleted) -15.52kB 0 bytes -100.0% 🗑️
assets/playground-panel-BUmICVUG.js (Deleted) -14.42kB 0 bytes -100.0% 🗑️
assets/fairness-CxM32vsG.js (Deleted) -10.73kB 0 bytes -100.0% 🗑️
assets/app.audit-C9Cn8Z8g.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-BXUZT2V3.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-WHbEUIQH.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-BOZ9RzJ9.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-CFm1PPJi.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-BP0cMbb6.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-Yer5jt8M.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-B-OvOzy_.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-jNXKicsX.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-DxCAtm58.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/docs.index-BxThIOud.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/api.index-u7GHyY0w.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-QGKrOFf3.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-hvbd79Za.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-DzUNkovT.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-Bp18xzky.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-CijLquJ6.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-BDM1tkoE.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-Cmd3hM1C.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-Cmyh2iNX.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-DA3Mi11y.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-DB7L8B8z.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-D9MIbuE8.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-OkIQzsf3.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-CGVgJIyd.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-C9XtAmf-.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-BF1oow8R.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-Bv9g2Zwf.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-DWh5NBRd.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-C1hh0nu_.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-BuEv2wGy.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-BRW0WOAP.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-iQ-TKYYQ.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-CgGBXmyG.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-BdCAxEPS.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-DLBjNsSY.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-B2G7f6gk.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-BnYT2B-6.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-UwvDmivt.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-E3C0Ukap.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-DAKicv5g.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-BjKRgknW.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-CmQVtBkC.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

@JSONbored

Copy link
Copy Markdown
Owner Author

Addressed the batching concern — it was right, and "safe at today's volume" is exactly the argument that ages badly (one fleet-wide backfill turns that statement into a multi-million-row delete).

The prune is now bounded like every other table, without giving up the atomicity the fold requires. Each slice picks a timestamp boundary and both statements share the identical occurred_at predicate, so the fold and the delete provably see the same rows while still committing together in one batch(). Keying the slice on the retention column rather than on rows avoids both alternatives' hazards: rowid/ctid is rewritten by pg-dialect to a physical location (the #9470 bug), and row-value tuple syntax has uneven SQLite support.

The boundary is taken inclusively (<= on the occurred_at at OFFSET batchSize - 1), which is load-bearing rather than cosmetic: an exclusive < against a run of rows sharing one timestamp selects zero rows and spins forever. I verified that directly — flipping it to < hangs the new tie test until the timeout. A slice is therefore batchSize rows plus any ties at the boundary, and the final slice takes the remainder.

Three tests added:

  • REGRESSION — 25 aged rows at batchSize: 10 drain across multiple bounded slices, with the cumulative public total unchanged.
  • INVARIANT — 12 rows sharing one timestamp still make progress (the anti-spin guard).
  • INVARIANTmaxPerTable still caps a single run, and the public total is exact at every point, not only once the cohort fully drains.

152 tests green across the retention, pg-retention, orb-outcomes, decision-record and public-stats suites; patch coverage still 0 uncovered changed lines.

@JSONbored
JSONbored merged commit 47f9ae4 into main Jul 28, 2026
10 checks passed
@JSONbored
JSONbored deleted the fix/retention-vs-verification branch July 28, 2026 07:27
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