Skip to content

feat(api): cursor fingerprinting — CURSOR_STALE (policy R5) - #249

Merged
Levalicious merged 1 commit into
developfrom
feature/api-cursors
Oct 8, 2026
Merged

Levalicious merged 1 commit into
developfrom
feature/api-cursors

Conversation

@Levalicious

Copy link
Copy Markdown
Owner

The last piece of docs/api-error-policy.md: pagination can no longer serve a silently shifted page.

What changed

  • Cursors are now "<index>:<fnv32(result set)>" — the fingerprint is computed over the serialized bytes pagination boundaries are cut on. If the set changed between pages (content or order), the request fails with a visible CURSOR_STALE telling the caller to re-run and page from the start. Malformed cursors are rejected the same way; bare-number cursors are still accepted for legacy callers (unchecked, documented).
  • Prerequisite fix: the sort functions tie-broke with Math.random(), which made any multi-page drain of a default-sorted list inherently nondeterministic — the exact silent-corruption class R5 exists to catch. Tie-breaks are now deterministic (by name), so result sets have a stable total order. This also explains several historical pagination false alarms in the KB.
  • nextCursor type: string | null; cursor schemas accept number | string.

Evidence

  • New API cursor integrity (R5) tests: stale-after-mutation → CURSOR_STALE; unchanged set drains cleanly via string cursors (no dupes, no gaps); malformed cursor rejected; legacy numeric cursor still works.
  • Full suite 196/196; fuzz clean (protoErr = 0).

KB notes at release time: H_PaginationFalseAlarm loses its "exhaust cursors" caveat once sets are fingerprinted, and absence conclusions become safe for paginated reads.

🤖 Generated with Claude Code

Cursors become "<index>:<fnv32 of the full result set>" (computed over the
serialized bytes pagination cuts on). A page request whose fingerprint no
longer matches the current set errors with CURSOR_STALE instead of serving
a shifted page; malformed cursors are rejected; bare-number cursors are
accepted for legacy callers (unchecked).

Prerequisite shipped in the same change: all sort tie-breaks were
Math.random(), which made any multi-page drain of a default-sorted list
nondeterministic. Tie-breaks are now deterministic (name), giving result
sets a stable total order (cf. Determinacy).

- paginateItems: cursor parse + fingerprint verify; nextCursor: string|null
- schemas: cursor fields accept number|string, described as opaque
- tests: 4 new R5 cases (stale after mutation, clean drain via string
  cursors, malformed rejected, legacy numbers)
- full suite 196/196; fuzz clean

Co-Authored-By: Claude Code <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Per-op benchmark — p50 cycles (base vs head)

base runs: 5, head runs: 5 (median of mins) · freq-correction ×1.0062 · threshold head−base > max(40 cyc, 5%)

op base head* Δ Δ%
lookup 115.9 114 -1.9 -1.6%
read_entity 40 40 +0.0 +0.0%
neighbors_d1 850.5 831 -19.5 -2.3%
neighbors_d2 2658.2 2632 -26.2 -1.0%
find_path_d6 175298 188567 +13269.0 +7.6% 🔴 REGRESSION
search 1186756 1186943 +187.0 +0.0%
entities_by_type 14038 14126 +88.0 +0.6%
random_walk_d5 1479.5 1504 +24.5 +1.7%
inc_walker_visit 17.5 18 +0.5 +2.9%
structural_sample 2574019 2594208 +20189.0 +0.8%
compute_merw_psi 912233 925380 +13147.0 +1.4%
create_entity 857 863 +6.0 +0.7%
create_relation 808 838 +30.0 +3.7%

* head frequency-corrected to base via the reference op.

Regressions detected.

@Levalicious
Levalicious merged commit 4ef3def into develop Oct 8, 2026
11 of 12 checks passed
@Levalicious
Levalicious deleted the feature/api-cursors branch October 8, 2026 07:12
@Levalicious Levalicious mentioned this pull request Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant