Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Live exact node-property reads now retain collision-safe property shards as
an independently addressed materialization root. Each node uses its full
BLAKE3 route key and a deterministic schema-v2 entry-bag envelope; writes and
reads enforce a 16 MiB shard ceiling, while reads also bound CBOR structure
before decoding. Git-backed runtimes resolve one shard through exact `git-cas`
bundle lookup and release the acquisition without replaying patches, hydrating
`WarpState`, or populating a WARP-owned property cache. Newly built property
roots, once assembled, remain pinned by the operation workspace until final
promotion.
The flat property-root profile also rejects more than 100,000 shards before
asset staging. Materialization descriptor/cache schema v3 replaces incomplete
v2 entries and removes the
corresponding legacy cache anchor through git-cas only after successful v3
retention. Schema-v3 materializations require the property root to be either
retained or explicitly empty; they reject `unavailable` property roots.
- Exact state-cache hits now retain and reopen coordinate-keyed materialization
descriptors through the `git-cas` cache API. Repeated materialization,
including through a fresh runtime adapter, resumes the retained node/edge
Expand Down
73 changes: 54 additions & 19 deletions docs/topics/cas-first-memoized-materialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,37 @@ The bounded-memory read path is optic/worldline/query work over a sharded or
streamed basis. The state cache is the replay-skipping compatibility bridge for
legacy materialization and checkpoint flows.

`RuntimeHost.hasNode()` is the first compatibility read to consume the
handle-first path directly when the runtime uses the built-in trie-backed state
session and matching materialization reader. On an exact retained-coordinate
hit, it acquires the materialization, opens only the node-liveness trie through
a bounded page cache, answers one membership question, and releases the
acquisition. It does not hydrate `WarpState`, build adjacency, publish a state
snapshot, or populate `_cachedState`. A cold handle miss still performs the
current materialization path before retaining and reading the new root. A
custom 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.
`RuntimeHost.hasNode()` and `RuntimeHost.getNodeProps()` consume the handle-first
path directly when the runtime uses the built-in trie-backed state session and
matching materialization reader. On an exact retained-coordinate hit, node
liveness opens only the required trie path through a bounded page cache. A
property read uses the node's full BLAKE3 route key to resolve one per-node
property shard by exact bundle member path. The schema-v2 shard envelope stores
the property bag as sorted key/value entries, preserving legal keys such as
`__proto__` without treating them as object structure. Writes reject an encoded
shard over 16 MiB; reads enforce the same byte ceiling plus CBOR container,
depth, and item limits before general decoding. The reader owns no cache.

Both exact paths release their operation borrow without hydrating `WarpState`,
building adjacency, publishing a state snapshot, or populating `_cachedState`.
The runtime storage adapter keeps one git-cas acquisition for the current
coordinate, retires it after in-flight readers finish when the coordinate
changes, and releases it from `RuntimeHost.close()`. A cold handle miss still
performs the current materialization path before retaining and reading the new
roots. Once assembled, a newly built property root joins the operation's
expiring git-cas workspace before state hashing and final promotion, so the
completed root remains reachable throughout promotion. A custom
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.

## `git-cas` Encapsulation

Expand Down Expand Up @@ -175,16 +195,31 @@ lost payload bytes, or run Git garbage collection.

## Current Limitations

