Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
git-cas `ExpiringSet`. Accepted nonces remain retained for WARP's fixed
ten-minute acceptance window across process restart, cannot be evicted under
capacity pressure, and become collectible only after expiry and sweep.
- Changed active and named seek cursors from mutable WARP refs to immutable
git-cas pages retained by one cache set. Active positions have a 30-day
pinned lifetime; saved cursors remain pinned until explicitly dropped.
Reads do not extend retention, and bounded opportunistic sweeps collect
expired active pages.

### Removed

Expand All @@ -99,6 +104,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`nonceEvictions`, and the inert public `auth.wallClockMs` option. Authenticated
serving now fails closed unless runtime storage supplies durable replay
protection.
- Removed the `refs/warp/<graph>/cursor/*` protocol and its public ref builders.
v19 intentionally does not migrate these operator-only v18 cursor refs; they
are ignored as inert state while authoritative graph history remains intact.

### Fixed

Expand Down
4 changes: 2 additions & 2 deletions bin/cli/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ async function getHookStatusForCheck(repoPath: string, hookPaths: HookPathPort):

/** Handles the `check` command: reports graph health, GC, and hook status. */
export async function handleCheck({ options }: { options: CliOptions }): Promise<{ payload: unknown; exitCode: number }> {
const { graph, graphName, persistence, hookPaths } = await openGraph(options);
const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
const { graph, graphName, persistence, cursorStore, hookPaths } = await openGraph(options);
const cursorInfo = await applyCursorCeiling(graph, cursorStore);
emitCursorWarning(cursorInfo, null);
const health = await getHealth(persistence);
const gcMetrics = await getGcMetrics(graph);
Expand Down
9 changes: 4 additions & 5 deletions bin/cli/commands/checkpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ type CheckpointPayload =
| { graph: string; status: 'coverage-synced' };

async function assertNoActiveCursor(
persistence: Awaited<ReturnType<typeof openGraph>>['persistence'],
graphName: string,
cursorStore: Awaited<ReturnType<typeof openGraph>>['cursorStore'],
): Promise<void> {
const cursor = await readActiveCursor(persistence, graphName);
const cursor = await readActiveCursor(cursorStore);
if (cursor !== null) {
throw usageError('checkpoint create refuses to run while seek cursor is active; run git warp seek --latest first');
}
Expand All @@ -38,8 +37,8 @@ async function checkpointStatus(options: CliOptions): Promise<{ payload: Checkpo
}

async function createCheckpoint(options: CliOptions): Promise<{ payload: CheckpointPayload; exitCode: number }> {
const { graph, graphName, persistence } = await openGraph(options);
await assertNoActiveCursor(persistence, graphName);
const { graph, graphName, cursorStore } = await openGraph(options);
await assertNoActiveCursor(cursorStore);
await graph.materialize();
const checkpoint = await graph.createCheckpoint();
return {
Expand Down
4 changes: 2 additions & 2 deletions bin/cli/commands/debug/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ type WarpCoreRuntime = WarpCore & RuntimeHostProduct;
* Opens a graph with debug context including cursor state for exploratory analysis.
*/
export async function openDebugContext(options: CliOptions): Promise<{ graph: WarpGraphInstance; graphName: string; persistence: Persistence; activeCursor: CursorBlob | null }> {
const { graph, graphName, persistence } = await openGraph(options);
const activeCursor = await readActiveCursor(persistence, graphName);
const { graph, graphName, persistence, cursorStore } = await openGraph(options);
const activeCursor = await readActiveCursor(cursorStore);
emitCursorWarning({
active: activeCursor !== null,
tick: activeCursor?.tick ?? null,
Expand Down
13 changes: 6 additions & 7 deletions bin/cli/commands/gc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ type GcPayload =
| { graph: string; result: ReturnType<Awaited<ReturnType<typeof openGraph>>['graph']['runGC']> };

async function assertNoActiveCursor(
persistence: Awaited<ReturnType<typeof openGraph>>['persistence'],
graphName: string,
cursorStore: Awaited<ReturnType<typeof openGraph>>['cursorStore'],
): Promise<void> {
const cursor = await readActiveCursor(persistence, graphName);
const cursor = await readActiveCursor(cursorStore);
if (cursor !== null) {
throw usageError('gc refuses to run while seek cursor is active; run git warp seek --latest first');
}
Expand All @@ -37,8 +36,8 @@ async function gcStatus(options: CliOptions): Promise<{ payload: GcPayload; exit
}

async function maybeRunGc(options: CliOptions): Promise<{ payload: GcPayload; exitCode: number }> {
const { graph, graphName, persistence } = await openGraph(options);
await assertNoActiveCursor(persistence, graphName);
const { graph, graphName, cursorStore } = await openGraph(options);
await assertNoActiveCursor(cursorStore);
await graph.materialize();
const result = graph.maybeRunGC();
return {
Expand All @@ -48,8 +47,8 @@ async function maybeRunGc(options: CliOptions): Promise<{ payload: GcPayload; ex
}

async function runGc(options: CliOptions): Promise<{ payload: GcPayload; exitCode: number }> {
const { graph, graphName, persistence } = await openGraph(options);
await assertNoActiveCursor(persistence, graphName);
const { graph, graphName, cursorStore } = await openGraph(options);
await assertNoActiveCursor(cursorStore);
await graph.materialize();
const result = graph.runGC();
return {
Expand Down
4 changes: 2 additions & 2 deletions bin/cli/commands/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ function toHistoryEntry(entry: WriterPatchEntry, writer: string): HistoryEntry {
/** Handles `git warp history`: lists a writer's patch chain. */
export default async function handleHistory({ options, args }: { options: CliOptions; args: string[] }): Promise<{ payload: HistoryPayload; exitCode: number }> {
const { values } = parseCommandArgs(args, HISTORY_OPTIONS, historySchema);
const { graph, graphName, persistence } = await openGraph(options);
const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
const { graph, graphName, cursorStore } = await openGraph(options);
const cursorInfo = await applyCursorCeiling(graph, cursorStore);
emitCursorWarning(cursorInfo, null);

const node = values.node ?? null;
Expand Down
11 changes: 9 additions & 2 deletions bin/cli/commands/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import {
parseWriterIdFromRef,
} from '../../../src/domain/utils/RefLayout.ts';
import { notFoundError } from '../infrastructure.ts';
import { createPersistence, listGraphNames, readActiveCursor, readCheckpointDate } from '../shared.ts';
import {
createPersistence,
createSeekCursorStore,
listGraphNames,
readActiveCursor,
readCheckpointDate,
} from '../shared.ts';
import type { CliOptions, Persistence, GraphInfoResult } from '../types.ts';

/** Collects metadata about a single graph (writer count, refs, patches, checkpoint). */
Expand Down Expand Up @@ -106,7 +112,8 @@ export default async function handleInfo({ options }: { options: CliOptions }):
includeWriterPatches: isViewMode,
includeCheckpointDate: isViewMode,
});
const activeCursor = await readActiveCursor(persistence, name);
const cursorStore = createSeekCursorStore(runtimeStorage, name);
const activeCursor = await readActiveCursor(cursorStore);
if (activeCursor) {
info.cursor = {
active: true,
Expand Down
11 changes: 9 additions & 2 deletions bin/cli/commands/materialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import type { CorePersistence } from '../../../src/domain/types/WarpPersistence.
import { openRuntimeHostProduct } from '../../../src/domain/warp/RuntimeHostProduct.ts';
import type RuntimeStorageProviderPort from '../../../src/ports/RuntimeStorageProviderPort.ts';
import { EXIT_CODES, notFoundError } from '../infrastructure.ts';
import { createPersistence, listGraphNames, readActiveCursor, emitCursorWarning } from '../shared.ts';
import {
createPersistence,
createSeekCursorStore,
listGraphNames,
readActiveCursor,
emitCursorWarning,
} from '../shared.ts';
import type { CliOptions, Persistence } from '../types.ts';

/** Materializes a single graph, creates a checkpoint, and returns summary stats. */
Expand Down Expand Up @@ -67,7 +73,8 @@ export default async function handleMaterialize({ options }: { options: CliOptio
let cursorWarningEmitted = false;
for (const name of targets) {
try {
const cursor = await readActiveCursor(persistence, name);
const cursorStore = createSeekCursorStore(runtimeStorage, name);
const cursor = await readActiveCursor(cursorStore);
const ceiling = cursor ? cursor.tick : undefined;
if (cursor && !cursorWarningEmitted) {
emitCursorWarning({ active: true, tick: cursor.tick, maxTick: null }, null);
Expand Down
4 changes: 2 additions & 2 deletions bin/cli/commands/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export default async function handlePath({ options, args }: { options: CliOption
const { values, positionals } = parseCommandArgs(args, PATH_OPTIONS, pathSchema, { allowPositionals: true });
const from = endpointFrom(values, positionals);
const to = endpointTo(values, positionals);
const { graph, graphName, persistence } = await openGraph(options);
const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
const { graph, graphName, cursorStore } = await openGraph(options);
const cursorInfo = await applyCursorCeiling(graph, cursorStore);
emitCursorWarning(cursorInfo, null);
await graph.materialize();

Expand Down
4 changes: 2 additions & 2 deletions bin/cli/commands/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ function buildQueryBuilder(base: QueryBuilder, values: QueryValues, args: readon
/** Handles `git warp query`: runs the public query builder from the CLI. */
export default async function handleQuery({ options, args }: { options: CliOptions; args: string[] }): Promise<QueryCommandResult> {
const { values } = parseCommandArgs(args, QUERY_OPTIONS, querySchema);
const { graph, graphName, persistence } = await openGraph(options);
const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
const { graph, graphName, cursorStore } = await openGraph(options);
const cursorInfo = await applyCursorCeiling(graph, cursorStore);
emitCursorWarning(cursorInfo, null);
await graph.materialize();

Expand Down
4 changes: 2 additions & 2 deletions bin/cli/commands/reindex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import type { CliOptions } from '../types.ts';
export default async function handleReindex({ options, args }: { options: CliOptions; args: string[] }): Promise<{ payload: unknown; exitCode: number }> {
parseCommandArgs(args, {}, reindexSchema);

const { graph, graphName, persistence } = await openGraph(options);
const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
const { graph, graphName, cursorStore } = await openGraph(options);
const cursorInfo = await applyCursorCeiling(graph, cursorStore);
emitCursorWarning(cursorInfo, null);

// Clear cached index to force full rebuild
Expand Down
32 changes: 13 additions & 19 deletions bin/cli/commands/seek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { EXIT_CODES, usageError, notFoundError, parseCommandArgs } from '../infr
import { seekSchema } from '../schemas.ts';
import { openGraph, readActiveCursor, writeActiveCursor } from '../shared.ts';
import type { WarpState } from '../../../src/domain/services/JoinReducer.ts';
import type { CliOptions, Persistence, WarpGraphInstance, WriterTickInfo, CursorBlob, SeekSpec } from '../types.ts';
import type { CliOptions, WarpGraphInstance, WriterTickInfo, CursorBlob, SeekSpec } from '../types.ts';

// ============================================================================
// Seek Arg Parser
Expand Down Expand Up @@ -177,10 +177,9 @@ function applyDiffLimit(diff: StateDiffResult, diffBaseline: string, baselineTic
// ============================================================================

/** Handles the bare `seek` (no action flags) by returning current cursor status. */
async function handleSeekStatus({ graph, graphName, persistence, activeCursor, ticks, maxTick, perWriter, frontierHash }: {
async function handleSeekStatus({ graph, graphName, activeCursor, ticks, maxTick, perWriter, frontierHash }: {
graph: WarpGraphInstance;
graphName: string;
persistence: Persistence;
activeCursor: CursorBlob | null;
ticks: number[];
maxTick: number;
Expand All @@ -191,11 +190,6 @@ async function handleSeekStatus({ graph, graphName, persistence, activeCursor, t
await graph.materialize({ ceiling: activeCursor.tick });
const nodes = await graph.getNodes();
const edges = await graph.getEdges();
const prevCounts = readSeekCounts(activeCursor);
const prevFrontierHash = typeof activeCursor.frontierHash === 'string' ? activeCursor.frontierHash : null;
if (prevCounts.nodes === null || prevCounts.edges === null || prevCounts.nodes !== nodes.length || prevCounts.edges !== edges.length || prevFrontierHash !== frontierHash) {
await writeActiveCursor(persistence, graphName, { tick: activeCursor.tick, mode: activeCursor.mode ?? 'lamport', nodes: nodes.length, edges: edges.length, frontierHash });
}
const diff = computeSeekStateDiff(activeCursor, { nodes: nodes.length, edges: edges.length }, frontierHash);
const tickReceipt = await buildTickReceipt({ tick: activeCursor.tick, perWriter, graph });
return {
Expand Down Expand Up @@ -246,13 +240,13 @@ async function handleSeekStatus({ graph, graphName, persistence, activeCursor, t
/** Handles the `git warp seek` command across all sub-actions. */
export default async function handleSeek({ options, args }: { options: CliOptions; args: string[] }): Promise<{ payload: unknown; exitCode: number }> {
const seekSpec = parseSeekArgs(args);
const { graph, graphName, persistence } = await openGraph(options);
const { graph, graphName, cursorStore } = await openGraph(options);

const activeCursor = await readActiveCursor(persistence, graphName);
const activeCursor = await readActiveCursor(cursorStore);
const { ticks, maxTick, perWriter } = await graph.discoverTicks();
const frontierHash = await computeFrontierHash(perWriter);
if (seekSpec.action === 'list') {
const saved = await listSavedCursors(persistence, graphName);
const saved = await listSavedCursors(cursorStore);
return {
payload: {
graph: graphName,
Expand All @@ -266,11 +260,11 @@ export default async function handleSeek({ options, args }: { options: CliOption
}
if (seekSpec.action === 'drop') {
const dropName = seekSpec.name as string;
const existing = await readSavedCursor(persistence, graphName, dropName);
const existing = await readSavedCursor(cursorStore, dropName);
if (!existing) {
throw notFoundError(`Saved cursor not found: ${dropName}`);
}
await deleteSavedCursor(persistence, graphName, dropName);
await deleteSavedCursor(cursorStore, dropName);
return {
payload: {
graph: graphName,
Expand All @@ -287,7 +281,7 @@ export default async function handleSeek({ options, args }: { options: CliOption
if (seekSpec.diff) {
sdResult = await computeStructuralDiff({ graph, prevTick, currentTick: maxTick, diffLimit: seekSpec.diffLimit });
}
await clearActiveCursor(persistence, graphName);
await clearActiveCursor(cursorStore);
// When --diff already materialized at maxTick, skip redundant re-materialize
if (!sdResult) {
await graph.materialize({ ceiling: maxTick });
Expand Down Expand Up @@ -319,7 +313,7 @@ export default async function handleSeek({ options, args }: { options: CliOption
if (!activeCursor) {
throw usageError('No active cursor to save. Use --tick first.');
}
await writeSavedCursor(persistence, graphName, seekSpec.name as string, activeCursor);
await writeSavedCursor(cursorStore, seekSpec.name as string, activeCursor);
return {
payload: {
graph: graphName,
Expand All @@ -332,7 +326,7 @@ export default async function handleSeek({ options, args }: { options: CliOption
}
if (seekSpec.action === 'load') {
const loadName = seekSpec.name as string;
const saved = await readSavedCursor(persistence, graphName, loadName);
const saved = await readSavedCursor(cursorStore, loadName);
if (!saved) {
throw notFoundError(`Saved cursor not found: ${loadName}`);
}
Expand All @@ -347,7 +341,7 @@ export default async function handleSeek({ options, args }: { options: CliOption
}
const nodes = await graph.getNodes();
const edges = await graph.getEdges();
await writeActiveCursor(persistence, graphName, { tick: saved.tick, mode: saved.mode ?? 'lamport', nodes: nodes.length, edges: edges.length, frontierHash });
await writeActiveCursor(cursorStore, { tick: saved.tick, mode: saved.mode ?? 'lamport', nodes: nodes.length, edges: edges.length, frontierHash });
const diff = computeSeekStateDiff(activeCursor, { nodes: nodes.length, edges: edges.length }, frontierHash);
const tickReceipt = await buildTickReceipt({ tick: saved.tick, perWriter, graph });
return {
Expand Down Expand Up @@ -383,7 +377,7 @@ export default async function handleSeek({ options, args }: { options: CliOption
}
const nodes = await graph.getNodes();
const edges = await graph.getEdges();
await writeActiveCursor(persistence, graphName, { tick: resolvedTick, mode: 'lamport', nodes: nodes.length, edges: edges.length, frontierHash });
await writeActiveCursor(cursorStore, { tick: resolvedTick, mode: 'lamport', nodes: nodes.length, edges: edges.length, frontierHash });
const diff = computeSeekStateDiff(activeCursor, { nodes: nodes.length, edges: edges.length }, frontierHash);
const tickReceipt = await buildTickReceipt({ tick: resolvedTick, perWriter, graph });
return {
Expand All @@ -407,5 +401,5 @@ export default async function handleSeek({ options, args }: { options: CliOption
}

// status (bare seek)
return await handleSeekStatus({ graph, graphName, persistence, activeCursor, ticks, maxTick, perWriter, frontierHash });
return await handleSeekStatus({ graph, graphName, activeCursor, ticks, maxTick, perWriter, frontierHash });
}
Loading
Loading