⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.
Context
GET /v1/public/decision-ledger/anchors (src/api/routes.ts:1341-1355) is the public listing of every anchoring attempt. Its route comment states the point: "This is what makes anchoring's own health a publicly checkable fact — a failure is recorded and served exactly like a success ... so 'anchoring has been failing for a week' is something anyone can observe."
loadPublicLedgerAnchors (src/review/ledger-anchor-persistence.ts:98-141) implements keyset pagination whose cursor does not match its sort key: the query filters WHERE created_at < ? (cursor: created_at only) while ordering BY created_at DESC, id DESC, and nextBefore = page[page.length - 1]?.createdAt (:127).
Because the ORDER BY tiebreaks on id but the WHERE clause filters on created_at alone, any group of rows sharing one created_at that straddles a page boundary is silently skipped: the next page starts strictly before that timestamp, so the remaining tied rows are never returned on any page.
Ties are not hypothetical here. recordLedgerAnchorAttempt (:47) defaults createdAt to nowIso() (src/utils/json.ts:14-16, millisecond precision), and one scheduler tick records an attempt per backend — LedgerAnchorBackend is "rekor" | "git" | "ots" | "bittensor" (:10), four rows written back to back. The function's own doc comment (:44-46) already concedes the collision risk: createdAt is injectable so a test can "control ordering deterministically instead of racing real wall-clock resolution across several inserts in a tight loop".
So an attempt — including a failure, the row this endpoint exists to make visible — can be permanently unreachable through the paginated public API while sitting in the table.
Requirements
- Make the cursor match the sort key: page on
(created_at, id), not created_at alone.
nextBefore must carry both components so the next page resumes exactly after the last returned row. Encode them in one opaque string cursor (the field stays a single string | null so the route and the published response shape are unchanged for a caller that only ever round-trips the value).
- The WHERE clause becomes the standard row-comparison form:
(created_at < :ts) OR (created_at = :ts AND id < :id), preserving ORDER BY created_at DESC, id DESC.
- A
before value that does not parse as the new cursor must be treated as "no cursor" (first page) rather than throwing — this is an unauthenticated public route.
LedgerAnchorListFilter.before's doc comment (:105) must be updated: it currently says "return rows strictly older than this ISO timestamp", which stops being true.
- No change to
recordLedgerAnchorAttempt, to the row shape, or to the backend/limit filters.
⚠️ Required pattern: keep the single env.DB.prepare(...) call and the existing fetch-one-extra-row limit + 1 technique at src/review/ledger-anchor-persistence.ts:120-128 — the change is the predicate and the cursor payload, not the pagination strategy. It does NOT satisfy this issue to switch to OFFSET-based paging; to widen the created_at comparison to <= (which re-serves rows and can loop forever); to add a second query to re-fetch ties; or to make nextBefore a structured object, which changes the published response shape.
Deliverables
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example changing the SQL without the five-tied-rows pagination test that proves no row is lost — does not resolve this issue.
Test Coverage Requirements
src/review/** and src/api/** are both inside Codecov's src/** include; the 99% branch-counted patch gate applies. Every arm of the new cursor handling must be covered: absent before, a well-formed cursor, a malformed cursor, a page that ends exactly on a tie boundary, and a final page (results.length <= limit -> nextBefore === null). The five-tied-rows test is the named regression test for this fix.
Expected Outcome
Every recorded anchoring attempt — success or failure — is reachable by paging the public listing, including the four same-tick rows one scheduler run writes for the four backends. "Anchoring has been failing" stays publicly observable rather than being hidden by a page boundary.
Links & Resources
src/review/ledger-anchor-persistence.ts:44-141; src/api/routes.ts:1336-1355; src/utils/json.ts:14-16. Related closed work: #9271, epic #9267.
Context
GET /v1/public/decision-ledger/anchors(src/api/routes.ts:1341-1355) is the public listing of every anchoring attempt. Its route comment states the point: "This is what makes anchoring's own health a publicly checkable fact — a failure is recorded and served exactly like a success ... so 'anchoring has been failing for a week' is something anyone can observe."loadPublicLedgerAnchors(src/review/ledger-anchor-persistence.ts:98-141) implements keyset pagination whose cursor does not match its sort key: the query filtersWHERE created_at < ?(cursor:created_atonly) while orderingBY created_at DESC, id DESC, andnextBefore = page[page.length - 1]?.createdAt(:127).Because the ORDER BY tiebreaks on
idbut the WHERE clause filters oncreated_atalone, any group of rows sharing onecreated_atthat straddles a page boundary is silently skipped: the next page starts strictly before that timestamp, so the remaining tied rows are never returned on any page.Ties are not hypothetical here.
recordLedgerAnchorAttempt(:47) defaultscreatedAttonowIso()(src/utils/json.ts:14-16, millisecond precision), and one scheduler tick records an attempt per backend —LedgerAnchorBackendis"rekor" | "git" | "ots" | "bittensor"(:10), four rows written back to back. The function's own doc comment (:44-46) already concedes the collision risk:createdAtis injectable so a test can "control ordering deterministically instead of racing real wall-clock resolution across several inserts in a tight loop".So an attempt — including a failure, the row this endpoint exists to make visible — can be permanently unreachable through the paginated public API while sitting in the table.
Requirements
(created_at, id), notcreated_atalone.nextBeforemust carry both components so the next page resumes exactly after the last returned row. Encode them in one opaque string cursor (the field stays a singlestring | nullso the route and the published response shape are unchanged for a caller that only ever round-trips the value).(created_at < :ts) OR (created_at = :ts AND id < :id), preservingORDER BY created_at DESC, id DESC.beforevalue that does not parse as the new cursor must be treated as "no cursor" (first page) rather than throwing — this is an unauthenticated public route.LedgerAnchorListFilter.before's doc comment (:105) must be updated: it currently says "return rows strictly older than this ISO timestamp", which stops being true.recordLedgerAnchorAttempt, to the row shape, or to thebackend/limitfilters.Deliverables
loadPublicLedgerAnchorsfilters on the composite(created_at, id)row comparison and returns anextBeforecursor carrying both values.beforevalue yields the first page (no throw, no 500).created_at(using the injectablecreatedAtparameter), pages through them withlimit: 2, and asserts the concatenation of all pages contains all fiveids exactly once. This test fails on currentmain.created_atvalues still returns rows newest-first with no duplicates and terminates withnextBefore === null.backendfilter and thelimitclamp (:99) behave exactly as before, includinglimitaboveMAX_ANCHOR_LIST_LIMITand below 1.GET /v1/public/decision-ledger/anchors?before=<garbage>answers 200 with the first page.All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example changing the SQL without the five-tied-rows pagination test that proves no row is lost — does not resolve this issue.
Test Coverage Requirements
src/review/**andsrc/api/**are both inside Codecov'ssrc/**include; the 99% branch-counted patch gate applies. Every arm of the new cursor handling must be covered: absentbefore, a well-formed cursor, a malformed cursor, a page that ends exactly on a tie boundary, and a final page (results.length <= limit->nextBefore === null). The five-tied-rows test is the named regression test for this fix.Expected Outcome
Every recorded anchoring attempt — success or failure — is reachable by paging the public listing, including the four same-tick rows one scheduler run writes for the four backends. "Anchoring has been failing" stays publicly observable rather than being hidden by a page boundary.
Links & Resources
src/review/ledger-anchor-persistence.ts:44-141;src/api/routes.ts:1336-1355;src/utils/json.ts:14-16. Related closed work: #9271, epic #9267.