- RuntimeHost exact node-liveness reads consume the handle-first result when
the built-in trie session and reader pair is active. Custom session openers,
properties, neighborhoods, list reads, checkpoint creation, and other
compatibility operations still own process-resident whole state.
- RuntimeHost exact node-liveness and node-property reads consume the
handle-first result when the built-in trie session and reader pair is active.
Custom session openers, neighborhoods, list reads, checkpoint creation, and
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 materialization descriptors currently carry node/edge trie roots;
property, frontier, edge-birth, adjacency, provenance-support, and roaring
roots are explicitly marked unavailable until their paged representations
land.
- Retained materialization descriptors currently carry node/edge trie roots and
a per-node property-shard root. 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.
- One node's complete encoded property bag must currently fit within the 16 MiB
shard limit. Property-key pagination or a property trie is not yet available.
- The first property-root profile stores one bundle member per property-bearing
node and therefore inherits the configured git-cas bundle-member ceiling
(100,000 by default). The profile preflights this count before staging any
shard assets. A hierarchical property root is required to exceed that ceiling
without widening a repository-wide safety limit.
- Individual staged shards remain unanchored until git-cas assembles and the
workspace checkpoints their property bundle. Supported operation therefore
relies on Git's ordinary unreachable-object grace period; concurrent
immediate-expiry pruning is outside the current write contract. A git-cas
scoped staging workspace is tracked in
[git-cas issue 75](https://github.com/git-stunts/git-cas/issues/75) to shorten
that interval without moving CAS lifecycle ownership into git-warp.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
- `WarpStateCachePort` remains a legacy full-snapshot compatibility cache with
a WARP-owned index. Ordinary bounded observers cannot rely on it as their
final storage contract.
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
},
"dependencies": {
"@git-stunts/alfred": "^0.10.4",
"@git-stunts/git-cas": "^6.3.0",
"@git-stunts/git-cas": "^6.5.0",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"@git-stunts/plumbing": "^3.1.0",
"@git-stunts/trailer-codec": "^2.1.1",
"@noble/hashes": "^2.2.0",
Expand Down
16 changes: 16 additions & 0 deletions src/domain/RuntimeHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import type CommitMessageCodecPort from '../ports/CommitMessageCodecPort.ts';
import type CheckpointStorePort from '../ports/CheckpointStorePort.ts';
import type IndexStorePort from '../ports/IndexStorePort.ts';
import type IntentStorePort from '../ports/IntentStorePort.ts';
import type MaterializationStorePort from '../ports/MaterializationStorePort.ts';
import type RuntimeStorageProviderPort from '../ports/RuntimeStorageProviderPort.ts';
import type { EffectPipeline } from './services/EffectPipeline.ts';
import type WarpState from './services/state/WarpState.ts';
Expand Down Expand Up @@ -256,6 +257,8 @@ export default class RuntimeHost {
_intentStore: IntentStorePort;
_stateHashService: StateHashService | null;
_auditService: AuditReceiptService | null;
_materializations: MaterializationStorePort;
_closePromise: Promise<void> | null;

/**
* Constructs a RuntimeHost instance with injected dependencies and configuration.
Expand Down Expand Up @@ -402,6 +405,7 @@ export default class RuntimeHost {
persistence: this._persistence,
checkpointStore,
materializations,
propertyStore: indexStore,
...(options.materializationRead === undefined
? {}
: { materializationRead: options.materializationRead }),
Expand All @@ -423,12 +427,24 @@ export default class RuntimeHost {
this._checkpointStore = checkpointStore;
this._indexStore = indexStore;
this._auditService = auditService || null;
this._materializations = materializations;
this._closePromise = null;
}

_readLiveNodePresence(nodeId: string): Promise<boolean | null> {
return this._materializeController.readLiveNodePresence(nodeId);
}

_readLiveNodeProperties(nodeId: string) {
return this._materializeController.readLiveNodeProperties(nodeId);
}

/** Releases local runtime resources without changing admitted history. */
close(): Promise<void> {
this._closePromise ??= this._materializations.close();
return this._closePromise;
}

/**
* Advanced substrate replay primitive over the live frontier.
*/
Expand Down
43 changes: 43 additions & 0 deletions src/domain/materialization/MaterializationPropertyProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import RouteKey from '../orset/route/RouteKey.ts';
import IndexError from '../errors/IndexError.ts';

/** Maximum encoded size of one node-property shard retained for bounded reads. */
export const MAX_MATERIALIZATION_PROPERTY_SHARD_BYTES = 16 * 1024 * 1024;

/** Maximum members admitted by the first flat property-root bundle profile. */
export const MAX_MATERIALIZATION_PROPERTY_SHARDS = 100_000;

/** Decoder limits applied before a retained property shard reaches the CBOR codec. */
export const MATERIALIZATION_PROPERTY_SHARD_READ_LIMITS = Object.freeze({
maxBytes: MAX_MATERIALIZATION_PROPERTY_SHARD_BYTES,
maxContainerEntries: 100_000,
maxDepth: 64,
maxItems: 1_000_000,
});

/** Full BLAKE3 routing key used by the retained property-root profile. */
export function materializationPropertyShardKey(nodeId: string): string {
return RouteKey.fromElement(nodeId).toHex();
}

/** Exact bundle-member path for one node's retained property shard. */
export function materializationPropertyShardPath(nodeId: string): string {
return `props_${materializationPropertyShardKey(nodeId)}.cbor`;
}

/** Fails before asset staging when the flat property-root profile cannot admit the graph. */
export function requireMaterializationPropertyShardCount(count: number): void {
if (!Number.isSafeInteger(count) || count < 0) {
throw propertyShardCountError(count, 'must be a non-negative safe integer');
}
if (count > MAX_MATERIALIZATION_PROPERTY_SHARDS) {
throw propertyShardCountError(count, 'exceeds the flat property-root limit');
}
}

function propertyShardCountError(count: number, reason: string): IndexError {
return new IndexError(`Materialization property shard count ${reason}`, {
code: 'E_INDEX_SHARD_COUNT_LIMIT',
context: { actual: count, maximum: MAX_MATERIALIZATION_PROPERTY_SHARDS },
});
}
9 changes: 9 additions & 0 deletions src/domain/materialization/MaterializationRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ export default class MaterializationRoot {
static unavailable(): MaterializationRoot {
return new MaterializationRoot('unavailable', null);
}

equals(other: MaterializationRoot): boolean {
if (!(other instanceof MaterializationRoot) || other.status !== this.status) {
return false;
}
return this.handle === null
? other.handle === null
: other.handle !== null && this.handle.equals(other.handle);
}
}

function rootError(message: string): WarpError {
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 @@ -65,6 +65,11 @@ export default class MaterializationRoots {
MATERIALIZATION_ROOT_NAMES.map((name) => rootEntry(name, this.roots[name])),
);
}

equals(other: MaterializationRoots): boolean {
return other instanceof MaterializationRoots
&& MATERIALIZATION_ROOT_NAMES.every((name) => this.roots[name].equals(other.roots[name]));
}
}

function rootEntry(
Expand Down
51 changes: 51 additions & 0 deletions src/domain/materialization/TrieMaterializationReader.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import type CodecPort from '../../ports/CodecPort.ts';
import type IndexStorePort from '../../ports/IndexStorePort.ts';
import MaterializationReadPort from '../../ports/MaterializationReadPort.ts';
import BundleHandle from '../storage/BundleHandle.ts';
import WarpError from '../errors/WarpError.ts';
import PageCache from '../orset/trie/PageCache.ts';
import TrieCursor from '../orset/trie/TrieCursor.ts';
import TrieGeometry from '../orset/trie/TrieGeometry.ts';
import type TrieStorePort from '../orset/trie/TrieStorePort.ts';
import { decodeCurrentPropertyShard } from '../services/index/PropertyIndexReader.ts';
import type { PropValue } from '../types/PropValue.ts';
import {
materializationPropertyShardKey,
materializationPropertyShardPath,
MATERIALIZATION_PROPERTY_SHARD_READ_LIMITS,
} from './MaterializationPropertyProfile.ts';

const MAX_RESIDENT_READ_PAGES = 256;

Expand All @@ -14,11 +22,13 @@ export default class TrieMaterializationReader extends MaterializationReadPort {
readonly #store: TrieStorePort;
readonly #codec: CodecPort;
readonly #geometry: TrieGeometry;
readonly #indexStore: IndexStorePort | null;

constructor(options: {
readonly store: TrieStorePort;
readonly codec: CodecPort;
readonly geometry?: TrieGeometry;
readonly indexStore?: IndexStorePort;
}) {
super();
requireOptions(options);
Expand All @@ -27,6 +37,9 @@ export default class TrieMaterializationReader extends MaterializationReadPort {
this.#geometry = options.geometry === undefined
? TrieGeometry.default16way()
: requireGeometry(options.geometry);
this.#indexStore = options.indexStore === undefined
? null
: requireIndexStore(options.indexStore);
Object.freeze(this);
}

Expand All @@ -46,6 +59,33 @@ export default class TrieMaterializationReader extends MaterializationReadPort {
});
return await cursor.contains(nodeId);
}

override async getNodeProperties(
propertiesRoot: BundleHandle,
nodeId: string,
): Promise<Readonly<Record<string, PropValue>> | null | undefined> {
if (!(propertiesRoot instanceof BundleHandle)) {
throw readerError('properties root must be a BundleHandle');
}
if (this.#indexStore === null) {
return undefined;
}
const path = materializationPropertyShardPath(nodeId);
const encoded = await this.#indexStore.decodeShardAt(
propertiesRoot,
path,
MATERIALIZATION_PROPERTY_SHARD_READ_LIMITS,
);
if (encoded === null) {
return null;
}
const shard = decodeCurrentPropertyShard(
encoded,
path,
materializationPropertyShardKey,
);
return shard.get(nodeId) ?? null;
}
}

