@hraness/oh 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -114
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +811 -97
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/graph.d.ts.map +1 -1
- package/dist/index.js +676 -61
- package/dist/libsql.d.ts.map +1 -1
- package/dist/libsql.js +162 -35
- package/dist/memory-page.js +2 -2
- package/dist/memory.d.ts +87 -6
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +1106 -148
- package/dist/operation.d.ts +3 -1
- package/dist/operation.d.ts.map +1 -1
- package/dist/projection-public.js +2 -2
- package/dist/projection-suss.js +2 -2
- package/dist/sdk.js +780 -88
- package/dist/semantic-cloud.js +2 -2
- package/dist/semantic.js +2 -2
- package/dist/sqlite/index.js +1251 -306
- package/dist/sqlite/port.d.ts +31 -3
- package/dist/sqlite/port.d.ts.map +1 -1
- package/dist/sqlite/store.d.ts +18 -2
- package/dist/sqlite/store.d.ts.map +1 -1
- package/dist/store.d.ts +3 -12
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +154 -32
- package/dist/sync.d.ts +7 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +668 -35
- package/package.json +5 -1
- package/skills/oh/SKILL.md +42 -16
- package/spec/README.md +2 -2
- package/spec/v1/memory.md +134 -16
- package/spec/v1/storage.md +8 -5
- package/spec/v1/store.md +20 -0
- package/spec/v1/sync.md +77 -8
- package/src/cli.test.ts +53 -1
- package/src/cli.ts +34 -8
- package/src/errors.test.ts +87 -0
- package/src/errors.ts +185 -0
- package/src/graph.ts +2 -2
- package/src/libsql.test.ts +36 -0
- package/src/libsql.ts +26 -5
- package/src/memory.test.ts +1488 -18
- package/src/memory.ts +1199 -122
- package/src/operation.ts +13 -3
- package/src/sqlite/port.test.ts +209 -0
- package/src/sqlite/port.ts +118 -4
- package/src/sqlite/store.test.ts +106 -1
- package/src/sqlite/store.ts +168 -30
- package/src/store.test.ts +12 -0
- package/src/store.ts +30 -20
- package/src/sync.test.ts +570 -2
- package/src/sync.ts +586 -36
package/src/memory.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { isProxy } from "node:util/types";
|
|
2
3
|
|
|
3
4
|
export * from "./memory-pages";
|
|
4
5
|
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
canonicalSha256,
|
|
8
9
|
hasExactKeys,
|
|
9
10
|
isPlainRecord,
|
|
11
|
+
orderedUnique,
|
|
10
12
|
parseCanonicalInstantV1,
|
|
11
13
|
parseSha256Hex,
|
|
12
14
|
safeCode,
|
|
@@ -16,10 +18,13 @@ import {
|
|
|
16
18
|
} from "./canonical";
|
|
17
19
|
import { type OhRecordCodecRegistry } from "./contract";
|
|
18
20
|
import {
|
|
21
|
+
canonicalKnowledgeGraphChangesV1,
|
|
19
22
|
createKnowledgeGraphRecordV1,
|
|
20
23
|
knowledgeGraphRecordRefV1,
|
|
24
|
+
type KnowledgeGraphChangeV1,
|
|
21
25
|
type KnowledgeGraphRecordV1,
|
|
22
26
|
} from "./graph";
|
|
27
|
+
import { OH_OPERATION_MAX_BYTES_V1, parseOhOperationV1 } from "./operation";
|
|
23
28
|
import {
|
|
24
29
|
OH_PROJECTION_SEMANTICS_V1,
|
|
25
30
|
OH_PROJECTION_LIMITS_V1,
|
|
@@ -44,15 +49,19 @@ import {
|
|
|
44
49
|
type OhProjectionSnapshotV1,
|
|
45
50
|
} from "./projection";
|
|
46
51
|
import {
|
|
52
|
+
OhConflictError,
|
|
47
53
|
OhIntegrityError,
|
|
48
54
|
OH_DEPENDENCY_CLOSURE_LIMITS_V1,
|
|
49
55
|
OhProfileError,
|
|
50
56
|
OhSemanticBundleIngressV1,
|
|
57
|
+
parseOhDependencyClosureV1,
|
|
51
58
|
parseOhHeadV1,
|
|
59
|
+
parseOhHeadRefV1,
|
|
52
60
|
parseOhStoreBindingV1,
|
|
53
61
|
verifyOhDependencyClosureAgainstV1,
|
|
54
62
|
type OhDependencyClosureV1,
|
|
55
63
|
type OhHeadV1,
|
|
64
|
+
type OhHeadRefV1,
|
|
56
65
|
type OhSnapshotV1,
|
|
57
66
|
type OhStoreBindingV1,
|
|
58
67
|
type OhStoreV1,
|
|
@@ -60,6 +69,9 @@ import {
|
|
|
60
69
|
export const OH_MEMORY_FORMAT_VERSION_V1 = 1 as const;
|
|
61
70
|
export const OH_MEMORY_CONFLICT_POLICY_V1 = "visible-conflicts.v1" as const;
|
|
62
71
|
export const OH_MEMORY_LIMITS_V1 = Object.freeze({
|
|
72
|
+
detachedCanonicalBreadth: 65_536,
|
|
73
|
+
detachedCanonicalDepth: 128,
|
|
74
|
+
detachedCanonicalNodes: 1_048_576,
|
|
63
75
|
explainCapabilityEntryBytes: 32 * 1024 * 1024,
|
|
64
76
|
explainCapabilities: 256,
|
|
65
77
|
explainCapabilityLifetimeMs: 15 * 60 * 1_000,
|
|
@@ -301,7 +313,7 @@ export interface OhMemoryAgentV1 {
|
|
|
301
313
|
remember(value: unknown): Promise<OhMemoryRememberReceiptV1>;
|
|
302
314
|
}
|
|
303
315
|
|
|
304
|
-
/** Additive
|
|
316
|
+
/** Additive query and pagination limits; V1 contracts are unchanged. */
|
|
305
317
|
export const OH_MEMORY_QUERY_LIMITS_V2 = Object.freeze({
|
|
306
318
|
bindingBytes: 64 * 1024,
|
|
307
319
|
bindings: 32,
|
|
@@ -410,6 +422,23 @@ export type OhMemoryQueryResultV2 = Readonly<{
|
|
|
410
422
|
v: 2;
|
|
411
423
|
}>;
|
|
412
424
|
|
|
425
|
+
export type OhMemoryContinuationErrorReasonV2 = "authentication" | "encoding" | "identity";
|
|
426
|
+
|
|
427
|
+
/** A caller-supplied V2 continuation cannot be decoded, authenticated, or rebound exactly. */
|
|
428
|
+
export class OhMemoryContinuationError extends OhIntegrityError {
|
|
429
|
+
declare readonly code: "memory-continuation";
|
|
430
|
+
declare readonly reason: OhMemoryContinuationErrorReasonV2;
|
|
431
|
+
|
|
432
|
+
constructor(reason: OhMemoryContinuationErrorReasonV2, message: string) {
|
|
433
|
+
super(message);
|
|
434
|
+
this.name = "OhMemoryContinuationError";
|
|
435
|
+
Object.defineProperties(this, {
|
|
436
|
+
code: { configurable: false, enumerable: true, value: "memory-continuation", writable: false },
|
|
437
|
+
reason: { configurable: false, enumerable: true, value: reason, writable: false },
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
413
442
|
export type OhMemoryExplanationV2 = Readonly<{
|
|
414
443
|
authority: "derived";
|
|
415
444
|
explanationSha256: Sha256Hex;
|
|
@@ -434,6 +463,91 @@ export interface OhMemoryAgentV2 {
|
|
|
434
463
|
remember(value: unknown): Promise<OhMemoryRememberReceiptV1>;
|
|
435
464
|
}
|
|
436
465
|
|
|
466
|
+
export const OH_MEMORY_AUTHORITY_LIMITS_V1 = Object.freeze({
|
|
467
|
+
adoptionReplacements: 128,
|
|
468
|
+
adoptionRequestBytes: OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes + 192 * 1024,
|
|
469
|
+
canonicalAdvanceOperations: 16_384,
|
|
470
|
+
canonicalAdvancePages: 64,
|
|
471
|
+
canonicalChangeFeedPage: 1_000,
|
|
472
|
+
canonicalChangeFeedPageBytes: 64 * 1024 * 1024,
|
|
473
|
+
retainedExplanationRoutes: OH_MEMORY_LIMITS_V1.explainCapabilities,
|
|
474
|
+
reportedAdoptionConflicts: 128,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
export type OhMemoryCanonicalAdvanceReceiptV1 = Readonly<{
|
|
478
|
+
authorityId: string;
|
|
479
|
+
bindingSha256: Sha256Hex;
|
|
480
|
+
head: OhHeadV1;
|
|
481
|
+
priorHead: OhHeadV1;
|
|
482
|
+
receiptSha256: Sha256Hex;
|
|
483
|
+
status: "advanced" | "unchanged";
|
|
484
|
+
v: 1;
|
|
485
|
+
}>;
|
|
486
|
+
|
|
487
|
+
export type OhMemoryAdoptionConflictEntryV1 = Readonly<{
|
|
488
|
+
canonicalRecordSha256: Sha256Hex | null;
|
|
489
|
+
key: string;
|
|
490
|
+
nominatedRecordSha256: Sha256Hex;
|
|
491
|
+
v: 1;
|
|
492
|
+
}>;
|
|
493
|
+
|
|
494
|
+
/** Host-only compare-and-swap evidence for one intentional canonical replacement. */
|
|
495
|
+
export type OhMemoryAdoptionReplacementV1 = Readonly<{
|
|
496
|
+
expectedPriorRecordSha256: Sha256Hex;
|
|
497
|
+
key: string;
|
|
498
|
+
v: 1;
|
|
499
|
+
}>;
|
|
500
|
+
|
|
501
|
+
export type OhMemoryAdoptionConflictV1 = Readonly<{
|
|
502
|
+
actualHead: OhHeadV1;
|
|
503
|
+
conflicts: readonly OhMemoryAdoptionConflictEntryV1[];
|
|
504
|
+
conflictsSha256: Sha256Hex;
|
|
505
|
+
expectedHead: OhHeadV1;
|
|
506
|
+
reportedConflicts: number;
|
|
507
|
+
totalConflicts: number;
|
|
508
|
+
truncated: boolean;
|
|
509
|
+
v: 1;
|
|
510
|
+
}>;
|
|
511
|
+
|
|
512
|
+
export class OhMemoryAdoptionConflictError extends OhConflictError {
|
|
513
|
+
declare readonly conflict: OhMemoryAdoptionConflictV1;
|
|
514
|
+
|
|
515
|
+
constructor(conflict: OhMemoryAdoptionConflictV1) {
|
|
516
|
+
super("The nominated records conflict with the current canonical memory head.");
|
|
517
|
+
this.name = "OhMemoryAdoptionConflictError";
|
|
518
|
+
Object.defineProperty(this, "conflict", { configurable: false, enumerable: true,
|
|
519
|
+
value: immutableClone(conflict), writable: false });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export type OhMemoryAdoptionReceiptV1 = Readonly<{
|
|
524
|
+
actorId: string;
|
|
525
|
+
authorityId: string;
|
|
526
|
+
bindingSha256: Sha256Hex;
|
|
527
|
+
head: OhHeadV1;
|
|
528
|
+
nominationSha256: Sha256Hex;
|
|
529
|
+
operationSha256: Sha256Hex | null;
|
|
530
|
+
priorHead: OhHeadV1;
|
|
531
|
+
receiptSha256: Sha256Hex;
|
|
532
|
+
status: "adopted" | "already-present";
|
|
533
|
+
v: 1;
|
|
534
|
+
}>;
|
|
535
|
+
|
|
536
|
+
export interface OhMemoryHostControlV1 {
|
|
537
|
+
adoptNomination(value: unknown): Promise<OhMemoryAdoptionReceiptV1>;
|
|
538
|
+
advanceCanonical(value: unknown): Promise<OhMemoryCanonicalAdvanceReceiptV1>;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export type OhMemoryAuthorityV1 = Readonly<{
|
|
542
|
+
agent: OhMemoryAgentV2;
|
|
543
|
+
host: OhMemoryHostControlV1;
|
|
544
|
+
}>;
|
|
545
|
+
|
|
546
|
+
export type OhMemoryAuthorityOptionsV1 = OhMemoryFacadeOptionsV2 & Readonly<{
|
|
547
|
+
adoptionActorId: string;
|
|
548
|
+
maximumCanonicalOperationBytes?: number;
|
|
549
|
+
}>;
|
|
550
|
+
|
|
437
551
|
type LaneSnapshot = Readonly<{
|
|
438
552
|
authorityId: string;
|
|
439
553
|
binding: OhStoreBindingV1;
|
|
@@ -480,6 +594,169 @@ function immutableClone<T>(value: T): T {
|
|
|
480
594
|
return value;
|
|
481
595
|
}
|
|
482
596
|
|
|
597
|
+
type DetachedCanonicalData = Readonly<{
|
|
598
|
+
canonical: string;
|
|
599
|
+
value: unknown;
|
|
600
|
+
}>;
|
|
601
|
+
|
|
602
|
+
function canonicalStringByteLength(
|
|
603
|
+
value: string,
|
|
604
|
+
label: string,
|
|
605
|
+
path: string,
|
|
606
|
+
maximumBytes: number,
|
|
607
|
+
): number {
|
|
608
|
+
let bytes = 2;
|
|
609
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
610
|
+
const code = value.charCodeAt(index);
|
|
611
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
612
|
+
const next = value.charCodeAt(index + 1);
|
|
613
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) {
|
|
614
|
+
throw new TypeError(`${label} contains invalid Unicode at ${path}.`);
|
|
615
|
+
}
|
|
616
|
+
bytes += 4;
|
|
617
|
+
index += 1;
|
|
618
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
619
|
+
throw new TypeError(`${label} contains invalid Unicode at ${path}.`);
|
|
620
|
+
} else if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x09
|
|
621
|
+
|| code === 0x0a || code === 0x0c || code === 0x0d) {
|
|
622
|
+
bytes += 2;
|
|
623
|
+
} else if (code <= 0x1f) {
|
|
624
|
+
bytes += 6;
|
|
625
|
+
} else if (code <= 0x7f) {
|
|
626
|
+
bytes += 1;
|
|
627
|
+
} else if (code <= 0x7ff) {
|
|
628
|
+
bytes += 2;
|
|
629
|
+
} else {
|
|
630
|
+
bytes += 3;
|
|
631
|
+
}
|
|
632
|
+
if (bytes > maximumBytes) {
|
|
633
|
+
throw new RangeError(`${label} exceeds its canonical byte bound.`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return bytes;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Takes one descriptor-based JSON snapshot of an external value. No accessor is
|
|
641
|
+
* invoked, proxies are refused at every depth, and callers validate and execute
|
|
642
|
+
* only the returned frozen graph and its matching canonical bytes.
|
|
643
|
+
*/
|
|
644
|
+
function detachCanonicalData(
|
|
645
|
+
value: unknown,
|
|
646
|
+
label: string,
|
|
647
|
+
maximumBytes: number,
|
|
648
|
+
): DetachedCanonicalData {
|
|
649
|
+
const ancestors = new Set<object>();
|
|
650
|
+
let bytes = 0;
|
|
651
|
+
let nodes = 0;
|
|
652
|
+
const spendBytes = (count: number): void => {
|
|
653
|
+
bytes += count;
|
|
654
|
+
if (bytes > maximumBytes) throw new RangeError(`${label} exceeds its canonical byte bound.`);
|
|
655
|
+
};
|
|
656
|
+
const detach = (candidate: unknown, path: string, depth: number): unknown => {
|
|
657
|
+
if (depth > OH_MEMORY_LIMITS_V1.detachedCanonicalDepth) {
|
|
658
|
+
throw new RangeError(`${label} exceeds its canonical nesting depth bound.`);
|
|
659
|
+
}
|
|
660
|
+
nodes += 1;
|
|
661
|
+
if (nodes > OH_MEMORY_LIMITS_V1.detachedCanonicalNodes) {
|
|
662
|
+
throw new RangeError(`${label} exceeds its canonical node bound.`);
|
|
663
|
+
}
|
|
664
|
+
if (candidate === null) {
|
|
665
|
+
spendBytes(4);
|
|
666
|
+
return candidate;
|
|
667
|
+
}
|
|
668
|
+
if (typeof candidate === "boolean") {
|
|
669
|
+
spendBytes(candidate ? 4 : 5);
|
|
670
|
+
return candidate;
|
|
671
|
+
}
|
|
672
|
+
if (typeof candidate === "string") {
|
|
673
|
+
spendBytes(canonicalStringByteLength(candidate, label, path, maximumBytes - bytes));
|
|
674
|
+
return candidate;
|
|
675
|
+
}
|
|
676
|
+
if (typeof candidate === "number") {
|
|
677
|
+
if (!Number.isFinite(candidate) || Object.is(candidate, -0)) {
|
|
678
|
+
throw new TypeError(`${label} contains a noncanonical number at ${path}.`);
|
|
679
|
+
}
|
|
680
|
+
spendBytes(utf8ByteLength(canonicalJson(candidate)));
|
|
681
|
+
return candidate;
|
|
682
|
+
}
|
|
683
|
+
if (typeof candidate !== "object") {
|
|
684
|
+
throw new TypeError(`${label} contains a non-JSON value at ${path}.`);
|
|
685
|
+
}
|
|
686
|
+
if (isProxy(candidate)) throw new TypeError(`${label} contains a proxy at ${path}.`);
|
|
687
|
+
if (ancestors.has(candidate)) throw new TypeError(`${label} contains a cycle at ${path}.`);
|
|
688
|
+
ancestors.add(candidate);
|
|
689
|
+
try {
|
|
690
|
+
if (Array.isArray(candidate)) {
|
|
691
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(candidate, "length");
|
|
692
|
+
const length = lengthDescriptor?.value;
|
|
693
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0) {
|
|
694
|
+
throw new TypeError(`${label} contains an invalid array at ${path}.`);
|
|
695
|
+
}
|
|
696
|
+
if (length > OH_MEMORY_LIMITS_V1.detachedCanonicalBreadth) {
|
|
697
|
+
throw new RangeError(`${label} exceeds its canonical breadth bound.`);
|
|
698
|
+
}
|
|
699
|
+
if (length > OH_MEMORY_LIMITS_V1.detachedCanonicalNodes - nodes) {
|
|
700
|
+
throw new RangeError(`${label} exceeds its canonical node bound.`);
|
|
701
|
+
}
|
|
702
|
+
spendBytes(2 + Math.max(0, length - 1));
|
|
703
|
+
const keys = Reflect.ownKeys(candidate);
|
|
704
|
+
if (keys.length !== length + 1 || !keys.includes("length")
|
|
705
|
+
|| keys.some((key) => key !== "length" && (typeof key !== "string"
|
|
706
|
+
|| !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length))) {
|
|
707
|
+
throw new TypeError(`${label} contains a non-data array at ${path}.`);
|
|
708
|
+
}
|
|
709
|
+
const detached: unknown[] = [];
|
|
710
|
+
for (let index = 0; index < length; index += 1) {
|
|
711
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, String(index));
|
|
712
|
+
if (descriptor === undefined || !descriptor.enumerable
|
|
713
|
+
|| descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
714
|
+
throw new TypeError(`${label} contains a non-data array entry at ${path}[${index}].`);
|
|
715
|
+
}
|
|
716
|
+
detached.push(detach(descriptor.value, `${path}[${index}]`, depth + 1));
|
|
717
|
+
}
|
|
718
|
+
return Object.freeze(detached);
|
|
719
|
+
}
|
|
720
|
+
const prototype = Object.getPrototypeOf(candidate);
|
|
721
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
722
|
+
throw new TypeError(`${label} contains a non-plain object at ${path}.`);
|
|
723
|
+
}
|
|
724
|
+
const keys = Reflect.ownKeys(candidate);
|
|
725
|
+
if (keys.some((key) => typeof key !== "string")) {
|
|
726
|
+
throw new TypeError(`${label} contains a symbol property at ${path}.`);
|
|
727
|
+
}
|
|
728
|
+
if (keys.length > OH_MEMORY_LIMITS_V1.detachedCanonicalBreadth) {
|
|
729
|
+
throw new RangeError(`${label} exceeds its canonical breadth bound.`);
|
|
730
|
+
}
|
|
731
|
+
if (keys.length > OH_MEMORY_LIMITS_V1.detachedCanonicalNodes - nodes) {
|
|
732
|
+
throw new RangeError(`${label} exceeds its canonical node bound.`);
|
|
733
|
+
}
|
|
734
|
+
spendBytes(2 + Math.max(0, keys.length - 1));
|
|
735
|
+
const detached: Record<string, unknown> = {};
|
|
736
|
+
for (const key of keys as string[]) {
|
|
737
|
+
spendBytes(canonicalStringByteLength(key, label, `${path}.<key>`,
|
|
738
|
+
maximumBytes - bytes - 1) + 1);
|
|
739
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
|
|
740
|
+
if (descriptor === undefined || !descriptor.enumerable
|
|
741
|
+
|| descriptor.get !== undefined || descriptor.set !== undefined) {
|
|
742
|
+
throw new TypeError(`${label} contains a non-data property at ${path}.${key}.`);
|
|
743
|
+
}
|
|
744
|
+
Object.defineProperty(detached, key, { configurable: false, enumerable: true,
|
|
745
|
+
value: detach(descriptor.value, `${path}.${key}`, depth + 1), writable: false });
|
|
746
|
+
}
|
|
747
|
+
return Object.freeze(detached);
|
|
748
|
+
} finally {
|
|
749
|
+
ancestors.delete(candidate);
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
const detached = detach(value, "$root", 0);
|
|
753
|
+
const canonical = canonicalJson(detached);
|
|
754
|
+
if (utf8ByteLength(canonical) !== bytes) {
|
|
755
|
+
throw new OhIntegrityError(`${label} canonical byte accounting did not reproduce its snapshot.`);
|
|
756
|
+
}
|
|
757
|
+
return Object.freeze({ canonical, value: detached });
|
|
758
|
+
}
|
|
759
|
+
|
|
483
760
|
function compareText(left: string, right: string): number {
|
|
484
761
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
485
762
|
}
|
|
@@ -517,15 +794,56 @@ function laneIdentity(value: LaneSnapshot): OhMemoryLaneIdentityV1 {
|
|
|
517
794
|
});
|
|
518
795
|
}
|
|
519
796
|
|
|
520
|
-
function
|
|
797
|
+
function parseDetachedStoreHead(value: unknown, label: string): OhHeadV1 {
|
|
798
|
+
const head = parseOhHeadV1(detachCanonicalData(value, `${label} head`, 4 * 1024).value);
|
|
799
|
+
if (head === null) throw new OhIntegrityError(`${label} returned an invalid head.`);
|
|
800
|
+
return immutableClone(head);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function parseDetachedStoreSnapshot(
|
|
804
|
+
value: unknown,
|
|
805
|
+
label: string,
|
|
806
|
+
expectedHead: OhHeadV1,
|
|
807
|
+
spaceId: string,
|
|
808
|
+
): Readonly<{ projectionSnapshot: OhProjectionSnapshotV1; snapshot: OhSnapshotV1 }> {
|
|
809
|
+
const detachedSnapshot = detachCanonicalData(value, `${label} snapshot`,
|
|
810
|
+
OH_MEMORY_LIMITS_V1.snapshotBytesPerLane);
|
|
811
|
+
if (!isPlainRecord(detachedSnapshot.value)
|
|
812
|
+
|| !hasExactKeys(detachedSnapshot.value, ["head", "records", "v"])
|
|
813
|
+
|| detachedSnapshot.value.v !== 1 || !Array.isArray(detachedSnapshot.value.records)) {
|
|
814
|
+
throw new OhIntegrityError(`${label} returned an invalid snapshot envelope.`);
|
|
815
|
+
}
|
|
816
|
+
const detached = detachedSnapshot.value as Record<string, unknown>;
|
|
817
|
+
const detachedHead = parseOhHeadV1(detached.head);
|
|
818
|
+
if (detachedHead === null) throw new OhIntegrityError(`${label} returned an invalid snapshot head.`);
|
|
819
|
+
const snapshot: OhSnapshotV1 = immutableClone({ head: detachedHead,
|
|
820
|
+
records: detached.records as OhSnapshotV1["records"], v: 1 });
|
|
821
|
+
if (!exactHead(snapshot.head, expectedHead)) {
|
|
822
|
+
throw new OhIntegrityError(`${label} snapshot differs from its pinned head.`);
|
|
823
|
+
}
|
|
824
|
+
let projectionSnapshot: OhProjectionSnapshotV1;
|
|
825
|
+
try {
|
|
826
|
+
projectionSnapshot = createOhProjectionSnapshotV1({
|
|
827
|
+
head: snapshot.head,
|
|
828
|
+
records: snapshot.records,
|
|
829
|
+
spaceId,
|
|
830
|
+
});
|
|
831
|
+
} catch {
|
|
832
|
+
throw new OhIntegrityError(`${label} returned invalid snapshot records.`);
|
|
833
|
+
}
|
|
834
|
+
return Object.freeze({ projectionSnapshot, snapshot });
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function datasetForSnapshot(
|
|
838
|
+
binding: OhStoreBindingV1,
|
|
839
|
+
snapshot: OhSnapshotV1,
|
|
840
|
+
validatedProjectionSnapshot?: OhProjectionSnapshotV1,
|
|
841
|
+
): Readonly<{
|
|
521
842
|
dataset: OhProjectionDatasetV1;
|
|
522
843
|
projectionSnapshot: OhProjectionSnapshotV1;
|
|
523
844
|
}> {
|
|
524
|
-
const projectionSnapshot = createOhProjectionSnapshotV1({
|
|
525
|
-
head: snapshot.head,
|
|
526
|
-
records: snapshot.records,
|
|
527
|
-
spaceId: binding.spaceId,
|
|
528
|
-
});
|
|
845
|
+
const projectionSnapshot = validatedProjectionSnapshot ?? createOhProjectionSnapshotV1({
|
|
846
|
+
head: snapshot.head, records: snapshot.records, spaceId: binding.spaceId });
|
|
529
847
|
const dataset = createOhProjectionDatasetV1({
|
|
530
848
|
extractorSha256: canonicalSha256({ extractor: "oh.memory.lane-structural", v: 1 }),
|
|
531
849
|
factPackId: "oh.memory.lane-structural",
|
|
@@ -541,32 +859,21 @@ async function readLane(
|
|
|
541
859
|
lane: OhMemoryLaneV1,
|
|
542
860
|
expectedHead?: OhHeadV1,
|
|
543
861
|
): Promise<LaneSnapshot> {
|
|
544
|
-
const
|
|
545
|
-
const head =
|
|
546
|
-
|
|
862
|
+
const label = `The ${lane} store`;
|
|
863
|
+
const head = expectedHead === undefined
|
|
864
|
+
? parseDetachedStoreHead(await authority.store.head(), label)
|
|
865
|
+
: immutableClone(expectedHead);
|
|
547
866
|
const returnedSnapshot = await authority.store.snapshot({
|
|
548
867
|
head: { operationSha256: head.operationSha256, sequence: head.sequence },
|
|
549
868
|
maximumRecords: OH_MEMORY_LIMITS_V1.maximumRecordsPerLane,
|
|
550
869
|
});
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
const detached = immutableClone(returnedSnapshot);
|
|
557
|
-
const detachedHead = parseOhHeadV1(detached.head);
|
|
558
|
-
if (detachedHead === null) throw new OhIntegrityError(`The ${lane} store returned an invalid snapshot head.`);
|
|
559
|
-
const snapshot: OhSnapshotV1 = immutableClone({ head: detachedHead,
|
|
560
|
-
records: detached.records, v: 1 });
|
|
561
|
-
if (!exactHead(snapshot.head, head)) {
|
|
562
|
-
throw new OhIntegrityError(`The ${lane} snapshot differs from its pinned head.`);
|
|
563
|
-
}
|
|
564
|
-
if (utf8ByteLength(canonicalJson(snapshot)) > OH_MEMORY_LIMITS_V1.snapshotBytesPerLane) {
|
|
565
|
-
throw new RangeError(`The ${lane} memory snapshot exceeds its canonical byte bound.`);
|
|
566
|
-
}
|
|
567
|
-
const projected = datasetForSnapshot(authority.binding, snapshot);
|
|
870
|
+
const parsed = parseDetachedStoreSnapshot(returnedSnapshot, label, head,
|
|
871
|
+
authority.binding.spaceId);
|
|
872
|
+
const projected = datasetForSnapshot(authority.binding, parsed.snapshot,
|
|
873
|
+
parsed.projectionSnapshot);
|
|
568
874
|
return Object.freeze({ authorityId: authority.authorityId, binding: authority.binding,
|
|
569
|
-
dataset: projected.dataset, lane, projectionSnapshot: projected.projectionSnapshot,
|
|
875
|
+
dataset: projected.dataset, lane, projectionSnapshot: projected.projectionSnapshot,
|
|
876
|
+
snapshot: parsed.snapshot });
|
|
570
877
|
}
|
|
571
878
|
|
|
572
879
|
function syntheticKey(lane: OhMemoryLaneV1, recordSha256: Sha256Hex): string {
|
|
@@ -845,9 +1152,11 @@ ReadonlyMap<string, OhMemoryNominationRouteV1> {
|
|
|
845
1152
|
}
|
|
846
1153
|
|
|
847
1154
|
function parseQueryRequest(value: unknown): Readonly<{ programId: string }> {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1155
|
+
const detached = detachCanonicalData(value, "The named memory query",
|
|
1156
|
+
OH_MEMORY_QUERY_LIMITS_V2.requestBytes).value;
|
|
1157
|
+
if (!isPlainRecord(detached) || !hasExactKeys(detached, ["programId", "v"])
|
|
1158
|
+
|| detached.v !== 1) throw new TypeError("Invalid named memory query.");
|
|
1159
|
+
const programId = safeCode(detached.programId, 128);
|
|
851
1160
|
if (programId === null) throw new TypeError("Invalid named memory query identity.");
|
|
852
1161
|
return { programId };
|
|
853
1162
|
}
|
|
@@ -857,32 +1166,81 @@ function parseExplainRequest(value: unknown): Readonly<{
|
|
|
857
1166
|
resultSha256: Sha256Hex;
|
|
858
1167
|
token: string;
|
|
859
1168
|
}> {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1169
|
+
const detached = detachCanonicalData(value, "The memory explanation request",
|
|
1170
|
+
OH_MEMORY_QUERY_LIMITS_V2.requestBytes).value;
|
|
1171
|
+
if (!isPlainRecord(detached)
|
|
1172
|
+
|| !hasExactKeys(detached, ["resultSha256", "row", "token", "v"])
|
|
1173
|
+
|| detached.v !== 1 || typeof detached.token !== "string" || detached.token.length !== 43
|
|
1174
|
+
|| !Number.isSafeInteger(detached.row) || (detached.row as number) < 0) {
|
|
863
1175
|
throw new TypeError("Invalid memory explanation request.");
|
|
864
1176
|
}
|
|
865
|
-
const resultSha256 = parseSha256Hex(
|
|
1177
|
+
const resultSha256 = parseSha256Hex(detached.resultSha256);
|
|
866
1178
|
if (resultSha256 === null) throw new TypeError("Invalid memory explanation result identity.");
|
|
867
|
-
return { resultSha256, row:
|
|
1179
|
+
return { resultSha256, row: detached.row as number, token: detached.token };
|
|
868
1180
|
}
|
|
869
1181
|
|
|
870
1182
|
function parseNominationRequest(value: unknown): Readonly<{
|
|
871
1183
|
nominationId: string;
|
|
872
1184
|
roots: readonly string[];
|
|
873
1185
|
}> {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1186
|
+
const detached = detachCanonicalData(value, "The memory nomination request",
|
|
1187
|
+
OH_MEMORY_QUERY_LIMITS_V2.requestBytes * 8).value;
|
|
1188
|
+
if (!isPlainRecord(detached) || !hasExactKeys(detached, ["nominationId", "roots", "v"])
|
|
1189
|
+
|| detached.v !== 1 || !Array.isArray(detached.roots) || detached.roots.length < 1
|
|
1190
|
+
|| detached.roots.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) {
|
|
877
1191
|
throw new TypeError("Invalid memory nomination request.");
|
|
878
1192
|
}
|
|
879
|
-
const nominationId = safeCode(
|
|
880
|
-
const roots =
|
|
1193
|
+
const nominationId = safeCode(detached.nominationId, 128);
|
|
1194
|
+
const roots = detached.roots.map((root) => safeCode(root, 512)).sort();
|
|
881
1195
|
if (nominationId === null || roots.some((root) => root === null)
|
|
882
1196
|
|| new Set(roots).size !== roots.length) throw new TypeError("Invalid memory nomination identity.");
|
|
883
1197
|
return { nominationId, roots: roots as readonly string[] };
|
|
884
1198
|
}
|
|
885
1199
|
|
|
1200
|
+
function parseDetachedMemoryNominationV1(value: unknown): OhMemoryNominationV1 | null {
|
|
1201
|
+
try {
|
|
1202
|
+
const keys = ownDataKeysV2(value, 7, "The memory nomination");
|
|
1203
|
+
if (keys.length !== 7 || !["closure", "destinationPurpose", "nominationId",
|
|
1204
|
+
"nominationSha256", "source", "status", "v"].every((key) => keys.includes(key))) return null;
|
|
1205
|
+
const record = value as Record<string, unknown>;
|
|
1206
|
+
if (record.status !== "prepared" || record.v !== 1) return null;
|
|
1207
|
+
const closure = parseOhDependencyClosureV1(record.closure);
|
|
1208
|
+
const destinationPurpose = safeCode(record.destinationPurpose, 256);
|
|
1209
|
+
const nominationId = safeCode(record.nominationId, 128);
|
|
1210
|
+
const nominationSha256 = parseSha256Hex(record.nominationSha256);
|
|
1211
|
+
if (closure === null || destinationPurpose === null || nominationId === null
|
|
1212
|
+
|| nominationSha256 === null || closure.binding.profile.profileKind !== "working"
|
|
1213
|
+
|| [...ownDataKeysV2(record.source, 5, "The memory nomination source")].sort().join("\0")
|
|
1214
|
+
!== ["authorityId", "bindingSha256", "head", "lane", "v"].sort().join("\0")) return null;
|
|
1215
|
+
const sourceRecord = record.source as Record<string, unknown>;
|
|
1216
|
+
if (sourceRecord.lane !== "working" || sourceRecord.v !== 1) return null;
|
|
1217
|
+
const authorityIdValue = safeCode(sourceRecord.authorityId, 128);
|
|
1218
|
+
const bindingSha256 = parseSha256Hex(sourceRecord.bindingSha256);
|
|
1219
|
+
const head = parseOhHeadV1(sourceRecord.head);
|
|
1220
|
+
if (authorityIdValue === null || bindingSha256 === null || head === null
|
|
1221
|
+
|| bindingSha256 !== closure.binding.bindingSha256
|
|
1222
|
+
|| !exactHead(head, closure.head)) return null;
|
|
1223
|
+
const source = { authorityId: authorityIdValue, bindingSha256, head,
|
|
1224
|
+
lane: "working" as const, v: 1 as const };
|
|
1225
|
+
const payload = { closure, destinationPurpose, nominationId, source,
|
|
1226
|
+
status: "prepared" as const, v: 1 as const };
|
|
1227
|
+
return canonicalSha256(payload) === nominationSha256
|
|
1228
|
+
? immutableClone({ ...payload, nominationSha256 }) : null;
|
|
1229
|
+
} catch {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
export function parseOhMemoryNominationV1(value: unknown): OhMemoryNominationV1 | null {
|
|
1235
|
+
try {
|
|
1236
|
+
const detached = detachCanonicalData(value, "The memory nomination",
|
|
1237
|
+
OH_MEMORY_AUTHORITY_LIMITS_V1.adoptionRequestBytes);
|
|
1238
|
+
return parseDetachedMemoryNominationV1(detached.value);
|
|
1239
|
+
} catch {
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
886
1244
|
function isoInstant(date: Date): string {
|
|
887
1245
|
const value = date.toISOString();
|
|
888
1246
|
if (parseCanonicalInstantV1(value) === null) throw new TypeError("The memory clock returned an invalid instant.");
|
|
@@ -923,12 +1281,15 @@ export async function createOhMemoryAgentV1(options: OhMemoryFacadeOptionsV1): P
|
|
|
923
1281
|
options.canonical.expectedBindingSha256, "canonical");
|
|
924
1282
|
const workingBinding = bindingFor(workingStore,
|
|
925
1283
|
options.working.expectedBindingSha256, "working");
|
|
926
|
-
const expectedCanonicalHead =
|
|
927
|
-
|
|
1284
|
+
const expectedCanonicalHead = parseMemoryAuthorityHead(options.canonical.expectedHead,
|
|
1285
|
+
"pinned canonical");
|
|
928
1286
|
const programs = resolvePrograms(options.programs);
|
|
929
1287
|
const extractors = resolveExtractors(options.extractors ?? []);
|
|
930
1288
|
const nominationRoutes = resolveNominationRoutes(options.nominationRoutes ?? []);
|
|
931
|
-
const ingress = new OhSemanticBundleIngressV1(
|
|
1289
|
+
const ingress = new OhSemanticBundleIngressV1(
|
|
1290
|
+
capacityGuardedWorkingStore(workingStore, workingBinding),
|
|
1291
|
+
workingCodecs,
|
|
1292
|
+
);
|
|
932
1293
|
const now = options.now ?? (() => new Date());
|
|
933
1294
|
const monotonicNow = options.monotonicNow ?? (() => performance.now());
|
|
934
1295
|
const capabilityLifetime = options.explainCapabilityLifetimeMs
|
|
@@ -961,20 +1322,25 @@ export async function createOhMemoryAgentV1(options: OhMemoryFacadeOptionsV1): P
|
|
|
961
1322
|
};
|
|
962
1323
|
|
|
963
1324
|
const remember = async (value: unknown): Promise<OhMemoryRememberReceiptV1> => {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
["expectedHead", "puts", "requestId", "tombstones", "v"]) || value.v !== 1) {
|
|
1325
|
+
const detached = detachCanonicalData(value, "The memory semantic bundle",
|
|
1326
|
+
OH_MEMORY_LIMITS_V1.rememberBytes).value;
|
|
1327
|
+
if (!isPlainRecord(detached) || !hasExactKeys(detached,
|
|
1328
|
+
["expectedHead", "puts", "requestId", "tombstones", "v"]) || detached.v !== 1) {
|
|
969
1329
|
throw new TypeError("Invalid memory remember request.");
|
|
970
1330
|
}
|
|
971
|
-
const requestId = safeCode(
|
|
1331
|
+
const requestId = safeCode(detached.requestId, 128);
|
|
972
1332
|
if (requestId === null) throw new TypeError("Invalid memory remember request identity.");
|
|
973
1333
|
const operationId = `memory_${canonicalSha256({ actorId: memoryActorId,
|
|
974
1334
|
bindingSha256: workingBinding.bindingSha256, requestId, v: 1 }).slice(0, 48)}`;
|
|
975
|
-
const
|
|
976
|
-
expectedHead:
|
|
977
|
-
puts:
|
|
1335
|
+
const returnedOperation = await ingress.commit({ actorId: memoryActorId,
|
|
1336
|
+
expectedHead: detached.expectedHead, instant: isoInstant(new Date(wallClock())), operationId,
|
|
1337
|
+
puts: detached.puts, tombstones: detached.tombstones, v: 1 });
|
|
1338
|
+
const operation = parseOhOperationV1(detachCanonicalData(returnedOperation,
|
|
1339
|
+
"The returned working memory operation", OH_MEMORY_LIMITS_V1.rememberBytes * 2).value);
|
|
1340
|
+
if (operation === null || operation.actorId !== memoryActorId
|
|
1341
|
+
|| operation.operationId !== operationId || operation.spaceId !== workingBinding.spaceId) {
|
|
1342
|
+
throw new OhIntegrityError("The working authority returned a different memory operation.");
|
|
1343
|
+
}
|
|
978
1344
|
const head: OhHeadV1 = { generation: operation.sequence,
|
|
979
1345
|
graphRevisionSha256: operation.graphRevisionSha256,
|
|
980
1346
|
operationSha256: operation.operationSha256, recordsSha256: operation.recordsSha256,
|
|
@@ -1075,10 +1441,13 @@ export async function createOhMemoryAgentV1(options: OhMemoryFacadeOptionsV1): P
|
|
|
1075
1441
|
const request = parseNominationRequest(value);
|
|
1076
1442
|
const route = nominationRoutes.get(request.nominationId);
|
|
1077
1443
|
if (route === undefined) throw new TypeError("Unknown named memory nomination route.");
|
|
1078
|
-
const head = parseOhHeadV1(
|
|
1444
|
+
const head = parseOhHeadV1(detachCanonicalData(await workingStore.head(),
|
|
1445
|
+
"The working nomination store head", 4 * 1024).value);
|
|
1079
1446
|
if (head === null) throw new OhIntegrityError("The working nomination store returned an invalid head.");
|
|
1080
|
-
const
|
|
1447
|
+
const returnedClosure = await workingStore.exportDependencyClosure({ head: {
|
|
1081
1448
|
operationSha256: head.operationSha256, sequence: head.sequence }, roots: request.roots });
|
|
1449
|
+
const closure = detachCanonicalData(returnedClosure, "The working nomination closure",
|
|
1450
|
+
OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes).value;
|
|
1082
1451
|
const verified = verifyOhDependencyClosureAgainstV1(closure, { binding: workingBinding, head });
|
|
1083
1452
|
if (!verified.ok) throw new OhIntegrityError("The working nomination closure failed exact verification.");
|
|
1084
1453
|
if (canonicalJson(verified.closure.roots) !== canonicalJson(request.roots)) {
|
|
@@ -1108,6 +1477,34 @@ type StoredExplanationV2 = Readonly<{
|
|
|
1108
1477
|
rows: readonly OhMemoryResultRowV2[];
|
|
1109
1478
|
}>;
|
|
1110
1479
|
|
|
1480
|
+
type OhMemoryRuntimeV2 = {
|
|
1481
|
+
readonly capabilityLifetime: number;
|
|
1482
|
+
explanationBytes: number;
|
|
1483
|
+
readonly explanations: Map<string, StoredExplanationV2>;
|
|
1484
|
+
lastMonotonicMs: number;
|
|
1485
|
+
lastWallClockMs: number;
|
|
1486
|
+
readonly monotonicNow: () => number;
|
|
1487
|
+
readonly now: () => Date;
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
function createOhMemoryRuntimeV2(options: OhMemoryFacadeOptionsV2): OhMemoryRuntimeV2 {
|
|
1491
|
+
const capabilityLifetime = options.explainCapabilityLifetimeMs
|
|
1492
|
+
?? OH_MEMORY_LIMITS_V1.explainCapabilityLifetimeMs;
|
|
1493
|
+
if (!Number.isSafeInteger(capabilityLifetime) || capabilityLifetime < 1_000
|
|
1494
|
+
|| capabilityLifetime > 60 * 60 * 1_000) {
|
|
1495
|
+
throw new RangeError("Invalid memory explanation capability lifetime.");
|
|
1496
|
+
}
|
|
1497
|
+
return {
|
|
1498
|
+
capabilityLifetime,
|
|
1499
|
+
explanationBytes: 0,
|
|
1500
|
+
explanations: new Map<string, StoredExplanationV2>(),
|
|
1501
|
+
lastMonotonicMs: -1,
|
|
1502
|
+
lastWallClockMs: Number.NEGATIVE_INFINITY,
|
|
1503
|
+
monotonicNow: options.monotonicNow ?? (() => performance.now()),
|
|
1504
|
+
now: options.now ?? (() => new Date()),
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1111
1508
|
type OhMemoryContinuationIdentityV2 = Readonly<{
|
|
1112
1509
|
bindingsSha256: Sha256Hex;
|
|
1113
1510
|
memorySha256: Sha256Hex;
|
|
@@ -1268,28 +1665,51 @@ function parseQueryRequestV2(value: unknown): Readonly<{
|
|
|
1268
1665
|
continuation: string | null;
|
|
1269
1666
|
programId: string;
|
|
1270
1667
|
}> {
|
|
1668
|
+
let detached: unknown;
|
|
1669
|
+
let detachedCanonical = "";
|
|
1271
1670
|
let keys: readonly string[];
|
|
1272
1671
|
try {
|
|
1273
|
-
|
|
1274
|
-
|
|
1672
|
+
const detachedData = detachCanonicalData(value, "The parameterized memory query",
|
|
1673
|
+
OH_MEMORY_QUERY_LIMITS_V2.requestBytes);
|
|
1674
|
+
detached = detachedData.value;
|
|
1675
|
+
detachedCanonical = detachedData.canonical;
|
|
1676
|
+
} catch (error) {
|
|
1677
|
+
if (error instanceof RangeError) throw error;
|
|
1678
|
+
if (error instanceof Error && error.message.includes("$root.bindings")) {
|
|
1679
|
+
throw new TypeError("Memory bindings must be JSON primitives.");
|
|
1680
|
+
}
|
|
1681
|
+
throw new TypeError("Invalid parameterized memory query.");
|
|
1682
|
+
}
|
|
1683
|
+
try {
|
|
1684
|
+
keys = ownDataKeysV2(detached, 4, "The parameterized memory query");
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
if (error instanceof Error && error.message.includes("$root.bindings")) {
|
|
1687
|
+
throw new TypeError("Memory bindings must be JSON primitives.");
|
|
1688
|
+
}
|
|
1275
1689
|
throw new TypeError("Invalid parameterized memory query.");
|
|
1276
1690
|
}
|
|
1277
1691
|
if (keys.length !== 4 || !["bindings", "continuation", "programId", "v"]
|
|
1278
1692
|
.every((key) => keys.includes(key))) {
|
|
1279
1693
|
throw new TypeError("Invalid parameterized memory query.");
|
|
1280
1694
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1695
|
+
if (utf8ByteLength(detachedCanonical) > OH_MEMORY_QUERY_LIMITS_V2.requestBytes) {
|
|
1696
|
+
throw new RangeError("The parameterized memory query exceeds its canonical byte bound.");
|
|
1697
|
+
}
|
|
1698
|
+
const record = detached as Record<string, unknown>;
|
|
1699
|
+
if (record.v !== 2) {
|
|
1284
1700
|
throw new TypeError("Invalid parameterized memory query.");
|
|
1285
1701
|
}
|
|
1702
|
+
if (record.continuation !== null && typeof record.continuation !== "string") {
|
|
1703
|
+
throw new OhMemoryContinuationError("encoding", "Invalid memory continuation encoding.");
|
|
1704
|
+
}
|
|
1286
1705
|
const programId = safeCode(record.programId, 128);
|
|
1287
1706
|
if (programId === null) throw new TypeError("Invalid parameterized memory query identity.");
|
|
1288
1707
|
const continuation = record.continuation as string | null;
|
|
1289
1708
|
if (typeof continuation === "string" && (continuation.length < 1
|
|
1290
1709
|
|| continuation.length > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes
|
|
1291
1710
|
|| utf8ByteLength(continuation) > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes)) {
|
|
1292
|
-
throw new
|
|
1711
|
+
throw new OhMemoryContinuationError("encoding",
|
|
1712
|
+
"The memory continuation exceeds its byte bound.");
|
|
1293
1713
|
}
|
|
1294
1714
|
const bindingKeys = ownDataKeysV2(record.bindings, OH_MEMORY_QUERY_LIMITS_V2.bindings,
|
|
1295
1715
|
"The parameterized memory query bindings");
|
|
@@ -1369,20 +1789,26 @@ function encodeContinuationV2(value: OhMemoryContinuationIdentityV2,
|
|
|
1369
1789
|
|
|
1370
1790
|
function parseContinuationV2(value: string, key: Uint8Array): OhMemoryContinuationV2 {
|
|
1371
1791
|
if (value.length < 1 || utf8ByteLength(value) > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes
|
|
1372
|
-
|| !/^[A-Za-z0-9_-]+$/u.test(value))
|
|
1792
|
+
|| !/^[A-Za-z0-9_-]+$/u.test(value)) {
|
|
1793
|
+
throw new OhMemoryContinuationError("encoding", "Invalid memory continuation encoding.");
|
|
1794
|
+
}
|
|
1373
1795
|
const bytes = Buffer.from(value, "base64url");
|
|
1374
1796
|
if (bytes.toString("base64url") !== value
|
|
1375
1797
|
|| bytes.byteLength > OH_MEMORY_QUERY_LIMITS_V2.continuationBytes) {
|
|
1376
|
-
throw new
|
|
1798
|
+
throw new OhMemoryContinuationError("encoding", "Invalid memory continuation encoding.");
|
|
1377
1799
|
}
|
|
1378
1800
|
const text = bytes.toString("utf8");
|
|
1379
1801
|
let decoded: unknown;
|
|
1380
|
-
try { decoded = JSON.parse(text); } catch {
|
|
1802
|
+
try { decoded = JSON.parse(text); } catch {
|
|
1803
|
+
throw new OhMemoryContinuationError("encoding", "Invalid memory continuation JSON.");
|
|
1804
|
+
}
|
|
1381
1805
|
if (!isPlainRecord(decoded)
|
|
1382
1806
|
|| !hasExactKeys(decoded, ["bindingsSha256", "continuationHmacSha256", "continuationSha256",
|
|
1383
1807
|
"memorySha256", "nextOffset", "pageSize", "programSha256", "projectionResultSha256",
|
|
1384
1808
|
"totalRows", "v"])
|
|
1385
|
-
|| decoded.v !== 2)
|
|
1809
|
+
|| decoded.v !== 2) {
|
|
1810
|
+
throw new OhMemoryContinuationError("encoding", "Invalid memory continuation payload.");
|
|
1811
|
+
}
|
|
1386
1812
|
const bindingsSha256 = parseSha256Hex(decoded.bindingsSha256);
|
|
1387
1813
|
const continuationHmacSha256 = parseSha256Hex(decoded.continuationHmacSha256);
|
|
1388
1814
|
const continuationSha256 = parseSha256Hex(decoded.continuationSha256);
|
|
@@ -1400,7 +1826,7 @@ function parseContinuationV2(value: string, key: Uint8Array): OhMemoryContinuati
|
|
|
1400
1826
|
|| (decoded.totalRows as number) > OH_MEMORY_QUERY_LIMITS_V2.maximumProgramRows
|
|
1401
1827
|
|| (decoded.nextOffset as number) >= (decoded.totalRows as number)
|
|
1402
1828
|
|| (decoded.nextOffset as number) % (decoded.pageSize as number) !== 0) {
|
|
1403
|
-
throw new
|
|
1829
|
+
throw new OhMemoryContinuationError("identity", "Invalid memory continuation identity.");
|
|
1404
1830
|
}
|
|
1405
1831
|
const identity: OhMemoryContinuationIdentityV2 = { bindingsSha256, memorySha256,
|
|
1406
1832
|
nextOffset: decoded.nextOffset as number,
|
|
@@ -1408,14 +1834,17 @@ function parseContinuationV2(value: string, key: Uint8Array): OhMemoryContinuati
|
|
|
1408
1834
|
totalRows: decoded.totalRows as number, v: 2 as const };
|
|
1409
1835
|
const signed: OhMemoryContinuationV2 = { ...identity, continuationSha256 };
|
|
1410
1836
|
const envelope: OhMemoryContinuationEnvelopeV2 = { ...signed, continuationHmacSha256 };
|
|
1411
|
-
if (canonicalJson(envelope) !== text)
|
|
1837
|
+
if (canonicalJson(envelope) !== text) {
|
|
1838
|
+
throw new OhMemoryContinuationError("encoding", "Invalid memory continuation payload.");
|
|
1839
|
+
}
|
|
1412
1840
|
const expectedHmac = continuationHmacV2(key, signed);
|
|
1413
1841
|
const receivedHmac = Buffer.from(continuationHmacSha256, "hex");
|
|
1414
1842
|
if (!timingSafeEqual(expectedHmac, receivedHmac)) {
|
|
1415
|
-
throw new
|
|
1843
|
+
throw new OhMemoryContinuationError("authentication",
|
|
1844
|
+
"The memory continuation is not an issued capability.");
|
|
1416
1845
|
}
|
|
1417
1846
|
if (canonicalSha256(identity) !== continuationSha256) {
|
|
1418
|
-
throw new
|
|
1847
|
+
throw new OhMemoryContinuationError("identity", "The memory continuation digest is invalid.");
|
|
1419
1848
|
}
|
|
1420
1849
|
return Object.freeze(signed);
|
|
1421
1850
|
}
|
|
@@ -1425,14 +1854,17 @@ function parseExplainRequestV2(value: unknown): Readonly<{
|
|
|
1425
1854
|
resultSha256: Sha256Hex;
|
|
1426
1855
|
token: string;
|
|
1427
1856
|
}> {
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1857
|
+
const detached = detachCanonicalData(value, "The V2 memory explanation request",
|
|
1858
|
+
OH_MEMORY_QUERY_LIMITS_V2.requestBytes).value;
|
|
1859
|
+
if (!isPlainRecord(detached)
|
|
1860
|
+
|| !hasExactKeys(detached, ["pageRow", "resultSha256", "token", "v"])
|
|
1861
|
+
|| detached.v !== 2 || typeof detached.token !== "string" || detached.token.length !== 43
|
|
1862
|
+
|| !Number.isSafeInteger(detached.pageRow) || (detached.pageRow as number) < 0) {
|
|
1431
1863
|
throw new TypeError("Invalid V2 memory explanation request.");
|
|
1432
1864
|
}
|
|
1433
|
-
const resultSha256 = parseSha256Hex(
|
|
1865
|
+
const resultSha256 = parseSha256Hex(detached.resultSha256);
|
|
1434
1866
|
if (resultSha256 === null) throw new TypeError("Invalid V2 memory explanation result identity.");
|
|
1435
|
-
return { pageRow:
|
|
1867
|
+
return { pageRow: detached.pageRow as number, resultSha256, token: detached.token };
|
|
1436
1868
|
}
|
|
1437
1869
|
|
|
1438
1870
|
/**
|
|
@@ -1440,7 +1872,10 @@ function parseExplainRequestV2(value: unknown): Readonly<{
|
|
|
1440
1872
|
* bindings and fail-closed stable pagination; V1 request and digest contracts
|
|
1441
1873
|
* remain untouched.
|
|
1442
1874
|
*/
|
|
1443
|
-
|
|
1875
|
+
async function createOhMemoryAgentV2WithRuntime(
|
|
1876
|
+
options: OhMemoryFacadeOptionsV2,
|
|
1877
|
+
sharedRuntime?: OhMemoryRuntimeV2,
|
|
1878
|
+
): Promise<OhMemoryAgentV2> {
|
|
1444
1879
|
const memoryActorId = safeCode(options.actorId, 128);
|
|
1445
1880
|
if (memoryActorId === null) throw new TypeError("Invalid host-bound memory actor ID.");
|
|
1446
1881
|
const continuationKey = continuationKeyV2(options.continuationKey);
|
|
@@ -1456,58 +1891,61 @@ export async function createOhMemoryAgentV2(options: OhMemoryFacadeOptionsV2): P
|
|
|
1456
1891
|
options.canonical.expectedBindingSha256, "canonical");
|
|
1457
1892
|
const workingBinding = bindingFor(workingStore,
|
|
1458
1893
|
options.working.expectedBindingSha256, "working");
|
|
1459
|
-
const expectedCanonicalHead =
|
|
1460
|
-
|
|
1894
|
+
const expectedCanonicalHead = parseMemoryAuthorityHead(options.canonical.expectedHead,
|
|
1895
|
+
"pinned canonical");
|
|
1461
1896
|
const programs = resolveProgramsV2(options.programs);
|
|
1462
1897
|
const extractors = resolveExtractors(options.extractors ?? []);
|
|
1463
1898
|
const nominationRoutes = resolveNominationRoutes(options.nominationRoutes ?? []);
|
|
1464
|
-
const ingress = new OhSemanticBundleIngressV1(
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
if (!Number.isSafeInteger(capabilityLifetime) || capabilityLifetime < 1_000
|
|
1470
|
-
|| capabilityLifetime > 60 * 60 * 1_000) {
|
|
1471
|
-
throw new RangeError("Invalid memory explanation capability lifetime.");
|
|
1472
|
-
}
|
|
1899
|
+
const ingress = new OhSemanticBundleIngressV1(
|
|
1900
|
+
capacityGuardedWorkingStore(workingStore, workingBinding),
|
|
1901
|
+
workingCodecs,
|
|
1902
|
+
);
|
|
1903
|
+
const runtime = sharedRuntime ?? createOhMemoryRuntimeV2(options);
|
|
1473
1904
|
const canonical = await readLane({ authorityId: canonicalAuthorityId,
|
|
1474
1905
|
binding: canonicalBinding, store: canonicalStore }, "canonical", expectedCanonicalHead);
|
|
1475
|
-
const explanations = new Map<string, StoredExplanationV2>();
|
|
1476
|
-
let explanationBytes = 0;
|
|
1477
|
-
let lastMonotonicMs = -1;
|
|
1478
|
-
let lastWallClockMs = Number.NEGATIVE_INFINITY;
|
|
1479
1906
|
const wallClock = () => {
|
|
1480
|
-
const milliseconds = clockMilliseconds(now);
|
|
1481
|
-
if (milliseconds < lastWallClockMs)
|
|
1482
|
-
|
|
1907
|
+
const milliseconds = clockMilliseconds(runtime.now);
|
|
1908
|
+
if (milliseconds < runtime.lastWallClockMs) {
|
|
1909
|
+
throw new OhProfileError("The memory wall clock regressed.");
|
|
1910
|
+
}
|
|
1911
|
+
runtime.lastWallClockMs = milliseconds;
|
|
1483
1912
|
return milliseconds;
|
|
1484
1913
|
};
|
|
1485
1914
|
const monotonicClock = () => {
|
|
1486
|
-
const milliseconds = monotonicMilliseconds(monotonicNow);
|
|
1487
|
-
if (milliseconds < lastMonotonicMs)
|
|
1488
|
-
|
|
1915
|
+
const milliseconds = monotonicMilliseconds(runtime.monotonicNow);
|
|
1916
|
+
if (milliseconds < runtime.lastMonotonicMs) {
|
|
1917
|
+
throw new OhProfileError("The memory monotonic clock regressed.");
|
|
1918
|
+
}
|
|
1919
|
+
runtime.lastMonotonicMs = milliseconds;
|
|
1489
1920
|
return milliseconds;
|
|
1490
1921
|
};
|
|
1491
1922
|
const deleteExplanation = (token: string) => {
|
|
1492
|
-
const stored = explanations.get(token);
|
|
1493
|
-
if (stored !== undefined && explanations.delete(token))
|
|
1923
|
+
const stored = runtime.explanations.get(token);
|
|
1924
|
+
if (stored !== undefined && runtime.explanations.delete(token)) {
|
|
1925
|
+
runtime.explanationBytes -= stored.bytes;
|
|
1926
|
+
}
|
|
1494
1927
|
};
|
|
1495
1928
|
|
|
1496
1929
|
const remember = async (value: unknown): Promise<OhMemoryRememberReceiptV1> => {
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
["expectedHead", "puts", "requestId", "tombstones", "v"]) || value.v !== 1) {
|
|
1930
|
+
const detached = detachCanonicalData(value, "The memory semantic bundle",
|
|
1931
|
+
OH_MEMORY_LIMITS_V1.rememberBytes).value;
|
|
1932
|
+
if (!isPlainRecord(detached) || !hasExactKeys(detached,
|
|
1933
|
+
["expectedHead", "puts", "requestId", "tombstones", "v"]) || detached.v !== 1) {
|
|
1502
1934
|
throw new TypeError("Invalid memory remember request.");
|
|
1503
1935
|
}
|
|
1504
|
-
const requestId = safeCode(
|
|
1936
|
+
const requestId = safeCode(detached.requestId, 128);
|
|
1505
1937
|
if (requestId === null) throw new TypeError("Invalid memory remember request identity.");
|
|
1506
1938
|
const operationId = `memory_${canonicalSha256({ actorId: memoryActorId,
|
|
1507
1939
|
bindingSha256: workingBinding.bindingSha256, requestId, v: 1 }).slice(0, 48)}`;
|
|
1508
|
-
const
|
|
1509
|
-
expectedHead:
|
|
1510
|
-
puts:
|
|
1940
|
+
const returnedOperation = await ingress.commit({ actorId: memoryActorId,
|
|
1941
|
+
expectedHead: detached.expectedHead, instant: isoInstant(new Date(wallClock())), operationId,
|
|
1942
|
+
puts: detached.puts, tombstones: detached.tombstones, v: 1 });
|
|
1943
|
+
const operation = parseOhOperationV1(detachCanonicalData(returnedOperation,
|
|
1944
|
+
"The returned working memory operation", OH_MEMORY_LIMITS_V1.rememberBytes * 2).value);
|
|
1945
|
+
if (operation === null || operation.actorId !== memoryActorId
|
|
1946
|
+
|| operation.operationId !== operationId || operation.spaceId !== workingBinding.spaceId) {
|
|
1947
|
+
throw new OhIntegrityError("The working authority returned a different memory operation.");
|
|
1948
|
+
}
|
|
1511
1949
|
const head: OhHeadV1 = { generation: operation.sequence,
|
|
1512
1950
|
graphRevisionSha256: operation.graphRevisionSha256,
|
|
1513
1951
|
operationSha256: operation.operationSha256, recordsSha256: operation.recordsSha256,
|
|
@@ -1533,7 +1971,8 @@ export async function createOhMemoryAgentV2(options: OhMemoryFacadeOptionsV2): P
|
|
|
1533
1971
|
|| requestedContinuation.totalRows > program.maximumRows
|
|
1534
1972
|
|| requestedContinuation.nextOffset >= requestedContinuation.totalRows
|
|
1535
1973
|
|| requestedContinuation.nextOffset % program.pageSize !== 0)) {
|
|
1536
|
-
throw new
|
|
1974
|
+
throw new OhMemoryContinuationError("identity",
|
|
1975
|
+
"The memory continuation does not match this exact program, binding, and page identity.");
|
|
1537
1976
|
}
|
|
1538
1977
|
const boundQuery = bindQueryV2(program.query, bound.bindings);
|
|
1539
1978
|
const working = await readLane({ authorityId: workingAuthorityId,
|
|
@@ -1571,13 +2010,15 @@ export async function createOhMemoryAgentV2(options: OhMemoryFacadeOptionsV2): P
|
|
|
1571
2010
|
if (requestedContinuation !== null
|
|
1572
2011
|
&& (requestedContinuation.memorySha256 !== identity.memorySha256
|
|
1573
2012
|
|| requestedContinuation.projectionResultSha256 !== projection.resultSha256)) {
|
|
1574
|
-
throw new
|
|
2013
|
+
throw new OhMemoryContinuationError("identity",
|
|
2014
|
+
"The memory continuation does not match this exact source and projection identity.");
|
|
1575
2015
|
}
|
|
1576
2016
|
if (requestedContinuation !== null
|
|
1577
2017
|
&& (requestedContinuation.totalRows !== projection.rows.length
|
|
1578
2018
|
|| requestedContinuation.nextOffset >= projection.rows.length
|
|
1579
2019
|
|| requestedContinuation.nextOffset % program.pageSize !== 0)) {
|
|
1580
|
-
throw new
|
|
2020
|
+
throw new OhMemoryContinuationError("identity",
|
|
2021
|
+
"The memory continuation does not match this exact row identity.");
|
|
1581
2022
|
}
|
|
1582
2023
|
const start = requestedContinuation?.nextOffset ?? 0;
|
|
1583
2024
|
const endExclusive = Math.min(start + program.pageSize, projection.rows.length);
|
|
@@ -1605,15 +2046,15 @@ export async function createOhMemoryAgentV2(options: OhMemoryFacadeOptionsV2): P
|
|
|
1605
2046
|
const resultPayload = immutableClone({ ...resultIdentityPayload, continuation });
|
|
1606
2047
|
const issuedAt = wallClock();
|
|
1607
2048
|
const issuedAtMonotonic = monotonicClock();
|
|
1608
|
-
const expiresAtMs = issuedAt + capabilityLifetime;
|
|
1609
|
-
const expiresAtMonotonicMs = issuedAtMonotonic + capabilityLifetime;
|
|
2049
|
+
const expiresAtMs = issuedAt + runtime.capabilityLifetime;
|
|
2050
|
+
const expiresAtMonotonicMs = issuedAtMonotonic + runtime.capabilityLifetime;
|
|
1610
2051
|
const expiresAt = isoInstant(new Date(expiresAtMs));
|
|
1611
2052
|
const pageBytePreflight = { ...resultPayload,
|
|
1612
2053
|
explainCapability: { expiresAt, token: "A".repeat(43), v: 2 as const }, resultSha256 };
|
|
1613
2054
|
if (utf8ByteLength(canonicalJson(pageBytePreflight)) > program.maximumPageBytes) {
|
|
1614
2055
|
throw new RangeError("The V2 memory page exceeds its host-declared canonical byte bound.");
|
|
1615
2056
|
}
|
|
1616
|
-
for (const [existingToken, stored] of explanations) {
|
|
2057
|
+
for (const [existingToken, stored] of runtime.explanations) {
|
|
1617
2058
|
if (issuedAtMonotonic >= stored.expiresAtMonotonicMs) deleteExplanation(existingToken);
|
|
1618
2059
|
}
|
|
1619
2060
|
const storedPayload = immutableClone({ expiresAtMonotonicMs, identity, page, proofs,
|
|
@@ -1623,16 +2064,16 @@ export async function createOhMemoryAgentV2(options: OhMemoryFacadeOptionsV2): P
|
|
|
1623
2064
|
|| storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityTotalBytes) {
|
|
1624
2065
|
throw new RangeError("The V2 memory explanation exceeds its retained capability bound.");
|
|
1625
2066
|
}
|
|
1626
|
-
while (explanations.size >= OH_MEMORY_LIMITS_V1.explainCapabilities
|
|
1627
|
-
|| explanationBytes + storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityTotalBytes) {
|
|
1628
|
-
const oldest = explanations.keys().next().value as string | undefined;
|
|
2067
|
+
while (runtime.explanations.size >= OH_MEMORY_LIMITS_V1.explainCapabilities
|
|
2068
|
+
|| runtime.explanationBytes + storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityTotalBytes) {
|
|
2069
|
+
const oldest = runtime.explanations.keys().next().value as string | undefined;
|
|
1629
2070
|
if (oldest === undefined) break;
|
|
1630
2071
|
deleteExplanation(oldest);
|
|
1631
2072
|
}
|
|
1632
2073
|
let token = randomBytes(32).toString("base64url");
|
|
1633
|
-
while (explanations.has(token)) token = randomBytes(32).toString("base64url");
|
|
1634
|
-
explanations.set(token, immutableClone({ ...storedPayload, bytes: storedBytes }));
|
|
1635
|
-
explanationBytes += storedBytes;
|
|
2074
|
+
while (runtime.explanations.has(token)) token = randomBytes(32).toString("base64url");
|
|
2075
|
+
runtime.explanations.set(token, immutableClone({ ...storedPayload, bytes: storedBytes }));
|
|
2076
|
+
runtime.explanationBytes += storedBytes;
|
|
1636
2077
|
const result = immutableClone({ ...resultPayload,
|
|
1637
2078
|
explainCapability: { expiresAt, token, v: 2 as const }, resultSha256 });
|
|
1638
2079
|
if (utf8ByteLength(canonicalJson(result)) > program.maximumPageBytes) {
|
|
@@ -1644,7 +2085,7 @@ export async function createOhMemoryAgentV2(options: OhMemoryFacadeOptionsV2): P
|
|
|
1644
2085
|
|
|
1645
2086
|
const explain = async (value: unknown): Promise<OhMemoryExplanationV2> => {
|
|
1646
2087
|
const request = parseExplainRequestV2(value);
|
|
1647
|
-
const stored = explanations.get(request.token);
|
|
2088
|
+
const stored = runtime.explanations.get(request.token);
|
|
1648
2089
|
const currentTime = monotonicClock();
|
|
1649
2090
|
if (stored === undefined || stored.resultSha256 !== request.resultSha256
|
|
1650
2091
|
|| currentTime >= stored.expiresAtMonotonicMs) {
|
|
@@ -1666,10 +2107,13 @@ export async function createOhMemoryAgentV2(options: OhMemoryFacadeOptionsV2): P
|
|
|
1666
2107
|
const request = parseNominationRequest(value);
|
|
1667
2108
|
const route = nominationRoutes.get(request.nominationId);
|
|
1668
2109
|
if (route === undefined) throw new TypeError("Unknown named memory nomination route.");
|
|
1669
|
-
const head = parseOhHeadV1(
|
|
2110
|
+
const head = parseOhHeadV1(detachCanonicalData(await workingStore.head(),
|
|
2111
|
+
"The working nomination store head", 4 * 1024).value);
|
|
1670
2112
|
if (head === null) throw new OhIntegrityError("The working nomination store returned an invalid head.");
|
|
1671
|
-
const
|
|
2113
|
+
const returnedClosure = await workingStore.exportDependencyClosure({ head: {
|
|
1672
2114
|
operationSha256: head.operationSha256, sequence: head.sequence }, roots: request.roots });
|
|
2115
|
+
const closure = detachCanonicalData(returnedClosure, "The working nomination closure",
|
|
2116
|
+
OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes).value;
|
|
1673
2117
|
const verified = verifyOhDependencyClosureAgainstV1(closure, { binding: workingBinding, head });
|
|
1674
2118
|
if (!verified.ok) throw new OhIntegrityError("The working nomination closure failed exact verification.");
|
|
1675
2119
|
if (canonicalJson(verified.closure.roots) !== canonicalJson(request.roots)) {
|
|
@@ -1684,3 +2128,636 @@ export async function createOhMemoryAgentV2(options: OhMemoryFacadeOptionsV2): P
|
|
|
1684
2128
|
|
|
1685
2129
|
return Object.freeze({ explain, nominate, query, remember });
|
|
1686
2130
|
}
|
|
2131
|
+
|
|
2132
|
+
export async function createOhMemoryAgentV2(
|
|
2133
|
+
options: OhMemoryFacadeOptionsV2,
|
|
2134
|
+
): Promise<OhMemoryAgentV2> {
|
|
2135
|
+
return await createOhMemoryAgentV2WithRuntime(options);
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
function parseDetachedMemoryAuthorityHead(value: unknown, label: string): OhHeadV1 {
|
|
2139
|
+
const head = parseOhHeadV1(value);
|
|
2140
|
+
if (head === null) throw new TypeError(`Invalid ${label} memory head.`);
|
|
2141
|
+
return immutableClone(head);
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
function parseMemoryAuthorityHead(value: unknown, label: string): OhHeadV1 {
|
|
2145
|
+
return parseDetachedMemoryAuthorityHead(detachCanonicalData(value,
|
|
2146
|
+
`The ${label} memory head`, 4 * 1024).value, label);
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
function parseCanonicalAdvanceRequest(value: unknown): Readonly<{
|
|
2150
|
+
expectedHead: OhHeadV1;
|
|
2151
|
+
nextHead: OhHeadV1;
|
|
2152
|
+
}> {
|
|
2153
|
+
const detached = detachCanonicalData(value, "The canonical memory advance request",
|
|
2154
|
+
OH_MEMORY_QUERY_LIMITS_V2.requestBytes).value;
|
|
2155
|
+
if (!isPlainRecord(detached) || !hasExactKeys(detached, ["expectedHead", "nextHead", "v"])
|
|
2156
|
+
|| detached.v !== 1) throw new TypeError("Invalid canonical memory advance request.");
|
|
2157
|
+
return Object.freeze({
|
|
2158
|
+
expectedHead: parseDetachedMemoryAuthorityHead(detached.expectedHead, "expected canonical"),
|
|
2159
|
+
nextHead: parseDetachedMemoryAuthorityHead(detached.nextHead, "next canonical"),
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function parseAdoptionRequest(value: unknown): Readonly<{
|
|
2164
|
+
expectedCanonicalHead: OhHeadV1;
|
|
2165
|
+
nomination: OhMemoryNominationV1;
|
|
2166
|
+
replacements: readonly OhMemoryAdoptionReplacementV1[];
|
|
2167
|
+
}> {
|
|
2168
|
+
const detached = detachCanonicalData(value, "The memory adoption request",
|
|
2169
|
+
OH_MEMORY_AUTHORITY_LIMITS_V1.adoptionRequestBytes).value;
|
|
2170
|
+
if (!isPlainRecord(detached)
|
|
2171
|
+
|| (!hasExactKeys(detached, ["expectedCanonicalHead", "nomination", "v"])
|
|
2172
|
+
&& !hasExactKeys(detached, ["expectedCanonicalHead", "nomination", "replacements", "v"]))
|
|
2173
|
+
|| detached.v !== 1) throw new TypeError("Invalid memory adoption request.");
|
|
2174
|
+
const nomination = parseDetachedMemoryNominationV1(detached.nomination);
|
|
2175
|
+
if (nomination === null) throw new TypeError("Invalid memory adoption nomination.");
|
|
2176
|
+
const replacementValues = "replacements" in detached ? detached.replacements : [];
|
|
2177
|
+
if (!Array.isArray(replacementValues)
|
|
2178
|
+
|| replacementValues.length > OH_MEMORY_AUTHORITY_LIMITS_V1.adoptionReplacements) {
|
|
2179
|
+
throw new TypeError("Invalid memory adoption replacements.");
|
|
2180
|
+
}
|
|
2181
|
+
const nominatedByKey = new Map(nomination.closure.records.map((record) => [record.key, record]));
|
|
2182
|
+
const replacements = replacementValues.map((replacement) => {
|
|
2183
|
+
if (!isPlainRecord(replacement)
|
|
2184
|
+
|| !hasExactKeys(replacement, ["expectedPriorRecordSha256", "key", "v"])
|
|
2185
|
+
|| replacement.v !== 1) throw new TypeError("Invalid memory adoption replacement.");
|
|
2186
|
+
const key = safeCode(replacement.key, 512);
|
|
2187
|
+
const expectedPriorRecordSha256 = parseSha256Hex(replacement.expectedPriorRecordSha256);
|
|
2188
|
+
const nominated = key === null ? undefined : nominatedByKey.get(key);
|
|
2189
|
+
if (key === null || expectedPriorRecordSha256 === null || nominated === undefined) {
|
|
2190
|
+
throw new TypeError("Invalid memory adoption replacement.");
|
|
2191
|
+
}
|
|
2192
|
+
return { expectedPriorRecordSha256, key, v: 1 as const };
|
|
2193
|
+
}).sort((left, right) => compareText(left.key, right.key));
|
|
2194
|
+
if (!orderedUnique(replacements, (replacement) => replacement.key)) {
|
|
2195
|
+
throw new TypeError("Memory adoption replacement keys must be unique.");
|
|
2196
|
+
}
|
|
2197
|
+
return Object.freeze({
|
|
2198
|
+
expectedCanonicalHead: parseDetachedMemoryAuthorityHead(detached.expectedCanonicalHead,
|
|
2199
|
+
"expected canonical adoption"),
|
|
2200
|
+
nomination,
|
|
2201
|
+
replacements: immutableClone(replacements),
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
function headRef(head: OhHeadV1): OhHeadRefV1 {
|
|
2206
|
+
return Object.freeze({ operationSha256: head.operationSha256, sequence: head.sequence });
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
async function proveCanonicalDescendant(
|
|
2210
|
+
authority: Readonly<{ authorityId: string; binding: OhStoreBindingV1; store: OhStoreV1 }>,
|
|
2211
|
+
priorHead: OhHeadV1,
|
|
2212
|
+
nextHead: OhHeadV1,
|
|
2213
|
+
requiredFirstHead?: OhHeadV1,
|
|
2214
|
+
): Promise<LaneSnapshot> {
|
|
2215
|
+
if (nextHead.sequence <= priorHead.sequence) {
|
|
2216
|
+
throw new OhConflictError("The next canonical memory head is not a descendant of the current pin.");
|
|
2217
|
+
}
|
|
2218
|
+
const distance = nextHead.sequence - priorHead.sequence;
|
|
2219
|
+
if (distance > OH_MEMORY_AUTHORITY_LIMITS_V1.canonicalAdvanceOperations
|
|
2220
|
+
|| Math.ceil(distance / OH_MEMORY_AUTHORITY_LIMITS_V1.canonicalChangeFeedPage)
|
|
2221
|
+
> OH_MEMORY_AUTHORITY_LIMITS_V1.canonicalAdvancePages) {
|
|
2222
|
+
throw new RangeError("The canonical memory advance exceeds its total proof bound; advance in host-reviewed chunks.");
|
|
2223
|
+
}
|
|
2224
|
+
const through = headRef(nextHead);
|
|
2225
|
+
let cursor = headRef(priorHead);
|
|
2226
|
+
let pageCount = 0;
|
|
2227
|
+
let reachedHead: OhHeadV1 | null = null;
|
|
2228
|
+
let firstHead: OhHeadV1 | null = null;
|
|
2229
|
+
while (cursor.sequence < through.sequence) {
|
|
2230
|
+
if (pageCount >= OH_MEMORY_AUTHORITY_LIMITS_V1.canonicalAdvancePages) {
|
|
2231
|
+
throw new RangeError("The canonical memory advance exceeded its page proof bound; advance in host-reviewed chunks.");
|
|
2232
|
+
}
|
|
2233
|
+
pageCount += 1;
|
|
2234
|
+
const remaining = through.sequence - cursor.sequence;
|
|
2235
|
+
const limit = Math.min(remaining, OH_MEMORY_AUTHORITY_LIMITS_V1.canonicalChangeFeedPage);
|
|
2236
|
+
const returnedData = detachCanonicalData(
|
|
2237
|
+
await authority.store.changesSince(cursor, { limit, through }),
|
|
2238
|
+
"The canonical change-feed page",
|
|
2239
|
+
OH_MEMORY_AUTHORITY_LIMITS_V1.canonicalChangeFeedPageBytes,
|
|
2240
|
+
).value;
|
|
2241
|
+
const returned = returnedData as Record<string, unknown>;
|
|
2242
|
+
if (!isPlainRecord(returned) || !hasExactKeys(returned,
|
|
2243
|
+
["from", "hasMore", "operations", "through", "to", "v"])
|
|
2244
|
+
|| returned.v !== 1 || typeof returned.hasMore !== "boolean"
|
|
2245
|
+
|| !Array.isArray(returned.operations) || returned.operations.length > limit) {
|
|
2246
|
+
throw new OhIntegrityError("The canonical change feed returned an invalid page envelope.");
|
|
2247
|
+
}
|
|
2248
|
+
const from = parseOhHeadRefV1(returned.from);
|
|
2249
|
+
const returnedThrough = parseOhHeadV1(returned.through);
|
|
2250
|
+
const returnedTo = parseOhHeadRefV1(returned.to);
|
|
2251
|
+
if (from === null || returnedThrough === null || returnedTo === null
|
|
2252
|
+
|| canonicalJson(from) !== canonicalJson(cursor)
|
|
2253
|
+
|| !exactHead(returnedThrough, nextHead)) {
|
|
2254
|
+
throw new OhIntegrityError("The canonical change feed changed its pinned bounds.");
|
|
2255
|
+
}
|
|
2256
|
+
let reached = cursor;
|
|
2257
|
+
for (const value of returned.operations) {
|
|
2258
|
+
const operation = parseOhOperationV1(value);
|
|
2259
|
+
if (operation === null || operation.spaceId !== authority.binding.spaceId
|
|
2260
|
+
|| operation.sequence !== reached.sequence + 1
|
|
2261
|
+
|| operation.parentOperationSha256 !== reached.operationSha256) {
|
|
2262
|
+
throw new OhIntegrityError("The canonical change feed contains a gap or different authority.");
|
|
2263
|
+
}
|
|
2264
|
+
reached = Object.freeze({ operationSha256: operation.operationSha256,
|
|
2265
|
+
sequence: operation.sequence });
|
|
2266
|
+
reachedHead = immutableClone({ generation: operation.sequence,
|
|
2267
|
+
graphRevisionSha256: operation.graphRevisionSha256,
|
|
2268
|
+
operationSha256: operation.operationSha256, recordsSha256: operation.recordsSha256,
|
|
2269
|
+
sequence: operation.sequence, v: 1 });
|
|
2270
|
+
firstHead ??= reachedHead;
|
|
2271
|
+
}
|
|
2272
|
+
if (canonicalJson(reached) !== canonicalJson(returnedTo)
|
|
2273
|
+
|| (returned.hasMore && returned.operations.length === 0)
|
|
2274
|
+
|| (returned.hasMore && reached.sequence >= through.sequence)
|
|
2275
|
+
|| reached.sequence > through.sequence
|
|
2276
|
+
|| (!returned.hasMore && canonicalJson(reached) !== canonicalJson(through))) {
|
|
2277
|
+
throw new OhIntegrityError("The canonical change feed did not prove the requested descendant.");
|
|
2278
|
+
}
|
|
2279
|
+
cursor = reached;
|
|
2280
|
+
}
|
|
2281
|
+
if (reachedHead === null || !exactHead(reachedHead, nextHead)) {
|
|
2282
|
+
throw new OhIntegrityError("The canonical change feed did not prove the requested full head.");
|
|
2283
|
+
}
|
|
2284
|
+
if (requiredFirstHead !== undefined
|
|
2285
|
+
&& (firstHead === null || !exactHead(firstHead, requiredFirstHead))) {
|
|
2286
|
+
throw new OhIntegrityError("The returned adoption operation is not on the current canonical path.");
|
|
2287
|
+
}
|
|
2288
|
+
return await readLane(authority, "canonical", nextHead);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
function canonicalAdvanceReceipt(
|
|
2292
|
+
authorityIdValue: string,
|
|
2293
|
+
bindingSha256: Sha256Hex,
|
|
2294
|
+
priorHead: OhHeadV1,
|
|
2295
|
+
head: OhHeadV1,
|
|
2296
|
+
status: OhMemoryCanonicalAdvanceReceiptV1["status"],
|
|
2297
|
+
): OhMemoryCanonicalAdvanceReceiptV1 {
|
|
2298
|
+
const payload = { authorityId: authorityIdValue, bindingSha256, head, priorHead, status,
|
|
2299
|
+
v: 1 as const };
|
|
2300
|
+
return immutableClone({ ...payload, receiptSha256: canonicalSha256(payload) });
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
function adoptionReceipt(
|
|
2304
|
+
actorId: string,
|
|
2305
|
+
authorityIdValue: string,
|
|
2306
|
+
bindingSha256: Sha256Hex,
|
|
2307
|
+
nominationSha256: Sha256Hex,
|
|
2308
|
+
operationSha256: Sha256Hex | null,
|
|
2309
|
+
priorHead: OhHeadV1,
|
|
2310
|
+
head: OhHeadV1,
|
|
2311
|
+
status: OhMemoryAdoptionReceiptV1["status"],
|
|
2312
|
+
): OhMemoryAdoptionReceiptV1 {
|
|
2313
|
+
const payload = { actorId, authorityId: authorityIdValue, bindingSha256, head,
|
|
2314
|
+
nominationSha256, operationSha256, priorHead, status, v: 1 as const };
|
|
2315
|
+
return immutableClone({ ...payload, receiptSha256: canonicalSha256(payload) });
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
function adoptionDifferences(
|
|
2319
|
+
snapshot: OhSnapshotV1,
|
|
2320
|
+
records: readonly KnowledgeGraphRecordV1[],
|
|
2321
|
+
): readonly OhMemoryAdoptionConflictEntryV1[] {
|
|
2322
|
+
const canonicalByKey = new Map(snapshot.records.map((record) => [record.key, record]));
|
|
2323
|
+
return immutableClone(records.flatMap((record) => {
|
|
2324
|
+
const canonicalRecord = canonicalByKey.get(record.key);
|
|
2325
|
+
return canonicalRecord?.recordSha256 === record.recordSha256 ? [] : [{
|
|
2326
|
+
canonicalRecordSha256: canonicalRecord?.recordSha256 ?? null,
|
|
2327
|
+
key: record.key,
|
|
2328
|
+
nominatedRecordSha256: record.recordSha256,
|
|
2329
|
+
v: 1 as const,
|
|
2330
|
+
}];
|
|
2331
|
+
}).sort((left, right) => compareText(left.key, right.key)));
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
function unauthorizedAdoptionDifferences(
|
|
2335
|
+
reviewedSnapshot: OhSnapshotV1,
|
|
2336
|
+
currentSnapshot: OhSnapshotV1,
|
|
2337
|
+
records: readonly KnowledgeGraphRecordV1[],
|
|
2338
|
+
replacements: readonly OhMemoryAdoptionReplacementV1[],
|
|
2339
|
+
): readonly OhMemoryAdoptionConflictEntryV1[] {
|
|
2340
|
+
const reviewedByKey = new Map(reviewedSnapshot.records.map((record) => [record.key, record]));
|
|
2341
|
+
const currentByKey = new Map(currentSnapshot.records.map((record) => [record.key, record]));
|
|
2342
|
+
const replacementByKey = new Map(replacements.map((replacement) => [replacement.key,
|
|
2343
|
+
replacement.expectedPriorRecordSha256]));
|
|
2344
|
+
const conflicts = records.flatMap((nominated) => {
|
|
2345
|
+
const reviewed = reviewedByKey.get(nominated.key);
|
|
2346
|
+
const expectedPriorRecordSha256 = replacementByKey.get(nominated.key);
|
|
2347
|
+
const alreadyEqual = reviewed?.recordSha256 === nominated.recordSha256;
|
|
2348
|
+
const authorized = reviewed === undefined
|
|
2349
|
+
? expectedPriorRecordSha256 === undefined
|
|
2350
|
+
: alreadyEqual
|
|
2351
|
+
? expectedPriorRecordSha256 === undefined
|
|
2352
|
+
|| expectedPriorRecordSha256 === reviewed.recordSha256
|
|
2353
|
+
: expectedPriorRecordSha256 === reviewed.recordSha256;
|
|
2354
|
+
if (authorized) return [];
|
|
2355
|
+
return [{
|
|
2356
|
+
canonicalRecordSha256: currentByKey.get(nominated.key)?.recordSha256 ?? null,
|
|
2357
|
+
key: nominated.key,
|
|
2358
|
+
nominatedRecordSha256: nominated.recordSha256,
|
|
2359
|
+
v: 1 as const,
|
|
2360
|
+
}];
|
|
2361
|
+
});
|
|
2362
|
+
return immutableClone(conflicts
|
|
2363
|
+
.sort((left, right) => compareText(left.key, right.key)));
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
async function assertWorkingCommitCapacity(
|
|
2367
|
+
store: OhStoreV1,
|
|
2368
|
+
binding: OhStoreBindingV1,
|
|
2369
|
+
changes: readonly KnowledgeGraphChangeV1[],
|
|
2370
|
+
head: OhHeadV1,
|
|
2371
|
+
): Promise<void> {
|
|
2372
|
+
const returnedSnapshot = await store.snapshot({
|
|
2373
|
+
head: { operationSha256: head.operationSha256, sequence: head.sequence },
|
|
2374
|
+
maximumRecords: OH_MEMORY_LIMITS_V1.maximumRecordsPerLane,
|
|
2375
|
+
});
|
|
2376
|
+
const { snapshot } = parseDetachedStoreSnapshot(returnedSnapshot,
|
|
2377
|
+
"The working capacity store", head, binding.spaceId);
|
|
2378
|
+
const recordsByKey = new Map(snapshot.records.map((record) => [record.key, record]));
|
|
2379
|
+
for (const change of canonicalKnowledgeGraphChangesV1(changes)) {
|
|
2380
|
+
if (change.kind === "put") recordsByKey.set(change.record.key, change.record);
|
|
2381
|
+
else recordsByKey.delete(change.key);
|
|
2382
|
+
}
|
|
2383
|
+
if (recordsByKey.size > OH_MEMORY_LIMITS_V1.maximumRecordsPerLane) {
|
|
2384
|
+
throw new RangeError("The remembered working memory would exceed its record snapshot bound.");
|
|
2385
|
+
}
|
|
2386
|
+
const nextSequence = snapshot.head.sequence + 1;
|
|
2387
|
+
if (!Number.isSafeInteger(nextSequence)) {
|
|
2388
|
+
throw new RangeError("The remembered working memory would exceed its head sequence bound.");
|
|
2389
|
+
}
|
|
2390
|
+
const records = [...recordsByKey.values()]
|
|
2391
|
+
.sort((left, right) => compareText(left.key, right.key));
|
|
2392
|
+
const placeholderDigest = canonicalSha256({ kind: "oh.memory.remember-capacity", v: 1 });
|
|
2393
|
+
const prospective: OhSnapshotV1 = {
|
|
2394
|
+
head: {
|
|
2395
|
+
generation: nextSequence,
|
|
2396
|
+
graphRevisionSha256: placeholderDigest,
|
|
2397
|
+
operationSha256: placeholderDigest,
|
|
2398
|
+
recordsSha256: canonicalSha256(records.map(knowledgeGraphRecordRefV1)),
|
|
2399
|
+
sequence: nextSequence,
|
|
2400
|
+
v: 1,
|
|
2401
|
+
},
|
|
2402
|
+
records,
|
|
2403
|
+
v: 1,
|
|
2404
|
+
};
|
|
2405
|
+
if (utf8ByteLength(canonicalJson(prospective)) > OH_MEMORY_LIMITS_V1.snapshotBytesPerLane) {
|
|
2406
|
+
throw new RangeError("The remembered working memory would exceed its snapshot byte bound.");
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
function capacityGuardedWorkingStore(store: OhStoreV1, binding: OhStoreBindingV1): OhStoreV1 {
|
|
2411
|
+
const guarded: OhStoreV1 = {
|
|
2412
|
+
binding,
|
|
2413
|
+
changesSince: async (from, options) => await store.changesSince(from, options),
|
|
2414
|
+
close: async () => await store.close(),
|
|
2415
|
+
commit: async (input) => {
|
|
2416
|
+
const current = parseDetachedStoreHead(await store.head(),
|
|
2417
|
+
"The working capacity store");
|
|
2418
|
+
// The store owns exact operation-id replay. A stale expected head may
|
|
2419
|
+
// therefore be either a harmless replay or a conflict. Delegate it
|
|
2420
|
+
// unchanged so a hypothetical reapplication cannot reject a replay.
|
|
2421
|
+
if (
|
|
2422
|
+
current.generation === input.expectedHead.generation
|
|
2423
|
+
&& current.operationSha256 === input.expectedHead.operationSha256
|
|
2424
|
+
) await assertWorkingCommitCapacity(store, binding, input.changes, current);
|
|
2425
|
+
return await store.commit(input);
|
|
2426
|
+
},
|
|
2427
|
+
exportDependencyClosure: async (input) => await store.exportDependencyClosure(input),
|
|
2428
|
+
head: async () => await store.head(),
|
|
2429
|
+
snapshot: async (options) => await store.snapshot(options),
|
|
2430
|
+
verify: async () => await store.verify(),
|
|
2431
|
+
};
|
|
2432
|
+
return Object.freeze(guarded);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
function assertAdoptionSnapshotCapacity(
|
|
2436
|
+
snapshot: OhSnapshotV1,
|
|
2437
|
+
changedRecords: readonly KnowledgeGraphRecordV1[],
|
|
2438
|
+
): void {
|
|
2439
|
+
const recordsByKey = new Map(snapshot.records.map((record) => [record.key, record]));
|
|
2440
|
+
for (const record of changedRecords) recordsByKey.set(record.key, record);
|
|
2441
|
+
if (recordsByKey.size > OH_MEMORY_LIMITS_V1.maximumRecordsPerLane) {
|
|
2442
|
+
throw new RangeError("The adopted canonical memory would exceed its record snapshot bound.");
|
|
2443
|
+
}
|
|
2444
|
+
const nextSequence = snapshot.head.sequence + 1;
|
|
2445
|
+
if (!Number.isSafeInteger(nextSequence)) {
|
|
2446
|
+
throw new RangeError("The adopted canonical memory would exceed its head sequence bound.");
|
|
2447
|
+
}
|
|
2448
|
+
const records = [...recordsByKey.values()]
|
|
2449
|
+
.sort((left, right) => compareText(left.key, right.key));
|
|
2450
|
+
const placeholderDigest = canonicalSha256({ kind: "oh.memory.adoption-capacity", v: 1 });
|
|
2451
|
+
const prospective: OhSnapshotV1 = {
|
|
2452
|
+
head: {
|
|
2453
|
+
generation: nextSequence,
|
|
2454
|
+
graphRevisionSha256: placeholderDigest,
|
|
2455
|
+
operationSha256: placeholderDigest,
|
|
2456
|
+
recordsSha256: canonicalSha256(records.map(knowledgeGraphRecordRefV1)),
|
|
2457
|
+
sequence: nextSequence,
|
|
2458
|
+
v: 1,
|
|
2459
|
+
},
|
|
2460
|
+
records,
|
|
2461
|
+
v: 1,
|
|
2462
|
+
};
|
|
2463
|
+
if (utf8ByteLength(canonicalJson(prospective)) > OH_MEMORY_LIMITS_V1.snapshotBytesPerLane) {
|
|
2464
|
+
throw new RangeError("The adopted canonical memory would exceed its canonical snapshot byte bound.");
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
function adoptionConflict(
|
|
2469
|
+
expectedHead: OhHeadV1,
|
|
2470
|
+
actualHead: OhHeadV1,
|
|
2471
|
+
completeConflicts: readonly OhMemoryAdoptionConflictEntryV1[],
|
|
2472
|
+
): OhMemoryAdoptionConflictError {
|
|
2473
|
+
const sorted = immutableClone([...completeConflicts]
|
|
2474
|
+
.sort((left, right) => compareText(left.key, right.key)));
|
|
2475
|
+
const conflicts = immutableClone(sorted.slice(0,
|
|
2476
|
+
OH_MEMORY_AUTHORITY_LIMITS_V1.reportedAdoptionConflicts));
|
|
2477
|
+
const conflict = immutableClone({ actualHead, conflicts,
|
|
2478
|
+
conflictsSha256: canonicalSha256({ conflicts: sorted, v: 1 }), expectedHead,
|
|
2479
|
+
reportedConflicts: conflicts.length, totalConflicts: sorted.length,
|
|
2480
|
+
truncated: conflicts.length !== sorted.length, v: 1 as const });
|
|
2481
|
+
return new OhMemoryAdoptionConflictError(conflict);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
/**
|
|
2485
|
+
* Creates the stable two-lane memory boundary. The agent object carries no
|
|
2486
|
+
* canonical mutation handle; trusted host code retains serialized rollover and
|
|
2487
|
+
* reviewed adoption controls separately.
|
|
2488
|
+
*/
|
|
2489
|
+
export async function createOhMemoryAuthorityV1(
|
|
2490
|
+
options: OhMemoryAuthorityOptionsV1,
|
|
2491
|
+
): Promise<OhMemoryAuthorityV1> {
|
|
2492
|
+
const maximumCanonicalOperationBytes = options.maximumCanonicalOperationBytes
|
|
2493
|
+
?? OH_OPERATION_MAX_BYTES_V1;
|
|
2494
|
+
if (!Number.isSafeInteger(maximumCanonicalOperationBytes)
|
|
2495
|
+
|| maximumCanonicalOperationBytes < 1
|
|
2496
|
+
|| maximumCanonicalOperationBytes > OH_OPERATION_MAX_BYTES_V1) {
|
|
2497
|
+
throw new TypeError("Invalid canonical memory operation byte bound.");
|
|
2498
|
+
}
|
|
2499
|
+
const memoryActorId = safeCode(options.actorId, 128);
|
|
2500
|
+
const adoptionActorId = safeCode(options.adoptionActorId, 128);
|
|
2501
|
+
if (memoryActorId === null || adoptionActorId === null) {
|
|
2502
|
+
throw new TypeError("Invalid host-bound memory authority actor ID.");
|
|
2503
|
+
}
|
|
2504
|
+
const canonicalStore = options.canonical.store;
|
|
2505
|
+
const workingStore = options.working.store;
|
|
2506
|
+
const canonicalAuthorityId = authorityId(options.canonical.authorityId);
|
|
2507
|
+
const workingAuthorityId = authorityId(options.working.authorityId);
|
|
2508
|
+
if (canonicalAuthorityId === workingAuthorityId) {
|
|
2509
|
+
throw new OhProfileError("Working and canonical memory must be distinct physical authorities.");
|
|
2510
|
+
}
|
|
2511
|
+
const canonicalBinding = bindingFor(canonicalStore,
|
|
2512
|
+
options.canonical.expectedBindingSha256, "canonical");
|
|
2513
|
+
const workingBinding = bindingFor(workingStore,
|
|
2514
|
+
options.working.expectedBindingSha256, "working");
|
|
2515
|
+
const initialCanonicalHead = parseMemoryAuthorityHead(options.canonical.expectedHead,
|
|
2516
|
+
"initial canonical");
|
|
2517
|
+
const workingCodecs = options.working.codecs;
|
|
2518
|
+
const explainCapabilityLifetimeMs = options.explainCapabilityLifetimeMs;
|
|
2519
|
+
const monotonicNow = options.monotonicNow;
|
|
2520
|
+
const now = options.now;
|
|
2521
|
+
const continuationKey = continuationKeyV2(options.continuationKey);
|
|
2522
|
+
const programs = Object.freeze([...resolveProgramsV2(options.programs).values()].map((program) =>
|
|
2523
|
+
immutableClone({ evaluation: program.evaluation, maximumPageBytes: program.maximumPageBytes,
|
|
2524
|
+
maximumRows: program.maximumRows, pageSize: program.pageSize, parameters: program.parameters,
|
|
2525
|
+
programId: program.programId, purpose: program.purpose, query: program.query,
|
|
2526
|
+
rulePack: program.rulePack, v: 2 as const })));
|
|
2527
|
+
const extractors = resolveExtractors(options.extractors ?? []);
|
|
2528
|
+
const nominationRoutes = Object.freeze([...resolveNominationRoutes(options.nominationRoutes ?? []).values()]);
|
|
2529
|
+
const routesById = new Map(nominationRoutes.map((route) => [route.nominationId, route]));
|
|
2530
|
+
const runtime = createOhMemoryRuntimeV2(options);
|
|
2531
|
+
|
|
2532
|
+
const createAgentAt = async (expectedHead: OhHeadV1) => await createOhMemoryAgentV2WithRuntime({
|
|
2533
|
+
actorId: memoryActorId,
|
|
2534
|
+
canonical: { authorityId: canonicalAuthorityId,
|
|
2535
|
+
expectedBindingSha256: canonicalBinding.bindingSha256, expectedHead, store: canonicalStore },
|
|
2536
|
+
continuationKey,
|
|
2537
|
+
...(explainCapabilityLifetimeMs === undefined ? {} : {
|
|
2538
|
+
explainCapabilityLifetimeMs,
|
|
2539
|
+
}),
|
|
2540
|
+
extractors,
|
|
2541
|
+
...(monotonicNow === undefined ? {} : { monotonicNow }),
|
|
2542
|
+
nominationRoutes,
|
|
2543
|
+
...(now === undefined ? {} : { now }),
|
|
2544
|
+
programs,
|
|
2545
|
+
working: { authorityId: workingAuthorityId, codecs: workingCodecs,
|
|
2546
|
+
expectedBindingSha256: workingBinding.bindingSha256, store: workingStore },
|
|
2547
|
+
}, runtime);
|
|
2548
|
+
|
|
2549
|
+
let activeCanonicalHead = initialCanonicalHead;
|
|
2550
|
+
let activeAgent = await createAgentAt(activeCanonicalHead);
|
|
2551
|
+
const agent: OhMemoryAgentV2 = Object.freeze({
|
|
2552
|
+
async explain(value: unknown) {
|
|
2553
|
+
const selected = activeAgent;
|
|
2554
|
+
return await selected.explain(value);
|
|
2555
|
+
},
|
|
2556
|
+
nominate(value: unknown) {
|
|
2557
|
+
const selected = activeAgent;
|
|
2558
|
+
return selected.nominate(value);
|
|
2559
|
+
},
|
|
2560
|
+
async query(value: unknown) {
|
|
2561
|
+
const selected = activeAgent;
|
|
2562
|
+
return await selected.query(value);
|
|
2563
|
+
},
|
|
2564
|
+
remember(value: unknown) {
|
|
2565
|
+
const selected = activeAgent;
|
|
2566
|
+
return selected.remember(value);
|
|
2567
|
+
},
|
|
2568
|
+
});
|
|
2569
|
+
|
|
2570
|
+
let hostTail: Promise<void> = Promise.resolve();
|
|
2571
|
+
const serialized = <T>(operation: () => Promise<T>): Promise<T> => {
|
|
2572
|
+
const result = hostTail.then(operation);
|
|
2573
|
+
hostTail = result.then(() => undefined, () => undefined);
|
|
2574
|
+
return result;
|
|
2575
|
+
};
|
|
2576
|
+
|
|
2577
|
+
const installCanonicalHead = async (head: OhHeadV1): Promise<void> => {
|
|
2578
|
+
const nextAgent = await createAgentAt(head);
|
|
2579
|
+
activeAgent = nextAgent;
|
|
2580
|
+
activeCanonicalHead = immutableClone(head);
|
|
2581
|
+
};
|
|
2582
|
+
|
|
2583
|
+
const canonicalAuthority = Object.freeze({ authorityId: canonicalAuthorityId,
|
|
2584
|
+
binding: canonicalBinding, store: canonicalStore });
|
|
2585
|
+
const readPhysicalCanonicalHead = async (): Promise<OhHeadV1> =>
|
|
2586
|
+
parseMemoryAuthorityHead(await canonicalStore.head(), "physical canonical");
|
|
2587
|
+
|
|
2588
|
+
const advanceCanonical = (value: unknown): Promise<OhMemoryCanonicalAdvanceReceiptV1> => {
|
|
2589
|
+
let request: ReturnType<typeof parseCanonicalAdvanceRequest>;
|
|
2590
|
+
try { request = parseCanonicalAdvanceRequest(value); } catch (error) { return Promise.reject(error); }
|
|
2591
|
+
return serialized(async () => {
|
|
2592
|
+
const priorHead = activeCanonicalHead;
|
|
2593
|
+
if (!exactHead(request.expectedHead, priorHead)) {
|
|
2594
|
+
throw new OhConflictError("The expected canonical memory head does not match the current pin.");
|
|
2595
|
+
}
|
|
2596
|
+
if (exactHead(request.nextHead, priorHead)) {
|
|
2597
|
+
return canonicalAdvanceReceipt(canonicalAuthorityId, canonicalBinding.bindingSha256,
|
|
2598
|
+
priorHead, priorHead, "unchanged");
|
|
2599
|
+
}
|
|
2600
|
+
await proveCanonicalDescendant(canonicalAuthority, priorHead, request.nextHead);
|
|
2601
|
+
await installCanonicalHead(request.nextHead);
|
|
2602
|
+
return canonicalAdvanceReceipt(canonicalAuthorityId, canonicalBinding.bindingSha256,
|
|
2603
|
+
priorHead, request.nextHead, "advanced");
|
|
2604
|
+
});
|
|
2605
|
+
};
|
|
2606
|
+
|
|
2607
|
+
const adoptNomination = (value: unknown): Promise<OhMemoryAdoptionReceiptV1> => {
|
|
2608
|
+
let request: ReturnType<typeof parseAdoptionRequest>;
|
|
2609
|
+
try { request = parseAdoptionRequest(value); } catch (error) { return Promise.reject(error); }
|
|
2610
|
+
return serialized(async () => {
|
|
2611
|
+
const route = routesById.get(request.nomination.nominationId);
|
|
2612
|
+
if (route === undefined || route.destinationPurpose !== request.nomination.destinationPurpose) {
|
|
2613
|
+
throw new OhProfileError("The memory nomination is not bound to this adoption route.");
|
|
2614
|
+
}
|
|
2615
|
+
if (request.nomination.source.authorityId !== workingAuthorityId
|
|
2616
|
+
|| request.nomination.source.bindingSha256 !== workingBinding.bindingSha256
|
|
2617
|
+
|| request.nomination.closure.binding.bindingSha256 !== workingBinding.bindingSha256) {
|
|
2618
|
+
throw new OhProfileError("The memory nomination is not from the bound working authority.");
|
|
2619
|
+
}
|
|
2620
|
+
const returnedReexport = await workingStore.exportDependencyClosure({
|
|
2621
|
+
head: headRef(request.nomination.source.head),
|
|
2622
|
+
maximumRecords: OH_DEPENDENCY_CLOSURE_LIMITS_V1.records,
|
|
2623
|
+
roots: request.nomination.closure.roots,
|
|
2624
|
+
});
|
|
2625
|
+
const reexported = detachCanonicalData(returnedReexport, "The working re-exported nomination",
|
|
2626
|
+
OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes);
|
|
2627
|
+
if (reexported.canonical !== canonicalJson(request.nomination.closure)) {
|
|
2628
|
+
throw new OhIntegrityError("The working authority did not re-export the nominated closure exactly.");
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
const priorHead = activeCanonicalHead;
|
|
2632
|
+
const physicalHead = await readPhysicalCanonicalHead();
|
|
2633
|
+
const replacementConflictsAt = async (currentLane: LaneSnapshot) => {
|
|
2634
|
+
if (request.replacements.length === 0) return [];
|
|
2635
|
+
const reviewedLane = exactHead(request.expectedCanonicalHead, currentLane.snapshot.head)
|
|
2636
|
+
? currentLane
|
|
2637
|
+
: await readLane(canonicalAuthority, "canonical", request.expectedCanonicalHead);
|
|
2638
|
+
return unauthorizedAdoptionDifferences(reviewedLane.snapshot, currentLane.snapshot,
|
|
2639
|
+
request.nomination.closure.records, request.replacements);
|
|
2640
|
+
};
|
|
2641
|
+
if (!exactHead(physicalHead, priorHead)) {
|
|
2642
|
+
const physicalLane = await proveCanonicalDescendant(canonicalAuthority, priorHead, physicalHead);
|
|
2643
|
+
const physicalDifferences = adoptionDifferences(physicalLane.snapshot,
|
|
2644
|
+
request.nomination.closure.records);
|
|
2645
|
+
if (physicalDifferences.length === 0) {
|
|
2646
|
+
const replacementConflicts = await replacementConflictsAt(physicalLane);
|
|
2647
|
+
if (replacementConflicts.length > 0) {
|
|
2648
|
+
throw adoptionConflict(request.expectedCanonicalHead, physicalHead, replacementConflicts);
|
|
2649
|
+
}
|
|
2650
|
+
await installCanonicalHead(physicalHead);
|
|
2651
|
+
return adoptionReceipt(adoptionActorId, canonicalAuthorityId,
|
|
2652
|
+
canonicalBinding.bindingSha256, request.nomination.nominationSha256, null,
|
|
2653
|
+
priorHead, physicalHead, "already-present");
|
|
2654
|
+
}
|
|
2655
|
+
throw adoptionConflict(request.expectedCanonicalHead, physicalHead, physicalDifferences);
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
const lane = await readLane(canonicalAuthority, "canonical", priorHead);
|
|
2659
|
+
const differences = adoptionDifferences(lane.snapshot, request.nomination.closure.records);
|
|
2660
|
+
if (!exactHead(request.expectedCanonicalHead, priorHead)) {
|
|
2661
|
+
if (differences.length === 0) {
|
|
2662
|
+
const replacementConflicts = await replacementConflictsAt(lane);
|
|
2663
|
+
if (replacementConflicts.length > 0) {
|
|
2664
|
+
throw adoptionConflict(request.expectedCanonicalHead, priorHead, replacementConflicts);
|
|
2665
|
+
}
|
|
2666
|
+
return adoptionReceipt(adoptionActorId, canonicalAuthorityId,
|
|
2667
|
+
canonicalBinding.bindingSha256, request.nomination.nominationSha256, null,
|
|
2668
|
+
priorHead, priorHead, "already-present");
|
|
2669
|
+
}
|
|
2670
|
+
throw adoptionConflict(request.expectedCanonicalHead, priorHead, differences);
|
|
2671
|
+
}
|
|
2672
|
+
if (differences.length === 0) {
|
|
2673
|
+
const replacementConflicts = await replacementConflictsAt(lane);
|
|
2674
|
+
if (replacementConflicts.length > 0) {
|
|
2675
|
+
throw adoptionConflict(request.expectedCanonicalHead, priorHead, replacementConflicts);
|
|
2676
|
+
}
|
|
2677
|
+
return adoptionReceipt(adoptionActorId, canonicalAuthorityId,
|
|
2678
|
+
canonicalBinding.bindingSha256, request.nomination.nominationSha256, null,
|
|
2679
|
+
priorHead, priorHead, "already-present");
|
|
2680
|
+
}
|
|
2681
|
+
const unauthorized = unauthorizedAdoptionDifferences(lane.snapshot, lane.snapshot,
|
|
2682
|
+
request.nomination.closure.records, request.replacements);
|
|
2683
|
+
if (unauthorized.length > 0) {
|
|
2684
|
+
throw adoptionConflict(request.expectedCanonicalHead, priorHead, unauthorized);
|
|
2685
|
+
}
|
|
2686
|
+
const changedKeys = new Set(differences.map(({ key }) => key));
|
|
2687
|
+
const changedRecords = request.nomination.closure.records
|
|
2688
|
+
.filter(({ key }) => changedKeys.has(key));
|
|
2689
|
+
if (changedRecords.length === 0) {
|
|
2690
|
+
return adoptionReceipt(adoptionActorId, canonicalAuthorityId,
|
|
2691
|
+
canonicalBinding.bindingSha256, request.nomination.nominationSha256, null,
|
|
2692
|
+
priorHead, priorHead, "already-present");
|
|
2693
|
+
}
|
|
2694
|
+
assertAdoptionSnapshotCapacity(lane.snapshot, changedRecords);
|
|
2695
|
+
const changes = canonicalKnowledgeGraphChangesV1(changedRecords
|
|
2696
|
+
.map((record): KnowledgeGraphChangeV1 => ({ kind: "put", record, v: 1 })));
|
|
2697
|
+
const operationId = `memory_adopt_${canonicalSha256({ actorId: adoptionActorId,
|
|
2698
|
+
bindingSha256: canonicalBinding.bindingSha256,
|
|
2699
|
+
nominationSha256: request.nomination.nominationSha256,
|
|
2700
|
+
priorHead, v: 1 }).slice(0, 48)}`;
|
|
2701
|
+
let returnedOperation: unknown;
|
|
2702
|
+
try {
|
|
2703
|
+
returnedOperation = await canonicalStore.commit({ actorId: adoptionActorId, changes,
|
|
2704
|
+
expectedHead: { generation: priorHead.generation,
|
|
2705
|
+
operationSha256: priorHead.operationSha256 },
|
|
2706
|
+
maximumOperationBytes: maximumCanonicalOperationBytes,
|
|
2707
|
+
operationId });
|
|
2708
|
+
} catch (error) {
|
|
2709
|
+
if (!(error instanceof OhConflictError)) throw error;
|
|
2710
|
+
const actualHead = await readPhysicalCanonicalHead();
|
|
2711
|
+
const actualLane = exactHead(actualHead, priorHead)
|
|
2712
|
+
? await readLane(canonicalAuthority, "canonical", actualHead)
|
|
2713
|
+
: await proveCanonicalDescendant(canonicalAuthority, priorHead, actualHead);
|
|
2714
|
+
const actualDifferences = adoptionDifferences(actualLane.snapshot,
|
|
2715
|
+
request.nomination.closure.records);
|
|
2716
|
+
if (actualDifferences.length === 0) {
|
|
2717
|
+
if (!exactHead(actualHead, priorHead)) {
|
|
2718
|
+
await installCanonicalHead(actualHead);
|
|
2719
|
+
}
|
|
2720
|
+
return adoptionReceipt(adoptionActorId, canonicalAuthorityId,
|
|
2721
|
+
canonicalBinding.bindingSha256, request.nomination.nominationSha256, null,
|
|
2722
|
+
priorHead, actualHead, "already-present");
|
|
2723
|
+
}
|
|
2724
|
+
throw adoptionConflict(request.expectedCanonicalHead, actualHead, actualDifferences);
|
|
2725
|
+
}
|
|
2726
|
+
const operation = parseOhOperationV1(detachCanonicalData(returnedOperation,
|
|
2727
|
+
"The returned canonical adoption operation",
|
|
2728
|
+
OH_MEMORY_AUTHORITY_LIMITS_V1.canonicalChangeFeedPageBytes).value);
|
|
2729
|
+
if (operation === null || operation.actorId !== adoptionActorId
|
|
2730
|
+
|| operation.operationId !== operationId || operation.spaceId !== canonicalBinding.spaceId
|
|
2731
|
+
|| operation.parentOperationSha256 !== priorHead.operationSha256
|
|
2732
|
+
|| operation.sequence !== priorHead.sequence + 1
|
|
2733
|
+
|| canonicalJson(operation.changes) !== canonicalJson(changes)) {
|
|
2734
|
+
throw new OhIntegrityError("The canonical authority returned a different adoption operation.");
|
|
2735
|
+
}
|
|
2736
|
+
const head: OhHeadV1 = immutableClone({ generation: operation.sequence,
|
|
2737
|
+
graphRevisionSha256: operation.graphRevisionSha256,
|
|
2738
|
+
operationSha256: operation.operationSha256, recordsSha256: operation.recordsSha256,
|
|
2739
|
+
sequence: operation.sequence, v: 1 });
|
|
2740
|
+
const actualHead = await readPhysicalCanonicalHead();
|
|
2741
|
+
if (!exactHead(actualHead, head)) {
|
|
2742
|
+
const actualLane = exactHead(actualHead, priorHead)
|
|
2743
|
+
? await readLane(canonicalAuthority, "canonical", actualHead)
|
|
2744
|
+
: await proveCanonicalDescendant(canonicalAuthority, priorHead, actualHead, head);
|
|
2745
|
+
const actualDifferences = adoptionDifferences(actualLane.snapshot,
|
|
2746
|
+
request.nomination.closure.records);
|
|
2747
|
+
if (actualDifferences.length !== 0) {
|
|
2748
|
+
throw adoptionConflict(request.expectedCanonicalHead, actualHead, actualDifferences);
|
|
2749
|
+
}
|
|
2750
|
+
await installCanonicalHead(actualHead);
|
|
2751
|
+
return adoptionReceipt(adoptionActorId, canonicalAuthorityId,
|
|
2752
|
+
canonicalBinding.bindingSha256, request.nomination.nominationSha256, null,
|
|
2753
|
+
priorHead, actualHead, "already-present");
|
|
2754
|
+
}
|
|
2755
|
+
await installCanonicalHead(actualHead);
|
|
2756
|
+
return adoptionReceipt(adoptionActorId, canonicalAuthorityId,
|
|
2757
|
+
canonicalBinding.bindingSha256, request.nomination.nominationSha256,
|
|
2758
|
+
operation.operationSha256, priorHead, actualHead, "adopted");
|
|
2759
|
+
});
|
|
2760
|
+
};
|
|
2761
|
+
|
|
2762
|
+
return Object.freeze({ agent, host: Object.freeze({ adoptNomination, advanceCanonical }) });
|
|
2763
|
+
}
|