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");