function requireOptions(options: object): void {
Expand Down Expand Up @@ -91,6 +131,17 @@ function requireGeometry(geometry: TrieGeometry): TrieGeometry {
return geometry;
}

function requireIndexStore(indexStore: IndexStorePort): IndexStorePort {
if (
indexStore === null
|| typeof indexStore !== 'object'
|| typeof indexStore.decodeShardAt !== 'function'
) {
throw readerError('indexStore must provide exact shard read operations');
}
return indexStore;
}

function readerError(message: string): WarpError {
return new WarpError(`Materialization reader ${message}`, 'E_MATERIALIZATION_RESUME');
}
4 changes: 2 additions & 2 deletions src/domain/orset/route/RouteKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ export default class RouteKey {
/**
* Return the route key as a lowercase hex string.
*
* Useful for logging and test assertions. Not for storage — storage
* uses the raw bytes.
* This is the canonical textual form for deterministic path names,
* diagnostics, and test assertions. Trie storage uses the raw bytes.
*/
toHex(): string {
let out = "";
Expand Down
6 changes: 4 additions & 2 deletions src/domain/orset/session/StateSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,12 @@ export default class StateSession {
const nodeFlusher = new TrieFlusher({
store: init.store,
codec: init.codec,
...(init.workspace === undefined ? {} : { staging: init.workspace }),
});
const edgeFlusher = new TrieFlusher({
store: init.store,
codec: init.codec,
...(init.workspace === undefined ? {} : { staging: init.workspace }),
});

return new StateSession({
Expand Down Expand Up @@ -439,9 +441,9 @@ function isPinnedWorkspaceWitness(
witness: Awaited<ReturnType<MaterializationWorkspacePort["checkpoint"]>>,
): witness is StorageRetentionWitness {
return witness instanceof StorageRetentionWitness &&
witness.policy === "pinned" &&
witness.policy === "evictable" &&
witness.reachability === "anchored" &&
witness.root.kind === "cache-set";
witness.root.kind === "root-set";
}

function requireFinalRetentionWitness(
Expand Down
Loading
Loading