diff --git a/README.md b/README.md
index 1032237..8130c36 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ direct libSQL authority also support Node 24 serverless runtimes. Install the
current immutable release directly from GitHub:
```sh
-bun add --global github:hraness/oh#v0.2.2
+bun add --global github:hraness/oh#v0.2.3
oh --help
```
@@ -100,7 +100,7 @@ For a project dependency, pin the same immutable release in `package.json`:
```json
{
"dependencies": {
- "@hraness/oh": "github:hraness/oh#v0.2.2"
+ "@hraness/oh": "github:hraness/oh#v0.2.3"
}
}
```
@@ -252,6 +252,78 @@ const result = await memory.query({
});
```
+When the model must bind a small set of values and traverse a larger result,
+use the additive V2 factory. The host still owns the query and rules. It names
+only query-body variables as parameters and fixes every evaluation, row, page,
+and page-byte limit before exposing the agent object:
+
+```ts
+import { createOhMemoryAgentV2 } from "@hraness/oh/experimental/memory";
+
+const memoryV2 = await createOhMemoryAgentV2({
+ actorId: "research.memory-agent",
+ canonical: {
+ authorityId: "project-reviewed",
+ expectedBindingSha256: canonical.store.binding.bindingSha256,
+ expectedHead: await canonical.store.head(),
+ store: canonical.store,
+ },
+ extractors: [valueChunkExtractor],
+ programs: [{
+ evaluation: {
+ maximumDerivedTuples: 8_192,
+ maximumProofDepth: 32,
+ maximumProofNodes: 1_024,
+ maximumResultBytes: 8 * 1024 * 1024,
+ maximumRounds: 64,
+ maximumTotalProofNodes: 16_384,
+ maximumWorkUnits: 1_000_000,
+ },
+ maximumPageBytes: 1024 * 1024,
+ maximumRows: 4_096,
+ pageSize: 128,
+ parameters: ["key", "lane"],
+ programId: "memory.value-chunks",
+ purpose: "answer.memory",
+ query: valueChunkQuery,
+ rulePack: valueChunkRules,
+ v: 2,
+ }],
+ working: {
+ authorityId: "thread-working",
+ codecs,
+ expectedBindingSha256: working.store.binding.bindingSha256,
+ store: working.store,
+ },
+});
+
+let continuation: string | null = null;
+do {
+ const page = await memoryV2.query({
+ bindings: { key: "entity:research", lane: "working" },
+ continuation,
+ programId: "memory.value-chunks",
+ v: 2,
+ });
+ continuation = page.continuation;
+} while (continuation !== null);
+```
+
+V2 evaluates one complete bounded result before paging it. Any projection row
+or byte truncation returns no page. A continuation is an authenticated bearer
+cursor that binds its offset to the exact physical heads, program, bindings,
+projection result, page size, and row count, so a working-head change fails
+instead of mixing snapshots. Pass it back only to the same exact named query;
+do not synthesize, edit, or log it.
+
+The factory generates a random continuation key by default, which makes a
+cursor valid only for that agent instance. If the host reconstructs agents or
+routes queries across replicas, pass the same host-owned 32 through 64 byte
+`Uint8Array` as `continuationKey`. The factory clones it; key rotation
+invalidates outstanding cursors. The result publishes a deterministic
+`continuationSha256` separately, and `resultSha256` commits that digest rather
+than the opaque key-dependent token.
+
The returned object has only `remember`, `query`, `explain`, and `nominate`.
The host fixes the working actor, each program purpose, and every nomination
destination before exposing those methods. `remember` accepts an idempotency
@@ -466,7 +538,7 @@ keep remote sync explicit.
You can also give an agent this prompt:
```text
-Install hraness/oh and its Oh Agent Skill from the immutable v0.2.2 tag at
+Install hraness/oh and its Oh Agent Skill from the immutable v0.2.3 tag at
https://github.com/hraness/oh. Verify the CLI with `oh --help` and `oh version`.
Do not create or modify an Oh database until I name its path and ask you to.
```
diff --git a/dist/cli.d.ts b/dist/cli.d.ts
index 23b8b75..90bdd35 100644
--- a/dist/cli.d.ts
+++ b/dist/cli.d.ts
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
-export declare const OH_PACKAGE_VERSION: "0.2.2";
+export declare const OH_PACKAGE_VERSION: "0.2.3";
export declare function runOhCli(arguments_: readonly string[]): Promise;
//# sourceMappingURL=cli.d.ts.map
\ No newline at end of file
diff --git a/dist/cli.js b/dist/cli.js
index 6f8aa58..db2c722 100755
--- a/dist/cli.js
+++ b/dist/cli.js
@@ -3072,7 +3072,7 @@ class Oh {
// src/cli.ts
import { readFile } from "fs/promises";
-var OH_PACKAGE_VERSION = "0.2.2";
+var OH_PACKAGE_VERSION = "0.2.3";
var KNOWN_OPTIONS = new Set([
"actor",
"after",
diff --git a/dist/memory.d.ts b/dist/memory.d.ts
index 3c2e0b8..3e72293 100644
--- a/dist/memory.d.ts
+++ b/dist/memory.d.ts
@@ -222,10 +222,145 @@ export interface OhMemoryAgentV1 {
query(value: unknown): Promise;
remember(value: unknown): Promise;
}
+/** Additive experimental query/pagination limits; V1 contracts are unchanged. */
+export declare const OH_MEMORY_QUERY_LIMITS_V2: Readonly<{
+ bindingBytes: number;
+ bindings: 32;
+ continuationBytes: number;
+ continuationKeyMaximumBytes: 64;
+ continuationKeyMinimumBytes: 32;
+ maximumPageBytes: number;
+ maximumPageRows: 256;
+ maximumProgramRows: 65536;
+ minimumPageBytes: number;
+ requestBytes: number;
+}>;
+export type OhMemoryEvaluationLimitsV2 = Readonly<{
+ maximumDerivedTuples: number;
+ maximumProofDepth: number;
+ maximumProofNodes: number;
+ maximumResultBytes: number;
+ maximumRounds: number;
+ maximumTotalProofNodes: number;
+ maximumWorkUnits: number;
+}>;
+/**
+ * A host-owned parameterized program. Parameter names refer only to variables
+ * in the query body, never to rule variables or projected output variables.
+ */
+export type OhMemoryNamedProgramV2 = Readonly<{
+ evaluation: OhMemoryEvaluationLimitsV2;
+ maximumPageBytes: number;
+ maximumRows: number;
+ pageSize: number;
+ parameters: readonly string[];
+ programId: string;
+ purpose: string;
+ query: OhProjectionQueryV1;
+ rulePack: OhProjectionRulePackV1;
+ v: 2;
+}>;
+export type OhMemoryFacadeOptionsV2 = Readonly & Readonly<{
+ /** Raw HMAC key for continuations that must survive agent reconstruction. */
+ continuationKey?: Uint8Array;
+ programs: readonly OhMemoryNamedProgramV2[];
+}>>;
+export type OhMemoryIdentityV2 = Readonly<{
+ bindings: Readonly>;
+ bindingsSha256: Sha256Hex;
+ boundQuerySha256: Sha256Hex;
+ canonical: OhMemoryLaneIdentityV1;
+ compositeDatasetSha256: Sha256Hex;
+ conflictPolicy: typeof OH_MEMORY_CONFLICT_POLICY_V1;
+ evaluationSha256: Sha256Hex;
+ memorySha256: Sha256Hex;
+ programId: string;
+ programSha256: Sha256Hex;
+ projectionSha256: Sha256Hex;
+ purpose: string;
+ rulePackSha256: Sha256Hex;
+ templateQuerySha256: Sha256Hex;
+ v: 2;
+ working: OhMemoryLaneIdentityV1;
+}>;
+export type OhMemoryResultRowV2 = Readonly<{
+ premiseAuthority: "canonical" | "unknown" | "working";
+ premiseLanes: readonly OhMemoryLaneV1[];
+ proofsTruncated: boolean;
+ resultRowSha256: Sha256Hex;
+ supportCount: number;
+ v: 2;
+ values: readonly OhProjectionAtomV1[];
+}>;
+export type OhMemoryPageV2 = Readonly<{
+ completeness: "complete" | "partial";
+ endExclusive: number;
+ hasMore: boolean;
+ maximumPageBytes: number;
+ pageSize: number;
+ returnedRows: number;
+ start: number;
+ totalRows: number;
+ truncation: Readonly<{
+ reasons: readonly [];
+ truncated: false;
+ v: 2;
+ }>;
+ v: 2;
+}>;
+export type OhMemoryQueryResultV2 = Readonly<{
+ authority: "derived";
+ conflicts: Readonly<{
+ count: number;
+ conflictsSha256: Sha256Hex;
+ v: 2;
+ }>;
+ continuation: string | null;
+ continuationSha256: Sha256Hex | null;
+ explainCapability: Readonly<{
+ expiresAt: string;
+ token: string;
+ v: 2;
+ }>;
+ identity: OhMemoryIdentityV2;
+ page: OhMemoryPageV2;
+ projectionResultSha256: Sha256Hex;
+ resultSha256: Sha256Hex;
+ rows: readonly OhMemoryResultRowV2[];
+ v: 2;
+}>;
+export type OhMemoryExplanationV2 = Readonly<{
+ authority: "derived";
+ explanationSha256: Sha256Hex;
+ identity: OhMemoryIdentityV2;
+ page: OhMemoryPageV2;
+ pageRow: number;
+ premiseAuthority: OhMemoryResultRowV2["premiseAuthority"];
+ premiseLanes: readonly OhMemoryLaneV1[];
+ proofs: readonly OhMemoryProofV1[];
+ proofsTruncated: boolean;
+ resultRowSha256: Sha256Hex;
+ resultSha256: Sha256Hex;
+ supportCount: number;
+ v: 2;
+ values: readonly OhProjectionAtomV1[];
+}>;
+export interface OhMemoryAgentV2 {
+ explain(value: unknown): Promise;
+ nominate(value: unknown): Promise;
+ query(value: unknown): Promise;
+ remember(value: unknown): Promise;
+}
/**
* Creates a model-facing memory surface over two host-bound physical Oh
* authorities. The returned object has no store, locator, rule, sync, canonical
* write, or purge handle.
*/
export declare function createOhMemoryAgentV1(options: OhMemoryFacadeOptionsV1): Promise;
+/**
+ * Creates the additive V2 memory facade. V2 adds only host-declared primitive
+ * bindings and fail-closed stable pagination; V1 request and digest contracts
+ * remain untouched.
+ */
+export declare function createOhMemoryAgentV2(options: OhMemoryFacadeOptionsV2): Promise;
//# sourceMappingURL=memory.d.ts.map
\ No newline at end of file
diff --git a/dist/memory.d.ts.map b/dist/memory.d.ts.map
index d65e9ce..fe9b04c 100644
--- a/dist/memory.d.ts.map
+++ b/dist/memory.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/dist/memory.js b/dist/memory.js
index 25d8bee..3f2444e 100644
--- a/dist/memory.js
+++ b/dist/memory.js
@@ -969,7 +969,7 @@ class OhSemanticBundleIngressV1 {
}
// src/memory.ts
-import { randomBytes as randomBytes2 } from "node:crypto";
+import { createHmac, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
// src/projection.ts
var OH_PROJECTION_FORMAT_VERSION_V1 = 1;
@@ -2283,6 +2283,18 @@ var OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1 = Object.freeze({
...memoryFactPackPayload,
extractorSha256: canonicalSha256(memoryFactPackPayload)
});
+var OH_MEMORY_QUERY_LIMITS_V2 = Object.freeze({
+ bindingBytes: 64 * 1024,
+ bindings: 32,
+ continuationBytes: 4 * 1024,
+ continuationKeyMaximumBytes: 64,
+ continuationKeyMinimumBytes: 32,
+ maximumPageBytes: 8 * 1024 * 1024,
+ maximumPageRows: 256,
+ maximumProgramRows: OH_PROJECTION_LIMITS_V1.queryResults,
+ minimumPageBytes: 64 * 1024,
+ requestBytes: 80 * 1024
+});
var builtInFactPolicy = Object.freeze({
extractorSha256: OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1.extractorSha256,
factPackId: OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1.factPackId,
@@ -2998,8 +3010,639 @@ async function createOhMemoryAgentV1(options) {
};
return Object.freeze({ explain, nominate, query, remember });
}
+function ownDataKeysV2(value, maximum, label) {
+ if (!isPlainRecord(value))
+ throw new TypeError(`${label} must be a plain data object.`);
+ const ownKeys = Reflect.ownKeys(value);
+ if (ownKeys.length > maximum)
+ throw new RangeError(`${label} has too many entries.`);
+ if (ownKeys.some((key) => typeof key !== "string")) {
+ throw new TypeError(`${label} must have only string data properties.`);
+ }
+ const keys = ownKeys;
+ for (const key of keys) {
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
+ if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) {
+ throw new TypeError(`${label} must have only enumerable data properties.`);
+ }
+ }
+ return keys;
+}
+function continuationKeyV2(value) {
+ if (value === undefined)
+ return Uint8Array.from(randomBytes2(32));
+ if (!(value instanceof Uint8Array) || value.byteLength < OH_MEMORY_QUERY_LIMITS_V2.continuationKeyMinimumBytes || value.byteLength > OH_MEMORY_QUERY_LIMITS_V2.continuationKeyMaximumBytes) {
+ throw new RangeError("The V2 memory continuation key must be 32 through 64 raw bytes.");
+ }
+ return Uint8Array.from(value);
+}
+function positiveBounded(value, maximum, label, minimum = 1) {
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
+ throw new RangeError(`${label} must be an integer from ${minimum} through ${maximum}.`);
+ }
+ return value;
+}
+function resolveEvaluationV2(value) {
+ if (!isPlainRecord(value) || !hasExactKeys(value, [
+ "maximumDerivedTuples",
+ "maximumProofDepth",
+ "maximumProofNodes",
+ "maximumResultBytes",
+ "maximumRounds",
+ "maximumTotalProofNodes",
+ "maximumWorkUnits"
+ ])) {
+ throw new TypeError("A V2 memory program must declare every projection evaluation limit.");
+ }
+ return Object.freeze({
+ maximumDerivedTuples: positiveBounded(value.maximumDerivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples, "maximumDerivedTuples"),
+ maximumProofDepth: positiveBounded(value.maximumProofDepth, OH_PROJECTION_LIMITS_V1.proofDepth, "maximumProofDepth"),
+ maximumProofNodes: positiveBounded(value.maximumProofNodes, OH_PROJECTION_LIMITS_V1.proofNodes, "maximumProofNodes"),
+ maximumResultBytes: positiveBounded(value.maximumResultBytes, OH_PROJECTION_LIMITS_V1.resultBytes, "maximumResultBytes", 64 * 1024),
+ maximumRounds: positiveBounded(value.maximumRounds, OH_PROJECTION_LIMITS_V1.rounds, "maximumRounds"),
+ maximumTotalProofNodes: positiveBounded(value.maximumTotalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes, "maximumTotalProofNodes"),
+ maximumWorkUnits: positiveBounded(value.maximumWorkUnits, OH_PROJECTION_LIMITS_V1.workUnits, "maximumWorkUnits")
+ });
+}
+function resolveProgramsV2(programs) {
+ if (!Array.isArray(programs) || programs.length < 1 || programs.length > OH_MEMORY_LIMITS_V1.maximumPrograms) {
+ throw new RangeError("Memory requires a bounded nonempty V2 named program registry.");
+ }
+ const resolved = new Map;
+ for (const candidate of programs) {
+ if (!isPlainRecord(candidate) || !hasExactKeys(candidate, [
+ "evaluation",
+ "maximumPageBytes",
+ "maximumRows",
+ "pageSize",
+ "parameters",
+ "programId",
+ "purpose",
+ "query",
+ "rulePack",
+ "v"
+ ]) || candidate.v !== 2 || !Array.isArray(candidate.parameters)) {
+ throw new TypeError("Invalid V2 named memory program.");
+ }
+ const programId = safeCode(candidate.programId, 128);
+ const purpose = safeCode(candidate.purpose, 256);
+ const query = parseOhProjectionQueryV1(candidate.query);
+ const rulePack = parseOhProjectionRulePackV1(candidate.rulePack);
+ const evaluation = resolveEvaluationV2(candidate.evaluation);
+ const maximumRows = positiveBounded(candidate.maximumRows, OH_MEMORY_QUERY_LIMITS_V2.maximumProgramRows, "maximumRows");
+ const pageSize = positiveBounded(candidate.pageSize, Math.min(maximumRows, OH_MEMORY_QUERY_LIMITS_V2.maximumPageRows), "pageSize");
+ const maximumPageBytes = positiveBounded(candidate.maximumPageBytes, OH_MEMORY_QUERY_LIMITS_V2.maximumPageBytes, "maximumPageBytes", OH_MEMORY_QUERY_LIMITS_V2.minimumPageBytes);
+ if (programId === null || purpose === null || query === null || rulePack === null || resolved.has(programId) || query.limit !== maximumRows || candidate.parameters.length > OH_MEMORY_QUERY_LIMITS_V2.bindings) {
+ throw new TypeError("Invalid or duplicate V2 named memory program.");
+ }
+ const parameters = candidate.parameters.map((parameter) => safeCode(parameter, 128)).sort();
+ if (parameters.some((parameter) => parameter === null) || new Set(parameters).size !== parameters.length) {
+ throw new TypeError("A V2 memory program has invalid or duplicate parameters.");
+ }
+ const queryVariables = new Set(query.where.flatMap((literal) => literal.terms.flatMap((term) => term.kind === "variable" ? [term.name] : [])));
+ if (parameters.some((parameter) => !queryVariables.has(parameter) || query.find.includes(parameter))) {
+ throw new TypeError("V2 parameters must be query-body variables that are not projected outputs.");
+ }
+ const detachedParameters = Object.freeze(parameters);
+ const programPayload = {
+ evaluation,
+ maximumPageBytes,
+ maximumRows,
+ pageSize,
+ parameters: detachedParameters,
+ programId,
+ purpose,
+ querySha256: query.querySha256,
+ rulePackSha256: rulePack.rulePackSha256,
+ v: 2
+ };
+ const program = immutableClone({
+ evaluation,
+ maximumPageBytes,
+ maximumRows,
+ pageSize,
+ parameters: detachedParameters,
+ programId,
+ programSha256: canonicalSha256(programPayload),
+ purpose,
+ query,
+ rulePack,
+ v: 2
+ });
+ resolved.set(programId, program);
+ }
+ return resolved;
+}
+function parsePrimitiveBindingV2(value) {
+ if (value !== null && typeof value !== "boolean" && typeof value !== "number" && typeof value !== "string")
+ throw new TypeError("Memory bindings must be JSON primitives.");
+ if (typeof value === "string" && value.length > OH_PROJECTION_LIMITS_V1.atomBytes) {
+ throw new RangeError("A memory binding exceeds the projection atom byte bound.");
+ }
+ if (typeof value === "number" && (!Number.isFinite(value) || Object.is(value, -0))) {
+ throw new TypeError("Memory bindings must be canonical finite JSON numbers.");
+ }
+ const serialized = canonicalJson(value);
+ if (utf8ByteLength(serialized) > OH_PROJECTION_LIMITS_V1.atomBytes) {
+ throw new RangeError("A memory binding exceeds the projection atom byte bound.");
+ }
+ return value;
+}
+function parseQueryRequestV2(value) {
+ let keys;
+ try {
+ keys = ownDataKeysV2(value, 4, "The parameterized memory query");
+ } catch {
+ throw new TypeError("Invalid parameterized memory query.");
+ }
+ if (keys.length !== 4 || !["bindings", "continuation", "programId", "v"].every((key) => keys.includes(key))) {
+ throw new TypeError("Invalid parameterized memory query.");
+ }
+ const record = value;
+ if (record.v !== 2 || record.continuation !== null && typeof record.continuation !== "string") {
+ throw new TypeError("Invalid parameterized memory query.");
+ }
+ const programId = safeCode(record.programId, 128);
+ if (programId === null)
+ throw new TypeError("Invalid parameterized memory query identity.");
+ const continuation = record.continuation;
+ if (typeof continuation === "string" && (continuation.length < 1 || continuation.length > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes || utf8ByteLength(continuation) > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes)) {
+ throw new RangeError("The memory continuation exceeds its byte bound.");
+ }
+ const bindingKeys = ownDataKeysV2(record.bindings, OH_MEMORY_QUERY_LIMITS_V2.bindings, "The parameterized memory query bindings");
+ const bindingRecord = record.bindings;
+ const bindings = {};
+ for (const key of bindingKeys) {
+ if (safeCode(key, 128) === null)
+ throw new TypeError("Invalid memory binding name.");
+ bindings[key] = parsePrimitiveBindingV2(bindingRecord[key]);
+ }
+ const boundedRequest = { bindings, continuation, programId, v: 2 };
+ if (utf8ByteLength(canonicalJson(boundedRequest)) > OH_MEMORY_QUERY_LIMITS_V2.requestBytes) {
+ throw new RangeError("The parameterized memory query exceeds its canonical byte bound.");
+ }
+ return { bindingsValue: immutableClone(bindings), continuation, programId };
+}
+function parseBindingsV2(value, parameters) {
+ if (!isPlainRecord(value) || !hasExactKeys(value, parameters)) {
+ throw new TypeError("Memory query bindings must exactly match the host-declared parameters.");
+ }
+ const bindings = {};
+ for (const parameter of parameters)
+ bindings[parameter] = value[parameter];
+ if (utf8ByteLength(canonicalJson(bindings)) > OH_MEMORY_QUERY_LIMITS_V2.bindingBytes) {
+ throw new RangeError("Memory query bindings exceed their canonical byte bound.");
+ }
+ const detached = immutableClone(bindings);
+ return Object.freeze({
+ bindings: detached,
+ bindingsSha256: canonicalSha256({ bindings: detached, parameters, v: 2 })
+ });
+}
+function bindQueryV2(query, bindings) {
+ const where = query.where.map((literal) => createOhProjectionLiteralV1({
+ relation: literal.relation,
+ terms: literal.terms.map((term) => term.kind === "variable" && Object.hasOwn(bindings, term.name) ? ohProjectionConstantV1(bindings[term.name]) : term)
+ }));
+ return createOhProjectionQueryV1({
+ find: query.find,
+ limit: query.limit,
+ queryId: query.queryId,
+ where
+ });
+}
+function publicRowV2(row, proofs) {
+ const lanes = new Set;
+ let unknown = row.proofsTruncated;
+ for (const proof of proofs)
+ unknown = collectLanes(proof, lanes) || unknown;
+ const premiseLanes = [...lanes].sort();
+ const premiseAuthority = unknown || premiseLanes.length === 0 ? "unknown" : premiseLanes.includes("working") ? "working" : "canonical";
+ const payload = {
+ premiseAuthority,
+ premiseLanes,
+ proofsTruncated: row.proofsTruncated,
+ supportCount: row.supportCount,
+ v: 2,
+ values: row.values
+ };
+ return Object.freeze({ ...payload, resultRowSha256: canonicalSha256(payload) });
+}
+function continuationHmacV2(key, value) {
+ return createHmac("sha256", key).update("oh.memory.continuation.v2\x00", "utf8").update(canonicalJson(value), "utf8").digest();
+}
+function encodeContinuationV2(value, key) {
+ const identity = immutableClone(value);
+ const continuationSha256 = canonicalSha256(identity);
+ const signed = immutableClone({ ...identity, continuationSha256 });
+ const envelope = immutableClone({
+ ...signed,
+ continuationHmacSha256: continuationHmacV2(key, signed).toString("hex")
+ });
+ const continuation = Buffer.from(canonicalJson(envelope), "utf8").toString("base64url");
+ if (utf8ByteLength(continuation) > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes) {
+ throw new RangeError("The issued memory continuation exceeds its byte bound.");
+ }
+ return Object.freeze({ continuation, continuationSha256 });
+}
+function parseContinuationV2(value, key) {
+ if (value.length < 1 || utf8ByteLength(value) > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes || !/^[A-Za-z0-9_-]+$/u.test(value))
+ throw new TypeError("Invalid memory continuation encoding.");
+ const bytes = Buffer.from(value, "base64url");
+ if (bytes.toString("base64url") !== value || bytes.byteLength > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes) {
+ throw new TypeError("Invalid memory continuation encoding.");
+ }
+ const text = bytes.toString("utf8");
+ let decoded;
+ try {
+ decoded = JSON.parse(text);
+ } catch {
+ throw new TypeError("Invalid memory continuation JSON.");
+ }
+ if (!isPlainRecord(decoded) || !hasExactKeys(decoded, [
+ "bindingsSha256",
+ "continuationHmacSha256",
+ "continuationSha256",
+ "memorySha256",
+ "nextOffset",
+ "pageSize",
+ "programSha256",
+ "projectionResultSha256",
+ "totalRows",
+ "v"
+ ]) || decoded.v !== 2)
+ throw new TypeError("Invalid memory continuation payload.");
+ const bindingsSha256 = parseSha256Hex(decoded.bindingsSha256);
+ const continuationHmacSha256 = parseSha256Hex(decoded.continuationHmacSha256);
+ const continuationSha256 = parseSha256Hex(decoded.continuationSha256);
+ const memorySha256 = parseSha256Hex(decoded.memorySha256);
+ const programSha256 = parseSha256Hex(decoded.programSha256);
+ const projectionResultSha256 = parseSha256Hex(decoded.projectionResultSha256);
+ if (bindingsSha256 === null || continuationHmacSha256 === null || continuationSha256 === null || memorySha256 === null || programSha256 === null || projectionResultSha256 === null || !Number.isSafeInteger(decoded.nextOffset) || decoded.nextOffset < 1 || decoded.nextOffset > OH_MEMORY_QUERY_LIMITS_V2.maximumProgramRows || !Number.isSafeInteger(decoded.pageSize) || decoded.pageSize < 1 || decoded.pageSize > OH_MEMORY_QUERY_LIMITS_V2.maximumPageRows || !Number.isSafeInteger(decoded.totalRows) || decoded.totalRows < 1 || decoded.totalRows > OH_MEMORY_QUERY_LIMITS_V2.maximumProgramRows || decoded.nextOffset >= decoded.totalRows || decoded.nextOffset % decoded.pageSize !== 0) {
+ throw new TypeError("Invalid memory continuation identity.");
+ }
+ const identity = {
+ bindingsSha256,
+ memorySha256,
+ nextOffset: decoded.nextOffset,
+ pageSize: decoded.pageSize,
+ programSha256,
+ projectionResultSha256,
+ totalRows: decoded.totalRows,
+ v: 2
+ };
+ const signed = { ...identity, continuationSha256 };
+ const envelope = { ...signed, continuationHmacSha256 };
+ if (canonicalJson(envelope) !== text)
+ throw new TypeError("Invalid memory continuation payload.");
+ const expectedHmac = continuationHmacV2(key, signed);
+ const receivedHmac = Buffer.from(continuationHmacSha256, "hex");
+ if (!timingSafeEqual(expectedHmac, receivedHmac)) {
+ throw new OhIntegrityError("The memory continuation is not an issued capability.");
+ }
+ if (canonicalSha256(identity) !== continuationSha256) {
+ throw new OhIntegrityError("The memory continuation digest is invalid.");
+ }
+ return Object.freeze(signed);
+}
+function parseExplainRequestV2(value) {
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["pageRow", "resultSha256", "token", "v"]) || value.v !== 2 || typeof value.token !== "string" || value.token.length !== 43 || !Number.isSafeInteger(value.pageRow) || value.pageRow < 0) {
+ throw new TypeError("Invalid V2 memory explanation request.");
+ }
+ const resultSha256 = parseSha256Hex(value.resultSha256);
+ if (resultSha256 === null)
+ throw new TypeError("Invalid V2 memory explanation result identity.");
+ return { pageRow: value.pageRow, resultSha256, token: value.token };
+}
+async function createOhMemoryAgentV2(options) {
+ const memoryActorId = safeCode(options.actorId, 128);
+ if (memoryActorId === null)
+ throw new TypeError("Invalid host-bound memory actor ID.");
+ const continuationKey = continuationKeyV2(options.continuationKey);
+ const canonicalStore = options.canonical.store;
+ const workingStore = options.working.store;
+ const workingCodecs = options.working.codecs;
+ const canonicalAuthorityId = authorityId(options.canonical.authorityId);
+ const workingAuthorityId = authorityId(options.working.authorityId);
+ if (canonicalAuthorityId === workingAuthorityId) {
+ throw new OhProfileError("Working and canonical memory must be distinct physical authorities.");
+ }
+ const canonicalBinding = bindingFor(canonicalStore, options.canonical.expectedBindingSha256, "canonical");
+ const workingBinding = bindingFor(workingStore, options.working.expectedBindingSha256, "working");
+ const expectedCanonicalHead = parseOhHeadV1(options.canonical.expectedHead);
+ if (expectedCanonicalHead === null)
+ throw new TypeError("Invalid pinned canonical memory head.");
+ const programs = resolveProgramsV2(options.programs);
+ const extractors = resolveExtractors(options.extractors ?? []);
+ const nominationRoutes = resolveNominationRoutes(options.nominationRoutes ?? []);
+ const ingress = new OhSemanticBundleIngressV1(workingStore, workingCodecs);
+ const now = options.now ?? (() => new Date);
+ const monotonicNow = options.monotonicNow ?? (() => performance.now());
+ const capabilityLifetime = options.explainCapabilityLifetimeMs ?? OH_MEMORY_LIMITS_V1.explainCapabilityLifetimeMs;
+ if (!Number.isSafeInteger(capabilityLifetime) || capabilityLifetime < 1000 || capabilityLifetime > 60 * 60 * 1000) {
+ throw new RangeError("Invalid memory explanation capability lifetime.");
+ }
+ const canonical = await readLane({
+ authorityId: canonicalAuthorityId,
+ binding: canonicalBinding,
+ store: canonicalStore
+ }, "canonical", expectedCanonicalHead);
+ const explanations = new Map;
+ let explanationBytes = 0;
+ let lastMonotonicMs = -1;
+ let lastWallClockMs = Number.NEGATIVE_INFINITY;
+ const wallClock = () => {
+ const milliseconds = clockMilliseconds(now);
+ if (milliseconds < lastWallClockMs)
+ throw new OhProfileError("The memory wall clock regressed.");
+ lastWallClockMs = milliseconds;
+ return milliseconds;
+ };
+ const monotonicClock = () => {
+ const milliseconds = monotonicMilliseconds(monotonicNow);
+ if (milliseconds < lastMonotonicMs)
+ throw new OhProfileError("The memory monotonic clock regressed.");
+ lastMonotonicMs = milliseconds;
+ return milliseconds;
+ };
+ const deleteExplanation = (token) => {
+ const stored = explanations.get(token);
+ if (stored !== undefined && explanations.delete(token))
+ explanationBytes -= stored.bytes;
+ };
+ const remember = async (value) => {
+ if (utf8ByteLength(canonicalJson(value)) > OH_MEMORY_LIMITS_V1.rememberBytes) {
+ throw new RangeError("The memory semantic bundle exceeds its canonical byte bound.");
+ }
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["expectedHead", "puts", "requestId", "tombstones", "v"]) || value.v !== 1) {
+ throw new TypeError("Invalid memory remember request.");
+ }
+ const requestId = safeCode(value.requestId, 128);
+ if (requestId === null)
+ throw new TypeError("Invalid memory remember request identity.");
+ const operationId = `memory_${canonicalSha256({
+ actorId: memoryActorId,
+ bindingSha256: workingBinding.bindingSha256,
+ requestId,
+ v: 1
+ }).slice(0, 48)}`;
+ const operation = await ingress.commit({
+ actorId: memoryActorId,
+ expectedHead: value.expectedHead,
+ instant: isoInstant(new Date(wallClock())),
+ operationId,
+ puts: value.puts,
+ tombstones: value.tombstones,
+ v: 1
+ });
+ const head = {
+ generation: operation.sequence,
+ graphRevisionSha256: operation.graphRevisionSha256,
+ operationSha256: operation.operationSha256,
+ recordsSha256: operation.recordsSha256,
+ sequence: operation.sequence,
+ v: 1
+ };
+ const payload = {
+ actorId: operation.actorId,
+ authorityId: workingAuthorityId,
+ bindingSha256: workingBinding.bindingSha256,
+ head,
+ instant: operation.instant,
+ lane: "working",
+ operationSha256: operation.operationSha256,
+ requestId,
+ status: "committed",
+ v: 1
+ };
+ return immutableClone({ ...payload, receiptSha256: canonicalSha256(payload) });
+ };
+ const query = async (value) => {
+ const request = parseQueryRequestV2(value);
+ const program = programs.get(request.programId);
+ if (program === undefined)
+ throw new TypeError("Unknown named V2 memory program.");
+ const bound = parseBindingsV2(request.bindingsValue, program.parameters);
+ const requestedContinuation = request.continuation === null ? null : parseContinuationV2(request.continuation, continuationKey);
+ if (requestedContinuation !== null && (requestedContinuation.bindingsSha256 !== bound.bindingsSha256 || requestedContinuation.pageSize !== program.pageSize || requestedContinuation.programSha256 !== program.programSha256 || requestedContinuation.totalRows > program.maximumRows || requestedContinuation.nextOffset >= requestedContinuation.totalRows || requestedContinuation.nextOffset % program.pageSize !== 0)) {
+ throw new OhIntegrityError("The memory continuation does not match this exact program, binding, and page identity.");
+ }
+ const boundQuery = bindQueryV2(program.query, bound.bindings);
+ const working = await readLane({
+ authorityId: workingAuthorityId,
+ binding: workingBinding,
+ store: workingStore
+ }, "working");
+ const composite = createCompositeDataset(canonical, working, extractors);
+ const projection = evaluateOhProjectionV1({
+ dataset: composite.dataset,
+ options: program.evaluation,
+ query: boundQuery,
+ rulePack: program.rulePack,
+ snapshot: composite.snapshot
+ });
+ if (projection.stats.truncated) {
+ const reasons = projection.stats.truncationReasons.join(", ");
+ throw new RangeError(`The V2 memory projection was truncated (${reasons}); no page was returned.`);
+ }
+ if (projection.rows.length > program.maximumRows) {
+ throw new RangeError("The V2 memory projection exceeds its host-declared row bound.");
+ }
+ const identityPayload = {
+ bindings: bound.bindings,
+ bindingsSha256: bound.bindingsSha256,
+ boundQuerySha256: boundQuery.querySha256,
+ canonical: laneIdentity(canonical),
+ compositeDatasetSha256: composite.dataset.datasetSha256,
+ conflictPolicy: OH_MEMORY_CONFLICT_POLICY_V1,
+ evaluationSha256: projection.identity.evaluationSha256,
+ programId: program.programId,
+ programSha256: program.programSha256,
+ projectionSha256: projection.identity.projectionSha256,
+ purpose: program.purpose,
+ rulePackSha256: program.rulePack.rulePackSha256,
+ templateQuerySha256: program.query.querySha256,
+ v: 2,
+ working: laneIdentity(working)
+ };
+ const identity = immutableClone({
+ ...identityPayload,
+ memorySha256: canonicalSha256(identityPayload)
+ });
+ if (requestedContinuation !== null && (requestedContinuation.memorySha256 !== identity.memorySha256 || requestedContinuation.projectionResultSha256 !== projection.resultSha256)) {
+ throw new OhIntegrityError("The memory continuation does not match this exact source and projection identity.");
+ }
+ if (requestedContinuation !== null && (requestedContinuation.totalRows !== projection.rows.length || requestedContinuation.nextOffset >= projection.rows.length || requestedContinuation.nextOffset % program.pageSize !== 0)) {
+ throw new OhIntegrityError("The memory continuation does not match this exact row identity.");
+ }
+ const start = requestedContinuation?.nextOffset ?? 0;
+ const endExclusive = Math.min(start + program.pageSize, projection.rows.length);
+ const projectionRows = projection.rows.slice(start, endExclusive);
+ const proofs = immutableClone(projectionRows.map((row) => row.proofs.map((proof) => mapProof(proof, composite.sources, composite.factPolicies))));
+ const rows = immutableClone(projectionRows.map((row, index) => publicRowV2(row, proofs[index])));
+ const hasMore = endExclusive < projection.rows.length;
+ const page = immutableClone({
+ completeness: hasMore ? "partial" : "complete",
+ endExclusive,
+ hasMore,
+ maximumPageBytes: program.maximumPageBytes,
+ pageSize: program.pageSize,
+ returnedRows: rows.length,
+ start,
+ totalRows: projection.rows.length,
+ truncation: { reasons: [], truncated: false, v: 2 },
+ v: 2
+ });
+ const issuedContinuation = hasMore ? encodeContinuationV2({
+ bindingsSha256: bound.bindingsSha256,
+ memorySha256: identity.memorySha256,
+ nextOffset: endExclusive,
+ pageSize: program.pageSize,
+ programSha256: program.programSha256,
+ projectionResultSha256: projection.resultSha256,
+ totalRows: projection.rows.length,
+ v: 2
+ }, continuationKey) : null;
+ const continuation = issuedContinuation?.continuation ?? null;
+ const continuationSha256 = issuedContinuation?.continuationSha256 ?? null;
+ const conflicts = immutableClone({
+ count: composite.conflicts.length,
+ conflictsSha256: canonicalSha256(composite.conflicts),
+ v: 2
+ });
+ const resultIdentityPayload = immutableClone({
+ authority: "derived",
+ conflicts,
+ continuationSha256,
+ identity,
+ page,
+ projectionResultSha256: projection.resultSha256,
+ rows,
+ v: 2
+ });
+ const resultSha256 = canonicalSha256(resultIdentityPayload);
+ const resultPayload = immutableClone({ ...resultIdentityPayload, continuation });
+ const issuedAt = wallClock();
+ const issuedAtMonotonic = monotonicClock();
+ const expiresAtMs = issuedAt + capabilityLifetime;
+ const expiresAtMonotonicMs = issuedAtMonotonic + capabilityLifetime;
+ const expiresAt = isoInstant(new Date(expiresAtMs));
+ const pageBytePreflight = {
+ ...resultPayload,
+ explainCapability: { expiresAt, token: "A".repeat(43), v: 2 },
+ resultSha256
+ };
+ if (utf8ByteLength(canonicalJson(pageBytePreflight)) > program.maximumPageBytes) {
+ throw new RangeError("The V2 memory page exceeds its host-declared canonical byte bound.");
+ }
+ for (const [existingToken, stored] of explanations) {
+ if (issuedAtMonotonic >= stored.expiresAtMonotonicMs)
+ deleteExplanation(existingToken);
+ }
+ const storedPayload = immutableClone({
+ expiresAtMonotonicMs,
+ identity,
+ page,
+ proofs,
+ resultSha256,
+ rows
+ });
+ const storedBytes = utf8ByteLength(canonicalJson(storedPayload)) + 128;
+ if (storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityEntryBytes || storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityTotalBytes) {
+ throw new RangeError("The V2 memory explanation exceeds its retained capability bound.");
+ }
+ while (explanations.size >= OH_MEMORY_LIMITS_V1.explainCapabilities || explanationBytes + storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityTotalBytes) {
+ const oldest = explanations.keys().next().value;
+ if (oldest === undefined)
+ break;
+ deleteExplanation(oldest);
+ }
+ let token = randomBytes2(32).toString("base64url");
+ while (explanations.has(token))
+ token = randomBytes2(32).toString("base64url");
+ explanations.set(token, immutableClone({ ...storedPayload, bytes: storedBytes }));
+ explanationBytes += storedBytes;
+ const result = immutableClone({
+ ...resultPayload,
+ explainCapability: { expiresAt, token, v: 2 },
+ resultSha256
+ });
+ if (utf8ByteLength(canonicalJson(result)) > program.maximumPageBytes) {
+ deleteExplanation(token);
+ throw new RangeError("The V2 memory page exceeds its host-declared canonical byte bound.");
+ }
+ return result;
+ };
+ const explain = async (value) => {
+ const request = parseExplainRequestV2(value);
+ const stored = explanations.get(request.token);
+ const currentTime = monotonicClock();
+ if (stored === undefined || stored.resultSha256 !== request.resultSha256 || currentTime >= stored.expiresAtMonotonicMs) {
+ deleteExplanation(request.token);
+ throw new OhProfileError("The V2 memory explanation capability is absent, expired, or misbound.");
+ }
+ const row = stored.rows[request.pageRow];
+ const proofs = stored.proofs[request.pageRow];
+ if (row === undefined || proofs === undefined)
+ throw new RangeError("The explanation page row is out of bounds.");
+ const payload = {
+ authority: "derived",
+ identity: stored.identity,
+ page: stored.page,
+ pageRow: request.pageRow,
+ premiseAuthority: row.premiseAuthority,
+ premiseLanes: row.premiseLanes,
+ proofs,
+ proofsTruncated: row.proofsTruncated,
+ resultRowSha256: row.resultRowSha256,
+ resultSha256: stored.resultSha256,
+ supportCount: row.supportCount,
+ v: 2,
+ values: row.values
+ };
+ return immutableClone({ ...payload, explanationSha256: canonicalSha256(payload) });
+ };
+ const nominate = async (value) => {
+ const request = parseNominationRequest(value);
+ const route = nominationRoutes.get(request.nominationId);
+ if (route === undefined)
+ throw new TypeError("Unknown named memory nomination route.");
+ const head = parseOhHeadV1(immutableClone(await workingStore.head()));
+ if (head === null)
+ throw new OhIntegrityError("The working nomination store returned an invalid head.");
+ const closure = await workingStore.exportDependencyClosure({ head: {
+ operationSha256: head.operationSha256,
+ sequence: head.sequence
+ }, roots: request.roots });
+ const verified = verifyOhDependencyClosureAgainstV1(closure, { binding: workingBinding, head });
+ if (!verified.ok)
+ throw new OhIntegrityError("The working nomination closure failed exact verification.");
+ if (canonicalJson(verified.closure.roots) !== canonicalJson(request.roots)) {
+ throw new OhIntegrityError("The working nomination closure substituted different roots.");
+ }
+ const source = Object.freeze({
+ authorityId: workingAuthorityId,
+ bindingSha256: workingBinding.bindingSha256,
+ head,
+ lane: "working",
+ v: 1
+ });
+ const payload = {
+ closure: verified.closure,
+ destinationPurpose: route.destinationPurpose,
+ nominationId: route.nominationId,
+ source,
+ status: "prepared",
+ v: 1
+ };
+ return immutableClone({ ...payload, nominationSha256: canonicalSha256(payload) });
+ };
+ return Object.freeze({ explain, nominate, query, remember });
+}
export {
+ createOhMemoryAgentV2,
createOhMemoryAgentV1,
+ OH_MEMORY_QUERY_LIMITS_V2,
OH_MEMORY_LIMITS_V1,
OH_MEMORY_FORMAT_VERSION_V1,
OH_MEMORY_CONFLICT_POLICY_V1,
diff --git a/package.json b/package.json
index 5d4ed47..80c1b18 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@hraness/oh",
- "version": "0.2.2",
+ "version": "0.2.3",
"description": "open-source tools for agentic research",
"type": "module",
"license": "MIT",
diff --git a/site/app/spec/page.tsx b/site/app/spec/page.tsx
index dd98a97..55eb11a 100644
--- a/site/app/spec/page.tsx
+++ b/site/app/spec/page.tsx
@@ -236,6 +236,11 @@ export default function Specification() {
programs see lane-tagged facts, visible conflicts, exact
physical authority and extractor digests, and bounded proofs
without receiving store locators or canonical mutation handles.
+ The additive V2 facade lets a host declare primitive query-body
+ parameters and stable bounded pages. Its authenticated bearer
+ cursors fail if the physical heads, program, bindings, or
+ complete result change; a stable host key carries exact cursors
+ across facade reconstruction.
Working nominations are verified dependency-closure proposals.
diff --git a/site/package.json b/site/package.json
index 3a920c2..b3493a8 100644
--- a/site/package.json
+++ b/site/package.json
@@ -1,6 +1,6 @@
{
"name": "oh-site",
- "version": "0.2.2",
+ "version": "0.2.3",
"private": true,
"packageManager": "bun@1.3.14",
"engines": {
diff --git a/site/public/spec/v1/memory.md b/site/public/spec/v1/memory.md
index 49b6199..3d5332f 100644
--- a/site/public/spec/v1/memory.md
+++ b/site/public/spec/v1/memory.md
@@ -70,6 +70,85 @@ silently canonical. Returned result, row, value, proof, source, and receipt
graphs are detached and deeply immutable, so a caller cannot mutate bytes after
their digest or explanation capability is issued.
+## Additive parameterized pagination (V2 experimental API)
+
+`createOhMemoryAgentV2` is an additive experimental query surface. It does not
+change a V1 request, result, digest preimage, factory, or type. Its `remember`
+and `nominate` methods continue to use the V1 semantic-bundle and nomination
+contracts. Only its `query` and `explain` envelopes use V2.
+
+A V2 named program is still entirely host-owned. In addition to the fixed
+purpose, rule pack, query, and extractor registry, the host declares:
+
+- the exact query-body variables that may receive parameters;
+- every projection evaluation limit;
+- the maximum complete result row count;
+- a page size of at most 256 rows; and
+- a canonical byte ceiling for each outward page.
+
+The host query limit MUST equal the declared maximum row count. A parameter
+variable MUST occur in the query body and MUST NOT be a projected output
+variable. Agent input supplies one exact object of bounded JSON primitive
+values for those names plus a program ID and either `null` or a continuation.
+It cannot supply a purpose, rule, query AST, evaluator option, page size, or
+source selector. Binding substitutes constants only into the fixed query body;
+the rules and projected output remain the registered program.
+
+The V2 identity includes the canonical bindings and their digest, the template
+and bound query digests, the complete program digest, and the same physical
+source and projection identities as V1. Thus a parameter value is part of both
+the projection identity and the memory identity, not an unrecorded filter.
+This supports a host extractor that emits bounded primitive value chunks while
+a named program binds `lane` and `key` and projects only chunk position and
+chunk content. Each chunk remains subject to the V1 16 KiB atom limit and the
+extractor's existing count and source rules.
+
+The evaluator computes one canonical, ordered result no larger than the
+host-declared row limit before it selects a page. A projection `query-limit` or
+`result-bytes` truncation returns no page. The outward page reports its start,
+end, configured and returned row counts, total rows, `hasMore`, `complete` or
+`partial` status, and explicit empty truncation evidence. Its configured slice
+must fit the host-declared page byte ceiling; the facade fails closed instead
+of silently shortening the slice. Every returned page therefore has
+`truncation.truncated: false`; `partial` means more exact pages exist, not that
+the projection is incomplete. Proof-budget truncation remains visible on
+the affected row as `proofsTruncated` and yields `unknown` premise authority,
+as in V1.
+
+A continuation is an authenticated bearer cursor, not knowledge authority. Its
+canonical envelope contains an unsigned cursor identity, a public
+`continuationSha256` digest of that identity, and a domain-separated
+HMAC-SHA-256. The identity binds the next offset to the exact program, bindings,
+complete projection result, page size, total row count, and composite memory
+identity. The HMAC makes only host-issued offsets usable; recomputing the public
+digest does not issue a cursor. The envelope is authenticated, not encrypted,
+and the same token can be replayed for the same exact page.
+
+By default the facade generates a private random continuation key, so its
+cursors are scoped to that facade instance. A host that must reconstruct the
+facade or route a cursor to another replica supplies the same 32 through 64 raw
+key bytes through `continuationKey`; the factory clones those bytes. The host
+keeps that key out of agent input and persisted results. Changing the key
+invalidates outstanding cursors.
+
+The request parser first establishes an exact shallow envelope, a bounded
+primitive binding map, and bounded strings before canonical serialization.
+After resolving the registered program and exact bindings, it authenticates a
+continuation and checks its program, binding, page-size, range, and alignment
+before reading the working store, invoking extractors, evaluating rules, or
+mapping proofs. Every valid continued call then rereads the current working
+head and rebuilds the projection. A head, source, result, or row-count change
+fails with an integrity error before proof mapping rather than mixing pages
+from two snapshots.
+
+An outward result publishes `continuationSha256` beside the opaque token, or
+`null` beside `null` on the final page. `resultSha256` commits that deterministic
+digest instead of the key-dependent token, so the same exact result identity is
+stable across signing keys. The actual token still counts toward the outward
+page-byte ceiling. A V2 explanation capability retains only its exact outward
+page and mapped physical proofs, and requires that page's result digest and
+page-local row index.
+
## Explanations and nominations
Query returns an opaque, random, short-lived explanation capability bound to
diff --git a/skills/oh/SKILL.md b/skills/oh/SKILL.md
index 1068630..ef0df70 100644
--- a/skills/oh/SKILL.md
+++ b/skills/oh/SKILL.md
@@ -30,7 +30,7 @@ oh --help
oh version
```
-The supported CLI is `@hraness/oh@0.2.2` from the immutable `v0.2.2` GitHub
+The supported CLI is `@hraness/oh@0.2.3` from the immutable `v0.2.3` GitHub
tag. It requires Bun 1.3.14 or newer. The versioned contract is published at
.
@@ -185,6 +185,19 @@ as derived. A nomination may select only a host-registered route and is a
prepared dependency-closure candidate for destination-owned review, not
permission to write durable knowledge or import the working operation chain.
+Use `createOhMemoryAgentV2` only when the host has registered primitive
+query-body parameters and fixed all projection, row, page, and page-byte
+limits. Expose only the exact bindings object, program ID, and continuation to
+the model. Do not expose parameter declarations, page size, or evaluator
+options as tool input. Follow `hasMore` until the continuation is `null`, and
+restart the named query after an integrity error; never combine pages across a
+working-head change. A V2 `query-limit` or `result-bytes` condition is a failed
+query, not a partial answer. Treat each continuation as a bearer cursor: pass
+it back unchanged only to the exact query and do not log or edit it. If the
+host reconstructs the facade or routes across replicas, it must provide the
+same private 32 through 64 byte `continuationKey` in host options; never expose
+that key as tool input. Keep row-level `proofsTruncated` evidence visible.
+
## Finish with evidence
Report the exact database and space, reads or mutations performed, final head
diff --git a/spec/v1/memory.md b/spec/v1/memory.md
index 49b6199..3d5332f 100644
--- a/spec/v1/memory.md
+++ b/spec/v1/memory.md
@@ -70,6 +70,85 @@ silently canonical. Returned result, row, value, proof, source, and receipt
graphs are detached and deeply immutable, so a caller cannot mutate bytes after
their digest or explanation capability is issued.
+## Additive parameterized pagination (V2 experimental API)
+
+`createOhMemoryAgentV2` is an additive experimental query surface. It does not
+change a V1 request, result, digest preimage, factory, or type. Its `remember`
+and `nominate` methods continue to use the V1 semantic-bundle and nomination
+contracts. Only its `query` and `explain` envelopes use V2.
+
+A V2 named program is still entirely host-owned. In addition to the fixed
+purpose, rule pack, query, and extractor registry, the host declares:
+
+- the exact query-body variables that may receive parameters;
+- every projection evaluation limit;
+- the maximum complete result row count;
+- a page size of at most 256 rows; and
+- a canonical byte ceiling for each outward page.
+
+The host query limit MUST equal the declared maximum row count. A parameter
+variable MUST occur in the query body and MUST NOT be a projected output
+variable. Agent input supplies one exact object of bounded JSON primitive
+values for those names plus a program ID and either `null` or a continuation.
+It cannot supply a purpose, rule, query AST, evaluator option, page size, or
+source selector. Binding substitutes constants only into the fixed query body;
+the rules and projected output remain the registered program.
+
+The V2 identity includes the canonical bindings and their digest, the template
+and bound query digests, the complete program digest, and the same physical
+source and projection identities as V1. Thus a parameter value is part of both
+the projection identity and the memory identity, not an unrecorded filter.
+This supports a host extractor that emits bounded primitive value chunks while
+a named program binds `lane` and `key` and projects only chunk position and
+chunk content. Each chunk remains subject to the V1 16 KiB atom limit and the
+extractor's existing count and source rules.
+
+The evaluator computes one canonical, ordered result no larger than the
+host-declared row limit before it selects a page. A projection `query-limit` or
+`result-bytes` truncation returns no page. The outward page reports its start,
+end, configured and returned row counts, total rows, `hasMore`, `complete` or
+`partial` status, and explicit empty truncation evidence. Its configured slice
+must fit the host-declared page byte ceiling; the facade fails closed instead
+of silently shortening the slice. Every returned page therefore has
+`truncation.truncated: false`; `partial` means more exact pages exist, not that
+the projection is incomplete. Proof-budget truncation remains visible on
+the affected row as `proofsTruncated` and yields `unknown` premise authority,
+as in V1.
+
+A continuation is an authenticated bearer cursor, not knowledge authority. Its
+canonical envelope contains an unsigned cursor identity, a public
+`continuationSha256` digest of that identity, and a domain-separated
+HMAC-SHA-256. The identity binds the next offset to the exact program, bindings,
+complete projection result, page size, total row count, and composite memory
+identity. The HMAC makes only host-issued offsets usable; recomputing the public
+digest does not issue a cursor. The envelope is authenticated, not encrypted,
+and the same token can be replayed for the same exact page.
+
+By default the facade generates a private random continuation key, so its
+cursors are scoped to that facade instance. A host that must reconstruct the
+facade or route a cursor to another replica supplies the same 32 through 64 raw
+key bytes through `continuationKey`; the factory clones those bytes. The host
+keeps that key out of agent input and persisted results. Changing the key
+invalidates outstanding cursors.
+
+The request parser first establishes an exact shallow envelope, a bounded
+primitive binding map, and bounded strings before canonical serialization.
+After resolving the registered program and exact bindings, it authenticates a
+continuation and checks its program, binding, page-size, range, and alignment
+before reading the working store, invoking extractors, evaluating rules, or
+mapping proofs. Every valid continued call then rereads the current working
+head and rebuilds the projection. A head, source, result, or row-count change
+fails with an integrity error before proof mapping rather than mixing pages
+from two snapshots.
+
+An outward result publishes `continuationSha256` beside the opaque token, or
+`null` beside `null` on the final page. `resultSha256` commits that deterministic
+digest instead of the key-dependent token, so the same exact result identity is
+stable across signing keys. The actual token still counts toward the outward
+page-byte ceiling. A V2 explanation capability retains only its exact outward
+page and mapped physical proofs, and requires that page's result digest and
+page-local row index.
+
## Explanations and nominations
Query returns an opaque, random, short-lived explanation capability bound to
diff --git a/src/cli.ts b/src/cli.ts
index 6297023..1f6349e 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -9,7 +9,7 @@ import { Oh } from "./sdk";
import { OH_SQLITE_SCHEMA_VERSION } from "./sqlite/migrations";
import { createOhSyncBundleV1, parseOhSyncBundleV1 } from "./sync";
-export const OH_PACKAGE_VERSION = "0.2.2" as const;
+export const OH_PACKAGE_VERSION = "0.2.3" as const;
type ParsedArguments = { options: Map; positionals: string[] };
type ValidatedInvocation = Readonly<{
diff --git a/src/memory.test.ts b/src/memory.test.ts
index 2f8cc24..982d2dc 100644
--- a/src/memory.test.ts
+++ b/src/memory.test.ts
@@ -1,11 +1,13 @@
import { describe, expect, test } from "bun:test";
-import { canonicalSha256, type JsonValue } from "./canonical";
+import { canonicalJson, canonicalSha256, type JsonValue } from "./canonical";
import { OhRecordCodecRegistry } from "./contract";
import { createKnowledgeGraphRecordV1 } from "./graph";
import {
createOhMemoryAgentV1,
+ createOhMemoryAgentV2,
OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1,
+ OH_MEMORY_QUERY_LIMITS_V2,
type OhMemoryAuthoritySourceV1,
type OhMemoryProofV1,
} from "./memory";
@@ -81,6 +83,90 @@ function nameProgram() {
} as const;
}
+function chunkProgram(maximumRows = 400, pageSize = 64,
+ maximumResultBytes = 8 * 1024 * 1024, maximumPageBytes = 1024 * 1024) {
+ const lane = ohProjectionVariableV1("lane");
+ const key = ohProjectionVariableV1("key");
+ const index = ohProjectionVariableV1("index");
+ const chunk = ohProjectionVariableV1("chunk");
+ const source = createOhProjectionLiteralV1({ relation: "domain.value-chunk",
+ terms: [lane, key, index, chunk] });
+ const visible = createOhProjectionLiteralV1({ relation: "domain.visible-value-chunk",
+ terms: [lane, key, index, chunk] });
+ return {
+ evaluation: {
+ maximumDerivedTuples: 2_048,
+ maximumProofDepth: 16,
+ maximumProofNodes: 16,
+ maximumResultBytes,
+ maximumRounds: 16,
+ maximumTotalProofNodes: 8_192,
+ maximumWorkUnits: 2_000_000,
+ },
+ maximumPageBytes,
+ maximumRows,
+ pageSize,
+ parameters: ["key", "lane"],
+ programId: "memory.value-chunks",
+ purpose: "answer.memory-value",
+ query: createOhProjectionQueryV1({ find: ["index", "chunk"], limit: maximumRows,
+ queryId: "memory.value-chunks", where: [visible] }),
+ rulePack: createOhProjectionRulePackV1({ rulePackId: "memory.value-chunks",
+ rulePackRevision: 1, rules: [createOhProjectionRuleV1({ body: [source], head: visible,
+ ruleId: "memory.value-chunks" })] }),
+ v: 2,
+ } as const;
+}
+
+async function fixtureV2(maximumRows = 400, pageSize = 64,
+ maximumResultBytes = 8 * 1024 * 1024, configuration: Readonly<{
+ chunkBytes?: number;
+ chunkCount?: number;
+ continuationKey?: Uint8Array;
+ extractorInvoked?: () => void;
+ maximumPageBytes?: number;
+ wrapWorkingStore?: (store: OhStoreV1) => OhStoreV1;
+ }> = {}) {
+ const canonical = createOhSqliteStoreAuthorityV1({ path: ":memory:",
+ profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:v2-canonical", spaceId: "v2-canonical" });
+ const working = createOhSqliteStoreAuthorityV1({ path: ":memory:",
+ profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:v2-working", spaceId: "v2-working" });
+ await put(canonical.store, "entity:chunked", "Chunked", "op_v2_chunked");
+ const chunkCount = configuration.chunkCount ?? 300;
+ const chunkBytes = configuration.chunkBytes ?? 0;
+ const baseProgram = chunkProgram(maximumRows, pageSize, maximumResultBytes,
+ configuration.maximumPageBytes ?? 1024 * 1024);
+ const expectedCanonicalHead = await canonical.store.head();
+ const selectedWorkingStore = configuration.wrapWorkingStore?.(working.store) ?? working.store;
+ const createAgent = async (continuationKey?: Uint8Array) => await createOhMemoryAgentV2({
+ actorId: "test.memory-agent-v2",
+ canonical: { authorityId: "authority.v2-canonical",
+ expectedBindingSha256: canonical.store.binding.bindingSha256,
+ expectedHead: expectedCanonicalHead, store: canonical.store },
+ ...(continuationKey === undefined ? {} : { continuationKey }),
+ extractors: [{ extractorId: "domain.value-chunks",
+ extractorSha256: canonicalSha256({ extractor: "domain.value-chunks", revision: 1 }),
+ relations: ["domain.value-chunk"],
+ extract: ({ lane, record }) => {
+ configuration.extractorInvoked?.();
+ return Array.from({ length: chunkCount }, (_, index) => ({
+ relation: "domain.value-chunk", tuple: [lane, record.key, index,
+ `chunk:${index.toString().padStart(3, "0")}${"x".repeat(chunkBytes)}`], v: 1 as const,
+ }));
+ },
+ }],
+ monotonicNow: () => 0,
+ now: () => new Date("2026-08-29T12:00:00.000Z"),
+ programs: [baseProgram, { ...baseProgram, maximumPageBytes: 1024 * 1024,
+ programId: "memory.value-chunks-alternate", purpose: "answer.memory-value-alternate" }],
+ working: { authorityId: "authority.v2-working", codecs: entityCodecs(),
+ expectedBindingSha256: selectedWorkingStore.binding.bindingSha256,
+ store: selectedWorkingStore },
+ });
+ const agent = await createAgent(configuration.continuationKey);
+ return { agent, canonical, createAgent, working };
+}
+
function physicalSources(proofs: readonly OhMemoryProofV1[]) {
const sources: OhMemoryAuthoritySourceV1[] = [];
const visit = (proof: OhMemoryProofV1) => {
@@ -91,6 +177,19 @@ function physicalSources(proofs: readonly OhMemoryProofV1[]) {
return sources;
}
+function countedWorkingStore(store: OhStoreV1, counter: { reads: number }): OhStoreV1 {
+ return new Proxy(store, {
+ get(target, property) {
+ const value = Reflect.get(target, property, target) as unknown;
+ if (typeof value !== "function") return value;
+ return (...args: unknown[]) => {
+ if (property === "head" || property === "snapshot") counter.reads += 1;
+ return Reflect.apply(value, target, args);
+ };
+ },
+ }) as OhStoreV1;
+}
+
async function fixture() {
const canonical = createOhSqliteStoreAuthorityV1({ path: ":memory:",
profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:canonical", spaceId: "canonical" });
@@ -144,6 +243,17 @@ describe("experimental composite Oh memory", () => {
value: { name: "Working" } }], tombstones: [], v: 1 });
const result = await value.agent.query({ programId: "memory.visible-records", v: 1 });
+ expect({ memorySha256: String(result.identity.memorySha256), resultSha256: String(result.resultSha256),
+ rowSha256: result.rows.map(({ resultRowSha256 }) => String(resultRowSha256)) }).toEqual({
+ memorySha256: "b2327cb013af29646ab6beaa953771153c10f076bb8f3e061f106b0a5f234a3a",
+ resultSha256: "44e9fb7af61aa2395f6e028acdfdf327f64c07be9dca40b5097f9d5d3248291e",
+ rowSha256: [
+ "9abe30330d18d8ee271f25f5db9e9a0bad09eaa8a10fb206b2850336f016903d",
+ "ccfc0b064abbbea14ddbdb17a03708cb151f166c82c9c028c159c1e07f10bff5",
+ "c4f1ce2d57b8324858892d7a1e3362a77b098d5a8e9b794df269f49dd25d1456",
+ "6ca55f381d076658aef1d4b7f238fc7730413c55a4474d3a96703912170c7485",
+ ],
+ });
expect(result.authority).toBe("derived");
expect(result.rows.map(({ values }) => values.slice(0, 2))).toEqual([
["canonical", "entity:canonical"], ["canonical", "entity:shared"],
@@ -374,3 +484,300 @@ describe("experimental composite Oh memory", () => {
await canonical.store.close(); await working.store.close();
});
});
+
+describe("experimental parameterized and paginated Oh memory V2", () => {
+ test("binds host-declared lane and key constants and returns more than 256 value chunks in stable pages",
+ async () => {
+ const value = await fixtureV2();
+ let continuation: string | null = null;
+ const rows: (readonly (boolean | null | number | string)[])[] = [];
+ const starts: number[] = [];
+ let first: Awaited> | undefined;
+ let later: Awaited> | undefined;
+ do {
+ const result = await value.agent.query({ bindings: {
+ key: "entity:chunked", lane: "canonical",
+ }, continuation, programId: "memory.value-chunks", v: 2 });
+ first ??= result;
+ if (result.page.start === 64) later = result;
+ starts.push(result.page.start);
+ rows.push(...result.rows.map((row) => row.values));
+ expect(result.page.truncation).toEqual({ reasons: [], truncated: false, v: 2 });
+ expect(result.page.hasMore).toBe(result.continuation !== null);
+ expect(result.page.hasMore).toBe(result.continuationSha256 !== null);
+ expect(result.page.completeness).toBe(result.page.hasMore ? "partial" : "complete");
+ continuation = result.continuation;
+ } while (continuation !== null);
+
+ expect(starts).toEqual([0, 64, 128, 192, 256]);
+ expect(rows).toHaveLength(300);
+ expect(rows[0]).toEqual([0, "chunk:000"]);
+ expect(rows).toContainEqual([299, "chunk:299"]);
+ expect(rows.map((row) => row[0] as number).sort((left, right) => left - right))
+ .toEqual(Array.from({ length: 300 }, (_, index) => index));
+ expect(first?.identity.bindings).toEqual({ key: "entity:chunked", lane: "canonical" });
+ expect(first?.identity.bindingsSha256).not.toBe(first?.identity.templateQuerySha256);
+ expect(first?.identity.boundQuerySha256).not.toBe(first?.identity.templateQuerySha256);
+ expect(Object.isFrozen(first?.identity.bindings)).toBe(true);
+ if (first === undefined) throw new Error("Expected a first V2 page.");
+ const explanation = await value.agent.explain({ pageRow: 0,
+ resultSha256: first.resultSha256, token: first.explainCapability.token, v: 2 });
+ expect(explanation.page).toEqual(first.page);
+ expect(explanation.resultSha256).toBe(first.resultSha256);
+ expect(physicalSources(explanation.proofs)).toEqual([expect.objectContaining({
+ key: "entity:chunked", lane: "canonical",
+ })]);
+ if (later === undefined) throw new Error("Expected a later V2 page.");
+ const laterExplanation = await value.agent.explain({ pageRow: 0,
+ resultSha256: later.resultSha256, token: later.explainCapability.token, v: 2 });
+ expect(laterExplanation.page.start).toBe(64);
+ expect(laterExplanation.pageRow).toBe(0);
+ expect(laterExplanation.values).toEqual(later.rows[0]!.values);
+ expect(physicalSources(laterExplanation.proofs)).toEqual([expect.objectContaining({
+ key: "entity:chunked", lane: "canonical",
+ })]);
+ await expect(value.agent.explain({ pageRow: 0, resultSha256: "f".repeat(64),
+ token: first.explainCapability.token, v: 2 })).rejects.toThrow(OhProfileError);
+ await value.canonical.store.close(); await value.working.store.close();
+ });
+
+ test("pins continuations to the exact source, program, bindings, and complete projection", async () => {
+ const value = await fixtureV2();
+ const first = await value.agent.query({ bindings: {
+ key: "entity:chunked", lane: "canonical",
+ }, continuation: null, programId: "memory.value-chunks", v: 2 });
+ expect(first.continuation).not.toBeNull();
+ const continuation = first.continuation!;
+ const last = continuation.at(-1);
+ const tampered = `${continuation.slice(0, -1)}${last === "A" ? "B" : "A"}`;
+ await expect(value.agent.query({ bindings: {
+ key: "entity:chunked", lane: "canonical",
+ }, continuation: tampered, programId: "memory.value-chunks", v: 2 }))
+ .rejects.toThrow();
+ const decoded = JSON.parse(Buffer.from(continuation, "base64url").toString("utf8")) as
+ Record;
+ const noncanonical = Buffer.from(JSON.stringify(Object.fromEntries(
+ Object.entries(decoded).reverse())), "utf8").toString("base64url");
+ await expect(value.agent.query({ bindings: {
+ key: "entity:chunked", lane: "canonical",
+ }, continuation: noncanonical, programId: "memory.value-chunks", v: 2 }))
+ .rejects.toThrow("Invalid memory continuation payload");
+ await expect(value.agent.query({ bindings: {
+ key: "entity:chunked", lane: "canonical",
+ }, continuation, programId: "memory.value-chunks-alternate", v: 2 }))
+ .rejects.toThrow("exact program, binding, and page identity");
+ await expect(value.agent.query({ bindings: {
+ key: "entity:other", lane: "canonical",
+ }, continuation, programId: "memory.value-chunks", v: 2 }))
+ .rejects.toThrow(OhIntegrityError);
+
+ const head = await value.working.store.head();
+ await value.agent.remember({ expectedHead: { generation: head.generation,
+ operationSha256: head.operationSha256 }, puts: [{ dependencies: [], key: "entity:head-change",
+ kind: "entity", v: 1, value: { name: "Head change" } }], requestId: "v2_head_change",
+ tombstones: [], v: 1 });
+ await expect(value.agent.query({ bindings: {
+ key: "entity:chunked", lane: "canonical",
+ }, continuation, programId: "memory.value-chunks", v: 2 }))
+ .rejects.toThrow("exact source and projection identity");
+ await value.canonical.store.close(); await value.working.store.close();
+ });
+
+ test("rejects an aligned offset forgery after every public cursor digest is recomputed", async () => {
+ const value = await fixtureV2(8, 2, 8 * 1024 * 1024, {
+ chunkCount: 8,
+ continuationKey: new Uint8Array(32).fill(7),
+ });
+ const request = { bindings: { key: "entity:chunked", lane: "canonical" },
+ continuation: null, programId: "memory.value-chunks", v: 2 as const };
+ const first = await value.agent.query(request);
+ if (first.continuation === null) throw new Error("Expected an issued V2 continuation.");
+ const decoded = JSON.parse(Buffer.from(first.continuation, "base64url").toString("utf8")) as
+ Record;
+ expect(decoded.nextOffset).toBe(2);
+ decoded.nextOffset = 6;
+ const unsigned = Object.fromEntries(Object.entries(decoded).filter(([key]) =>
+ key !== "continuationHmacSha256" && key !== "continuationSha256"));
+ decoded.continuationSha256 = canonicalSha256(unsigned);
+ const forged = Buffer.from(canonicalJson(decoded), "utf8").toString("base64url");
+ await expect(value.agent.query({ ...request, continuation: forged }))
+ .rejects.toThrow("not an issued capability");
+
+ const replayOne = await value.agent.query({ ...request, continuation: first.continuation });
+ const replayTwo = await value.agent.query({ ...request, continuation: first.continuation });
+ expect(replayOne.page.start).toBe(2);
+ expect(replayTwo.page).toEqual(replayOne.page);
+ expect(replayTwo.rows).toEqual(replayOne.rows);
+ expect(replayTwo.resultSha256).toBe(replayOne.resultSha256);
+ await value.canonical.store.close(); await value.working.store.close();
+ });
+
+ test("scopes default cursors to one agent and preserves keyed cursors and public digests", async () => {
+ const mutableHostKey = new Uint8Array(32).fill(3);
+ const persistedHostKey = mutableHostKey.slice();
+ const value = await fixtureV2(400, 64, 8 * 1024 * 1024, {
+ continuationKey: mutableHostKey,
+ });
+ mutableHostKey.fill(9);
+ const request = { bindings: { key: "entity:chunked", lane: "canonical" },
+ continuation: null, programId: "memory.value-chunks", v: 2 as const };
+ const keyed = await value.agent.query(request);
+ if (keyed.continuation === null) throw new Error("Expected a keyed continuation.");
+ const { continuation: _opaqueContinuation, explainCapability: _explainCapability,
+ resultSha256: _resultSha256, ...deterministicResultIdentity } = keyed;
+ expect(canonicalSha256(deterministicResultIdentity)).toBe(keyed.resultSha256);
+
+ const otherKeyAgent = await value.createAgent(new Uint8Array(32).fill(4));
+ const otherKey = await otherKeyAgent.query(request);
+ expect(otherKey.continuation).not.toBe(keyed.continuation);
+ expect(otherKey.continuationSha256).toBe(keyed.continuationSha256);
+ expect(otherKey.resultSha256).toBe(keyed.resultSha256);
+ await expect(otherKeyAgent.query({ ...request, continuation: keyed.continuation }))
+ .rejects.toThrow("not an issued capability");
+
+ const reconstructed = await value.createAgent(persistedHostKey);
+ const resumed = await reconstructed.query({ ...request, continuation: keyed.continuation });
+ expect(resumed.page.start).toBe(64);
+
+ const localOne = await value.createAgent();
+ const localFirst = await localOne.query(request);
+ if (localFirst.continuation === null) throw new Error("Expected a local continuation.");
+ const localTwo = await value.createAgent();
+ await expect(localTwo.query({ ...request, continuation: localFirst.continuation }))
+ .rejects.toThrow("not an issued capability");
+ await expect(value.createAgent(new Uint8Array(
+ OH_MEMORY_QUERY_LIMITS_V2.continuationKeyMinimumBytes - 1)))
+ .rejects.toThrow("32 through 64 raw bytes");
+ await expect(value.createAgent(new Uint8Array(
+ OH_MEMORY_QUERY_LIMITS_V2.continuationKeyMaximumBytes + 1)))
+ .rejects.toThrow("32 through 64 raw bytes");
+ await value.canonical.store.close(); await value.working.store.close();
+ });
+
+ test("authenticates and statically binds cursors before working reads or extraction", async () => {
+ const counter = { extractorInvocations: 0, workingReads: 0 };
+ const key = new Uint8Array(32).fill(5);
+ const value = await fixtureV2(400, 64, 8 * 1024 * 1024, {
+ continuationKey: key,
+ extractorInvoked: () => { counter.extractorInvocations += 1; },
+ wrapWorkingStore: (store) => countedWorkingStore(store, {
+ get reads() { return counter.workingReads; },
+ set reads(value) { counter.workingReads = value; },
+ }),
+ });
+ const request = { bindings: { key: "entity:chunked", lane: "canonical" },
+ continuation: null, programId: "memory.value-chunks", v: 2 as const };
+ const first = await value.agent.query(request);
+ if (first.continuation === null) throw new Error("Expected an issued continuation.");
+ counter.extractorInvocations = 0;
+ counter.workingReads = 0;
+
+ await expect(value.agent.query({ ...request, continuation: "!" })).rejects.toThrow();
+ const invalidMacPayload = JSON.parse(
+ Buffer.from(first.continuation, "base64url").toString("utf8"),
+ ) as Record;
+ invalidMacPayload.continuationHmacSha256 = invalidMacPayload.continuationHmacSha256
+ === "f".repeat(64) ? "e".repeat(64) : "f".repeat(64);
+ const invalidMac = Buffer.from(canonicalJson(invalidMacPayload), "utf8").toString("base64url");
+ await expect(value.agent.query({ ...request, continuation: invalidMac }))
+ .rejects.toThrow("not an issued capability");
+ await expect(value.agent.query({ ...request, continuation: first.continuation,
+ programId: "memory.value-chunks-alternate" }))
+ .rejects.toThrow("exact program, binding, and page identity");
+ await expect(value.agent.query({ ...request,
+ bindings: { key: "entity:other", lane: "canonical" },
+ continuation: first.continuation }))
+ .rejects.toThrow("exact program, binding, and page identity");
+ expect(counter).toEqual({ extractorInvocations: 0, workingReads: 0 });
+ await value.canonical.store.close(); await value.working.store.close();
+ });
+
+ test("bounds and flattens V2 query input before canonical serialization", async () => {
+ const value = await fixtureV2();
+ const base = { continuation: null, programId: "memory.value-chunks", v: 2 as const };
+ const cyclic: Record = {};
+ cyclic.self = cyclic;
+ await expect(value.agent.query({ ...base,
+ bindings: { key: "entity:chunked", lane: cyclic } }))
+ .rejects.toThrow("JSON primitives");
+ await expect(value.agent.query({ ...base,
+ bindings: { key: "entity:chunked", lane: "x".repeat(16 * 1024 + 1) } }))
+ .rejects.toThrow("atom byte bound");
+ await expect(value.agent.query({ ...base, bindings: Object.fromEntries(
+ Array.from({ length: OH_MEMORY_QUERY_LIMITS_V2.bindings + 1 }, (_, index) =>
+ [`p${index}`, index]),
+ ) })).rejects.toThrow("too many entries");
+ await expect(value.agent.query({ ...base, bindings: Object.fromEntries(
+ Array.from({ length: 6 }, (_, index) => [`p${index}`, "x".repeat(14 * 1024)]),
+ ) })).rejects.toThrow("canonical byte bound");
+ await expect(value.agent.query({ ...base, bindings: {},
+ continuation: "A".repeat(OH_MEMORY_QUERY_LIMITS_V2.continuationBytes + 1) }))
+ .rejects.toThrow("continuation exceeds its byte bound");
+ await value.canonical.store.close(); await value.working.store.close();
+ });
+
+ test("fails closed instead of returning a silently row- or byte-truncated page", async () => {
+ const rowBound = await fixtureV2(256, 64);
+ await expect(rowBound.agent.query({ bindings: {
+ key: "entity:chunked", lane: "canonical",
+ }, continuation: null, programId: "memory.value-chunks", v: 2 }))
+ .rejects.toThrow("projection was truncated (query-limit)");
+ await rowBound.canonical.store.close(); await rowBound.working.store.close();
+
+ const byteBound = await fixtureV2(400, 64, 64 * 1024);
+ await expect(byteBound.agent.query({ bindings: {
+ key: "entity:chunked", lane: "canonical",
+ }, continuation: null, programId: "memory.value-chunks", v: 2 }))
+ .rejects.toThrow("projection was truncated (result-bytes)");
+ await byteBound.canonical.store.close(); await byteBound.working.store.close();
+
+ const pageBound = await fixtureV2(400, 64, 8 * 1024 * 1024,
+ { chunkBytes: 2_048, maximumPageBytes: 64 * 1024 });
+ await expect(pageBound.agent.query({ bindings: {
+ key: "entity:chunked", lane: "canonical",
+ }, continuation: null, programId: "memory.value-chunks", v: 2 }))
+ .rejects.toThrow("page exceeds its host-declared canonical byte bound");
+ const recovered = await pageBound.agent.query({ bindings: {
+ key: "entity:chunked", lane: "canonical",
+ }, continuation: null, programId: "memory.value-chunks-alternate", v: 2 });
+ const explanation = await pageBound.agent.explain({ pageRow: 0,
+ resultSha256: recovered.resultSha256, token: recovered.explainCapability.token, v: 2 });
+ expect(explanation.values).toEqual(recovered.rows[0]!.values);
+ await pageBound.canonical.store.close(); await pageBound.working.store.close();
+ });
+
+ test("reports zero and exact-page results as complete without a continuation", async () => {
+ const value = await fixtureV2(400, 64, 8 * 1024 * 1024, { chunkCount: 64 });
+ const zero = await value.agent.query({ bindings: {
+ key: "entity:absent", lane: "canonical",
+ }, continuation: null, programId: "memory.value-chunks", v: 2 });
+ expect(zero.rows).toEqual([]);
+ expect(zero.page).toMatchObject({ completeness: "complete", endExclusive: 0,
+ hasMore: false, returnedRows: 0, start: 0, totalRows: 0 });
+ expect(zero.continuation).toBeNull();
+ expect(zero.continuationSha256).toBeNull();
+
+ const exact = await value.agent.query({ bindings: {
+ key: "entity:chunked", lane: "canonical",
+ }, continuation: null, programId: "memory.value-chunks", v: 2 });
+ expect(exact.rows).toHaveLength(64);
+ expect(exact.page).toMatchObject({ completeness: "complete", endExclusive: 64,
+ hasMore: false, returnedRows: 64, start: 0, totalRows: 64 });
+ expect(exact.continuation).toBeNull();
+ expect(exact.continuationSha256).toBeNull();
+ await value.canonical.store.close(); await value.working.store.close();
+ });
+
+ test("accepts only exact primitive bindings and rejects caller-supplied query policy", async () => {
+ const value = await fixtureV2();
+ await expect(value.agent.query({ bindings: { key: "entity:chunked" }, continuation: null,
+ programId: "memory.value-chunks", v: 2 })).rejects.toThrow("exactly match");
+ await expect(value.agent.query({ bindings: { key: "entity:chunked", lane: { value: "canonical" } },
+ continuation: null, programId: "memory.value-chunks", v: 2 }))
+ .rejects.toThrow("JSON primitives");
+ await expect(value.agent.query({ bindings: { key: "entity:chunked", lane: "canonical" },
+ continuation: null, programId: "memory.value-chunks", query: {}, v: 2 }))
+ .rejects.toThrow("Invalid parameterized memory query");
+ await value.canonical.store.close(); await value.working.store.close();
+ });
+});
diff --git a/src/memory.ts b/src/memory.ts
index 0a78b19..6d56e12 100644
--- a/src/memory.ts
+++ b/src/memory.ts
@@ -1,4 +1,4 @@
-import { randomBytes } from "node:crypto";
+import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
import {
canonicalJson,
@@ -23,9 +23,12 @@ import {
OH_PROJECTION_LIMITS_V1,
createOhProjectionDatasetV1,
createOhProjectionFactV1,
+ createOhProjectionLiteralV1,
+ createOhProjectionQueryV1,
createOhProjectionRecordFactsV1,
createOhProjectionSnapshotV1,
evaluateOhProjectionV1,
+ ohProjectionConstantV1,
parseOhProjectionQueryV1,
parseOhProjectionRulePackV1,
type OhProjectionAtomV1,
@@ -296,6 +299,139 @@ export interface OhMemoryAgentV1 {
remember(value: unknown): Promise;
}
+/** Additive experimental query/pagination limits; V1 contracts are unchanged. */
+export const OH_MEMORY_QUERY_LIMITS_V2 = Object.freeze({
+ bindingBytes: 64 * 1024,
+ bindings: 32,
+ continuationBytes: 4 * 1024,
+ continuationKeyMaximumBytes: 64,
+ continuationKeyMinimumBytes: 32,
+ maximumPageBytes: 8 * 1024 * 1024,
+ maximumPageRows: 256,
+ maximumProgramRows: OH_PROJECTION_LIMITS_V1.queryResults,
+ minimumPageBytes: 64 * 1024,
+ requestBytes: 80 * 1024,
+});
+
+export type OhMemoryEvaluationLimitsV2 = Readonly<{
+ maximumDerivedTuples: number;
+ maximumProofDepth: number;
+ maximumProofNodes: number;
+ maximumResultBytes: number;
+ maximumRounds: number;
+ maximumTotalProofNodes: number;
+ maximumWorkUnits: number;
+}>;
+
+/**
+ * A host-owned parameterized program. Parameter names refer only to variables
+ * in the query body, never to rule variables or projected output variables.
+ */
+export type OhMemoryNamedProgramV2 = Readonly<{
+ evaluation: OhMemoryEvaluationLimitsV2;
+ maximumPageBytes: number;
+ maximumRows: number;
+ pageSize: number;
+ parameters: readonly string[];
+ programId: string;
+ purpose: string;
+ query: OhProjectionQueryV1;
+ rulePack: OhProjectionRulePackV1;
+ v: 2;
+}>;
+
+export type OhMemoryFacadeOptionsV2 = Readonly<
+ Omit & Readonly<{
+ /** Raw HMAC key for continuations that must survive agent reconstruction. */
+ continuationKey?: Uint8Array;
+ programs: readonly OhMemoryNamedProgramV2[];
+ }>
+>;
+
+export type OhMemoryIdentityV2 = Readonly<{
+ bindings: Readonly>;
+ bindingsSha256: Sha256Hex;
+ boundQuerySha256: Sha256Hex;
+ canonical: OhMemoryLaneIdentityV1;
+ compositeDatasetSha256: Sha256Hex;
+ conflictPolicy: typeof OH_MEMORY_CONFLICT_POLICY_V1;
+ evaluationSha256: Sha256Hex;
+ memorySha256: Sha256Hex;
+ programId: string;
+ programSha256: Sha256Hex;
+ projectionSha256: Sha256Hex;
+ purpose: string;
+ rulePackSha256: Sha256Hex;
+ templateQuerySha256: Sha256Hex;
+ v: 2;
+ working: OhMemoryLaneIdentityV1;
+}>;
+
+export type OhMemoryResultRowV2 = Readonly<{
+ premiseAuthority: "canonical" | "unknown" | "working";
+ premiseLanes: readonly OhMemoryLaneV1[];
+ proofsTruncated: boolean;
+ resultRowSha256: Sha256Hex;
+ supportCount: number;
+ v: 2;
+ values: readonly OhProjectionAtomV1[];
+}>;
+
+export type OhMemoryPageV2 = Readonly<{
+ completeness: "complete" | "partial";
+ endExclusive: number;
+ hasMore: boolean;
+ maximumPageBytes: number;
+ pageSize: number;
+ returnedRows: number;
+ start: number;
+ totalRows: number;
+ truncation: Readonly<{
+ reasons: readonly [];
+ truncated: false;
+ v: 2;
+ }>;
+ v: 2;
+}>;
+
+export type OhMemoryQueryResultV2 = Readonly<{
+ authority: "derived";
+ conflicts: Readonly<{ count: number; conflictsSha256: Sha256Hex; v: 2 }>;
+ continuation: string | null;
+ continuationSha256: Sha256Hex | null;
+ explainCapability: Readonly<{ expiresAt: string; token: string; v: 2 }>;
+ identity: OhMemoryIdentityV2;
+ page: OhMemoryPageV2;
+ projectionResultSha256: Sha256Hex;
+ resultSha256: Sha256Hex;
+ rows: readonly OhMemoryResultRowV2[];
+ v: 2;
+}>;
+
+export type OhMemoryExplanationV2 = Readonly<{
+ authority: "derived";
+ explanationSha256: Sha256Hex;
+ identity: OhMemoryIdentityV2;
+ page: OhMemoryPageV2;
+ pageRow: number;
+ premiseAuthority: OhMemoryResultRowV2["premiseAuthority"];
+ premiseLanes: readonly OhMemoryLaneV1[];
+ proofs: readonly OhMemoryProofV1[];
+ proofsTruncated: boolean;
+ resultRowSha256: Sha256Hex;
+ resultSha256: Sha256Hex;
+ supportCount: number;
+ v: 2;
+ values: readonly OhProjectionAtomV1[];
+}>;
+
+export interface OhMemoryAgentV2 {
+ explain(value: unknown): Promise;
+ nominate(value: unknown): Promise;
+ query(value: unknown): Promise;
+ remember(value: unknown): Promise;
+}
+
type LaneSnapshot = Readonly<{
authorityId: string;
binding: OhStoreBindingV1;
@@ -955,3 +1091,594 @@ export async function createOhMemoryAgentV1(options: OhMemoryFacadeOptionsV1): P
return Object.freeze({ explain, nominate, query, remember });
}
+
+type ResolvedMemoryProgramV2 = OhMemoryNamedProgramV2 & Readonly<{
+ programSha256: Sha256Hex;
+}>;
+
+type StoredExplanationV2 = Readonly<{
+ bytes: number;
+ expiresAtMonotonicMs: number;
+ identity: OhMemoryIdentityV2;
+ page: OhMemoryPageV2;
+ proofs: readonly (readonly OhMemoryProofV1[])[];
+ resultSha256: Sha256Hex;
+ rows: readonly OhMemoryResultRowV2[];
+}>;
+
+type OhMemoryContinuationIdentityV2 = Readonly<{
+ bindingsSha256: Sha256Hex;
+ memorySha256: Sha256Hex;
+ nextOffset: number;
+ pageSize: number;
+ programSha256: Sha256Hex;
+ projectionResultSha256: Sha256Hex;
+ totalRows: number;
+ v: 2;
+}>;
+
+type OhMemoryContinuationV2 = OhMemoryContinuationIdentityV2 & Readonly<{
+ continuationSha256: Sha256Hex;
+}>;
+
+type OhMemoryContinuationEnvelopeV2 = OhMemoryContinuationV2 & Readonly<{
+ continuationHmacSha256: Sha256Hex;
+}>;
+
+function ownDataKeysV2(value: unknown, maximum: number, label: string): readonly string[] {
+ if (!isPlainRecord(value)) throw new TypeError(`${label} must be a plain data object.`);
+ const ownKeys = Reflect.ownKeys(value);
+ if (ownKeys.length > maximum) throw new RangeError(`${label} has too many entries.`);
+ if (ownKeys.some((key) => typeof key !== "string")) {
+ throw new TypeError(`${label} must have only string data properties.`);
+ }
+ const keys = ownKeys as string[];
+ for (const key of keys) {
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
+ if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) {
+ throw new TypeError(`${label} must have only enumerable data properties.`);
+ }
+ }
+ return keys;
+}
+
+function continuationKeyV2(value: Uint8Array | undefined): Uint8Array {
+ if (value === undefined) return Uint8Array.from(randomBytes(32));
+ if (!(value instanceof Uint8Array)
+ || value.byteLength < OH_MEMORY_QUERY_LIMITS_V2.continuationKeyMinimumBytes
+ || value.byteLength > OH_MEMORY_QUERY_LIMITS_V2.continuationKeyMaximumBytes) {
+ throw new RangeError("The V2 memory continuation key must be 32 through 64 raw bytes.");
+ }
+ return Uint8Array.from(value);
+}
+
+function positiveBounded(value: unknown, maximum: number, label: string, minimum = 1): number {
+ if (!Number.isSafeInteger(value) || (value as number) < minimum || (value as number) > maximum) {
+ throw new RangeError(`${label} must be an integer from ${minimum} through ${maximum}.`);
+ }
+ return value as number;
+}
+
+function resolveEvaluationV2(value: unknown): OhMemoryEvaluationLimitsV2 {
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["maximumDerivedTuples", "maximumProofDepth",
+ "maximumProofNodes", "maximumResultBytes", "maximumRounds", "maximumTotalProofNodes",
+ "maximumWorkUnits"])) {
+ throw new TypeError("A V2 memory program must declare every projection evaluation limit.");
+ }
+ return Object.freeze({
+ maximumDerivedTuples: positiveBounded(value.maximumDerivedTuples,
+ OH_PROJECTION_LIMITS_V1.derivedTuples, "maximumDerivedTuples"),
+ maximumProofDepth: positiveBounded(value.maximumProofDepth,
+ OH_PROJECTION_LIMITS_V1.proofDepth, "maximumProofDepth"),
+ maximumProofNodes: positiveBounded(value.maximumProofNodes,
+ OH_PROJECTION_LIMITS_V1.proofNodes, "maximumProofNodes"),
+ maximumResultBytes: positiveBounded(value.maximumResultBytes,
+ OH_PROJECTION_LIMITS_V1.resultBytes, "maximumResultBytes", 64 * 1024),
+ maximumRounds: positiveBounded(value.maximumRounds,
+ OH_PROJECTION_LIMITS_V1.rounds, "maximumRounds"),
+ maximumTotalProofNodes: positiveBounded(value.maximumTotalProofNodes,
+ OH_PROJECTION_LIMITS_V1.totalProofNodes, "maximumTotalProofNodes"),
+ maximumWorkUnits: positiveBounded(value.maximumWorkUnits,
+ OH_PROJECTION_LIMITS_V1.workUnits, "maximumWorkUnits"),
+ });
+}
+
+function resolveProgramsV2(programs: readonly OhMemoryNamedProgramV2[]):
+ReadonlyMap {
+ if (!Array.isArray(programs) || programs.length < 1
+ || programs.length > OH_MEMORY_LIMITS_V1.maximumPrograms) {
+ throw new RangeError("Memory requires a bounded nonempty V2 named program registry.");
+ }
+ const resolved = new Map();
+ for (const candidate of programs) {
+ if (!isPlainRecord(candidate) || !hasExactKeys(candidate, ["evaluation", "maximumPageBytes",
+ "maximumRows", "pageSize", "parameters", "programId", "purpose", "query", "rulePack", "v"])
+ || candidate.v !== 2 || !Array.isArray(candidate.parameters)) {
+ throw new TypeError("Invalid V2 named memory program.");
+ }
+ const programId = safeCode(candidate.programId, 128);
+ const purpose = safeCode(candidate.purpose, 256);
+ const query = parseOhProjectionQueryV1(candidate.query);
+ const rulePack = parseOhProjectionRulePackV1(candidate.rulePack);
+ const evaluation = resolveEvaluationV2(candidate.evaluation);
+ const maximumRows = positiveBounded(candidate.maximumRows,
+ OH_MEMORY_QUERY_LIMITS_V2.maximumProgramRows, "maximumRows");
+ const pageSize = positiveBounded(candidate.pageSize,
+ Math.min(maximumRows, OH_MEMORY_QUERY_LIMITS_V2.maximumPageRows), "pageSize");
+ const maximumPageBytes = positiveBounded(candidate.maximumPageBytes,
+ OH_MEMORY_QUERY_LIMITS_V2.maximumPageBytes, "maximumPageBytes",
+ OH_MEMORY_QUERY_LIMITS_V2.minimumPageBytes);
+ if (programId === null || purpose === null || query === null || rulePack === null
+ || resolved.has(programId) || query.limit !== maximumRows
+ || candidate.parameters.length > OH_MEMORY_QUERY_LIMITS_V2.bindings) {
+ throw new TypeError("Invalid or duplicate V2 named memory program.");
+ }
+ const parameters = candidate.parameters.map((parameter) => safeCode(parameter, 128)).sort();
+ if (parameters.some((parameter) => parameter === null)
+ || new Set(parameters).size !== parameters.length) {
+ throw new TypeError("A V2 memory program has invalid or duplicate parameters.");
+ }
+ const queryVariables = new Set(query.where.flatMap((literal) => literal.terms.flatMap((term) =>
+ term.kind === "variable" ? [term.name] : [])));
+ if ((parameters as readonly string[]).some((parameter) => !queryVariables.has(parameter)
+ || query.find.includes(parameter))) {
+ throw new TypeError("V2 parameters must be query-body variables that are not projected outputs.");
+ }
+ const detachedParameters = Object.freeze(parameters as string[]);
+ const programPayload = {
+ evaluation,
+ maximumPageBytes,
+ maximumRows,
+ pageSize,
+ parameters: detachedParameters,
+ programId,
+ purpose,
+ querySha256: query.querySha256,
+ rulePackSha256: rulePack.rulePackSha256,
+ v: 2 as const,
+ };
+ const program = immutableClone({ evaluation, maximumPageBytes, maximumRows, pageSize,
+ parameters: detachedParameters, programId, programSha256: canonicalSha256(programPayload),
+ purpose, query, rulePack, v: 2 as const });
+ resolved.set(programId, program);
+ }
+ return resolved;
+}
+
+function parsePrimitiveBindingV2(value: unknown): JsonPrimitive {
+ if (value !== null && typeof value !== "boolean" && typeof value !== "number"
+ && typeof value !== "string") throw new TypeError("Memory bindings must be JSON primitives.");
+ if (typeof value === "string" && value.length > OH_PROJECTION_LIMITS_V1.atomBytes) {
+ throw new RangeError("A memory binding exceeds the projection atom byte bound.");
+ }
+ if (typeof value === "number" && (!Number.isFinite(value) || Object.is(value, -0))) {
+ throw new TypeError("Memory bindings must be canonical finite JSON numbers.");
+ }
+ const serialized = canonicalJson(value);
+ if (utf8ByteLength(serialized) > OH_PROJECTION_LIMITS_V1.atomBytes) {
+ throw new RangeError("A memory binding exceeds the projection atom byte bound.");
+ }
+ return value as JsonPrimitive;
+}
+
+function parseQueryRequestV2(value: unknown): Readonly<{
+ bindingsValue: Readonly>;
+ continuation: string | null;
+ programId: string;
+}> {
+ let keys: readonly string[];
+ try {
+ keys = ownDataKeysV2(value, 4, "The parameterized memory query");
+ } catch {
+ throw new TypeError("Invalid parameterized memory query.");
+ }
+ if (keys.length !== 4 || !["bindings", "continuation", "programId", "v"]
+ .every((key) => keys.includes(key))) {
+ throw new TypeError("Invalid parameterized memory query.");
+ }
+ const record = value as Record;
+ if (record.v !== 2 || (record.continuation !== null
+ && typeof record.continuation !== "string")) {
+ throw new TypeError("Invalid parameterized memory query.");
+ }
+ const programId = safeCode(record.programId, 128);
+ if (programId === null) throw new TypeError("Invalid parameterized memory query identity.");
+ const continuation = record.continuation as string | null;
+ if (typeof continuation === "string" && (continuation.length < 1
+ || continuation.length > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes
+ || utf8ByteLength(continuation) > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes)) {
+ throw new RangeError("The memory continuation exceeds its byte bound.");
+ }
+ const bindingKeys = ownDataKeysV2(record.bindings, OH_MEMORY_QUERY_LIMITS_V2.bindings,
+ "The parameterized memory query bindings");
+ const bindingRecord = record.bindings as Record;
+ const bindings: Record = {};
+ for (const key of bindingKeys) {
+ if (safeCode(key, 128) === null) throw new TypeError("Invalid memory binding name.");
+ bindings[key] = parsePrimitiveBindingV2(bindingRecord[key]);
+ }
+ const boundedRequest = { bindings, continuation, programId, v: 2 as const };
+ if (utf8ByteLength(canonicalJson(boundedRequest)) > OH_MEMORY_QUERY_LIMITS_V2.requestBytes) {
+ throw new RangeError("The parameterized memory query exceeds its canonical byte bound.");
+ }
+ return { bindingsValue: immutableClone(bindings), continuation, programId };
+}
+
+function parseBindingsV2(value: Readonly>,
+ parameters: readonly string[]): Readonly<{
+ bindings: Readonly>;
+ bindingsSha256: Sha256Hex;
+}> {
+ if (!isPlainRecord(value) || !hasExactKeys(value, parameters)) {
+ throw new TypeError("Memory query bindings must exactly match the host-declared parameters.");
+ }
+ const bindings: Record = {};
+ for (const parameter of parameters) bindings[parameter] = value[parameter]!;
+ if (utf8ByteLength(canonicalJson(bindings)) > OH_MEMORY_QUERY_LIMITS_V2.bindingBytes) {
+ throw new RangeError("Memory query bindings exceed their canonical byte bound.");
+ }
+ const detached = immutableClone(bindings);
+ return Object.freeze({ bindings: detached,
+ bindingsSha256: canonicalSha256({ bindings: detached, parameters, v: 2 }) });
+}
+
+function bindQueryV2(query: OhProjectionQueryV1,
+ bindings: Readonly>): OhProjectionQueryV1 {
+ const where = query.where.map((literal) => createOhProjectionLiteralV1({
+ relation: literal.relation,
+ terms: literal.terms.map((term) => term.kind === "variable" && Object.hasOwn(bindings, term.name)
+ ? ohProjectionConstantV1(bindings[term.name]!) : term),
+ }));
+ return createOhProjectionQueryV1({ find: query.find, limit: query.limit,
+ queryId: query.queryId, where });
+}
+
+function publicRowV2(row: OhProjectionResultRowV1,
+ proofs: readonly OhMemoryProofV1[]): OhMemoryResultRowV2 {
+ const lanes = new Set();
+ let unknown = row.proofsTruncated;
+ for (const proof of proofs) unknown = collectLanes(proof, lanes) || unknown;
+ const premiseLanes = [...lanes].sort() as readonly OhMemoryLaneV1[];
+ const premiseAuthority: OhMemoryResultRowV2["premiseAuthority"] = unknown || premiseLanes.length === 0
+ ? "unknown" : premiseLanes.includes("working") ? "working" : "canonical";
+ const payload = { premiseAuthority, premiseLanes, proofsTruncated: row.proofsTruncated,
+ supportCount: row.supportCount, v: 2 as const, values: row.values };
+ return Object.freeze({ ...payload, resultRowSha256: canonicalSha256(payload) });
+}
+
+function continuationHmacV2(key: Uint8Array, value: OhMemoryContinuationV2): Buffer {
+ return createHmac("sha256", key).update("oh.memory.continuation.v2\0", "utf8")
+ .update(canonicalJson(value), "utf8").digest();
+}
+
+function encodeContinuationV2(value: OhMemoryContinuationIdentityV2,
+ key: Uint8Array): Readonly<{ continuation: string; continuationSha256: Sha256Hex }> {
+ const identity = immutableClone(value);
+ const continuationSha256 = canonicalSha256(identity);
+ const signed = immutableClone({ ...identity, continuationSha256 });
+ const envelope: OhMemoryContinuationEnvelopeV2 = immutableClone({ ...signed,
+ continuationHmacSha256: continuationHmacV2(key, signed).toString("hex") as Sha256Hex });
+ const continuation = Buffer.from(canonicalJson(envelope), "utf8").toString("base64url");
+ if (utf8ByteLength(continuation) > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes) {
+ throw new RangeError("The issued memory continuation exceeds its byte bound.");
+ }
+ return Object.freeze({ continuation, continuationSha256 });
+}
+
+function parseContinuationV2(value: string, key: Uint8Array): OhMemoryContinuationV2 {
+ if (value.length < 1 || utf8ByteLength(value) > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes
+ || !/^[A-Za-z0-9_-]+$/u.test(value)) throw new TypeError("Invalid memory continuation encoding.");
+ const bytes = Buffer.from(value, "base64url");
+ if (bytes.toString("base64url") !== value
+ || bytes.byteLength > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes) {
+ throw new TypeError("Invalid memory continuation encoding.");
+ }
+ const text = bytes.toString("utf8");
+ let decoded: unknown;
+ try { decoded = JSON.parse(text); } catch { throw new TypeError("Invalid memory continuation JSON."); }
+ if (!isPlainRecord(decoded)
+ || !hasExactKeys(decoded, ["bindingsSha256", "continuationHmacSha256", "continuationSha256",
+ "memorySha256", "nextOffset", "pageSize", "programSha256", "projectionResultSha256",
+ "totalRows", "v"])
+ || decoded.v !== 2) throw new TypeError("Invalid memory continuation payload.");
+ const bindingsSha256 = parseSha256Hex(decoded.bindingsSha256);
+ const continuationHmacSha256 = parseSha256Hex(decoded.continuationHmacSha256);
+ const continuationSha256 = parseSha256Hex(decoded.continuationSha256);
+ const memorySha256 = parseSha256Hex(decoded.memorySha256);
+ const programSha256 = parseSha256Hex(decoded.programSha256);
+ const projectionResultSha256 = parseSha256Hex(decoded.projectionResultSha256);
+ if (bindingsSha256 === null || continuationHmacSha256 === null
+ || continuationSha256 === null || memorySha256 === null || programSha256 === null
+ || projectionResultSha256 === null
+ || !Number.isSafeInteger(decoded.nextOffset) || (decoded.nextOffset as number) < 1
+ || (decoded.nextOffset as number) > OH_MEMORY_QUERY_LIMITS_V2.maximumProgramRows
+ || !Number.isSafeInteger(decoded.pageSize) || (decoded.pageSize as number) < 1
+ || (decoded.pageSize as number) > OH_MEMORY_QUERY_LIMITS_V2.maximumPageRows
+ || !Number.isSafeInteger(decoded.totalRows) || (decoded.totalRows as number) < 1
+ || (decoded.totalRows as number) > OH_MEMORY_QUERY_LIMITS_V2.maximumProgramRows
+ || (decoded.nextOffset as number) >= (decoded.totalRows as number)
+ || (decoded.nextOffset as number) % (decoded.pageSize as number) !== 0) {
+ throw new TypeError("Invalid memory continuation identity.");
+ }
+ const identity: OhMemoryContinuationIdentityV2 = { bindingsSha256, memorySha256,
+ nextOffset: decoded.nextOffset as number,
+ pageSize: decoded.pageSize as number, programSha256, projectionResultSha256,
+ totalRows: decoded.totalRows as number, v: 2 as const };
+ const signed: OhMemoryContinuationV2 = { ...identity, continuationSha256 };
+ const envelope: OhMemoryContinuationEnvelopeV2 = { ...signed, continuationHmacSha256 };
+ if (canonicalJson(envelope) !== text) throw new TypeError("Invalid memory continuation payload.");
+ const expectedHmac = continuationHmacV2(key, signed);
+ const receivedHmac = Buffer.from(continuationHmacSha256, "hex");
+ if (!timingSafeEqual(expectedHmac, receivedHmac)) {
+ throw new OhIntegrityError("The memory continuation is not an issued capability.");
+ }
+ if (canonicalSha256(identity) !== continuationSha256) {
+ throw new OhIntegrityError("The memory continuation digest is invalid.");
+ }
+ return Object.freeze(signed);
+}
+
+function parseExplainRequestV2(value: unknown): Readonly<{
+ pageRow: number;
+ resultSha256: Sha256Hex;
+ token: string;
+}> {
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["pageRow", "resultSha256", "token", "v"])
+ || value.v !== 2 || typeof value.token !== "string" || value.token.length !== 43
+ || !Number.isSafeInteger(value.pageRow) || (value.pageRow as number) < 0) {
+ throw new TypeError("Invalid V2 memory explanation request.");
+ }
+ const resultSha256 = parseSha256Hex(value.resultSha256);
+ if (resultSha256 === null) throw new TypeError("Invalid V2 memory explanation result identity.");
+ return { pageRow: value.pageRow as number, resultSha256, token: value.token };
+}
+
+/**
+ * Creates the additive V2 memory facade. V2 adds only host-declared primitive
+ * bindings and fail-closed stable pagination; V1 request and digest contracts
+ * remain untouched.
+ */
+export async function createOhMemoryAgentV2(options: OhMemoryFacadeOptionsV2): Promise {
+ const memoryActorId = safeCode(options.actorId, 128);
+ if (memoryActorId === null) throw new TypeError("Invalid host-bound memory actor ID.");
+ const continuationKey = continuationKeyV2(options.continuationKey);
+ const canonicalStore = options.canonical.store;
+ const workingStore = options.working.store;
+ const workingCodecs = options.working.codecs;
+ const canonicalAuthorityId = authorityId(options.canonical.authorityId);
+ const workingAuthorityId = authorityId(options.working.authorityId);
+ if (canonicalAuthorityId === workingAuthorityId) {
+ throw new OhProfileError("Working and canonical memory must be distinct physical authorities.");
+ }
+ const canonicalBinding = bindingFor(canonicalStore,
+ options.canonical.expectedBindingSha256, "canonical");
+ const workingBinding = bindingFor(workingStore,
+ options.working.expectedBindingSha256, "working");
+ const expectedCanonicalHead = parseOhHeadV1(options.canonical.expectedHead);
+ if (expectedCanonicalHead === null) throw new TypeError("Invalid pinned canonical memory head.");
+ const programs = resolveProgramsV2(options.programs);
+ const extractors = resolveExtractors(options.extractors ?? []);
+ const nominationRoutes = resolveNominationRoutes(options.nominationRoutes ?? []);
+ const ingress = new OhSemanticBundleIngressV1(workingStore, workingCodecs);
+ const now = options.now ?? (() => new Date());
+ const monotonicNow = options.monotonicNow ?? (() => performance.now());
+ const capabilityLifetime = options.explainCapabilityLifetimeMs
+ ?? OH_MEMORY_LIMITS_V1.explainCapabilityLifetimeMs;
+ if (!Number.isSafeInteger(capabilityLifetime) || capabilityLifetime < 1_000
+ || capabilityLifetime > 60 * 60 * 1_000) {
+ throw new RangeError("Invalid memory explanation capability lifetime.");
+ }
+ const canonical = await readLane({ authorityId: canonicalAuthorityId,
+ binding: canonicalBinding, store: canonicalStore }, "canonical", expectedCanonicalHead);
+ const explanations = new Map();
+ let explanationBytes = 0;
+ let lastMonotonicMs = -1;
+ let lastWallClockMs = Number.NEGATIVE_INFINITY;
+ const wallClock = () => {
+ const milliseconds = clockMilliseconds(now);
+ if (milliseconds < lastWallClockMs) throw new OhProfileError("The memory wall clock regressed.");
+ lastWallClockMs = milliseconds;
+ return milliseconds;
+ };
+ const monotonicClock = () => {
+ const milliseconds = monotonicMilliseconds(monotonicNow);
+ if (milliseconds < lastMonotonicMs) throw new OhProfileError("The memory monotonic clock regressed.");
+ lastMonotonicMs = milliseconds;
+ return milliseconds;
+ };
+ const deleteExplanation = (token: string) => {
+ const stored = explanations.get(token);
+ if (stored !== undefined && explanations.delete(token)) explanationBytes -= stored.bytes;
+ };
+
+ const remember = async (value: unknown): Promise => {
+ if (utf8ByteLength(canonicalJson(value)) > OH_MEMORY_LIMITS_V1.rememberBytes) {
+ throw new RangeError("The memory semantic bundle exceeds its canonical byte bound.");
+ }
+ if (!isPlainRecord(value) || !hasExactKeys(value,
+ ["expectedHead", "puts", "requestId", "tombstones", "v"]) || value.v !== 1) {
+ throw new TypeError("Invalid memory remember request.");
+ }
+ const requestId = safeCode(value.requestId, 128);
+ if (requestId === null) throw new TypeError("Invalid memory remember request identity.");
+ const operationId = `memory_${canonicalSha256({ actorId: memoryActorId,
+ bindingSha256: workingBinding.bindingSha256, requestId, v: 1 }).slice(0, 48)}`;
+ const operation = await ingress.commit({ actorId: memoryActorId,
+ expectedHead: value.expectedHead, instant: isoInstant(new Date(wallClock())), operationId,
+ puts: value.puts, tombstones: value.tombstones, v: 1 });
+ const head: OhHeadV1 = { generation: operation.sequence,
+ graphRevisionSha256: operation.graphRevisionSha256,
+ operationSha256: operation.operationSha256, recordsSha256: operation.recordsSha256,
+ sequence: operation.sequence, v: 1 };
+ const payload = { actorId: operation.actorId, authorityId: workingAuthorityId,
+ bindingSha256: workingBinding.bindingSha256, head, instant: operation.instant,
+ lane: "working" as const, operationSha256: operation.operationSha256, requestId,
+ status: "committed" as const, v: 1 as const };
+ return immutableClone({ ...payload, receiptSha256: canonicalSha256(payload) });
+ };
+
+ const query = async (value: unknown): Promise => {
+ const request = parseQueryRequestV2(value);
+ const program = programs.get(request.programId);
+ if (program === undefined) throw new TypeError("Unknown named V2 memory program.");
+ const bound = parseBindingsV2(request.bindingsValue, program.parameters);
+ const requestedContinuation = request.continuation === null
+ ? null : parseContinuationV2(request.continuation, continuationKey);
+ if (requestedContinuation !== null
+ && (requestedContinuation.bindingsSha256 !== bound.bindingsSha256
+ || requestedContinuation.pageSize !== program.pageSize
+ || requestedContinuation.programSha256 !== program.programSha256
+ || requestedContinuation.totalRows > program.maximumRows
+ || requestedContinuation.nextOffset >= requestedContinuation.totalRows
+ || requestedContinuation.nextOffset % program.pageSize !== 0)) {
+ throw new OhIntegrityError("The memory continuation does not match this exact program, binding, and page identity.");
+ }
+ const boundQuery = bindQueryV2(program.query, bound.bindings);
+ const working = await readLane({ authorityId: workingAuthorityId,
+ binding: workingBinding, store: workingStore }, "working");
+ const composite = createCompositeDataset(canonical, working, extractors);
+ const projection = evaluateOhProjectionV1({ dataset: composite.dataset,
+ options: program.evaluation, query: boundQuery, rulePack: program.rulePack,
+ snapshot: composite.snapshot });
+ if (projection.stats.truncated) {
+ const reasons = projection.stats.truncationReasons.join(", ");
+ throw new RangeError(`The V2 memory projection was truncated (${reasons}); no page was returned.`);
+ }
+ if (projection.rows.length > program.maximumRows) {
+ throw new RangeError("The V2 memory projection exceeds its host-declared row bound.");
+ }
+ const identityPayload = {
+ bindings: bound.bindings,
+ bindingsSha256: bound.bindingsSha256,
+ boundQuerySha256: boundQuery.querySha256,
+ canonical: laneIdentity(canonical),
+ compositeDatasetSha256: composite.dataset.datasetSha256,
+ conflictPolicy: OH_MEMORY_CONFLICT_POLICY_V1,
+ evaluationSha256: projection.identity.evaluationSha256,
+ programId: program.programId,
+ programSha256: program.programSha256,
+ projectionSha256: projection.identity.projectionSha256,
+ purpose: program.purpose,
+ rulePackSha256: program.rulePack.rulePackSha256,
+ templateQuerySha256: program.query.querySha256,
+ v: 2 as const,
+ working: laneIdentity(working),
+ };
+ const identity: OhMemoryIdentityV2 = immutableClone({ ...identityPayload,
+ memorySha256: canonicalSha256(identityPayload) });
+ if (requestedContinuation !== null
+ && (requestedContinuation.memorySha256 !== identity.memorySha256
+ || requestedContinuation.projectionResultSha256 !== projection.resultSha256)) {
+ throw new OhIntegrityError("The memory continuation does not match this exact source and projection identity.");
+ }
+ if (requestedContinuation !== null
+ && (requestedContinuation.totalRows !== projection.rows.length
+ || requestedContinuation.nextOffset >= projection.rows.length
+ || requestedContinuation.nextOffset % program.pageSize !== 0)) {
+ throw new OhIntegrityError("The memory continuation does not match this exact row identity.");
+ }
+ const start = requestedContinuation?.nextOffset ?? 0;
+ const endExclusive = Math.min(start + program.pageSize, projection.rows.length);
+ const projectionRows = projection.rows.slice(start, endExclusive);
+ const proofs = immutableClone(projectionRows.map((row) => row.proofs.map((proof) =>
+ mapProof(proof, composite.sources, composite.factPolicies))));
+ const rows = immutableClone(projectionRows.map((row, index) => publicRowV2(row, proofs[index]!)));
+ const hasMore = endExclusive < projection.rows.length;
+ const page: OhMemoryPageV2 = immutableClone({ completeness: hasMore ? "partial" : "complete",
+ endExclusive, hasMore, maximumPageBytes: program.maximumPageBytes, pageSize: program.pageSize,
+ returnedRows: rows.length, start, totalRows: projection.rows.length,
+ truncation: { reasons: [], truncated: false, v: 2 as const }, v: 2 as const });
+ const issuedContinuation = hasMore ? encodeContinuationV2({ bindingsSha256: bound.bindingsSha256,
+ memorySha256: identity.memorySha256, nextOffset: endExclusive, pageSize: program.pageSize,
+ programSha256: program.programSha256, projectionResultSha256: projection.resultSha256,
+ totalRows: projection.rows.length, v: 2 }, continuationKey) : null;
+ const continuation = issuedContinuation?.continuation ?? null;
+ const continuationSha256 = issuedContinuation?.continuationSha256 ?? null;
+ const conflicts = immutableClone({ count: composite.conflicts.length,
+ conflictsSha256: canonicalSha256(composite.conflicts), v: 2 as const });
+ const resultIdentityPayload = immutableClone({ authority: "derived" as const, conflicts,
+ continuationSha256,
+ identity, page, projectionResultSha256: projection.resultSha256, rows, v: 2 as const });
+ const resultSha256 = canonicalSha256(resultIdentityPayload);
+ const resultPayload = immutableClone({ ...resultIdentityPayload, continuation });
+ const issuedAt = wallClock();
+ const issuedAtMonotonic = monotonicClock();
+ const expiresAtMs = issuedAt + capabilityLifetime;
+ const expiresAtMonotonicMs = issuedAtMonotonic + capabilityLifetime;
+ const expiresAt = isoInstant(new Date(expiresAtMs));
+ const pageBytePreflight = { ...resultPayload,
+ explainCapability: { expiresAt, token: "A".repeat(43), v: 2 as const }, resultSha256 };
+ if (utf8ByteLength(canonicalJson(pageBytePreflight)) > program.maximumPageBytes) {
+ throw new RangeError("The V2 memory page exceeds its host-declared canonical byte bound.");
+ }
+ for (const [existingToken, stored] of explanations) {
+ if (issuedAtMonotonic >= stored.expiresAtMonotonicMs) deleteExplanation(existingToken);
+ }
+ const storedPayload = immutableClone({ expiresAtMonotonicMs, identity, page, proofs,
+ resultSha256, rows });
+ const storedBytes = utf8ByteLength(canonicalJson(storedPayload)) + 128;
+ if (storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityEntryBytes
+ || storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityTotalBytes) {
+ throw new RangeError("The V2 memory explanation exceeds its retained capability bound.");
+ }
+ while (explanations.size >= OH_MEMORY_LIMITS_V1.explainCapabilities
+ || explanationBytes + storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityTotalBytes) {
+ const oldest = explanations.keys().next().value as string | undefined;
+ if (oldest === undefined) break;
+ deleteExplanation(oldest);
+ }
+ let token = randomBytes(32).toString("base64url");
+ while (explanations.has(token)) token = randomBytes(32).toString("base64url");
+ explanations.set(token, immutableClone({ ...storedPayload, bytes: storedBytes }));
+ explanationBytes += storedBytes;
+ const result = immutableClone({ ...resultPayload,
+ explainCapability: { expiresAt, token, v: 2 as const }, resultSha256 });
+ if (utf8ByteLength(canonicalJson(result)) > program.maximumPageBytes) {
+ deleteExplanation(token);
+ throw new RangeError("The V2 memory page exceeds its host-declared canonical byte bound.");
+ }
+ return result;
+ };
+
+ const explain = async (value: unknown): Promise => {
+ const request = parseExplainRequestV2(value);
+ const stored = explanations.get(request.token);
+ const currentTime = monotonicClock();
+ if (stored === undefined || stored.resultSha256 !== request.resultSha256
+ || currentTime >= stored.expiresAtMonotonicMs) {
+ deleteExplanation(request.token);
+ throw new OhProfileError("The V2 memory explanation capability is absent, expired, or misbound.");
+ }
+ const row = stored.rows[request.pageRow];
+ const proofs = stored.proofs[request.pageRow];
+ if (row === undefined || proofs === undefined) throw new RangeError("The explanation page row is out of bounds.");
+ const payload = { authority: "derived" as const, identity: stored.identity, page: stored.page,
+ pageRow: request.pageRow, premiseAuthority: row.premiseAuthority,
+ premiseLanes: row.premiseLanes, proofs, proofsTruncated: row.proofsTruncated,
+ resultRowSha256: row.resultRowSha256, resultSha256: stored.resultSha256,
+ supportCount: row.supportCount, v: 2 as const, values: row.values };
+ return immutableClone({ ...payload, explanationSha256: canonicalSha256(payload) });
+ };
+
+ const nominate = async (value: unknown): Promise => {
+ const request = parseNominationRequest(value);
+ const route = nominationRoutes.get(request.nominationId);
+ if (route === undefined) throw new TypeError("Unknown named memory nomination route.");
+ const head = parseOhHeadV1(immutableClone(await workingStore.head()));
+ if (head === null) throw new OhIntegrityError("The working nomination store returned an invalid head.");
+ const closure = await workingStore.exportDependencyClosure({ head: {
+ operationSha256: head.operationSha256, sequence: head.sequence }, roots: request.roots });
+ const verified = verifyOhDependencyClosureAgainstV1(closure, { binding: workingBinding, head });
+ if (!verified.ok) throw new OhIntegrityError("The working nomination closure failed exact verification.");
+ if (canonicalJson(verified.closure.roots) !== canonicalJson(request.roots)) {
+ throw new OhIntegrityError("The working nomination closure substituted different roots.");
+ }
+ const source = Object.freeze({ authorityId: workingAuthorityId,
+ bindingSha256: workingBinding.bindingSha256, head, lane: "working" as const, v: 1 as const });
+ const payload = { closure: verified.closure, destinationPurpose: route.destinationPurpose,
+ nominationId: route.nominationId, source, status: "prepared" as const, v: 1 as const };
+ return immutableClone({ ...payload, nominationSha256: canonicalSha256(payload) });
+ };
+
+ return Object.freeze({ explain, nominate, query, remember });
+}
diff --git a/tests/node-portable-types.ts b/tests/node-portable-types.ts
index 5742306..225a848 100644
--- a/tests/node-portable-types.ts
+++ b/tests/node-portable-types.ts
@@ -4,7 +4,10 @@ import {
} from "@hraness/oh/store";
import {
createOhMemoryAgentV1,
+ createOhMemoryAgentV2,
type OhMemoryFacadeOptionsV1,
+ type OhMemoryFacadeOptionsV2,
+ type OhMemoryQueryResultV2,
type OhMemoryRememberReceiptV1,
} from "@hraness/oh/experimental/memory";
@@ -12,6 +15,11 @@ import {
// runtime exercise lives in node-portable.mjs; this catches declaration drift.
export const portableCodecs = new OhRecordCodecRegistry();
export const portableMemoryFactory: typeof createOhMemoryAgentV1 = createOhMemoryAgentV1;
+export const portableMemoryFactoryV2: typeof createOhMemoryAgentV2 = createOhMemoryAgentV2;
+export const portableMemoryContinuationKey:
+NonNullable = new Uint8Array(32);
export type PortableMemoryOptions = OhMemoryFacadeOptionsV1;
+export type PortableMemoryOptionsV2 = OhMemoryFacadeOptionsV2;
+export type PortableMemoryResultV2 = OhMemoryQueryResultV2;
export type PortableMemoryReceipt = OhMemoryRememberReceiptV1;
export type PortableStore = OhStoreV1;
diff --git a/tests/node-portable.mjs b/tests/node-portable.mjs
index f8c19f1..4f91676 100644
--- a/tests/node-portable.mjs
+++ b/tests/node-portable.mjs
@@ -14,6 +14,7 @@ assert.equal(typeof libsql.createOhLibSqlStoreAuthorityV1, "function");
assert.equal(typeof libsql.openExistingOhLibSqlStoreAuthorityV1, "function");
assert.equal(typeof libsql.purgeOhLibSqlWorkingSpaceV1, "function");
assert.equal(typeof memory.createOhMemoryAgentV1, "function");
+assert.equal(typeof memory.createOhMemoryAgentV2, "function");
assert.equal(store.OH_WORKING_STORE_PROFILE_V1.profileKind, "working");
assert.equal(store.OH_WORKING_STORE_PROFILE_V1.capabilities.operationReplication, false);
assert.equal(store.OH_WORKING_STORE_PROFILE_V1.capabilities.wholeSpacePurge, true);
@@ -71,3 +72,41 @@ assert.equal(result.authority, "derived");
assert.equal(result.identity.purpose, "node.portability");
assert.deepEqual(result.rows, []);
assert.equal(Object.isFrozen(result.rows), true);
+
+const queryV2 = projection.createOhProjectionQueryV1({
+ find: ["digest"], limit: 10, queryId: "memory.node-visible-v2",
+ where: [visible],
+});
+const agentV2 = await memory.createOhMemoryAgentV2({
+ actorId: "node.memory-agent-v2",
+ canonical: { authorityId: "node.canonical-v2",
+ expectedBindingSha256: canonicalStore.binding.bindingSha256,
+ expectedHead: await canonicalStore.head(), store: canonicalStore },
+ continuationKey: new Uint8Array(32).fill(1),
+ monotonicNow: () => 0,
+ now: () => new Date("2026-08-29T12:00:00.000Z"),
+ programs: [{
+ evaluation: { maximumDerivedTuples: 100, maximumProofDepth: 16,
+ maximumProofNodes: 16, maximumResultBytes: 1024 * 1024, maximumRounds: 16,
+ maximumTotalProofNodes: 100, maximumWorkUnits: 10_000 },
+ maximumPageBytes: 256 * 1024,
+ maximumRows: 10,
+ pageSize: 5,
+ parameters: ["key", "lane"],
+ programId: "memory.node-visible-v2",
+ purpose: "node.portability-v2",
+ query: queryV2,
+ rulePack,
+ v: 2,
+ }],
+ working: { authorityId: "node.working-v2", codecs: new store.OhRecordCodecRegistry(),
+ expectedBindingSha256: workingStore.binding.bindingSha256, store: workingStore },
+});
+const resultV2 = await agentV2.query({ bindings: { key: "entity:portable", lane: "working" },
+ continuation: null, programId: "memory.node-visible-v2", v: 2 });
+assert.equal(resultV2.identity.purpose, "node.portability-v2");
+assert.equal(resultV2.page.completeness, "complete");
+assert.equal(resultV2.page.hasMore, false);
+assert.equal(resultV2.continuation, null);
+assert.equal(resultV2.continuationSha256, null);
+assert.deepEqual(resultV2.rows, []);
diff --git a/tests/public-surface.test.ts b/tests/public-surface.test.ts
index 33da791..82a7594 100644
--- a/tests/public-surface.test.ts
+++ b/tests/public-surface.test.ts
@@ -31,6 +31,7 @@ const markdownFiles = [
"spec/v1/sync.md",
"spec/v1/embedding.md",
"spec/v1/projection.md",
+ "spec/v1/memory.md",
"spec/v1/migration.md",
"skills/oh/SKILL.md",
] as const;
@@ -152,7 +153,7 @@ describe("public identity and documentation", () => {
]);
expect(readme.startsWith(`# ${tagline}\n`)).toBe(true);
expect(packageJson.name).toBe("@hraness/oh");
- expect(packageJson.version).toBe("0.2.2");
+ expect(packageJson.version).toBe("0.2.3");
expect(packageJson.description).toBe(tagline);
expect(packageJson.homepage).toBe("https://oh.computer");
expect(packageJson.license).toBe("MIT");
@@ -365,6 +366,9 @@ describe("versioned public contract", () => {
expect(version.memory).toEqual({ specification: "./v1/memory.md" });
const memory = await readFile(join(root, "spec/v1/memory.md"), "utf8");
expect(memory).toContain("One kernel, two authorities");
+ expect(memory).toContain("createOhMemoryAgentV2");
+ expect(memory).toContain("authenticated bearer cursor, not knowledge authority");
+ expect(memory).toContain("`resultSha256` commits that deterministic");
expect(memory).toContain("It does not sync the working operation chain");
});
diff --git a/tests/site-surface.test.ts b/tests/site-surface.test.ts
index 6bab0f2..838592a 100644
--- a/tests/site-surface.test.ts
+++ b/tests/site-surface.test.ts
@@ -73,6 +73,7 @@ describe("public site surface", () => {
const workflow = await readFile(join(root, ".github/workflows/ci.yml"), "utf8");
expect(specification).toContain('import contract from "../../public/spec/v1/contract.json"');
expect(specification).toContain("contract.recordKinds.map");
+ expect(specification).toContain("The additive V2 facade");
expect(workflow).toContain("Require an exact public specification mirror");
expect(workflow).toContain("working-directory: site");
expect(workflow).toContain("bun run lint");