Skip to content

Retain seek cursors with git-cas - #777

Merged
flyingrobots merged 3 commits into
mainfrom
v19/expiry-seek-cursors
Jul 25, 2026
Merged

Retain seek cursors with git-cas#777
flyingrobots merged 3 commits into
mainfrom
v19/expiry-seek-cursors

Conversation

@flyingrobots

@flyingrobots flyingrobots commented Jul 25, 2026

Copy link
Copy Markdown
Member

Summary

  • add a graph-scoped seek-cursor storage port backed by immutable git-cas pages and the fixed git-warp.seek-cursors cache namespace
  • pin active cursors for 30 days, keep named cursors pinned until explicit drop, avoid read-based lifetime extension, and sweep on first use plus a bounded daily cadence
  • migrate every CLI cursor consumer to the runtime storage provider and fail closed when durable cursor retention is unavailable
  • remove the mutable refs/warp/<graph>/cursor/* protocol and its ref builders; v19 intentionally leaves old operator-only cursor refs inert rather than migrating non-authoritative state

Issue scope: replay admission landed in #776; this PR completes the remaining active/saved cursor half of #740.

Closes #740.

Validation

  • npm run typecheck
  • npm run lint
  • npm run lint:md
  • npm run lint:semgrep
  • npm run typecheck:surface
  • npm run typecheck:policy
  • npx vitest run test/unit test/integration — 632 passed files, 1 skipped; 7,475 passed tests, 2 skipped
  • PATH="$PWD/bin:$PATH" bats test/bats/ — 111/111 passed
  • review repair: cursor unit/integration 9/9; seek Bats 27/27, including an expiry-preservation probe backed by CacheSet.inspect()
  • pre-push IRONCLAD static gates and all stable unit shards passed

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@flyingrobots, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ff2a0c24-8105-48e9-bae0-e9515116b8a5

📥 Commits

Reviewing files that changed from the base of the PR and between 6205b23 and b95cfa2.

📒 Files selected for processing (8)
  • bin/cli/commands/seek.ts
  • src/domain/errors/PersistenceError.ts
  • src/domain/utils/validateSavedCursorName.ts
  • src/infrastructure/adapters/GitCasSeekCursorStoreAdapter.ts
  • test/bats/cli-seek.bats
  • test/bats/helpers/inspect-seek-cursor-expiry.ts
  • test/unit/domain/utils/validateSavedCursorName.test.ts
  • test/unit/infrastructure/adapters/GitCasSeekCursorStoreAdapter.test.ts
📝 Walkthrough

Walkthrough

Seek cursor state is migrated from Git refs to graph-scoped git-cas stores. The new adapter supports immutable pages, active-cursor expiry, named cursor retention, listing, validation, and sweeping. CLI commands and tests now use the cursor-store abstraction.

Changes

Seek cursor storage migration

Layer / File(s) Summary
Cursor store contract and CAS adapter
src/ports/*, src/infrastructure/adapters/*, src/domain/utils/*, bin/cli/types.ts
Defines cursor-store contracts and persists active and saved cursors as git-cas pages with pinning, expiry, validation, listing, deletion, and sweeping.
CLI cursor-store integration
bin/cli/shared.ts, bin/cli/commands/*
Routes graph opening, cursor ceilings, seek operations, active-cursor guards, and saved-cursor helpers through graph-scoped cursor stores.
Migration validation and retention documentation
test/unit/infrastructure/adapters/*, test/integration/infrastructure/adapters/*, test/bats/cli-seek.bats, test/unit/domain/utils/RefLayout.test.ts, docs/TECHNICAL_TEARDOWN.md, CHANGELOG.md
Tests CAS persistence, restart recovery, retention and sweep behavior, absence of cursor refs, and documents the v19 storage model and removed protocol.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Poem

A rabbit hops where cursor refs once lay,
CAS pages hold the trail today.
Active ticks expire, saved names stay,
Sweeps clear old paths without delay.
“Hop, hop!” says Bun, “the store’s in play!”

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The cursor-retention work matches #740, but the linked issue also requires replay-set/nonce-LRU changes and replay guarantees that are not shown here. Add the SyncAuthService replay-set migration, capacity-safe nonce retention behavior, and restart/sweep coverage required by #740.
Docstring Coverage ⚠️ Warning Docstring coverage is 51.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The changes stay focused on cursor storage migration, CLI rewiring, docs, and tests, with no obvious unrelated code paths added.
Title check ✅ Passed The title is concise and accurately summarizes the main change: retaining seek cursors with git-cas.
Description check ✅ Passed The description covers the summary, issue reference, and validation, but it omits the explicit ADR checks section required by the template.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Release Preflight

  • package version: 18.2.1
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If this PR is from a release/* branch and merges to main, Main Push Release Branch Check will run final preflight and create v18.2.1. A maintainer who is a JSR @git-stunts scope member must then dispatch the Release workflow manually.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bin/cli/commands/seek.ts`:
- Line 198: Update the status flow around writeActiveCursor so read-only status
checks do not refresh active-cursor retention. Keep recalculated frontier
metadata ephemeral, or preserve the existing expiration when persisting updated
metadata, while retaining normal write behavior elsewhere.

In `@src/infrastructure/adapters/GitCasSeekCursorStoreAdapter.ts`:
- Around line 171-176: Centralize the new error codes on PersistenceError by
adding static members for E_CURSOR_RETENTION and E_CURSOR_CORRUPT alongside
E_MISSING_OBJECT, E_REF_NOT_FOUND, and E_REF_IO. Update the raw string usages in
the seek-cursor retention and corruption paths to reference those members
consistently.
- Around line 86-103: The saved-cursor operations readSaved, writeSaved,
deleteSaved, and savedCursorName must validate bookmark names with a
cursor-name-specific validator instead of validateWriterId. Add or reuse
validation that reports invalid saved-cursor names in cursor-name terms, and
apply it consistently across all four methods while preserving the existing
validation behavior for writer IDs.
- Around line 139-189: Update _maintain so sweep failures are isolated from
foreground cursor operations: invoke this.sweep without allowing a rejected
sweep to propagate through _read, writeActive, writeSaved, listSaved, or
_remove. Ensure a failed maintenance attempt advances or otherwise throttles the
next sweep retry, while preserving normal successful sweep scheduling and
expiration behavior.
- Around line 139-147: Update _readPage to accept a PageHandle, remove
toString() from hit.handle when _read calls it, and add toString() to
entry.handle in listSaved() so both cache paths pass the normalized handle type.

In
`@test/integration/infrastructure/adapters/GitCasSeekCursorStoreAdapter.integration.test.ts`:
- Around line 1-108: Keep this integration test focused on restart persistence
and confirming no mutable cursor refs are written; do not add TTL or retention
coverage here because createSeekCursorStore lacks clock injection. Ensure the
unit tests for GitCasSeekCursorStoreAdapter directly cover 30-day active-cursor
expiry and retention sweep behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 745c8eca-0524-4079-84f9-e1732260193c

📥 Commits

Reviewing files that changed from the base of the PR and between 53fa3a0 and 6205b23.

📒 Files selected for processing (28)
  • CHANGELOG.md
  • bin/cli/commands/check.ts
  • bin/cli/commands/checkpoint.ts
  • bin/cli/commands/debug/shared.ts
  • bin/cli/commands/gc.ts
  • bin/cli/commands/history.ts
  • bin/cli/commands/info.ts
  • bin/cli/commands/materialize.ts
  • bin/cli/commands/path.ts
  • bin/cli/commands/query.ts
  • bin/cli/commands/reindex.ts
  • bin/cli/commands/seek.ts
  • bin/cli/commands/seekCursorHelpers.ts
  • bin/cli/commands/tree.ts
  • bin/cli/commands/verify-index.ts
  • bin/cli/shared.ts
  • bin/cli/types.ts
  • docs/TECHNICAL_TEARDOWN.md
  • src/domain/utils/RefLayout.ts
  • src/domain/utils/parseCursorBlob.ts
  • src/infrastructure/adapters/GitCasRepositoryAdapter.ts
  • src/infrastructure/adapters/GitCasSeekCursorStoreAdapter.ts
  • src/ports/RuntimeStorageProviderPort.ts
  • src/ports/SeekCursorStorePort.ts
  • test/bats/cli-seek.bats
  • test/integration/infrastructure/adapters/GitCasSeekCursorStoreAdapter.integration.test.ts
  • test/unit/domain/utils/RefLayout.test.ts
  • test/unit/infrastructure/adapters/GitCasSeekCursorStoreAdapter.test.ts
💤 Files with no reviewable changes (2)
  • test/unit/domain/utils/RefLayout.test.ts
  • src/domain/utils/RefLayout.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: preflight
  • GitHub Check: test-node (22)
  • GitHub Check: coverage-threshold
🧰 Additional context used
📓 Path-based instructions (5)
src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,tsx,js,jsx}: Do not introduce any, as any, as unknown as, unknown (outside adapters), Record<string, unknown> (outside adapters), *Like placeholder types, JSON.parse/JSON.stringify (outside adapters), fetch (outside adapters), process.env (outside adapters), @ts-ignore, or z.any() in core code; use validated boundary models and ports instead.
Use constructor-injected ports for external capabilities; do not rely on ambient dependencies for I/O, clocks, persistence, or entropy.
Do not create utils.ts, helpers.ts, misc.ts, or common.ts; name files after the actual concept they model.
Prefer one file per class, type, or object; if a file accumulates peer concepts, split it.
Keep helper corridors, fake shape trust, transitional duplication, and compile-time theater out of the codebase; runtime-honest TypeScript must reflect actual behavior.
No enum usage; prefer runtime-backed domain forms and unions.
Do not use boolean trap parameters; prefer named option objects or separate methods.
Avoid magic strings or numbers when a named constant should exist.
Keep domain bytes as Uint8Array; Buffer belongs in infrastructure adapters.

Files:

  • src/domain/utils/parseCursorBlob.ts
  • src/ports/RuntimeStorageProviderPort.ts
  • src/infrastructure/adapters/GitCasRepositoryAdapter.ts
  • src/ports/SeekCursorStorePort.ts
  • src/infrastructure/adapters/GitCasSeekCursorStoreAdapter.ts
src/domain/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/domain/**/*.{ts,tsx,js,jsx}: In src/domain/**, do not use Date.now(), new Date(), Date(), performance.now(), Math.random(), crypto.randomUUID(), crypto.getRandomValues(), setTimeout, setInterval, raw new Error(...)/new TypeError(...), or direct imports from Node built-ins; time, entropy, and external capabilities must enter through ports or parameters, and domain errors should extend WarpError.
Construct domain objects only in core when doing so establishes validated runtime truth; do not build infrastructure adapters, host APIs, persistence implementations, wall clocks, or entropy sources inside core.
Prefer discriminated unions and explicit result types instead of boolean-flag bags, and model expected failures as return values rather than exceptions.
src/domain/ must not import host APIs or Node-specific globals; hexagonal architecture boundaries are mandatory.
Domain code must not use the wall clock directly; time must enter through a port or parameter.

Files:

  • src/domain/utils/parseCursorBlob.ts
src/domain/**/!(*.test).{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use explicit domain concepts with validated constructors, Object.freeze, and instanceof dispatch; domain objects should be runtime-backed nouns, not ad hoc shape bags.

Files:

  • src/domain/utils/parseCursorBlob.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Do not use direct imports from src/infrastructure/** in src/domain/** or src/ports/**; depend on a port instead.
Do not use direct Node built-ins in src/domain/** or src/ports/**; use a port instead.

Files:

  • src/domain/utils/parseCursorBlob.ts
  • bin/cli/commands/tree.ts
  • bin/cli/commands/verify-index.ts
  • bin/cli/commands/query.ts
  • src/ports/RuntimeStorageProviderPort.ts
  • bin/cli/commands/path.ts
  • bin/cli/commands/materialize.ts
  • test/integration/infrastructure/adapters/GitCasSeekCursorStoreAdapter.integration.test.ts
  • bin/cli/commands/check.ts
  • bin/cli/commands/history.ts
  • bin/cli/commands/reindex.ts
  • bin/cli/commands/debug/shared.ts
  • bin/cli/types.ts
  • src/infrastructure/adapters/GitCasRepositoryAdapter.ts
  • bin/cli/commands/gc.ts
  • src/ports/SeekCursorStorePort.ts
  • bin/cli/commands/checkpoint.ts
  • bin/cli/commands/info.ts
  • bin/cli/commands/seekCursorHelpers.ts
  • test/unit/infrastructure/adapters/GitCasSeekCursorStoreAdapter.test.ts
  • bin/cli/commands/seek.ts
  • src/infrastructure/adapters/GitCasSeekCursorStoreAdapter.ts
  • bin/cli/shared.ts
src/ports/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

In src/ports/**, do not import Node built-ins or src/infrastructure/**; ports must stay abstract and depend only on boundary-safe types.

Files:

  • src/ports/RuntimeStorageProviderPort.ts
  • src/ports/SeekCursorStorePort.ts
🧠 Learnings (1)
📚 Learning: 2026-03-08T19:50:17.519Z
Learnt from: flyingrobots
Repo: git-stunts/git-warp PR: 65
File: CHANGELOG.md:88-88
Timestamp: 2026-03-08T19:50:17.519Z
Learning: Follow the Keep a Changelog convention for CHANGELOG.md. Allow duplicate subheadings across versions (e.g., '### Added', '### Fixed'). Configure markdownlint MD024 with {"siblings_only": true} to avoid cross-version false positives.

Applied to files:

  • CHANGELOG.md
🔇 Additional comments (26)
src/ports/SeekCursorStorePort.ts (1)

1-35: LGTM!

src/ports/RuntimeStorageProviderPort.ts (1)

17-17: LGTM!

Also applies to: 42-47

src/infrastructure/adapters/GitCasSeekCursorStoreAdapter.ts (1)

31-37: 🚀 Performance & Scalability | 🏗️ Heavy lift

Verify the cost of cross-graph sweeping on effectively every CLI invocation.

CURSOR_NAMESPACE is one constant shared by all graphs (scoping is only in the JSON key), and _nextSweepAtMs is in-memory, so it resets to 0 every time a new adapter is constructed. Since the CLI constructs a fresh GitCasRepositoryAdapter/GitCasSeekCursorStoreAdapter per process, this means: (a) sweep() — which operates on the entire shared git-warp.seek-cursors cache, not just the current graph — runs on virtually every CLI command touching a cursor rather than at the stated "bounded daily cadence", and (b) listSaved()'s cache.inspect() pagination must walk and client-side-filter entries from all graphs to find this graph's saved cursors. Whether this is a real cost depends on internal @git-stunts/git-cas sweep()/inspect() complexity, which isn't visible here.

Please confirm whether CacheSet.sweep()/inspect() in @git-stunts/git-cas v6.5.3 are cheap regardless of shared-namespace size (e.g. index-backed by expiry rather than a full linear scan), or whether per-graph namespacing / a persisted last-sweep marker is warranted.

Also applies to: 105-127, 185-199

src/infrastructure/adapters/GitCasRepositoryAdapter.ts (2)

5-5: LGTM!

Also applies to: 37-38, 85-85


137-152: 🎯 Functional Correctness

No change needed.

GitCasFacade.caches is compatible with Pick<CacheCapability, 'open'>, so the assertion is only a narrowing.

bin/cli/commands/info.ts (1)

12-18: LGTM!

Also applies to: 115-116

bin/cli/commands/materialize.ts (1)

6-12: LGTM!

Also applies to: 76-77

bin/cli/commands/tree.ts (1)

195-196: LGTM!

bin/cli/commands/verify-index.ts (1)

21-22: LGTM!

src/domain/utils/parseCursorBlob.ts (1)

2-2: LGTM!

bin/cli/types.ts (1)

3-3: LGTM!

Also applies to: 14-14

bin/cli/commands/seekCursorHelpers.ts (1)

5-30: LGTM!

bin/cli/commands/debug/shared.ts (1)

28-29: LGTM!

test/unit/infrastructure/adapters/GitCasSeekCursorStoreAdapter.test.ts (1)

1-224: LGTM!

test/bats/cli-seek.bats (1)

121-127: LGTM!

docs/TECHNICAL_TEARDOWN.md (1)

433-441: LGTM!

Also applies to: 469-482

CHANGELOG.md (1)

88-92: LGTM!

Also applies to: 107-109

bin/cli/shared.ts (1)

9-18: LGTM!

Also applies to: 121-121, 137-157, 177-187

bin/cli/commands/seek.ts (1)

19-20: LGTM!

Also applies to: 181-184, 250-256, 270-274, 291-291, 323-323, 336-336, 351-351, 387-387, 411-411

bin/cli/commands/check.ts (1)

149-150: LGTM!

bin/cli/commands/checkpoint.ts (1)

21-23: LGTM!

Also applies to: 40-41

bin/cli/commands/gc.ts (1)

21-23: LGTM!

Also applies to: 39-40, 50-51

bin/cli/commands/history.ts (1)

64-65: LGTM!

bin/cli/commands/path.ts (1)

71-72: LGTM!

bin/cli/commands/query.ts (1)

160-161: LGTM!

bin/cli/commands/reindex.ts (1)

13-14: LGTM!

Comment thread bin/cli/commands/seek.ts Outdated
Comment thread src/infrastructure/adapters/GitCasSeekCursorStoreAdapter.ts
Comment thread src/infrastructure/adapters/GitCasSeekCursorStoreAdapter.ts
Comment thread src/infrastructure/adapters/GitCasSeekCursorStoreAdapter.ts
Comment thread src/infrastructure/adapters/GitCasSeekCursorStoreAdapter.ts
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 25, 2026
@github-actions

Copy link
Copy Markdown

Release Preflight

  • package version: 18.2.1
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If this PR is from a release/* branch and merges to main, Main Push Release Branch Check will run final preflight and create v18.2.1. A maintainer who is a JSR @git-stunts scope member must then dispatch the Release workflow manually.

@github-actions

Copy link
Copy Markdown

Release Preflight

  • package version: 18.2.1
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If this PR is from a release/* branch and merges to main, Main Push Release Branch Check will run final preflight and create v18.2.1. A maintainer who is a JSR @git-stunts scope member must then dispatch the Release workflow manually.

@flyingrobots
flyingrobots merged commit 0c32cbd into main Jul 25, 2026
17 checks passed
@flyingrobots
flyingrobots deleted the v19/expiry-seek-cursors branch July 25, 2026 04:21
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.

Move cursor and replay retention to git-cas expiry collections

1 participant