Skip to content

retention: pruning breaks two consumers that assume permanence — ledger verification (false tamper signal) and the cumulative public counter #9474

Description

@JSONbored

Summary

Two retention rules interact badly with consumers that assume their tables are permanent. Both are latent today and become visible on a schedule, which is why they are worth fixing before they surface.

1. decision_records 180-day prune manufactures a false tamper signal

src/db/retention.ts:57 prunes decision_records at 180 days. But verifyDecisionLedger requires the record for every ledger row — src/review/decision-record.ts:460-463 maps recordsById.get(row.recordId) and yields {kind: "missing_record"} when absent — and ledger rows are never pruned. Verification also walks from afterSeq = 0 genesis (:412).

So roughly 180 days after the rule first bites, any full-chain verification reports the chain broken at the first pruned record. For a public tamper-evidence feature, that is the worst possible false positive: a legitimate retention policy is indistinguishable from evidence of tampering, and the feature's own documentation teaches readers to treat it as the latter.

The retention comment describes decision_records as "a contributor's evidentiary trail" kept longer than most — the omission is that the ledger references it forever.

Deliverables

  • Choose and implement one: exempt records still referenced below the newest anchor; teach the verifier a pruned-before-<watermark> tolerance tied to the retention cutoff; or start verification at the oldest surviving record's seq. Whichever is chosen must be legible to an external skeptic — the point of the feature is that a third party can check it.
  • Document the chosen semantics in the public verification contract alongside the existing short_tail semantics.
  • Test: prune a record below the anchor watermark, then verify ⇒ chain reports valid, with the pruning explicit rather than silent.

2. orb_pr_outcomes 90-day prune will shrink the cumulative public counter

#9415 added { table: "orb_pr_outcomes", column: "occurred_at", days: 90 } (src/db/retention.ts:77). But getOrbGlobalStats SUMs the entire tablesrc/orb/outcomes.ts:63-92:

SUM(CASE WHEN o.outcome = 'merged' THEN 1 ELSE 0 END) AS merged,
SUM(CASE WHEN o.outcome = 'closed' THEN 1 ELSE 0 END) AS closed,
COUNT(*) AS total
FROM orb_pr_outcomes o ...

and src/review/public-stats.ts:465-468 folds that into the cumulative homepage totals:

totals.merged  += orb.merged;
totals.closed  += orb.closed;
totals.handled += orb.total;

Once rows begin aging past 90 days, the "all-time" counters plateau and then decrease — a public number visibly going backwards, roughly from 2026-10-25 given #9415's merge date.

Deliverables

  • Fold pruned rows into a durable running total before deletion (a small orb_outcome_totals roll-up updated by the prune), or exclude orb_pr_outcomes from retention and bound it another way.
  • Test: prune rows older than the window ⇒ the cumulative public total is unchanged.
  • Check the same pattern for the other cumulative public surfaces that fold pruned tables — computeFleetAnalytics is the obvious neighbour to audit.

Expected outcome

Retention never changes the meaning of a published number, and never produces a signal that reads as tampering.

Metadata

Metadata

Assignees

Labels

gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.maintainer-onlyOwner-only work — yields no Gittensor points.orbGittensory Orb related - maintainer self-hosting analytics.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions