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
54 changes: 43 additions & 11 deletions docs/topics/cas-first-memoized-materialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,19 @@ operation follows this coordinate-first lifecycle:
[current frontier]
|
v
[state-cache exact hit?] ---- yes ---> [reopen retained roots; zero patch replay]
[retained exact hit?] ------- yes ---> [load basis; reuse roots; zero patch replay]
|
no
v
[compatible predecessor?] --- yes ---> [replay suffix, publish snapshot]
[retained predecessor?] ----- yes ---> [load basis; replay suffix]
|
no
v
[state-cache exact hit?] ---- yes ---> [reopen roots; zero patch replay]
|
no
v
[state-cache predecessor?] -- yes ---> [replay suffix, publish snapshot]
|
no
v
Expand All @@ -62,7 +70,12 @@ WARP state coordinate:

This coordinate belongs to `git-warp`; it is not a `git-cas` concept.

### 2. Check the WARP state cache
### 2. Check retained materializations, then the WARP state cache

The compatibility path first asks `MaterializationStorePort` for an exact
retained materialization and then for a causally compatible retained
predecessor. Only when neither retained resume applies does it consult the
legacy `WarpStateCachePort`.

The runtime asks `WarpStateCachePort` for an exact snapshot at that coordinate.
On a hit, it asks `MaterializationStorePort` for the matching retained-root
Expand All @@ -84,6 +97,16 @@ that cached coordinate, then publish a fresh snapshot for the current frontier.
Until cache payloads carry provenance indexes, that derived snapshot retains a
degraded provenance posture rather than claiming support for the cached prefix.

Retained materializations can now satisfy the same two resume cases without a
separate state-cache hit. Descriptor schema v4 retains a canonical replay basis
beside the node, edge, and property roots. An exact retained hit validates and
loads that basis, reuses the retained roots, and performs no patch replay. When
there is no exact hit, the adapter inspects at most 1,024 current-schema cache
entries in pages of 100. It validates their descriptors and coordinates, checks
causal ancestry, and resumes the newest compatible predecessor by replaying only
the suffix. Receipt-producing reads still use the ordinary replay path, as do
diff-producing predecessor reads.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### 3. Fall back to replay and publish

When there is no usable cached snapshot, the runtime falls back to the existing
Expand Down Expand Up @@ -126,13 +149,14 @@ state-session opener owns its root storage and
encoding, so git-warp does not pair it with the default reader and instead
preserves the compatibility fallback.

The property-root contract advances the retained-materialization descriptor and
coordinate cache key to schema v3. A v3 exact miss leaves any corresponding v2
entry anchored until replacement succeeds. Successful v3 retention then removes
the incompatible v2 anchor through the git-cas cache API. A v2 descriptor may
still be structurally valid, but it cannot satisfy the v3 root profile because
its property root may be unavailable. New v3 descriptors reject an unavailable
property root; an empty graph records the root as explicitly empty.
The replay-basis contract advances the retained-materialization descriptor and
coordinate cache key to schema v4. A v4 exact miss leaves corresponding legacy
entries anchored until replacement succeeds. Successful v4 retention then
removes incompatible v2 and v3 anchors through the git-cas cache API. Legacy
descriptors may still be structurally valid, but they cannot satisfy the v4 root
profile because their property or replay-basis root may be unavailable. New v4
descriptors reject either root as unavailable; an empty graph still records the
property root as explicitly empty.

## `git-cas` Encapsulation

Expand Down Expand Up @@ -201,8 +225,13 @@ lost payload bytes, or run Git garbage collection.
other compatibility operations still own process-resident whole state.
- Exact state-cache hits bypass replay, but full materialization still hydrates
a full `WarpState`, scans retained node/edge tries, and builds full adjacency.
- Retained exact and predecessor resume load a complete canonical `WarpState`
replay basis before they reuse roots or replay a suffix. This eliminates
redundant prefix replay but is still a whole-state compatibility bridge, not
the bounded-memory observer representation.
- Retained materialization descriptors currently carry node/edge trie roots and
a per-node property-shard root. Frontier, edge-birth, adjacency,
a per-node property-shard root plus the full-state replay basis. Frontier,
edge-birth, adjacency,
provenance-support, and roaring roots remain explicitly unavailable until
their paged representations land. Cold property-root construction still
projects a complete `WarpState`; only exact retained reads avoid that state.
Expand All @@ -225,6 +254,9 @@ lost payload bytes, or run Git garbage collection.
sharded basis format should make optic reads avoid full-state hydration.
- Cache coordinates must stay schema/version aware. A snapshot is reusable only
when WARP semantics say the coordinate is compatible.
- Compatible-predecessor lookup is deliberately bounded to 1,024 inspected
materialization entries. Exceeding that bound fails closed instead of silently
selecting from an incomplete cache scan.
- Retention repair cannot restore payload objects that Git has already pruned;
those entries remain visible as doctor findings until normal cache lifecycle
replacement or explicit operator cleanup.
Expand Down
5 changes: 5 additions & 0 deletions src/domain/materialization/MaterializationRoots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const MATERIALIZATION_ROOT_NAMES = defineRootNames(
'node-alive',
'properties',
'provenance-support',
'replay-basis',
'roaring-indexes',
);

Expand All @@ -22,6 +23,7 @@ export type MaterializationRootsOptions = Readonly<{
nodeAlive: MaterializationRoot;
properties: MaterializationRoot;
provenanceSupport: MaterializationRoot;
replayBasis: MaterializationRoot;
roaringIndexes: MaterializationRoot;
}>;

Expand All @@ -35,6 +37,7 @@ export default class MaterializationRoots {
readonly nodeAlive: MaterializationRoot;
readonly properties: MaterializationRoot;
readonly provenanceSupport: MaterializationRoot;
readonly replayBasis: MaterializationRoot;
readonly roaringIndexes: MaterializationRoot;

constructor(options: MaterializationRootsOptions) {
Expand All @@ -47,6 +50,7 @@ export default class MaterializationRoots {
'node-alive': requireRoot(options.nodeAlive, 'nodeAlive'),
properties: requireRoot(options.properties, 'properties'),
'provenance-support': requireRoot(options.provenanceSupport, 'provenanceSupport'),
'replay-basis': requireRoot(options.replayBasis, 'replayBasis'),
'roaring-indexes': requireRoot(options.roaringIndexes, 'roaringIndexes'),
} satisfies Record<MaterializationRootName, MaterializationRoot>);
this.adjacency = this.roots.adjacency;
Expand All @@ -56,6 +60,7 @@ export default class MaterializationRoots {
this.nodeAlive = this.roots['node-alive'];
this.properties = this.roots.properties;
this.provenanceSupport = this.roots['provenance-support'];
this.replayBasis = this.roots['replay-basis'];
this.roaringIndexes = this.roots['roaring-indexes'];
Object.freeze(this);
}
Expand Down
14 changes: 13 additions & 1 deletion src/domain/materialization/TrieMaterializationReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ export default class TrieMaterializationReader extends MaterializationReadPort {
readonly #codec: CodecPort;
readonly #geometry: TrieGeometry;
readonly #indexStore: IndexStorePort | null;
readonly #pageCache: PageCache;

constructor(options: {
readonly store: TrieStorePort;
readonly codec: CodecPort;
readonly geometry?: TrieGeometry;
readonly indexStore?: IndexStorePort;
readonly pageCache?: PageCache;
}) {
super();
requireOptions(options);
Expand All @@ -40,6 +42,9 @@ export default class TrieMaterializationReader extends MaterializationReadPort {
this.#indexStore = options.indexStore === undefined
? null
: requireIndexStore(options.indexStore);
this.#pageCache = options.pageCache === undefined
? new PageCache({ maxResident: MAX_RESIDENT_READ_PAGES })
: requirePageCache(options.pageCache);
Object.freeze(this);
}

Expand All @@ -55,7 +60,7 @@ export default class TrieMaterializationReader extends MaterializationReadPort {
store: this.#store,
geometry: this.#geometry,
codec: this.#codec,
pageCache: new PageCache({ maxResident: MAX_RESIDENT_READ_PAGES }),
pageCache: this.#pageCache,
});
return await cursor.contains(nodeId);
}
Expand Down Expand Up @@ -142,6 +147,13 @@ function requireIndexStore(indexStore: IndexStorePort): IndexStorePort {
return indexStore;
}

function requirePageCache(pageCache: PageCache): PageCache {
if (!(pageCache instanceof PageCache)) {
throw readerError('pageCache must be a PageCache instance');
}
return pageCache;
}

function readerError(message: string): WarpError {
return new WarpError(`Materialization reader ${message}`, 'E_MATERIALIZATION_RESUME');
}
99 changes: 99 additions & 0 deletions src/domain/services/controllers/MaterializationRetention.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import MaterializationCoordinate from '../../materialization/MaterializationCoordinate.ts';
import type MaterializationHandle from '../../materialization/MaterializationHandle.ts';
import WarpError from '../../errors/WarpError.ts';
import {
materializationSessionOpen,
} from './MaterializeSessionBridge.ts';
import type {
MaterializeReduceOutput,
} from './MaterializeController.ts';
import type { MaterializeDeps } from './MaterializeDeps.ts';
import type {
MaterializeResultBuildInput,
} from './MaterializeStrategyRuntime.ts';

/** Publishes session roots through their git-cas workspace retention scope. */
export async function resolveMaterializationRetention(input: {
readonly deps: MaterializeDeps;
readonly params: MaterializeResultBuildInput;
readonly stateHash: string;
}): Promise<MaterializationHandle | undefined> {
const retained = resolveExistingMaterialization(input.params, input.stateHash);
return retained ?? await publishMaterialization(input);
}

function resolveExistingMaterialization(
params: MaterializeResultBuildInput,
stateHash: string,
): MaterializationHandle | undefined {
const retained = params.materialization;
if (retained === undefined) {
return undefined;
}
if (retained.stateHash !== stateHash) {
throw retentionError('retained handle state hash does not match resumed state');
}
if (!rootsMatch(params, retained)) {
return undefined;
}
params.reduced.acceptMaterialization?.(retained.retention);
return retained;
}

function rootsMatch(
params: MaterializeResultBuildInput,
retained: MaterializationHandle,
): boolean {
return params.reduced.roots === undefined
|| retained.roots.equals(params.reduced.roots);
}

async function publishMaterialization(input: {
readonly deps: MaterializeDeps;
readonly params: MaterializeResultBuildInput;
readonly stateHash: string;
}): Promise<MaterializationHandle | undefined> {
const { params } = input;
if (params.reduced.roots === undefined || params.frontier === null) {
await acceptSessionWithoutMaterialization(params.reduced);
return undefined;
}
const request = {
coordinate: new MaterializationCoordinate({
frontier: params.frontier,
ceiling: params.ceiling,
}),
roots: params.reduced.roots,
stateHash: input.stateHash,
replayBasis: params.reduced.state,
};
const materialization = params.reduced.workspace === undefined
? await input.deps.materializations.retain(request)
: await params.reduced.workspace.promote(request);
params.reduced.acceptMaterialization?.(materialization.retention);
return materialization;
}

async function acceptSessionWithoutMaterialization(
reduced: MaterializeReduceOutput,
): Promise<void> {
if (reduced.acceptMaterialization === undefined) {
return;
}
if (reduced.roots === undefined || reduced.workspace === undefined) {
throw retentionError('prepared session is missing roots or workspace retention');
}
const roots = materializationSessionOpen(reduced.roots);
if (roots === null) {
throw retentionError('prepared session roots cannot be checkpointed');
}
const witness = await reduced.workspace.checkpoint({
nodeAliveRoot: roots.nodeAliveRootOid,
edgeAliveRoot: roots.edgeAliveRootOid,
});
reduced.acceptMaterialization(witness);
}

function retentionError(message: string): WarpError {
return new WarpError(message, 'E_MATERIALIZATION_RESUME');
}
Loading
Loading