@world-engines/ladybug-bridge 0.1.0-alpha.0

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/dist/index.js ADDED
@@ -0,0 +1,3781 @@
1
+ import lbug, {} from "@ladybugdb/wasm-core";
2
+ import { isSerializedPropEnvelope } from "@world-engines/protocol-ts/entity-prop-envelope";
3
+ export { canonicalLadybugRelationId, createViewAccessPolicySubject, derivedGraphViewAccessPolicy, deriveViewAccessPolicySubjectId, deriveViewAccessPolicySubjectIdForTarget, isViewReadable, isViewWritable, resolveEdgeViewAccessMode, resolveOverlayLeafViewAccessMode, resolveViewAccessMode, sealViewAccessPolicy, validateViewAccessPolicy, viewAccessPolicyAddressForTarget, VIEW_ACCESS_POLICY_DENY_ROOT, VIEW_ACCESS_POLICY_ROOT_SUBJECT_ID, VIEW_ACCESS_POLICY_SCHEMA_VERSION, } from "./view-access-policy.js";
4
+ const ENTITY_ANALYZER_PROJECTION_CONTRACT_VERSION = 1;
5
+ const worldMemoryHostAuthorities = new WeakSet();
6
+ export function createWorldMemoryHostAuthority() {
7
+ const authority = Object.freeze({});
8
+ worldMemoryHostAuthorities.add(authority);
9
+ return authority;
10
+ }
11
+ // Issue 17 tips4 middle-layer structured recall view (director / LLM traversal-friendly)
12
+ // Emitted alongside GraphRagResult by the wasm-core recall_and_expand_structured helper
13
+ // so agents can enumerate hit entities plus discover [Object] expansion hints and trigger_lock
14
+ // signals without having to re-scan raw nested props themselves.
15
+ //
16
+ // Wire shape mirrors the Rust `RecallStructured` type (serde untagged) in
17
+ // `src/player-client/wasm-core/src/storage/retrieval_diagnostics.rs`:
18
+ // - Leaf primitives serialize as raw JSON values (string / number / bool / null / array)
19
+ // - Object markers serialize as { type: "object", inner_keys: [...], expandable: true }
20
+ // - trigger_lock is omitted when absent (Option::None → serde `skip_serializing_if`)
21
+ export const STRUCTURED_TRIGGER_LOCK_REASON_LOCKED_UNTIL = "locked_until";
22
+ export const STRUCTURED_TRIGGER_LOCK_REASON_ENTER_WHEN = "enter_when";
23
+ export function isStructuredObjectValue(v) {
24
+ return (typeof v === "object"
25
+ && v !== null
26
+ && v.type === "object"
27
+ && Array.isArray(v.inner_keys));
28
+ }
29
+ const ENTITY_TABLE = "Entity";
30
+ const ENTITY_LEXICAL_PROJECTION_TABLE = "EntityLexicalProjection";
31
+ const RELATION_TABLE = "Relation";
32
+ const LATCH_TABLE = "TriggerLeafLatch";
33
+ const ATTR_HISTORY_TABLE = "AttrHistory";
34
+ const TRANSCRIPT_MESSAGE_TABLE = "Message";
35
+ const WM_EPISODE_TABLE = "WmEpisode";
36
+ const WM_CLAIM_ROOT_TABLE = "WmClaimRoot";
37
+ const WM_CLAIM_TABLE = "WmClaim";
38
+ const WM_DERIVED_TABLE = "WmDerived";
39
+ export const WM_EPISODE_VECTOR_INDEX = "WM_EPISODE_VECTOR_INDEX";
40
+ export const WM_EPISODE_FTS_INDEX = "WM_EPISODE_FTS_INDEX";
41
+ export const WM_DERIVED_VECTOR_INDEX = "WM_DERIVED_VECTOR_INDEX";
42
+ export const WM_DERIVED_FTS_INDEX = "WM_DERIVED_FTS_INDEX";
43
+ const WORLD_MEMORY_EMBEDDING_DIM = 4096;
44
+ const WORLD_MEMORY_SCHEMA_VERSION = 1;
45
+ const WORLD_MEMORY_TABLE_PATTERN = /\bWm(?:Episode|ClaimRoot|Claim|Derived)(?:SupportsClaim|SupersedesClaim|ContradictsClaim|FromEpisode|FromClaim|MentionsEntity|AboutEntity)?\b/i;
46
+ const MEMORY_RELATION_DEFINITIONS = Object.freeze({
47
+ WmEpisodeSupportsClaim: { sourceTable: WM_EPISODE_TABLE, targetTable: WM_CLAIM_TABLE },
48
+ WmClaimSupersedesClaim: { sourceTable: WM_CLAIM_TABLE, targetTable: WM_CLAIM_TABLE },
49
+ WmClaimContradictsClaim: { sourceTable: WM_CLAIM_TABLE, targetTable: WM_CLAIM_TABLE },
50
+ WmDerivedFromEpisode: { sourceTable: WM_DERIVED_TABLE, targetTable: WM_EPISODE_TABLE },
51
+ WmDerivedFromClaim: { sourceTable: WM_DERIVED_TABLE, targetTable: WM_CLAIM_TABLE },
52
+ WmEpisodeMentionsEntity: { sourceTable: WM_EPISODE_TABLE, targetTable: ENTITY_TABLE },
53
+ WmDerivedMentionsEntity: { sourceTable: WM_DERIVED_TABLE, targetTable: ENTITY_TABLE },
54
+ WmClaimAboutEntity: { sourceTable: WM_CLAIM_TABLE, targetTable: ENTITY_TABLE }
55
+ });
56
+ const ATTR_HISTORY_MAX_RETAIN = 20;
57
+ const VECTOR_INDEX = "emb_idx";
58
+ const FTS_INDEX = "fts_idx";
59
+ const TRANSCRIPT_FTS_INDEX = "transcript_fts_idx";
60
+ const FALLBACK_VECTOR_METRIC = "cosine";
61
+ const FALLBACK_FTS_STEMMER = "none";
62
+ const ENTITY_FTS_TOKENIZER = "simple";
63
+ const IN_MEMORY_PATH = ":memory:";
64
+ const MAX_DEPTH = 64;
65
+ // 场景初始快照 (作者 editor 预置的实体/关系) 经 bulkUpsert 加载时打的 messageId/turnId sentinel
66
+ // 与 Rust 侧 `world_db::SNAPSHOT_PROVENANCE_MESSAGE_ID` 保持字符串字面量一致 (镜像常量 两端各自定义)
67
+ // 标记"非某条对话消息产生的写入" rollbackAfterMessage 拒绝把它当合法 pivot 目标 防止把它误判成
68
+ // 可回滚到的历史消息 (sentinel 命中 messageId 精确等值查询是安全的 不影响真实消息用 UUID/ULID 的场景)
69
+ export const SNAPSHOT_PROVENANCE_MESSAGE_ID = "__initial_snapshot__";
70
+ const VECTOR_METRIC_WHITELIST = [
71
+ "cosine",
72
+ "l2",
73
+ "l2sq",
74
+ "dotproduct"
75
+ ];
76
+ const HNSW_MU_MIN = 10;
77
+ const HNSW_MU_MAX = 200;
78
+ const HNSW_ML_MIN = 30;
79
+ const HNSW_ML_MAX = 300;
80
+ const HNSW_EFC_MIN = 50;
81
+ const HNSW_EFC_MAX = 500;
82
+ const HNSW_EFS_MIN = 20;
83
+ const HNSW_EFS_MAX = 500;
84
+ const BM25_K1_MIN = 0.5;
85
+ const BM25_K1_MAX = 3.0;
86
+ const BM25_B_MIN = 0.0;
87
+ const BM25_B_MAX = 1.0;
88
+ function pickEnum(value, whitelist, fallback) {
89
+ if (value === undefined)
90
+ return fallback;
91
+ return whitelist.includes(value) ? value : fallback;
92
+ }
93
+ function clampInt(value, min, max) {
94
+ if (value === undefined)
95
+ return undefined;
96
+ if (!Number.isFinite(value))
97
+ return undefined;
98
+ const rounded = Math.round(value);
99
+ if (rounded < min)
100
+ return min;
101
+ if (rounded > max)
102
+ return max;
103
+ return rounded;
104
+ }
105
+ function clampDouble(value, min, max) {
106
+ if (value === undefined)
107
+ return undefined;
108
+ if (!Number.isFinite(value))
109
+ return undefined;
110
+ if (value < min)
111
+ return min;
112
+ if (value > max)
113
+ return max;
114
+ return value;
115
+ }
116
+ function escapeStopwordsToken(value) {
117
+ return value.replace(/[^A-Za-z0-9_\-]/g, "");
118
+ }
119
+ function asString(value) {
120
+ return typeof value === "string" ? value : String(value ?? "");
121
+ }
122
+ function asNullableString(value) {
123
+ if (value === null || value === undefined)
124
+ return null;
125
+ return typeof value === "string" ? value : String(value);
126
+ }
127
+ function asNumber(value) {
128
+ if (typeof value === "number")
129
+ return value;
130
+ if (typeof value === "bigint")
131
+ return Number(value);
132
+ const parsed = Number(value);
133
+ return Number.isFinite(parsed) ? parsed : 0;
134
+ }
135
+ function asNumberArray(value) {
136
+ if (value === null || value === undefined)
137
+ return [];
138
+ if (!Array.isArray(value))
139
+ throw new Error("expected finite number array");
140
+ const result = [];
141
+ for (const entry of value) {
142
+ if (typeof entry !== "number" || !Number.isFinite(entry)) {
143
+ throw new Error("expected finite number array");
144
+ }
145
+ result.push(entry);
146
+ }
147
+ return result;
148
+ }
149
+ function asLatchState(value) {
150
+ const s = asString(value);
151
+ return s === "open" || s === "closed" || s === "dead" ? s : "open";
152
+ }
153
+ function asLatchSource(value) {
154
+ const s = asString(value);
155
+ return s === "judge" ? "judge" : "deterministic";
156
+ }
157
+ function asPivot(value) {
158
+ if (value === null || value === undefined)
159
+ return null;
160
+ if (typeof value === "number" && Number.isFinite(value))
161
+ return value;
162
+ if (typeof value === "bigint")
163
+ return Number(value);
164
+ const parsed = Number(value);
165
+ return Number.isFinite(parsed) ? parsed : null;
166
+ }
167
+ function parseProps(value) {
168
+ if (typeof value !== "string")
169
+ return value ?? null;
170
+ if (value.length === 0)
171
+ return null;
172
+ let parsed;
173
+ try {
174
+ parsed = JSON.parse(value);
175
+ }
176
+ catch {
177
+ return value;
178
+ }
179
+ isSerializedPropEnvelope(parsed);
180
+ return parsed;
181
+ }
182
+ function encodeProps(props) {
183
+ if (typeof props === "string") {
184
+ try {
185
+ isSerializedPropEnvelope(JSON.parse(props));
186
+ }
187
+ catch (error) {
188
+ if (error instanceof SyntaxError)
189
+ return props;
190
+ throw error;
191
+ }
192
+ return props;
193
+ }
194
+ const value = props ?? {};
195
+ isSerializedPropEnvelope(value);
196
+ return JSON.stringify(value);
197
+ }
198
+ function assertAnalyzerProjection(projection) {
199
+ if (projection === undefined)
200
+ return null;
201
+ if (projection.contractVersion !== ENTITY_ANALYZER_PROJECTION_CONTRACT_VERSION) {
202
+ throw new Error("entity analyzer projection contractVersion mismatch");
203
+ }
204
+ if (projection.analyzerId.trim().length === 0 || projection.analyzerVersion.trim().length === 0) {
205
+ throw new Error("entity analyzer projection identity must be non-empty");
206
+ }
207
+ const allowed = new Set(["ready", "unavailable", "failed"]);
208
+ if (!allowed.has(projection.lexicalStatus) || !allowed.has(projection.relationalStatus)) {
209
+ throw new Error("entity analyzer projection contains an invalid arm status");
210
+ }
211
+ if (projection.lexicalStatus !== "ready" && projection.lexicalText.length > 0) {
212
+ throw new Error("non-ready lexical projection must not carry lexicalText");
213
+ }
214
+ if (projection.relationalStatus !== "ready" && projection.relationSignals.length > 0) {
215
+ throw new Error("non-ready relational projection must not carry relation signals");
216
+ }
217
+ for (const token of projection.tokens) {
218
+ if (!Number.isSafeInteger(token.start) ||
219
+ !Number.isSafeInteger(token.end) ||
220
+ token.start < 0 ||
221
+ token.end < token.start) {
222
+ throw new Error("entity analyzer projection token range is invalid");
223
+ }
224
+ }
225
+ if (projection.lexicalStatus === "ready") {
226
+ const terms = new Set();
227
+ for (const token of projection.tokens) {
228
+ if (token.normalized.length > 0)
229
+ terms.add(token.normalized);
230
+ if (token.stem.length > 0)
231
+ terms.add(token.stem);
232
+ }
233
+ if ([...terms].join(" ") !== projection.lexicalText) {
234
+ throw new Error("entity analyzer projection lexicalText mismatch");
235
+ }
236
+ }
237
+ for (const signal of projection.relationSignals) {
238
+ if (signal.version !== projection.analyzerVersion) {
239
+ throw new Error("entity analyzer relation signal version mismatch");
240
+ }
241
+ if (signal.tokenIndexes.some((index) => !Number.isSafeInteger(index) || index < 0 || index >= projection.tokens.length)) {
242
+ throw new Error("entity analyzer relation signal token index is invalid");
243
+ }
244
+ if (signal.kind === "relation" &&
245
+ (signal.canonical === undefined || signal.canonical.trim().length === 0)) {
246
+ throw new Error("entity analyzer relation signal canonical kind is missing");
247
+ }
248
+ }
249
+ return projection;
250
+ }
251
+ function encodeAnalyzerProjection(projection) {
252
+ return canonicalJson(projection);
253
+ }
254
+ async function sha256HexUtf8(value) {
255
+ const subtle = globalThis.crypto?.subtle;
256
+ if (!subtle)
257
+ throw new Error("ladybug state digest requires Web Crypto");
258
+ const digest = new Uint8Array(await subtle.digest("SHA-256", new TextEncoder().encode(value)));
259
+ return [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
260
+ }
261
+ function assertMemoryIdentifier(field, value) {
262
+ if (typeof value !== "string" || value.trim().length === 0) {
263
+ throw new Error(`world memory ${field} must be a non-empty string`);
264
+ }
265
+ return value;
266
+ }
267
+ function assertMemoryInteger(field, value, nullable = false) {
268
+ if (!Number.isSafeInteger(value) || (!nullable && value < 0)) {
269
+ throw new Error(`world memory ${field} must be a safe integer${nullable ? "" : " >= 0"}`);
270
+ }
271
+ return value;
272
+ }
273
+ function assertMemoryNullableInteger(field, value) {
274
+ return value === null ? null : assertMemoryInteger(field, value, true);
275
+ }
276
+ function assertMemoryConfidence(value) {
277
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
278
+ throw new Error("world memory confidence must be finite and in [0, 1]");
279
+ }
280
+ return value;
281
+ }
282
+ function assertMemorySchemaVersion(value) {
283
+ if (value !== WORLD_MEMORY_SCHEMA_VERSION) {
284
+ throw new Error(`world memory schema_version must be ${WORLD_MEMORY_SCHEMA_VERSION}`);
285
+ }
286
+ return value;
287
+ }
288
+ function snapshotCanonicalStringIds(field, values, allowEmpty) {
289
+ if (!Array.isArray(values))
290
+ throw new Error(`world memory ${field} must be an array`);
291
+ const normalized = values.map((value) => assertMemoryIdentifier(field, value));
292
+ if (!allowEmpty && normalized.length === 0) {
293
+ throw new Error(`world memory ${field} must not be empty`);
294
+ }
295
+ if (new Set(normalized).size !== normalized.length) {
296
+ throw new Error(`world memory ${field} must not contain duplicates`);
297
+ }
298
+ const sorted = [...normalized].sort((left, right) => left.localeCompare(right, "und"));
299
+ if (sorted.some((value, index) => value !== normalized[index])) {
300
+ throw new Error(`world memory ${field} must use canonical sorted order`);
301
+ }
302
+ return Object.freeze(sorted);
303
+ }
304
+ function encodeCanonicalStringIds(field, values, allowEmpty) {
305
+ return JSON.stringify(snapshotCanonicalStringIds(field, values, allowEmpty));
306
+ }
307
+ function decodeCanonicalStringIds(field, value, allowEmpty) {
308
+ if (typeof value !== "string")
309
+ throw new Error(`world memory ${field} storage value is not a string`);
310
+ let parsed;
311
+ try {
312
+ parsed = JSON.parse(value);
313
+ }
314
+ catch {
315
+ throw new Error(`world memory ${field} storage value is not JSON`);
316
+ }
317
+ if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
318
+ throw new Error(`world memory ${field} storage value is not a string array`);
319
+ }
320
+ const ids = snapshotCanonicalStringIds(field, parsed, allowEmpty);
321
+ if (JSON.stringify(ids) !== value) {
322
+ throw new Error(`world memory ${field} storage value is not canonical`);
323
+ }
324
+ return ids;
325
+ }
326
+ function canonicalJson(value) {
327
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
328
+ return JSON.stringify(value);
329
+ }
330
+ if (typeof value === "number") {
331
+ if (!Number.isFinite(value))
332
+ throw new Error("world memory canonical JSON contains a non-finite number");
333
+ return JSON.stringify(Object.is(value, -0) ? 0 : value);
334
+ }
335
+ if (Array.isArray(value))
336
+ return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
337
+ if (typeof value === "object") {
338
+ const record = value;
339
+ const keys = Object.keys(record).sort((left, right) => left.localeCompare(right, "und"));
340
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
341
+ }
342
+ throw new Error("world memory canonical JSON contains an unsupported value");
343
+ }
344
+ function assertCanonicalJson(value) {
345
+ if (typeof value !== "string" || value.length === 0) {
346
+ throw new Error("world memory canonical_value must be non-empty canonical JSON");
347
+ }
348
+ let parsed;
349
+ try {
350
+ parsed = JSON.parse(value);
351
+ }
352
+ catch {
353
+ throw new Error("world memory canonical_value must be valid JSON");
354
+ }
355
+ if (canonicalJson(parsed) !== value) {
356
+ throw new Error("world memory canonical_value must use canonical JSON encoding");
357
+ }
358
+ return value;
359
+ }
360
+ function snapshotMemoryEmbedding(value) {
361
+ if (!Array.isArray(value) || value.length !== WORLD_MEMORY_EMBEDDING_DIM) {
362
+ throw new Error(`world memory embedding must contain ${WORLD_MEMORY_EMBEDDING_DIM} values`);
363
+ }
364
+ const embedding = value.map((entry) => {
365
+ if (!Number.isFinite(entry))
366
+ throw new Error("world memory embedding values must be finite");
367
+ return entry;
368
+ });
369
+ return Object.freeze(embedding);
370
+ }
371
+ function assertMemorySynopsis(value) {
372
+ assertMemoryIdentifier("synopsis", value);
373
+ if ([...value].length > 800)
374
+ throw new Error("world memory synopsis exceeds 800 Unicode scalars");
375
+ return value;
376
+ }
377
+ function assertMemoryRelationKind(value) {
378
+ if (!Object.prototype.hasOwnProperty.call(MEMORY_RELATION_DEFINITIONS, value)) {
379
+ throw new Error(`unknown world memory relation kind ${value}`);
380
+ }
381
+ return value;
382
+ }
383
+ function snapshotMemoryClosureIds(field, values) {
384
+ if (values === undefined)
385
+ return Object.freeze([]);
386
+ return snapshotCanonicalStringIds(field, values, true);
387
+ }
388
+ function assertDepth(depth) {
389
+ if (!Number.isInteger(depth) || depth < 0 || depth > MAX_DEPTH) {
390
+ throw new Error(`depth must be an integer in [0, ${MAX_DEPTH}] got ${depth}`);
391
+ }
392
+ return depth;
393
+ }
394
+ function assertTopK(topK) {
395
+ if (!Number.isInteger(topK) || topK <= 0) {
396
+ throw new Error(`top_k must be a positive integer got ${topK}`);
397
+ }
398
+ return topK;
399
+ }
400
+ function assertTranscriptIdentifier(field, value) {
401
+ if (typeof value !== "string" || value.trim().length === 0) {
402
+ throw new Error(`transcript ${field} must be a non-empty string`);
403
+ }
404
+ return value;
405
+ }
406
+ function assertTranscriptScope(scope) {
407
+ assertTranscriptIdentifier("scenario_id", scope.scenarioId);
408
+ assertTranscriptIdentifier("save_id", scope.saveId);
409
+ assertTranscriptIdentifier("worldline_id", scope.worldlineId);
410
+ return scope;
411
+ }
412
+ function snapshotTranscriptScope(scope) {
413
+ assertTranscriptScope(scope);
414
+ return Object.freeze({
415
+ scenarioId: scope.scenarioId,
416
+ saveId: scope.saveId,
417
+ worldlineId: scope.worldlineId
418
+ });
419
+ }
420
+ const TRANSCRIPT_SHA256_HEX = /^[0-9a-f]{64}$/;
421
+ function snapshotTranscriptJournalIdentity(identity) {
422
+ assertTranscriptIdentifier("journal_idempotency_key", identity.idempotencyKey);
423
+ if (!TRANSCRIPT_SHA256_HEX.test(identity.payloadSha256)) {
424
+ throw new Error("transcript journal_payload_sha256 must be lowercase sha256 hex");
425
+ }
426
+ assertTranscriptIdentifier("journal_phase", identity.phase);
427
+ if (!Number.isSafeInteger(identity.phaseRevision) || identity.phaseRevision <= 0) {
428
+ throw new Error("transcript journal_phase_revision must be a positive safe integer");
429
+ }
430
+ return Object.freeze({
431
+ idempotencyKey: identity.idempotencyKey,
432
+ payloadSha256: identity.payloadSha256,
433
+ phase: identity.phase,
434
+ phaseRevision: identity.phaseRevision
435
+ });
436
+ }
437
+ function transcriptJournalIdentityMatches(left, right) {
438
+ return left.idempotencyKey === right.idempotencyKey
439
+ && left.payloadSha256 === right.payloadSha256
440
+ && left.phase === right.phase
441
+ && left.phaseRevision === right.phaseRevision;
442
+ }
443
+ function assertActiveReachableMessageIds(values, requiredMessageId) {
444
+ if (!Array.isArray(values)) {
445
+ throw new Error("transcript active_reachable_message_ids must be an array");
446
+ }
447
+ const normalized = values.map((value) => assertTranscriptIdentifier("active_reachable_message_id", value));
448
+ const unique = [...new Set(normalized)];
449
+ if (unique.length !== normalized.length) {
450
+ throw new Error("transcript message authority contains duplicate ids");
451
+ }
452
+ if (requiredMessageId !== undefined && !unique.includes(requiredMessageId)) {
453
+ throw new Error("transcript message is not active and reachable");
454
+ }
455
+ return Object.freeze(unique);
456
+ }
457
+ function sameTranscriptAuthority(left, right) {
458
+ if (left.scope.scenarioId !== right.scope.scenarioId
459
+ || left.scope.saveId !== right.scope.saveId
460
+ || left.scope.worldlineId !== right.scope.worldlineId
461
+ || left.operation !== right.operation
462
+ || left.lineageDigest !== right.lineageDigest
463
+ || left.mutationPayloadSha256 !== right.mutationPayloadSha256
464
+ || left.activeReachableMessageIds.length !== right.activeReachableMessageIds.length
465
+ || left.authorizedMutationMessageIds.length !== right.authorizedMutationMessageIds.length
466
+ || left.requiredUpserts.length !== right.requiredUpserts.length
467
+ || !sameTranscriptSuffixSpec(left.suffix, right.suffix)) {
468
+ return false;
469
+ }
470
+ if ((left.journal === null) !== (right.journal === null))
471
+ return false;
472
+ if (left.journal !== null && right.journal !== null && !transcriptJournalIdentityMatches(left.journal, right.journal)) {
473
+ return false;
474
+ }
475
+ return left.activeReachableMessageIds.every((value, index) => value === right.activeReachableMessageIds[index])
476
+ && left.authorizedMutationMessageIds.every((value, index) => value === right.authorizedMutationMessageIds[index])
477
+ && left.requiredUpserts.every((value, index) => {
478
+ const other = right.requiredUpserts[index];
479
+ return other !== undefined
480
+ && value.messageId === other.messageId
481
+ && value.sourceLedgerSeq === other.sourceLedgerSeq;
482
+ });
483
+ }
484
+ function assertTranscriptTimestamp(field, value) {
485
+ if (!Number.isSafeInteger(value) || value < 0) {
486
+ throw new Error(`transcript ${field} must be a non-negative safe integer`);
487
+ }
488
+ return value;
489
+ }
490
+ function assertTranscriptLedgerSeq(value, allowZero = false) {
491
+ if (!Number.isSafeInteger(value) || value < (allowZero ? 0 : 1)) {
492
+ throw new Error(`transcript source_ledger_seq must be a ${allowZero ? "non-negative" : "positive"} safe integer`);
493
+ }
494
+ return value;
495
+ }
496
+ function snapshotTranscriptSuffixSpec(value, allowNull = false) {
497
+ if (value === null) {
498
+ if (allowNull)
499
+ return null;
500
+ throw new Error("transcript suffix mutation is required");
501
+ }
502
+ if (typeof value !== "object") {
503
+ throw new Error("transcript suffix mutation is invalid");
504
+ }
505
+ if (value.mode !== "tombstone" && value.mode !== "delete") {
506
+ throw new Error(`transcript suffix mode invalid: ${String(value.mode)}`);
507
+ }
508
+ return Object.freeze({
509
+ afterSourceLedgerSeq: assertTranscriptLedgerSeq(value.afterSourceLedgerSeq, true),
510
+ mode: value.mode,
511
+ updatedAtMs: assertTranscriptTimestamp("suffix_updated_at_ms", value.updatedAtMs)
512
+ });
513
+ }
514
+ function sameTranscriptSuffixSpec(left, right) {
515
+ if (left === null || right === null)
516
+ return left === right;
517
+ return left.afterSourceLedgerSeq === right.afterSourceLedgerSeq
518
+ && left.mode === right.mode
519
+ && left.updatedAtMs === right.updatedAtMs;
520
+ }
521
+ function assertTranscriptRole(value) {
522
+ if (value !== "user" && value !== "assistant") {
523
+ throw new Error(`transcript role must be user or assistant got ${value}`);
524
+ }
525
+ return value;
526
+ }
527
+ function assertTranscriptStatus(value) {
528
+ if (value !== "streaming"
529
+ && value !== "done"
530
+ && value !== "failed"
531
+ && value !== "aborted"
532
+ && value !== "tombstoned") {
533
+ throw new Error(`transcript status invalid: ${value}`);
534
+ }
535
+ return value;
536
+ }
537
+ function validateTranscriptRecord(record) {
538
+ assertTranscriptScope(record);
539
+ assertTranscriptIdentifier("message_id", record.messageId);
540
+ assertTranscriptIdentifier("turn_id", record.turnId);
541
+ assertTranscriptRole(record.role);
542
+ assertTranscriptStatus(record.status);
543
+ if (typeof record.content !== "string")
544
+ throw new Error("transcript content must be a string");
545
+ assertTranscriptTimestamp("created_at_ms", record.createdAtMs);
546
+ assertTranscriptTimestamp("updated_at_ms", record.updatedAtMs);
547
+ if (record.updatedAtMs < record.createdAtMs) {
548
+ throw new Error("transcript updated_at_ms precedes created_at_ms");
549
+ }
550
+ assertTranscriptLedgerSeq(record.sourceLedgerSeq);
551
+ if (record.status === "done" && record.content.length === 0) {
552
+ throw new Error("transcript done content must not be empty");
553
+ }
554
+ return record;
555
+ }
556
+ function snapshotTranscriptMutationRecords(scope, messages) {
557
+ if (!Array.isArray(messages)) {
558
+ throw new Error("transcript mutation messages must be an array");
559
+ }
560
+ return Object.freeze(messages.map((message) => Object.freeze(validateTranscriptRecord({
561
+ ...scope,
562
+ messageId: message.messageId,
563
+ turnId: message.turnId,
564
+ role: message.role,
565
+ content: message.content,
566
+ status: message.status,
567
+ createdAtMs: message.createdAtMs,
568
+ updatedAtMs: message.updatedAtMs,
569
+ sourceLedgerSeq: message.sourceLedgerSeq
570
+ }))));
571
+ }
572
+ async function transcriptMutationPayloadSha256FromRecords(scope, records, suffix) {
573
+ const payloadImage = JSON.stringify([
574
+ scope.scenarioId,
575
+ scope.saveId,
576
+ scope.worldlineId,
577
+ records.map((record) => [
578
+ record.messageId,
579
+ record.turnId,
580
+ record.role,
581
+ record.content,
582
+ record.status,
583
+ record.createdAtMs,
584
+ record.updatedAtMs,
585
+ record.sourceLedgerSeq
586
+ ]),
587
+ suffix === null
588
+ ? null
589
+ : [suffix.afterSourceLedgerSeq, suffix.mode, suffix.updatedAtMs]
590
+ ]);
591
+ return sha256HexUtf8(`ladybug-transcript-mutation-payload-v1\u0000${payloadImage}`);
592
+ }
593
+ /**
594
+ * Produces the host journal's canonical digest for one ordered transcript
595
+ * mutation. The bridge recomputes the same digest before touching Ladybug.
596
+ */
597
+ export async function computeTranscriptMutationPayloadSha256(input) {
598
+ const scope = snapshotTranscriptScope(input.scope);
599
+ const records = snapshotTranscriptMutationRecords(scope, input.messages);
600
+ const suffix = input.suffix === undefined
601
+ ? null
602
+ : snapshotTranscriptSuffixSpec(input.suffix);
603
+ return transcriptMutationPayloadSha256FromRecords(scope, records, suffix);
604
+ }
605
+ function transcriptIdentityMatches(left, right) {
606
+ return left.scenarioId === right.scenarioId
607
+ && left.saveId === right.saveId
608
+ && left.worldlineId === right.worldlineId
609
+ && left.messageId === right.messageId
610
+ && left.turnId === right.turnId
611
+ && left.role === right.role
612
+ && left.createdAtMs === right.createdAtMs
613
+ && left.sourceLedgerSeq === right.sourceLedgerSeq;
614
+ }
615
+ function transcriptRecordMatches(left, right) {
616
+ return transcriptIdentityMatches(left, right)
617
+ && left.content === right.content
618
+ && left.status === right.status
619
+ && left.updatedAtMs === right.updatedAtMs;
620
+ }
621
+ function assertTranscriptRecordSetIdentities(records) {
622
+ const messageIds = new Set();
623
+ const ledgerSeqs = new Set();
624
+ for (const record of records) {
625
+ if (messageIds.has(record.messageId)) {
626
+ throw new Error("transcript scope contains duplicate message_id");
627
+ }
628
+ if (ledgerSeqs.has(record.sourceLedgerSeq)) {
629
+ throw new Error("transcript scope contains duplicate source_ledger_seq");
630
+ }
631
+ messageIds.add(record.messageId);
632
+ ledgerSeqs.add(record.sourceLedgerSeq);
633
+ }
634
+ }
635
+ function buildIndexedSearchText(content) {
636
+ const normalized = content.normalize("NFKC").toLocaleLowerCase("und");
637
+ const tokens = new Set();
638
+ const segments = normalized.match(/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+|[\p{Letter}\p{Number}]+/gu) ?? [];
639
+ for (const segment of segments) {
640
+ const chars = [...segment];
641
+ const isCjk = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(segment);
642
+ if (!isCjk) {
643
+ tokens.add(segment);
644
+ continue;
645
+ }
646
+ for (let width = 1; width <= Math.min(3, chars.length); width += 1) {
647
+ for (let offset = 0; offset + width <= chars.length; offset += 1) {
648
+ tokens.add(chars.slice(offset, offset + width).join(""));
649
+ }
650
+ }
651
+ }
652
+ return [...tokens].join(" ");
653
+ }
654
+ async function collectRows(result) {
655
+ if (!result.isSuccess()) {
656
+ const message = await result.getErrorMessage();
657
+ await result.close();
658
+ throw new Error(message);
659
+ }
660
+ const rows = await result.getAllObjects();
661
+ await result.close();
662
+ return rows;
663
+ }
664
+ export class LadybugBridge {
665
+ dim;
666
+ variant;
667
+ db = null;
668
+ conn = null;
669
+ ready = false;
670
+ vectorIndexBuilt = false;
671
+ ftsIndexBuilt = false;
672
+ vectorIndexedEntityCount = 0;
673
+ ftsIndexedEntityCount = 0;
674
+ memoryEpisodeVectorIndexBuilt = false;
675
+ memoryEpisodeFtsIndexBuilt = false;
676
+ memoryDerivedVectorIndexBuilt = false;
677
+ memoryDerivedFtsIndexBuilt = false;
678
+ transcriptFtsBuilt = false;
679
+ transcriptQueue = Promise.resolve();
680
+ transcriptOperationReservations = 0;
681
+ transcriptOperationActive = false;
682
+ graphRollbackInProgress = false;
683
+ initializing = false;
684
+ closing = false;
685
+ transcriptReachabilityResolver;
686
+ worldMemoryHostAuthority;
687
+ // Trusted WorldMemory operations share one queue so capture/restore never
688
+ // expose a mixed view across mutation slots.
689
+ worldMemoryQueue = Promise.resolve();
690
+ worldMemoryOperationReservations = 0;
691
+ worldMemoryRevision = 0;
692
+ _writeSeq = 0;
693
+ tx = null;
694
+ assertWorldMemoryHostAuthority(authority) {
695
+ if (this.worldMemoryHostAuthority === null
696
+ || authority !== this.worldMemoryHostAuthority
697
+ || !worldMemoryHostAuthorities.has(authority)) {
698
+ throw new Error("world memory trusted host authority mismatch");
699
+ }
700
+ }
701
+ createWorldMemoryHostAdapter(authority) {
702
+ this.assertWorldMemoryHostAuthority(authority);
703
+ return Object.freeze({
704
+ upsertMemoryEpisode: (row) => this.withWorldMemoryHostLock(async () => { await this.upsertMemoryEpisode(row); this.advanceWorldMemoryRevision(); }),
705
+ upsertMemoryClaimRoot: (row) => this.withWorldMemoryHostLock(async () => { await this.upsertMemoryClaimRoot(row); this.advanceWorldMemoryRevision(); }),
706
+ upsertMemoryClaim: (row) => this.withWorldMemoryHostLock(async () => { await this.upsertMemoryClaim(row); this.advanceWorldMemoryRevision(); }),
707
+ upsertMemoryDerived: (row) => this.withWorldMemoryHostLock(async () => { await this.upsertMemoryDerived(row); this.advanceWorldMemoryRevision(); }),
708
+ upsertMemoryRelation: (row) => this.withWorldMemoryHostLock(async () => { await this.upsertMemoryRelation(row); this.advanceWorldMemoryRevision(); }),
709
+ getClaimRoot: (id) => this.withWorldMemoryHostLock(() => this.getClaimRoot(id)),
710
+ getClaimsByRoot: (rootId) => this.withWorldMemoryHostLock(() => this.getClaimsByRoot(rootId)),
711
+ resolveClaimsForEntities: (entityIds) => this.withWorldMemoryHostLock(() => this.resolveClaimsForEntities(entityIds)),
712
+ vectorSearchEpisodes: (query, topK, config) => this.withWorldMemoryHostLock(() => this.vectorSearchEpisodes(query, topK, config)),
713
+ fullTextSearchEpisodes: (query, topK, config) => this.withWorldMemoryHostLock(() => this.fullTextSearchEpisodes(query, topK, config)),
714
+ vectorSearchDerived: (query, topK, config) => this.withWorldMemoryHostLock(() => this.vectorSearchDerived(query, topK, config)),
715
+ fullTextSearchDerived: (query, topK, config) => this.withWorldMemoryHostLock(() => this.fullTextSearchDerived(query, topK, config)),
716
+ deleteMemoryClosure: (closure) => this.withWorldMemoryHostLock(async () => {
717
+ const result = await this.deleteMemoryClosure(closure);
718
+ this.advanceWorldMemoryRevision();
719
+ return result;
720
+ }),
721
+ snapshotWorldMemory: () => this.withWorldMemoryHostLock(() => this.snapshotWorldMemory()),
722
+ captureWorldMemoryView: () => this.withWorldMemoryHostLock(() => this.captureWorldMemoryView()),
723
+ worldMemoryStateDigest: () => this.withWorldMemoryHostLock(async () => (this.worldMemorySnapshotSha256(await this.snapshotWorldMemory()))),
724
+ stateIdentity: () => this.withWorldMemoryHostLock(() => this.captureStateIdentity()),
725
+ restoreWorldMemorySnapshot: (snapshot) => this.withWorldMemoryHostLock(async () => { await this.restoreWorldMemorySnapshotFromHost(snapshot); this.advanceWorldMemoryRevision(); })
726
+ });
727
+ }
728
+ createWasmRuntimeAdapter(authority) {
729
+ this.assertWorldMemoryHostAuthority(authority);
730
+ return Object.freeze({
731
+ allEntities: this.allEntities.bind(this),
732
+ allRelations: this.allRelations.bind(this),
733
+ attrHistoryCheck: this.attrHistoryCheck.bind(this),
734
+ beginTransaction: () => this.withWorldMemoryHostLock(async () => { this.beginTransaction(); }),
735
+ bulkUpsertEntities: this.bulkUpsertEntities.bind(this),
736
+ bulkUpsertRelations: this.bulkUpsertRelations.bind(this),
737
+ close: this.close.bind(this),
738
+ commitTransaction: () => this.withWorldMemoryHostLock(async () => { this.commitTransaction(); }),
739
+ createFtsIndex: this.createFtsIndex.bind(this),
740
+ createVectorIndex: this.createVectorIndex.bind(this),
741
+ deleteEntity: this.deleteEntity.bind(this),
742
+ deleteRelation: this.deleteRelation.bind(this),
743
+ diagnostics: this.diagnostics.bind(this),
744
+ dropFtsIndex: this.dropFtsIndex.bind(this),
745
+ dropVectorIndex: this.dropVectorIndex.bind(this),
746
+ entityCount: this.entityCount.bind(this),
747
+ findByKind: this.findByKind.bind(this),
748
+ fullTextSearch: this.fullTextSearch.bind(this),
749
+ getById: this.getById.bind(this),
750
+ hasEmbedding: this.hasEmbedding.bind(this),
751
+ hasFtsIndex: this.hasFtsIndex.bind(this),
752
+ hasVectorIndex: this.hasVectorIndex.bind(this),
753
+ init: this.init.bind(this),
754
+ latchClear: this.latchClear.bind(this),
755
+ latchCommitBatch: this.latchCommitBatch.bind(this),
756
+ latchLoad: this.latchLoad.bind(this),
757
+ latchRollback: this.latchRollback.bind(this),
758
+ neighbors: this.neighbors.bind(this),
759
+ pruneAttrHistory: this.pruneAttrHistory.bind(this),
760
+ query: this.queryForWasmRuntime.bind(this),
761
+ queryAttrHistory: this.queryAttrHistory.bind(this),
762
+ recallAndExpand: this.recallAndExpand.bind(this),
763
+ recallCandidateRecord: this.recallCandidateRecord.bind(this),
764
+ relationCount: this.relationCount.bind(this),
765
+ rollbackAfterMessage: this.rollbackAfterMessage.bind(this),
766
+ rollbackAttrHistoryAfterMessage: this.rollbackAttrHistoryAfterMessage.bind(this),
767
+ rollbackTransaction: () => this.withWorldMemoryHostLock(() => this.rollbackTransaction()),
768
+ worldMemoryStateDigest: () => this.withWorldMemoryHostLock(async () => (this.worldMemorySnapshotSha256(await this.snapshotWorldMemory()))),
769
+ restoreWorldMemorySnapshotInTransaction: (snapshot) => this.withWorldMemoryHostLock(async () => {
770
+ await this.restoreWorldMemorySnapshotInTransaction(snapshot);
771
+ this.advanceWorldMemoryRevision();
772
+ }),
773
+ stateDigest: this.stateDigest.bind(this),
774
+ upsertAttrHistory: this.upsertAttrHistory.bind(this),
775
+ upsertEntity: this.upsertEntity.bind(this),
776
+ upsertRelation: this.upsertRelation.bind(this),
777
+ vectorSearch: this.vectorSearch.bind(this)
778
+ });
779
+ }
780
+ nextSeq() {
781
+ this._writeSeq += 1;
782
+ return this._writeSeq;
783
+ }
784
+ async withWorldMemoryHostLock(fn) {
785
+ if (this.closing)
786
+ throw new Error("world memory host operation cannot start while ladybug bridge is closing");
787
+ this.worldMemoryOperationReservations += 1;
788
+ const previous = this.worldMemoryQueue;
789
+ let release;
790
+ this.worldMemoryQueue = new Promise((resolve) => { release = resolve; });
791
+ try {
792
+ await previous;
793
+ return await fn();
794
+ }
795
+ finally {
796
+ this.worldMemoryOperationReservations -= 1;
797
+ release();
798
+ }
799
+ }
800
+ advanceWorldMemoryRevision() {
801
+ if (this.worldMemoryRevision >= Number.MAX_SAFE_INTEGER)
802
+ throw new Error("world memory revision exhausted");
803
+ this.worldMemoryRevision += 1;
804
+ }
805
+ async captureWorldMemoryView() {
806
+ if (this.tx !== null || this.graphRollbackInProgress)
807
+ throw new Error("world memory view capture requires no active transaction");
808
+ const snapshot = await this.snapshotWorldMemory();
809
+ const snapshotSha256 = await this.worldMemorySnapshotSha256(snapshot);
810
+ return Object.freeze({ snapshot, identity: Object.freeze({ snapshotSha256, revision: this.worldMemoryRevision }) });
811
+ }
812
+ async worldMemorySnapshotSha256(snapshot) {
813
+ return await sha256HexUtf8(`ladybug-world-memory-view-v1\u0000${canonicalJson(snapshot)}`);
814
+ }
815
+ async captureStateIdentity() {
816
+ for (let attempt = 0; attempt < 3; attempt += 1) {
817
+ const writeSequence = this._writeSeq;
818
+ const physicalStateSha256 = await this.stateDigest();
819
+ if (this._writeSeq === writeSequence)
820
+ return Object.freeze({ physicalStateSha256, writeSequence });
821
+ }
822
+ throw new Error("world memory state changed while capturing identity");
823
+ }
824
+ async withTranscriptLock(fn) {
825
+ if (this.closing) {
826
+ throw new Error("transcript operation cannot start while ladybug bridge is closing");
827
+ }
828
+ this.transcriptOperationReservations += 1;
829
+ const previous = this.transcriptQueue;
830
+ let release;
831
+ this.transcriptQueue = new Promise((resolve) => {
832
+ release = resolve;
833
+ });
834
+ try {
835
+ await previous;
836
+ return await fn();
837
+ }
838
+ finally {
839
+ this.transcriptOperationReservations -= 1;
840
+ release();
841
+ }
842
+ }
843
+ async withTranscriptOperationIsolation(fn) {
844
+ if (this.graphRollbackInProgress) {
845
+ throw new Error("transcript operation cannot overlap graph rollback replay");
846
+ }
847
+ if (this.tx !== null) {
848
+ throw new Error("transcript operation cannot overlap a graph transaction");
849
+ }
850
+ if (this.transcriptOperationActive) {
851
+ throw new Error("transcript operation isolation is already active");
852
+ }
853
+ this.transcriptOperationActive = true;
854
+ try {
855
+ return await fn();
856
+ }
857
+ finally {
858
+ this.transcriptOperationActive = false;
859
+ }
860
+ }
861
+ async resolveTranscriptAuthority(scope, operation, journal, requiredMessageIds = []) {
862
+ const resolver = this.transcriptReachabilityResolver;
863
+ if (resolver === null) {
864
+ throw new Error("transcript reachability authority is unavailable");
865
+ }
866
+ const authorizedScope = snapshotTranscriptScope(scope);
867
+ const authorizedJournal = journal === null ? null : snapshotTranscriptJournalIdentity(journal);
868
+ if (operation === "mutation" && authorizedJournal === null) {
869
+ throw new Error("transcript mutation journal identity is required");
870
+ }
871
+ if (operation === "recall" && authorizedJournal !== null) {
872
+ throw new Error("transcript recall cannot carry mutation journal authority");
873
+ }
874
+ const snapshot = await resolver(Object.freeze({
875
+ scope: authorizedScope,
876
+ operation,
877
+ journal: authorizedJournal
878
+ }));
879
+ if (typeof snapshot !== "object" || snapshot === null) {
880
+ throw new Error("transcript reachability authority returned no snapshot");
881
+ }
882
+ assertTranscriptScope(snapshot);
883
+ if (snapshot.scenarioId !== authorizedScope.scenarioId
884
+ || snapshot.saveId !== authorizedScope.saveId
885
+ || snapshot.worldlineId !== authorizedScope.worldlineId) {
886
+ throw new Error("transcript reachability authority scope mismatch");
887
+ }
888
+ const lineageDigest = assertTranscriptIdentifier("lineage_digest", snapshot.lineageDigest);
889
+ const activeReachableMessageIds = assertActiveReachableMessageIds(snapshot.activeReachableMessageIds);
890
+ if (snapshot.pendingJournal !== null && (typeof snapshot.pendingJournal !== "object"
891
+ || snapshot.pendingJournal === undefined)) {
892
+ throw new Error("transcript pending journal authority is invalid");
893
+ }
894
+ if (operation === "recall") {
895
+ if (snapshot.pendingJournal !== null) {
896
+ throw new Error("transcript recall blocked by pending journal");
897
+ }
898
+ if (requiredMessageIds.length > 0) {
899
+ throw new Error("transcript recall cannot request mutation message authority");
900
+ }
901
+ return Object.freeze({
902
+ scope: authorizedScope,
903
+ operation,
904
+ journal: null,
905
+ lineageDigest,
906
+ mutationPayloadSha256: null,
907
+ activeReachableMessageIds,
908
+ authorizedMutationMessageIds: Object.freeze([]),
909
+ requiredUpserts: Object.freeze([]),
910
+ suffix: null
911
+ });
912
+ }
913
+ const pending = snapshot.pendingJournal;
914
+ if (pending === null || authorizedJournal === null) {
915
+ throw new Error("transcript mutation is not backed by a pending journal");
916
+ }
917
+ const pendingIdentity = snapshotTranscriptJournalIdentity(pending.identity);
918
+ if (!transcriptJournalIdentityMatches(pendingIdentity, authorizedJournal)) {
919
+ throw new Error("transcript mutation journal authority mismatch");
920
+ }
921
+ if (!TRANSCRIPT_SHA256_HEX.test(pending.mutationPayloadSha256)) {
922
+ throw new Error("transcript mutation_payload_sha256 must be lowercase sha256 hex");
923
+ }
924
+ const authorizedMutationMessageIds = assertActiveReachableMessageIds(pending.authorizedMutationMessageIds);
925
+ const mutationSet = new Set(authorizedMutationMessageIds);
926
+ for (const messageId of activeReachableMessageIds) {
927
+ if (!mutationSet.has(messageId)) {
928
+ throw new Error("transcript pending journal omits an active reachable message");
929
+ }
930
+ }
931
+ for (const messageId of requiredMessageIds) {
932
+ assertTranscriptIdentifier("required_mutation_message_id", messageId);
933
+ if (!mutationSet.has(messageId)) {
934
+ throw new Error("transcript message is not authorized by the pending journal");
935
+ }
936
+ }
937
+ if (!Array.isArray(pending.requiredUpserts)) {
938
+ throw new Error("transcript pending journal required_upserts must be an array");
939
+ }
940
+ const requiredUpserts = Object.freeze(pending.requiredUpserts.map((entry) => {
941
+ if (typeof entry !== "object" || entry === null) {
942
+ throw new Error("transcript pending journal upsert identity is invalid");
943
+ }
944
+ const messageId = assertTranscriptIdentifier("required_upsert_message_id", entry.messageId);
945
+ if (!mutationSet.has(messageId)) {
946
+ throw new Error("transcript required upsert is not mutation-authorized");
947
+ }
948
+ return Object.freeze({
949
+ messageId,
950
+ sourceLedgerSeq: assertTranscriptLedgerSeq(entry.sourceLedgerSeq)
951
+ });
952
+ }));
953
+ const requiredUpsertIds = requiredUpserts.map((entry) => entry.messageId);
954
+ if (new Set(requiredUpsertIds).size !== requiredUpsertIds.length) {
955
+ throw new Error("transcript pending journal contains duplicate required upserts");
956
+ }
957
+ const requiredUpsertSeqs = requiredUpserts.map((entry) => entry.sourceLedgerSeq);
958
+ if (new Set(requiredUpsertSeqs).size !== requiredUpsertSeqs.length) {
959
+ throw new Error("transcript pending journal contains duplicate required upsert ledger identities");
960
+ }
961
+ const suffix = snapshotTranscriptSuffixSpec(pending.suffix, true);
962
+ return Object.freeze({
963
+ scope: authorizedScope,
964
+ operation,
965
+ journal: pendingIdentity,
966
+ lineageDigest,
967
+ mutationPayloadSha256: pending.mutationPayloadSha256,
968
+ activeReachableMessageIds,
969
+ authorizedMutationMessageIds,
970
+ requiredUpserts,
971
+ suffix
972
+ });
973
+ }
974
+ async assertTranscriptReachabilityCoverage(scope, reachable, allowedMissingMessageIds = new Set(), records) {
975
+ const scoped = records ?? await this.listTranscriptScope(scope);
976
+ const present = new Map(scoped.map((record) => [record.messageId, record]));
977
+ const missing = reachable.filter((messageId) => !allowedMissingMessageIds.has(messageId) && !present.has(messageId));
978
+ if (missing.length > 0) {
979
+ throw new Error(`transcript active lineage is incomplete: ${missing.join(",")}`);
980
+ }
981
+ const tombstoned = reachable.filter((messageId) => !allowedMissingMessageIds.has(messageId) && present.get(messageId)?.status === "tombstoned");
982
+ if (tombstoned.length > 0) {
983
+ throw new Error(`transcript active lineage is tombstoned: ${tombstoned.join(",")}`);
984
+ }
985
+ }
986
+ isInTransaction() {
987
+ return this.tx !== null || this.graphRollbackInProgress;
988
+ }
989
+ // Nested begin joins the outer tx (depth counter). Only the outermost commit
990
+ // clears the log; inner commit just decrements. Rollback at any depth unwinds
991
+ // the entire outer tx (2PC-style flat semantics, consistent with SQLite's
992
+ // BEGIN/COMMIT without savepoints).
993
+ beginTransaction() {
994
+ if (this.closing) {
995
+ throw new Error("graph transaction cannot start while ladybug bridge is closing");
996
+ }
997
+ if (this.graphRollbackInProgress) {
998
+ throw new Error("graph transaction cannot start during rollback replay");
999
+ }
1000
+ if (this.transcriptOperationReservations > 0 || this.transcriptOperationActive) {
1001
+ throw new Error("graph transaction cannot overlap a transcript operation");
1002
+ }
1003
+ if (this.tx !== null) {
1004
+ this.tx = { active: true, ops: this.tx.ops, depth: this.tx.depth + 1 };
1005
+ return;
1006
+ }
1007
+ this.tx = { active: true, ops: [], depth: 1 };
1008
+ }
1009
+ commitTransaction() {
1010
+ if (this.graphRollbackInProgress) {
1011
+ throw new Error("graph transaction commit cannot overlap rollback replay");
1012
+ }
1013
+ if (this.transcriptOperationReservations > 0 || this.transcriptOperationActive) {
1014
+ throw new Error("graph transaction commit cannot overlap a transcript operation");
1015
+ }
1016
+ if (this.tx === null)
1017
+ throw new Error("no active transaction to commit");
1018
+ if (this.tx.depth > 1) {
1019
+ this.tx = { active: true, ops: this.tx.ops, depth: this.tx.depth - 1 };
1020
+ return;
1021
+ }
1022
+ this.tx = null;
1023
+ }
1024
+ async rollbackTransaction() {
1025
+ if (this.graphRollbackInProgress) {
1026
+ throw new Error("graph transaction rollback replay is already in progress");
1027
+ }
1028
+ if (this.transcriptOperationReservations > 0 || this.transcriptOperationActive) {
1029
+ throw new Error("graph transaction rollback cannot overlap a transcript operation");
1030
+ }
1031
+ if (this.tx === null)
1032
+ throw new Error("no active transaction to rollback");
1033
+ const original = this.tx;
1034
+ const ops = original.ops.slice().reverse();
1035
+ this.tx = null; // detach so undo replay does not re-record itself
1036
+ this.graphRollbackInProgress = true;
1037
+ try {
1038
+ for (const op of ops) {
1039
+ await this.applyUndoOp(op);
1040
+ }
1041
+ }
1042
+ catch (error) {
1043
+ // Every inverse is idempotent (MERGE/SET or exact DELETE). Restoring the
1044
+ // original log makes a transient bridge failure safely retryable instead
1045
+ // of stranding Rust with an active batch and no compensating state.
1046
+ this.tx = original;
1047
+ throw error;
1048
+ }
1049
+ finally {
1050
+ this.graphRollbackInProgress = false;
1051
+ }
1052
+ this.advanceWorldMemoryRevision();
1053
+ }
1054
+ async withTransaction(fn) {
1055
+ this.beginTransaction();
1056
+ try {
1057
+ const out = await fn();
1058
+ this.commitTransaction();
1059
+ return out;
1060
+ }
1061
+ catch (e) {
1062
+ await this.rollbackTransaction();
1063
+ throw e;
1064
+ }
1065
+ }
1066
+ async applyUndoOp(op) {
1067
+ if (op.kind === "world-memory-restore") {
1068
+ await this.restoreWorldMemorySnapshot(op.snapshot);
1069
+ return;
1070
+ }
1071
+ if (op.kind === "transcript-restore-scope") {
1072
+ await this.restoreTranscriptScope(op.scope, op.records, true);
1073
+ return;
1074
+ }
1075
+ if (op.kind === "attr-history-restore") {
1076
+ await this.runDuringRollback(`MATCH (a:${ATTR_HISTORY_TABLE} {entityId: $entityId, attrPath: $attrPath}) DETACH DELETE a;`, { entityId: op.entityId, attrPath: op.attrPath });
1077
+ await this.restoreAttrHistoryRowsDuringRollback(op.rows);
1078
+ return;
1079
+ }
1080
+ if (op.kind === "attr-history-restore-all") {
1081
+ await this.queryDuringRollback(`MATCH (a:${ATTR_HISTORY_TABLE}) DETACH DELETE a;`);
1082
+ await this.restoreAttrHistoryRowsDuringRollback(op.rows);
1083
+ return;
1084
+ }
1085
+ if (op.kind === "latch-restore-scenario") {
1086
+ await this.runDuringRollback(`MATCH (l:${LATCH_TABLE} {scenarioId: $scenarioId}) DETACH DELETE l;`, { scenarioId: op.scenarioId });
1087
+ await this.restoreLatchRowsDuringRollback(op.scenarioId, op.rows);
1088
+ return;
1089
+ }
1090
+ if (op.kind === "entity-delete") {
1091
+ await this.runDuringRollback(`MATCH (e:${ENTITY_TABLE} {id: $id}) DETACH DELETE e;`, { id: op.id });
1092
+ return;
1093
+ }
1094
+ if (op.kind === "entity-lexical-projection-delete") {
1095
+ await this.runDuringRollback(`MATCH (p:${ENTITY_LEXICAL_PROJECTION_TABLE} {entityId: $entityId}) DETACH DELETE p;`, { entityId: op.entityId });
1096
+ return;
1097
+ }
1098
+ if (op.kind === "entity-lexical-projection-restore") {
1099
+ await this.runDuringRollback(`MERGE (p:${ENTITY_LEXICAL_PROJECTION_TABLE} {entityId: $entityId}) ` +
1100
+ "SET p.lexicalText = $lexicalText, p.projectionJson = $projectionJson, " +
1101
+ "p.analyzerVersion = $analyzerVersion, p.lexicalStatus = $lexicalStatus, " +
1102
+ "p.relationalStatus = $relationalStatus, p.writeSeq = $writeSeq;", {
1103
+ entityId: op.entityId,
1104
+ lexicalText: op.lexicalText,
1105
+ projectionJson: op.projectionJson,
1106
+ analyzerVersion: op.analyzerVersion,
1107
+ lexicalStatus: op.lexicalStatus,
1108
+ relationalStatus: op.relationalStatus,
1109
+ writeSeq: op.writeSeq
1110
+ });
1111
+ return;
1112
+ }
1113
+ if (op.kind === "entity-restore") {
1114
+ if (op.embedding && !this.vectorIndexBuilt) {
1115
+ await this.runDuringRollback(`MERGE (e:${ENTITY_TABLE} {id: $id}) ` +
1116
+ "SET e.kind = $kind, e.props = $props, e.embedding = $embedding, " +
1117
+ "e.messageId = $messageId, e.turnId = $turnId, e.writeSeq = $writeSeq;", {
1118
+ id: op.id,
1119
+ kind: op.entityKind,
1120
+ props: op.props,
1121
+ embedding: [...op.embedding],
1122
+ messageId: op.messageId,
1123
+ turnId: op.turnId,
1124
+ writeSeq: op.writeSeq
1125
+ });
1126
+ return;
1127
+ }
1128
+ await this.runDuringRollback(`MERGE (e:${ENTITY_TABLE} {id: $id}) ` +
1129
+ "SET e.kind = $kind, e.props = $props, e.embedding = NULL, " +
1130
+ "e.messageId = $messageId, e.turnId = $turnId, e.writeSeq = $writeSeq;", {
1131
+ id: op.id,
1132
+ kind: op.entityKind,
1133
+ props: op.props,
1134
+ messageId: op.messageId,
1135
+ turnId: op.turnId,
1136
+ writeSeq: op.writeSeq
1137
+ });
1138
+ return;
1139
+ }
1140
+ if (op.kind === "relation-delete") {
1141
+ await this.runDuringRollback(`MATCH (s:${ENTITY_TABLE} {id: $src})-[r:${RELATION_TABLE} {kind: $kind}]->` +
1142
+ `(d:${ENTITY_TABLE} {id: $dst}) DELETE r;`, { src: op.src, dst: op.dst, kind: op.relKind });
1143
+ return;
1144
+ }
1145
+ // relation-restore
1146
+ await this.runDuringRollback(`MATCH (s:${ENTITY_TABLE} {id: $src}), (d:${ENTITY_TABLE} {id: $dst}) ` +
1147
+ `MERGE (s)-[r:${RELATION_TABLE} {kind: $kind}]->(d) ` +
1148
+ "SET r.props = $props, r.messageId = $messageId, " +
1149
+ "r.turnId = $turnId, r.writeSeq = $writeSeq;", {
1150
+ src: op.src,
1151
+ dst: op.dst,
1152
+ kind: op.relKind,
1153
+ props: op.props,
1154
+ messageId: op.messageId,
1155
+ turnId: op.turnId,
1156
+ writeSeq: op.writeSeq
1157
+ });
1158
+ }
1159
+ async restoreAttrHistoryRowsDuringRollback(rows) {
1160
+ if (rows.length === 0)
1161
+ return;
1162
+ const payload = rows.map((row) => ({
1163
+ attrHistoryId: row.attrHistoryId,
1164
+ entityId: row.entityId,
1165
+ attrPath: row.attrPath,
1166
+ oldValueText: row.oldValueText,
1167
+ newValueText: row.newValueText,
1168
+ replacedAtMessageId: row.replacedAtMessageId,
1169
+ replacedAtTurnId: row.replacedAtTurnId,
1170
+ replacedAtWriteSeq: row.replacedAtWriteSeq,
1171
+ superseded: row.superseded
1172
+ }));
1173
+ await this.runDuringRollback(`UNWIND $rows AS row MERGE (a:${ATTR_HISTORY_TABLE} {attrHistoryId: row.attrHistoryId}) ` +
1174
+ "SET a.entityId = row.entityId, a.attrPath = row.attrPath, " +
1175
+ "a.oldValueText = row.oldValueText, a.newValueText = row.newValueText, " +
1176
+ "a.replacedAtMessageId = row.replacedAtMessageId, a.replacedAtTurnId = row.replacedAtTurnId, " +
1177
+ "a.replacedAtWriteSeq = row.replacedAtWriteSeq, a.superseded = row.superseded;", { rows: payload });
1178
+ }
1179
+ async restoreLatchRowsDuringRollback(scenarioId, rows) {
1180
+ if (rows.length === 0)
1181
+ return;
1182
+ const payload = rows.map((row) => ({
1183
+ pk: this.latchPk(scenarioId, row.leaf_id),
1184
+ scenarioId,
1185
+ leafId: row.leaf_id,
1186
+ state: row.state,
1187
+ firstFiredAtMs: row.first_fired_at_ms,
1188
+ lastEvaluatedAtMs: row.last_evaluated_at_ms,
1189
+ fireCount: row.fire_count,
1190
+ source: row.source,
1191
+ confidence: row.confidence
1192
+ }));
1193
+ await this.runDuringRollback(`UNWIND $rows AS row MERGE (l:${LATCH_TABLE} {pk: row.pk}) ` +
1194
+ "SET l.scenarioId = row.scenarioId, l.leafId = row.leafId, l.state = row.state, " +
1195
+ "l.firstFiredAtMs = row.firstFiredAtMs, l.lastEvaluatedAtMs = row.lastEvaluatedAtMs, " +
1196
+ "l.fireCount = row.fireCount, l.source = row.source, l.confidence = row.confidence;", { rows: payload });
1197
+ }
1198
+ async snapshotEntity(id) {
1199
+ const rows = await this.runInternal(`MATCH (e:${ENTITY_TABLE} {id: $id}) ` +
1200
+ "RETURN e.kind AS kind, e.props AS props, e.embedding AS embedding, " +
1201
+ "e.messageId AS messageId, e.turnId AS turnId, e.writeSeq AS writeSeq;", { id });
1202
+ if (rows.length === 0)
1203
+ return null;
1204
+ const row = rows[0];
1205
+ const embRaw = row.embedding;
1206
+ let embedding = null;
1207
+ if (Array.isArray(embRaw)) {
1208
+ embedding = embRaw.map((n) => (typeof n === "number" ? n : Number(n)));
1209
+ }
1210
+ return {
1211
+ kind: "entity-restore",
1212
+ id,
1213
+ entityKind: asString(row.kind),
1214
+ props: asString(row.props),
1215
+ embedding,
1216
+ messageId: asString(row.messageId),
1217
+ turnId: asString(row.turnId),
1218
+ writeSeq: asNumber(row.writeSeq)
1219
+ };
1220
+ }
1221
+ async snapshotEntityAnalyzerProjection(entityId) {
1222
+ const rows = await this.runInternal(`MATCH (p:${ENTITY_LEXICAL_PROJECTION_TABLE} {entityId: $entityId}) ` +
1223
+ "RETURN p.lexicalText AS lexicalText, p.projectionJson AS projectionJson, " +
1224
+ "p.analyzerVersion AS analyzerVersion, p.lexicalStatus AS lexicalStatus, " +
1225
+ "p.relationalStatus AS relationalStatus, p.writeSeq AS writeSeq;", { entityId });
1226
+ if (rows.length === 0)
1227
+ return null;
1228
+ const row = rows[0];
1229
+ return {
1230
+ kind: "entity-lexical-projection-restore",
1231
+ entityId,
1232
+ lexicalText: asString(row.lexicalText),
1233
+ projectionJson: asString(row.projectionJson),
1234
+ analyzerVersion: asString(row.analyzerVersion),
1235
+ lexicalStatus: asString(row.lexicalStatus),
1236
+ relationalStatus: asString(row.relationalStatus),
1237
+ writeSeq: asNumber(row.writeSeq)
1238
+ };
1239
+ }
1240
+ async snapshotRelation(src, dst, kind) {
1241
+ const rows = await this.runInternal(`MATCH (s:${ENTITY_TABLE} {id: $src})-[r:${RELATION_TABLE} {kind: $kind}]->` +
1242
+ `(d:${ENTITY_TABLE} {id: $dst}) ` +
1243
+ "RETURN r.props AS props, r.messageId AS messageId, " +
1244
+ "r.turnId AS turnId, r.writeSeq AS writeSeq;", { src, dst, kind });
1245
+ if (rows.length === 0)
1246
+ return null;
1247
+ const row = rows[0];
1248
+ return {
1249
+ kind: "relation-restore",
1250
+ src,
1251
+ dst,
1252
+ relKind: kind,
1253
+ props: asString(row.props),
1254
+ messageId: asString(row.messageId),
1255
+ turnId: asString(row.turnId),
1256
+ writeSeq: asNumber(row.writeSeq)
1257
+ };
1258
+ }
1259
+ async recordEntityUndoBeforeWrite(id) {
1260
+ if (this.tx === null)
1261
+ return;
1262
+ const priorProjection = await this.snapshotEntityAnalyzerProjection(id);
1263
+ this.tx.ops.push(priorProjection ?? { kind: "entity-lexical-projection-delete", entityId: id });
1264
+ const prior = await this.snapshotEntity(id);
1265
+ if (prior) {
1266
+ this.tx.ops.push(prior);
1267
+ }
1268
+ else {
1269
+ this.tx.ops.push({ kind: "entity-delete", id });
1270
+ }
1271
+ }
1272
+ async recordRelationUndoBeforeWrite(src, dst, kind) {
1273
+ if (this.tx === null)
1274
+ return;
1275
+ const prior = await this.snapshotRelation(src, dst, kind);
1276
+ if (prior) {
1277
+ this.tx.ops.push(prior);
1278
+ }
1279
+ else {
1280
+ this.tx.ops.push({ kind: "relation-delete", src, dst, relKind: kind });
1281
+ }
1282
+ }
1283
+ async recordEntityUndoBeforeDelete(id) {
1284
+ if (this.tx === null)
1285
+ return;
1286
+ // Snapshot the entity itself + every adjacent relation so DETACH DELETE can
1287
+ // be reversed. Relations are snapshotted first in the ops array so that on
1288
+ // reverse-replay the entity is restored first then the relations (DAG order).
1289
+ const prior = await this.snapshotEntity(id);
1290
+ if (!prior)
1291
+ return;
1292
+ const priorProjection = await this.snapshotEntityAnalyzerProjection(id);
1293
+ if (priorProjection)
1294
+ this.tx.ops.push(priorProjection);
1295
+ const rels = await this.runInternal(`MATCH (s:${ENTITY_TABLE})-[r:${RELATION_TABLE}]->(d:${ENTITY_TABLE}) ` +
1296
+ "WHERE s.id = $id OR d.id = $id " +
1297
+ "RETURN s.id AS src, d.id AS dst, r.kind AS kind, r.props AS props, " +
1298
+ "r.messageId AS messageId, r.turnId AS turnId, r.writeSeq AS writeSeq;", { id });
1299
+ // Push relations FIRST (so reverse-replay pops them LAST, after entity restored)
1300
+ for (const row of rels) {
1301
+ this.tx.ops.push({
1302
+ kind: "relation-restore",
1303
+ src: asString(row.src),
1304
+ dst: asString(row.dst),
1305
+ relKind: asString(row.kind),
1306
+ props: asString(row.props),
1307
+ messageId: asString(row.messageId),
1308
+ turnId: asString(row.turnId),
1309
+ writeSeq: asNumber(row.writeSeq)
1310
+ });
1311
+ }
1312
+ this.tx.ops.push(prior);
1313
+ }
1314
+ async recordRelationUndoBeforeDelete(src, dst) {
1315
+ if (this.tx === null)
1316
+ return;
1317
+ const rows = await this.runInternal(`MATCH (s:${ENTITY_TABLE} {id: $src})-[r:${RELATION_TABLE}]->` +
1318
+ `(d:${ENTITY_TABLE} {id: $dst}) ` +
1319
+ "RETURN r.kind AS kind, r.props AS props, " +
1320
+ "r.messageId AS messageId, r.turnId AS turnId, r.writeSeq AS writeSeq;", { src, dst });
1321
+ for (const row of rows) {
1322
+ this.tx.ops.push({
1323
+ kind: "relation-restore",
1324
+ src,
1325
+ dst,
1326
+ relKind: asString(row.kind),
1327
+ props: asString(row.props),
1328
+ messageId: asString(row.messageId),
1329
+ turnId: asString(row.turnId),
1330
+ writeSeq: asNumber(row.writeSeq)
1331
+ });
1332
+ }
1333
+ }
1334
+ async recordAttrHistoryUndo(entityId, attrPath) {
1335
+ if (this.tx === null)
1336
+ return;
1337
+ this.tx.ops.push({
1338
+ kind: "attr-history-restore",
1339
+ entityId,
1340
+ attrPath,
1341
+ rows: await this.queryAttrHistory(entityId, attrPath)
1342
+ });
1343
+ }
1344
+ async recordAllAttrHistoryUndo() {
1345
+ if (this.tx === null)
1346
+ return;
1347
+ const rows = await this.runInternal(`MATCH (a:${ATTR_HISTORY_TABLE}) ` +
1348
+ "RETURN a.attrHistoryId AS attrHistoryId, a.entityId AS entityId, " +
1349
+ "a.attrPath AS attrPath, a.oldValueText AS oldValueText, a.newValueText AS newValueText, " +
1350
+ "a.replacedAtMessageId AS replacedAtMessageId, a.replacedAtTurnId AS replacedAtTurnId, " +
1351
+ "a.replacedAtWriteSeq AS replacedAtWriteSeq, a.superseded AS superseded " +
1352
+ "ORDER BY a.replacedAtWriteSeq;", {});
1353
+ this.tx.ops.push({
1354
+ kind: "attr-history-restore-all",
1355
+ rows: rows.map((row) => this.toAttrHistory(row))
1356
+ });
1357
+ }
1358
+ async recordLatchScenarioUndo(scenarioId) {
1359
+ if (this.tx === null)
1360
+ return;
1361
+ this.tx.ops.push({
1362
+ kind: "latch-restore-scenario",
1363
+ scenarioId,
1364
+ rows: await this.latchLoad(scenarioId)
1365
+ });
1366
+ }
1367
+ async recordRollbackAfterMessageUndo(pivot) {
1368
+ if (this.tx === null)
1369
+ return;
1370
+ const projections = await this.runInternal(`MATCH (p:${ENTITY_LEXICAL_PROJECTION_TABLE}) WHERE p.writeSeq > $pivot ` +
1371
+ "RETURN p.entityId AS entityId, p.lexicalText AS lexicalText, " +
1372
+ "p.projectionJson AS projectionJson, p.analyzerVersion AS analyzerVersion, " +
1373
+ "p.lexicalStatus AS lexicalStatus, p.relationalStatus AS relationalStatus, " +
1374
+ "p.writeSeq AS writeSeq;", { pivot });
1375
+ for (const row of projections) {
1376
+ this.tx.ops.push({
1377
+ kind: "entity-lexical-projection-restore",
1378
+ entityId: asString(row.entityId),
1379
+ lexicalText: asString(row.lexicalText),
1380
+ projectionJson: asString(row.projectionJson),
1381
+ analyzerVersion: asString(row.analyzerVersion),
1382
+ lexicalStatus: asString(row.lexicalStatus),
1383
+ relationalStatus: asString(row.relationalStatus),
1384
+ writeSeq: asNumber(row.writeSeq)
1385
+ });
1386
+ }
1387
+ const relations = await this.runInternal(`MATCH (s:${ENTITY_TABLE})-[r:${RELATION_TABLE}]->(d:${ENTITY_TABLE}) ` +
1388
+ "WHERE r.writeSeq > $pivot OR s.writeSeq > $pivot OR d.writeSeq > $pivot " +
1389
+ "RETURN s.id AS src, d.id AS dst, r.kind AS kind, r.props AS props, " +
1390
+ "r.messageId AS messageId, r.turnId AS turnId, r.writeSeq AS writeSeq;", { pivot });
1391
+ for (const row of relations) {
1392
+ this.tx.ops.push({
1393
+ kind: "relation-restore",
1394
+ src: asString(row.src),
1395
+ dst: asString(row.dst),
1396
+ relKind: asString(row.kind),
1397
+ props: asString(row.props),
1398
+ messageId: asString(row.messageId),
1399
+ turnId: asString(row.turnId),
1400
+ writeSeq: asNumber(row.writeSeq)
1401
+ });
1402
+ }
1403
+ const entities = await this.runInternal(`MATCH (e:${ENTITY_TABLE}) WHERE e.writeSeq > $pivot ` +
1404
+ "RETURN e.id AS id, e.kind AS kind, e.props AS props, e.embedding AS embedding, " +
1405
+ "e.messageId AS messageId, e.turnId AS turnId, e.writeSeq AS writeSeq;", { pivot });
1406
+ for (const row of entities) {
1407
+ const embedding = Array.isArray(row.embedding)
1408
+ ? row.embedding.map((value) => (typeof value === "number" ? value : Number(value)))
1409
+ : null;
1410
+ this.tx.ops.push({
1411
+ kind: "entity-restore",
1412
+ id: asString(row.id),
1413
+ entityKind: asString(row.kind),
1414
+ props: asString(row.props),
1415
+ embedding,
1416
+ messageId: asString(row.messageId),
1417
+ turnId: asString(row.turnId),
1418
+ writeSeq: asNumber(row.writeSeq)
1419
+ });
1420
+ }
1421
+ }
1422
+ constructor(options, variant = lbug) {
1423
+ if (!Number.isInteger(options.dim) || options.dim <= 0) {
1424
+ throw new Error(`dim must be a positive integer got ${options.dim}`);
1425
+ }
1426
+ this.dim = options.dim;
1427
+ this.variant = variant;
1428
+ this.transcriptReachabilityResolver = options.resolveTranscriptReachability ?? null;
1429
+ if (options.worldMemoryHostAuthority !== undefined
1430
+ && !worldMemoryHostAuthorities.has(options.worldMemoryHostAuthority)) {
1431
+ throw new Error("world memory host authority was not created by this bridge module");
1432
+ }
1433
+ this.worldMemoryHostAuthority = options.worldMemoryHostAuthority ?? null;
1434
+ if (options.workerPath && variant.setWorkerPath) {
1435
+ variant.setWorkerPath(options.workerPath);
1436
+ }
1437
+ }
1438
+ async init() {
1439
+ if (this.closing) {
1440
+ throw new Error("ladybug bridge cannot initialize while closing");
1441
+ }
1442
+ if (this.initializing) {
1443
+ throw new Error("ladybug bridge initialization is already in progress");
1444
+ }
1445
+ if (this.graphRollbackInProgress) {
1446
+ throw new Error("ladybug bridge cannot initialize during rollback replay");
1447
+ }
1448
+ if (this.ready)
1449
+ return;
1450
+ this.initializing = true;
1451
+ try {
1452
+ this.db = new this.variant.Database(IN_MEMORY_PATH);
1453
+ await this.db.init();
1454
+ this.conn = new this.variant.Connection(this.db);
1455
+ await this.conn.init();
1456
+ await this.createSchema();
1457
+ this.ready = true;
1458
+ }
1459
+ finally {
1460
+ this.initializing = false;
1461
+ }
1462
+ }
1463
+ requireConnUnchecked() {
1464
+ if (!this.conn)
1465
+ throw new Error("ladybug bridge not initialized");
1466
+ return this.conn;
1467
+ }
1468
+ requireConn() {
1469
+ if (this.closing)
1470
+ throw new Error("ladybug bridge is closing");
1471
+ if (this.graphRollbackInProgress)
1472
+ throw new Error("ladybug bridge graph rollback replay is in progress");
1473
+ return this.requireConnUnchecked();
1474
+ }
1475
+ async queryDuringRollback(statement) {
1476
+ if (!this.graphRollbackInProgress) {
1477
+ throw new Error("ladybug rollback query requires active rollback replay");
1478
+ }
1479
+ return collectRows(await this.requireConnUnchecked().query(statement));
1480
+ }
1481
+ async runDuringRollback(statement, params) {
1482
+ if (!this.graphRollbackInProgress) {
1483
+ throw new Error("ladybug rollback statement requires active rollback replay");
1484
+ }
1485
+ const conn = this.requireConnUnchecked();
1486
+ const prepared = await conn.prepare(statement);
1487
+ if (!prepared.isSuccess()) {
1488
+ const message = await prepared.getErrorMessage();
1489
+ await prepared.close();
1490
+ throw new Error(message);
1491
+ }
1492
+ const result = await conn.execute(prepared, params);
1493
+ await prepared.close();
1494
+ return collectRows(result);
1495
+ }
1496
+ assertStatementCannotAddressWorldMemory(statement) {
1497
+ if (WORLD_MEMORY_TABLE_PATTERN.test(statement)) {
1498
+ throw new Error("world memory schema is only available through the trusted host adapter");
1499
+ }
1500
+ }
1501
+ assertBusinessStatementCannotAddressWorldMemory(statement) {
1502
+ this.assertStatementCannotAddressWorldMemory(statement);
1503
+ if (this.worldMemoryHostAuthority !== null) {
1504
+ throw new Error("trusted WorldMemory bridge does not expose raw query or run");
1505
+ }
1506
+ }
1507
+ async queryForWasmRuntime(statement) {
1508
+ this.assertStatementCannotAddressWorldMemory(statement);
1509
+ return this.queryInternal(statement);
1510
+ }
1511
+ async queryInternal(statement) {
1512
+ return collectRows(await this.requireConn().query(statement));
1513
+ }
1514
+ async runInternal(statement, params) {
1515
+ const conn = this.requireConn();
1516
+ const prepared = await conn.prepare(statement);
1517
+ if (!prepared.isSuccess()) {
1518
+ const message = await prepared.getErrorMessage();
1519
+ await prepared.close();
1520
+ throw new Error(message);
1521
+ }
1522
+ const result = await conn.execute(prepared, params);
1523
+ await prepared.close();
1524
+ return collectRows(result);
1525
+ }
1526
+ async query(statement) {
1527
+ this.assertBusinessStatementCannotAddressWorldMemory(statement);
1528
+ return this.queryInternal(statement);
1529
+ }
1530
+ async run(statement, params) {
1531
+ this.assertBusinessStatementCannotAddressWorldMemory(statement);
1532
+ return this.runInternal(statement, params);
1533
+ }
1534
+ async queryWorldMemory(statement) {
1535
+ if (!WORLD_MEMORY_TABLE_PATTERN.test(statement)) {
1536
+ throw new Error("world memory internal query must address a dedicated Wm table");
1537
+ }
1538
+ return this.queryInternal(statement);
1539
+ }
1540
+ async runWorldMemory(statement, params) {
1541
+ if (!WORLD_MEMORY_TABLE_PATTERN.test(statement)) {
1542
+ throw new Error("world memory internal statement must address a dedicated Wm table");
1543
+ }
1544
+ return this.runInternal(statement, params);
1545
+ }
1546
+ /**
1547
+ * Redacted full-state CAS used by the Runtime 2 transaction barrier. Raw
1548
+ * rows never cross the bridge; only a domain-separated digest is returned.
1549
+ */
1550
+ async stateDigest() {
1551
+ const entities = await this.runInternal(`MATCH (e:${ENTITY_TABLE}) ` +
1552
+ "RETURN e.id AS id, e.kind AS kind, e.props AS props, e.embedding AS embedding, " +
1553
+ "e.messageId AS messageId, e.turnId AS turnId, e.writeSeq AS writeSeq ORDER BY e.id;", {});
1554
+ const entityLexicalProjections = await this.runInternal(`MATCH (p:${ENTITY_LEXICAL_PROJECTION_TABLE}) ` +
1555
+ "RETURN p.entityId AS entityId, p.lexicalText AS lexicalText, " +
1556
+ "p.projectionJson AS projectionJson, p.analyzerVersion AS analyzerVersion, " +
1557
+ "p.lexicalStatus AS lexicalStatus, p.relationalStatus AS relationalStatus, " +
1558
+ "p.writeSeq AS writeSeq ORDER BY entityId;", {});
1559
+ const relations = await this.runInternal(`MATCH (s:${ENTITY_TABLE})-[r:${RELATION_TABLE}]->(d:${ENTITY_TABLE}) ` +
1560
+ "RETURN s.id AS src, d.id AS dst, r.kind AS kind, r.props AS props, " +
1561
+ "r.messageId AS messageId, r.turnId AS turnId, r.writeSeq AS writeSeq " +
1562
+ "ORDER BY src, dst, kind;", {});
1563
+ const attrHistory = await this.runInternal(`MATCH (a:${ATTR_HISTORY_TABLE}) ` +
1564
+ "RETURN a.attrHistoryId AS attrHistoryId, a.entityId AS entityId, " +
1565
+ "a.attrPath AS attrPath, a.oldValueText AS oldValueText, a.newValueText AS newValueText, " +
1566
+ "a.replacedAtMessageId AS replacedAtMessageId, a.replacedAtTurnId AS replacedAtTurnId, " +
1567
+ "a.replacedAtWriteSeq AS replacedAtWriteSeq, a.superseded AS superseded " +
1568
+ "ORDER BY attrHistoryId;", {});
1569
+ const latches = await this.runInternal(`MATCH (l:${LATCH_TABLE}) ` +
1570
+ "RETURN l.pk AS pk, l.scenarioId AS scenarioId, l.leafId AS leafId, l.state AS state, " +
1571
+ "l.firstFiredAtMs AS firstFiredAtMs, l.lastEvaluatedAtMs AS lastEvaluatedAtMs, " +
1572
+ "l.fireCount AS fireCount, l.source AS source, l.confidence AS confidence ORDER BY pk;", {});
1573
+ const transcriptMessages = await this.runInternal(`MATCH (m:${TRANSCRIPT_MESSAGE_TABLE}) ` +
1574
+ "RETURN m.scenarioId AS scenarioId, m.saveId AS saveId, m.worldlineId AS worldlineId, " +
1575
+ "m.messageId AS messageId, m.turnId AS turnId, m.role AS role, m.content AS content, " +
1576
+ "m.status AS status, m.createdAtMs AS createdAtMs, m.updatedAtMs AS updatedAtMs, " +
1577
+ "m.sourceLedgerSeq AS sourceLedgerSeq ORDER BY scenarioId, saveId, worldlineId, sourceLedgerSeq, messageId;", {});
1578
+ const canonical = JSON.stringify({
1579
+ schemaVersion: 3,
1580
+ dim: this.dim,
1581
+ entities,
1582
+ entityLexicalProjections,
1583
+ relations,
1584
+ attrHistory,
1585
+ latches,
1586
+ transcriptMessages
1587
+ });
1588
+ return sha256HexUtf8(`ladybug-runtime2-state-v3\u0000${canonical}`);
1589
+ }
1590
+ async recallCandidateRecord(id) {
1591
+ const rows = await this.runInternal(`MATCH (e:${ENTITY_TABLE} {id: $id}) RETURN ` +
1592
+ "e.id AS id, e.kind AS kind, e.props AS props, e.embedding AS embedding, e.messageId AS messageId, " +
1593
+ "e.turnId AS turnId, e.writeSeq AS recordedRevision;", { id });
1594
+ if (rows.length === 0)
1595
+ return null;
1596
+ if (rows.length !== 1)
1597
+ throw new Error(`recall candidate identity is not unique: ${id}`);
1598
+ const row = rows[0];
1599
+ const recordedRevision = asNumber(row.recordedRevision);
1600
+ const writeRevision = this._writeSeq;
1601
+ if (!Number.isSafeInteger(recordedRevision) || recordedRevision < 1
1602
+ || !Number.isSafeInteger(writeRevision) || writeRevision < recordedRevision) {
1603
+ throw new Error(`recall candidate revision invalid: ${id}`);
1604
+ }
1605
+ const messageId = asString(row.messageId);
1606
+ const turnId = asString(row.turnId);
1607
+ if (messageId.length === 0 || turnId.length === 0 || /[\0\r\n]/.test(messageId) || /[\0\r\n]/.test(turnId)) {
1608
+ throw new Error(`recall candidate write identity invalid: ${id}`);
1609
+ }
1610
+ return Object.freeze({
1611
+ entity: Object.freeze(this.toEntity(row)),
1612
+ embedding: Object.freeze(asNumberArray(row.embedding)),
1613
+ messageId,
1614
+ turnId,
1615
+ recordedRevision,
1616
+ writeRevision
1617
+ });
1618
+ }
1619
+ async createSchema() {
1620
+ await this.queryInternal(`CREATE NODE TABLE IF NOT EXISTS ${ENTITY_TABLE}(` +
1621
+ `id STRING, kind STRING, props STRING, embedding FLOAT[${this.dim}], ` +
1622
+ `messageId STRING, turnId STRING, writeSeq INT64, PRIMARY KEY (id));`);
1623
+ await this.queryInternal(`CREATE NODE TABLE IF NOT EXISTS ${ENTITY_LEXICAL_PROJECTION_TABLE}(` +
1624
+ "entityId STRING, lexicalText STRING, projectionJson STRING, analyzerVersion STRING, " +
1625
+ "lexicalStatus STRING, relationalStatus STRING, writeSeq INT64, PRIMARY KEY (entityId));");
1626
+ await this.queryInternal(`CREATE REL TABLE IF NOT EXISTS ${RELATION_TABLE}(` +
1627
+ `FROM ${ENTITY_TABLE} TO ${ENTITY_TABLE}, kind STRING, props STRING, ` +
1628
+ `messageId STRING, turnId STRING, writeSeq INT64);`);
1629
+ await this.queryInternal(`CREATE NODE TABLE IF NOT EXISTS ${LATCH_TABLE}(` +
1630
+ "pk STRING, scenarioId STRING, leafId STRING, state STRING, " +
1631
+ "firstFiredAtMs INT64, lastEvaluatedAtMs INT64, fireCount INT64, " +
1632
+ "source STRING, confidence DOUBLE, " +
1633
+ "PRIMARY KEY (pk));");
1634
+ await this.queryInternal(`CREATE NODE TABLE IF NOT EXISTS ${ATTR_HISTORY_TABLE}(` +
1635
+ "attrHistoryId STRING, entityId STRING, attrPath STRING, " +
1636
+ "oldValueText STRING, newValueText STRING, " +
1637
+ "replacedAtMessageId STRING, replacedAtTurnId STRING, " +
1638
+ "replacedAtWriteSeq INT64, superseded BOOL, " +
1639
+ "PRIMARY KEY (attrHistoryId));");
1640
+ await this.queryInternal(`CREATE NODE TABLE IF NOT EXISTS ${TRANSCRIPT_MESSAGE_TABLE}(` +
1641
+ "pk STRING, scenarioId STRING, saveId STRING, worldlineId STRING, " +
1642
+ "messageId STRING, turnId STRING, role STRING, content STRING, searchText STRING, status STRING, " +
1643
+ "createdAtMs INT64, updatedAtMs INT64, sourceLedgerSeq INT64, " +
1644
+ "PRIMARY KEY (pk));");
1645
+ await this.createWorldMemorySchema();
1646
+ }
1647
+ async createWorldMemorySchema() {
1648
+ await this.queryWorldMemory(`CREATE NODE TABLE IF NOT EXISTS ${WM_EPISODE_TABLE}(` +
1649
+ `id STRING, sourceMessageIds STRING, turnBundleId STRING, synopsis STRING, ` +
1650
+ `searchText STRING, embedding FLOAT[${WORLD_MEMORY_EMBEDDING_DIM}], sourceDigest STRING, ` +
1651
+ `worldTimeFrom INT64, worldTimeTo INT64, recordedWriteSeq INT64, reachability STRING, ` +
1652
+ `model STRING, provider STRING, attemptId STRING, schemaVersion INT32, PRIMARY KEY (id));`);
1653
+ await this.queryWorldMemory(`CREATE NODE TABLE IF NOT EXISTS ${WM_CLAIM_ROOT_TABLE}(` +
1654
+ `id STRING, subjectKind STRING, entityId STRING, attrPath STRING, relationKey STRING, ` +
1655
+ `currentClaimId STRING, latestRevision INT64, schemaVersion INT32, PRIMARY KEY (id));`);
1656
+ await this.queryWorldMemory(`CREATE NODE TABLE IF NOT EXISTS ${WM_CLAIM_TABLE}(` +
1657
+ `id STRING, rootId STRING, authorityClass STRING, status STRING, canonicalValue STRING, ` +
1658
+ `canonicalValueSha256 STRING, confidence DOUBLE, recordedFromWriteSeq INT64, ` +
1659
+ `recordedToWriteSeq INT64, validFromWorldSeconds INT64, validToWorldSeconds INT64, ` +
1660
+ `sourceDigest STRING, sourceEpisodeIds STRING, sourceMessageIds STRING, revision INT64, ` +
1661
+ `schemaVersion INT32, PRIMARY KEY (id));`);
1662
+ await this.queryWorldMemory(`CREATE NODE TABLE IF NOT EXISTS ${WM_DERIVED_TABLE}(` +
1663
+ `id STRING, derivedKind STRING, text STRING, searchText STRING, ` +
1664
+ `embedding FLOAT[${WORLD_MEMORY_EMBEDDING_DIM}], ` +
1665
+ `authorityClass STRING, status STRING, sourceDigest STRING, coverageIds STRING, ` +
1666
+ `summarizedThroughOpId STRING, validFromWorldSeconds INT64, validToWorldSeconds INT64, ` +
1667
+ `recordedWriteSeq INT64, model STRING, provider STRING, promptVersion STRING, ` +
1668
+ `tokenizerVersion STRING, schemaVersion INT32, PRIMARY KEY (id));`);
1669
+ for (const [relationTable, definition] of Object.entries(MEMORY_RELATION_DEFINITIONS)) {
1670
+ await this.queryWorldMemory(`CREATE REL TABLE IF NOT EXISTS ${relationTable}(` +
1671
+ `FROM ${definition.sourceTable} TO ${definition.targetTable});`);
1672
+ }
1673
+ await this.ensureWorldMemoryIndexes();
1674
+ }
1675
+ async executeWorldMemoryQuery(statement, duringRollback = false) {
1676
+ return duringRollback
1677
+ ? this.queryDuringRollback(statement)
1678
+ : this.queryWorldMemory(statement);
1679
+ }
1680
+ async ensureWorldMemoryIndexes(duringRollback = false) {
1681
+ if (!this.memoryEpisodeVectorIndexBuilt) {
1682
+ await this.executeWorldMemoryQuery(`CALL CREATE_VECTOR_INDEX('${WM_EPISODE_TABLE}', '${WM_EPISODE_VECTOR_INDEX}', ` +
1683
+ "'embedding', metric := 'cosine');", duringRollback);
1684
+ this.memoryEpisodeVectorIndexBuilt = true;
1685
+ }
1686
+ if (!this.memoryDerivedVectorIndexBuilt) {
1687
+ await this.executeWorldMemoryQuery(`CALL CREATE_VECTOR_INDEX('${WM_DERIVED_TABLE}', '${WM_DERIVED_VECTOR_INDEX}', ` +
1688
+ "'embedding', metric := 'cosine');", duringRollback);
1689
+ this.memoryDerivedVectorIndexBuilt = true;
1690
+ }
1691
+ if (!this.memoryEpisodeFtsIndexBuilt) {
1692
+ await this.executeWorldMemoryQuery(`CALL CREATE_FTS_INDEX('${WM_EPISODE_TABLE}', '${WM_EPISODE_FTS_INDEX}', ['searchText'], ` +
1693
+ "stemmer := 'none', tokenizer := 'simple');", duringRollback);
1694
+ this.memoryEpisodeFtsIndexBuilt = true;
1695
+ }
1696
+ if (!this.memoryDerivedFtsIndexBuilt) {
1697
+ await this.executeWorldMemoryQuery(`CALL CREATE_FTS_INDEX('${WM_DERIVED_TABLE}', '${WM_DERIVED_FTS_INDEX}', ['searchText'], ` +
1698
+ "stemmer := 'none', tokenizer := 'simple');", duringRollback);
1699
+ this.memoryDerivedFtsIndexBuilt = true;
1700
+ }
1701
+ }
1702
+ async dropMemoryEpisodeIndexes(duringRollback = false) {
1703
+ if (this.memoryEpisodeVectorIndexBuilt) {
1704
+ await this.executeWorldMemoryQuery(`CALL DROP_VECTOR_INDEX('${WM_EPISODE_TABLE}', '${WM_EPISODE_VECTOR_INDEX}');`, duringRollback);
1705
+ this.memoryEpisodeVectorIndexBuilt = false;
1706
+ }
1707
+ if (this.memoryEpisodeFtsIndexBuilt) {
1708
+ await this.executeWorldMemoryQuery(`CALL DROP_FTS_INDEX('${WM_EPISODE_TABLE}', '${WM_EPISODE_FTS_INDEX}');`, duringRollback);
1709
+ this.memoryEpisodeFtsIndexBuilt = false;
1710
+ }
1711
+ }
1712
+ async dropMemoryDerivedIndexes(duringRollback = false) {
1713
+ if (this.memoryDerivedVectorIndexBuilt) {
1714
+ await this.executeWorldMemoryQuery(`CALL DROP_VECTOR_INDEX('${WM_DERIVED_TABLE}', '${WM_DERIVED_VECTOR_INDEX}');`, duringRollback);
1715
+ this.memoryDerivedVectorIndexBuilt = false;
1716
+ }
1717
+ if (this.memoryDerivedFtsIndexBuilt) {
1718
+ await this.executeWorldMemoryQuery(`CALL DROP_FTS_INDEX('${WM_DERIVED_TABLE}', '${WM_DERIVED_FTS_INDEX}');`, duringRollback);
1719
+ this.memoryDerivedFtsIndexBuilt = false;
1720
+ }
1721
+ }
1722
+ async rebuildMemoryEpisodeIndexes(duringRollback = false) {
1723
+ if (!this.memoryEpisodeVectorIndexBuilt) {
1724
+ await this.executeWorldMemoryQuery(`CALL CREATE_VECTOR_INDEX('${WM_EPISODE_TABLE}', '${WM_EPISODE_VECTOR_INDEX}', ` +
1725
+ "'embedding', metric := 'cosine');", duringRollback);
1726
+ this.memoryEpisodeVectorIndexBuilt = true;
1727
+ }
1728
+ if (!this.memoryEpisodeFtsIndexBuilt) {
1729
+ await this.executeWorldMemoryQuery(`CALL CREATE_FTS_INDEX('${WM_EPISODE_TABLE}', '${WM_EPISODE_FTS_INDEX}', ['searchText'], ` +
1730
+ "stemmer := 'none', tokenizer := 'simple');", duringRollback);
1731
+ this.memoryEpisodeFtsIndexBuilt = true;
1732
+ }
1733
+ }
1734
+ async rebuildMemoryDerivedIndexes(duringRollback = false) {
1735
+ if (!this.memoryDerivedVectorIndexBuilt) {
1736
+ await this.executeWorldMemoryQuery(`CALL CREATE_VECTOR_INDEX('${WM_DERIVED_TABLE}', '${WM_DERIVED_VECTOR_INDEX}', ` +
1737
+ "'embedding', metric := 'cosine');", duringRollback);
1738
+ this.memoryDerivedVectorIndexBuilt = true;
1739
+ }
1740
+ if (!this.memoryDerivedFtsIndexBuilt) {
1741
+ await this.executeWorldMemoryQuery(`CALL CREATE_FTS_INDEX('${WM_DERIVED_TABLE}', '${WM_DERIVED_FTS_INDEX}', ['searchText'], ` +
1742
+ "stemmer := 'none', tokenizer := 'simple');", duringRollback);
1743
+ this.memoryDerivedFtsIndexBuilt = true;
1744
+ }
1745
+ }
1746
+ toMemoryEpisode(row) {
1747
+ const embedding = Array.isArray(row.embedding)
1748
+ ? row.embedding.map((value) => (typeof value === "number" ? value : Number(value)))
1749
+ : [];
1750
+ return Object.freeze({
1751
+ id: assertMemoryIdentifier("episode.id", asString(row.id)),
1752
+ sourceMessageIds: decodeCanonicalStringIds("episode.source_message_ids", row.sourceMessageIds, false),
1753
+ turnBundleId: assertMemoryIdentifier("episode.turn_bundle_id", asString(row.turnBundleId)),
1754
+ synopsis: assertMemorySynopsis(asString(row.synopsis)),
1755
+ embedding: snapshotMemoryEmbedding(embedding),
1756
+ sourceDigest: assertMemoryIdentifier("episode.source_digest", asString(row.sourceDigest)),
1757
+ worldTimeFrom: assertMemoryInteger("episode.world_time_from", asNumber(row.worldTimeFrom), true),
1758
+ worldTimeTo: row.worldTimeTo == null
1759
+ ? null
1760
+ : assertMemoryNullableInteger("episode.world_time_to", asNumber(row.worldTimeTo)),
1761
+ recordedWriteSeq: assertMemoryInteger("episode.recorded_write_seq", asNumber(row.recordedWriteSeq)),
1762
+ reachability: assertMemoryIdentifier("episode.reachability", asString(row.reachability)),
1763
+ model: assertMemoryIdentifier("episode.model", asString(row.model)),
1764
+ provider: assertMemoryIdentifier("episode.provider", asString(row.provider)),
1765
+ attemptId: assertMemoryIdentifier("episode.attempt_id", asString(row.attemptId)),
1766
+ schemaVersion: assertMemorySchemaVersion(asNumber(row.schemaVersion))
1767
+ });
1768
+ }
1769
+ toMemoryClaimRoot(row) {
1770
+ const entityId = asNullableString(row.entityId);
1771
+ const attrPath = asNullableString(row.attrPath);
1772
+ const relationKey = asNullableString(row.relationKey);
1773
+ const currentClaimId = asNullableString(row.currentClaimId);
1774
+ return Object.freeze({
1775
+ id: assertMemoryIdentifier("claim_root.id", asString(row.id)),
1776
+ subjectKind: assertMemoryIdentifier("claim_root.subject_kind", asString(row.subjectKind)),
1777
+ entityId: entityId === null ? null : assertMemoryIdentifier("claim_root.entity_id", entityId),
1778
+ attrPath: attrPath === null ? null : assertMemoryIdentifier("claim_root.attr_path", attrPath),
1779
+ relationKey: relationKey === null
1780
+ ? null
1781
+ : assertMemoryIdentifier("claim_root.relation_key", relationKey),
1782
+ currentClaimId: currentClaimId === null
1783
+ ? null
1784
+ : assertMemoryIdentifier("claim_root.current_claim_id", currentClaimId),
1785
+ latestRevision: assertMemoryInteger("claim_root.latest_revision", asNumber(row.latestRevision)),
1786
+ schemaVersion: assertMemorySchemaVersion(asNumber(row.schemaVersion))
1787
+ });
1788
+ }
1789
+ toMemoryClaim(row) {
1790
+ const authorityClass = asString(row.authorityClass);
1791
+ if (authorityClass !== "world_fact") {
1792
+ throw new Error("world memory claim authority_class must be world_fact");
1793
+ }
1794
+ return Object.freeze({
1795
+ id: assertMemoryIdentifier("claim.id", asString(row.id)),
1796
+ rootId: assertMemoryIdentifier("claim.root_id", asString(row.rootId)),
1797
+ authorityClass,
1798
+ status: assertMemoryIdentifier("claim.status", asString(row.status)),
1799
+ canonicalValue: assertCanonicalJson(asString(row.canonicalValue)),
1800
+ canonicalValueSha256: assertMemoryIdentifier("claim.canonical_value_sha256", asString(row.canonicalValueSha256)),
1801
+ confidence: assertMemoryConfidence(asNumber(row.confidence)),
1802
+ recordedFromWriteSeq: assertMemoryInteger("claim.recorded_from_write_seq", asNumber(row.recordedFromWriteSeq)),
1803
+ recordedToWriteSeq: row.recordedToWriteSeq == null
1804
+ ? null
1805
+ : assertMemoryNullableInteger("claim.recorded_to_write_seq", asNumber(row.recordedToWriteSeq)),
1806
+ validFromWorldSeconds: assertMemoryInteger("claim.valid_from_world_seconds", asNumber(row.validFromWorldSeconds), true),
1807
+ validToWorldSeconds: row.validToWorldSeconds == null
1808
+ ? null
1809
+ : assertMemoryNullableInteger("claim.valid_to_world_seconds", asNumber(row.validToWorldSeconds)),
1810
+ sourceDigest: assertMemoryIdentifier("claim.source_digest", asString(row.sourceDigest)),
1811
+ sourceEpisodeIds: decodeCanonicalStringIds("claim.source_episode_ids", row.sourceEpisodeIds, true),
1812
+ sourceMessageIds: decodeCanonicalStringIds("claim.source_message_ids", row.sourceMessageIds, true),
1813
+ revision: assertMemoryInteger("claim.revision", asNumber(row.revision)),
1814
+ schemaVersion: assertMemorySchemaVersion(asNumber(row.schemaVersion))
1815
+ });
1816
+ }
1817
+ toMemoryDerived(row) {
1818
+ const derivedKind = asString(row.derivedKind);
1819
+ if (!["observation", "experience", "summary"].includes(derivedKind)) {
1820
+ throw new Error(`unknown world memory derived kind ${derivedKind}`);
1821
+ }
1822
+ const authorityClass = asString(row.authorityClass);
1823
+ if (authorityClass !== derivedKind) {
1824
+ throw new Error("world memory derived authority_class must equal derived_kind");
1825
+ }
1826
+ const embedding = Array.isArray(row.embedding)
1827
+ ? row.embedding.map((value) => (typeof value === "number" ? value : Number(value)))
1828
+ : [];
1829
+ return Object.freeze({
1830
+ id: assertMemoryIdentifier("derived.id", asString(row.id)),
1831
+ derivedKind,
1832
+ text: assertMemoryIdentifier("derived.text", asString(row.text)),
1833
+ embedding: snapshotMemoryEmbedding(embedding),
1834
+ authorityClass,
1835
+ status: assertMemoryIdentifier("derived.status", asString(row.status)),
1836
+ sourceDigest: assertMemoryIdentifier("derived.source_digest", asString(row.sourceDigest)),
1837
+ coverageIds: decodeCanonicalStringIds("derived.coverage_ids", row.coverageIds, false),
1838
+ summarizedThroughOpId: asNullableString(row.summarizedThroughOpId),
1839
+ validFromWorldSeconds: assertMemoryInteger("derived.valid_from_world_seconds", asNumber(row.validFromWorldSeconds), true),
1840
+ validToWorldSeconds: row.validToWorldSeconds == null
1841
+ ? null
1842
+ : assertMemoryNullableInteger("derived.valid_to_world_seconds", asNumber(row.validToWorldSeconds)),
1843
+ recordedWriteSeq: assertMemoryInteger("derived.recorded_write_seq", asNumber(row.recordedWriteSeq)),
1844
+ model: assertMemoryIdentifier("derived.model", asString(row.model)),
1845
+ provider: assertMemoryIdentifier("derived.provider", asString(row.provider)),
1846
+ promptVersion: assertMemoryIdentifier("derived.prompt_version", asString(row.promptVersion)),
1847
+ tokenizerVersion: assertMemoryIdentifier("derived.tokenizer_version", asString(row.tokenizerVersion)),
1848
+ schemaVersion: assertMemorySchemaVersion(asNumber(row.schemaVersion))
1849
+ });
1850
+ }
1851
+ async listMemoryEpisodes(duringRollback = false) {
1852
+ const statement = `MATCH (e:${WM_EPISODE_TABLE}) RETURN ` +
1853
+ "e.id AS id, e.sourceMessageIds AS sourceMessageIds, e.turnBundleId AS turnBundleId, " +
1854
+ "e.synopsis AS synopsis, e.embedding AS embedding, e.sourceDigest AS sourceDigest, " +
1855
+ "e.worldTimeFrom AS worldTimeFrom, e.worldTimeTo AS worldTimeTo, " +
1856
+ "e.recordedWriteSeq AS recordedWriteSeq, e.reachability AS reachability, " +
1857
+ "e.model AS model, e.provider AS provider, e.attemptId AS attemptId, " +
1858
+ "e.schemaVersion AS schemaVersion ORDER BY id;";
1859
+ const rows = duringRollback
1860
+ ? await this.queryDuringRollback(statement)
1861
+ : await this.runWorldMemory(statement, {});
1862
+ return rows.map((row) => this.toMemoryEpisode(row));
1863
+ }
1864
+ async listMemoryClaimRoots(duringRollback = false) {
1865
+ const statement = `MATCH (r:${WM_CLAIM_ROOT_TABLE}) RETURN ` +
1866
+ "r.id AS id, r.subjectKind AS subjectKind, r.entityId AS entityId, r.attrPath AS attrPath, " +
1867
+ "r.relationKey AS relationKey, r.currentClaimId AS currentClaimId, " +
1868
+ "r.latestRevision AS latestRevision, r.schemaVersion AS schemaVersion ORDER BY id;";
1869
+ const rows = duringRollback
1870
+ ? await this.queryDuringRollback(statement)
1871
+ : await this.runWorldMemory(statement, {});
1872
+ return rows.map((row) => this.toMemoryClaimRoot(row));
1873
+ }
1874
+ memoryClaimProjection(alias) {
1875
+ return `${alias}.id AS id, ${alias}.rootId AS rootId, ${alias}.authorityClass AS authorityClass, ` +
1876
+ `${alias}.status AS status, ${alias}.canonicalValue AS canonicalValue, ` +
1877
+ `${alias}.canonicalValueSha256 AS canonicalValueSha256, ${alias}.confidence AS confidence, ` +
1878
+ `${alias}.recordedFromWriteSeq AS recordedFromWriteSeq, ` +
1879
+ `${alias}.recordedToWriteSeq AS recordedToWriteSeq, ` +
1880
+ `${alias}.validFromWorldSeconds AS validFromWorldSeconds, ` +
1881
+ `${alias}.validToWorldSeconds AS validToWorldSeconds, ${alias}.sourceDigest AS sourceDigest, ` +
1882
+ `${alias}.sourceEpisodeIds AS sourceEpisodeIds, ${alias}.sourceMessageIds AS sourceMessageIds, ` +
1883
+ `${alias}.revision AS revision, ${alias}.schemaVersion AS schemaVersion`;
1884
+ }
1885
+ async listMemoryClaims(duringRollback = false) {
1886
+ const statement = `MATCH (c:${WM_CLAIM_TABLE}) RETURN ${this.memoryClaimProjection("c")} ` +
1887
+ "ORDER BY rootId, revision, id;";
1888
+ const rows = duringRollback
1889
+ ? await this.queryDuringRollback(statement)
1890
+ : await this.runWorldMemory(statement, {});
1891
+ return rows.map((row) => this.toMemoryClaim(row));
1892
+ }
1893
+ async listMemoryDerived(duringRollback = false) {
1894
+ const statement = `MATCH (d:${WM_DERIVED_TABLE}) RETURN ` +
1895
+ "d.id AS id, d.derivedKind AS derivedKind, d.text AS text, d.embedding AS embedding, " +
1896
+ "d.authorityClass AS authorityClass, d.status AS status, d.sourceDigest AS sourceDigest, " +
1897
+ "d.coverageIds AS coverageIds, d.summarizedThroughOpId AS summarizedThroughOpId, " +
1898
+ "d.validFromWorldSeconds AS validFromWorldSeconds, d.validToWorldSeconds AS validToWorldSeconds, " +
1899
+ "d.recordedWriteSeq AS recordedWriteSeq, d.model AS model, d.provider AS provider, " +
1900
+ "d.promptVersion AS promptVersion, d.tokenizerVersion AS tokenizerVersion, " +
1901
+ "d.schemaVersion AS schemaVersion ORDER BY id;";
1902
+ const rows = duringRollback
1903
+ ? await this.queryDuringRollback(statement)
1904
+ : await this.runWorldMemory(statement, {});
1905
+ return rows.map((row) => this.toMemoryDerived(row));
1906
+ }
1907
+ async listMemoryRelations(duringRollback = false) {
1908
+ const relations = [];
1909
+ for (const [kind, definition] of Object.entries(MEMORY_RELATION_DEFINITIONS)) {
1910
+ const statement = `MATCH (s:${definition.sourceTable})-[r:${kind}]->` +
1911
+ `(d:${definition.targetTable}) RETURN s.id AS sourceId, d.id AS targetId ` +
1912
+ "ORDER BY sourceId, targetId;";
1913
+ const rows = duringRollback
1914
+ ? await this.queryDuringRollback(statement)
1915
+ : await this.runWorldMemory(statement, {});
1916
+ for (const row of rows) {
1917
+ relations.push(Object.freeze({
1918
+ kind: assertMemoryRelationKind(kind),
1919
+ sourceId: assertMemoryIdentifier("relation.source_id", asString(row.sourceId)),
1920
+ targetId: assertMemoryIdentifier("relation.target_id", asString(row.targetId))
1921
+ }));
1922
+ }
1923
+ }
1924
+ return relations;
1925
+ }
1926
+ async snapshotWorldMemory(requireComplete = true) {
1927
+ const snapshot = this.normalizeWorldMemorySnapshot({
1928
+ episodes: Object.freeze(await this.listMemoryEpisodes()),
1929
+ claimRoots: Object.freeze(await this.listMemoryClaimRoots()),
1930
+ claims: Object.freeze(await this.listMemoryClaims()),
1931
+ derived: Object.freeze(await this.listMemoryDerived()),
1932
+ relations: Object.freeze(await this.listMemoryRelations()),
1933
+ episodeVectorIndexBuilt: this.memoryEpisodeVectorIndexBuilt,
1934
+ episodeFtsIndexBuilt: this.memoryEpisodeFtsIndexBuilt,
1935
+ derivedVectorIndexBuilt: this.memoryDerivedVectorIndexBuilt,
1936
+ derivedFtsIndexBuilt: this.memoryDerivedFtsIndexBuilt
1937
+ }, requireComplete);
1938
+ await this.assertWorldMemoryExternalEntityReferences(snapshot);
1939
+ return snapshot;
1940
+ }
1941
+ async recordWorldMemoryUndo() {
1942
+ if (this.tx === null) {
1943
+ throw new Error("world memory mutation requires an active Ladybug graph transaction");
1944
+ }
1945
+ await this.recordWorldMemoryUndoIfTransaction();
1946
+ }
1947
+ async recordWorldMemoryUndoIfTransaction() {
1948
+ if (this.tx === null)
1949
+ return;
1950
+ this.tx.ops.push({
1951
+ kind: "world-memory-restore",
1952
+ snapshot: await this.snapshotWorldMemory(false)
1953
+ });
1954
+ }
1955
+ normalizeMemoryEpisode(row) {
1956
+ return this.toMemoryEpisode({
1957
+ ...row,
1958
+ sourceMessageIds: encodeCanonicalStringIds("episode.source_message_ids", row.sourceMessageIds, false),
1959
+ embedding: [...row.embedding]
1960
+ });
1961
+ }
1962
+ normalizeMemoryClaimRoot(row) {
1963
+ const normalized = this.toMemoryClaimRoot({ ...row });
1964
+ const attributeSubject = normalized.entityId !== null
1965
+ && normalized.attrPath !== null
1966
+ && normalized.relationKey === null;
1967
+ const relationSubject = normalized.attrPath === null
1968
+ && normalized.relationKey !== null;
1969
+ if (!attributeSubject && !relationSubject) {
1970
+ throw new Error("world memory claim root must identify exactly one attribute or relation subject");
1971
+ }
1972
+ if ((attributeSubject && normalized.subjectKind !== "attribute")
1973
+ || (relationSubject && normalized.subjectKind !== "relation")) {
1974
+ throw new Error("world memory claim root subject_kind does not match its typed subject");
1975
+ }
1976
+ return normalized;
1977
+ }
1978
+ normalizeMemoryClaim(row) {
1979
+ const normalized = this.toMemoryClaim({
1980
+ ...row,
1981
+ sourceEpisodeIds: encodeCanonicalStringIds("claim.source_episode_ids", row.sourceEpisodeIds, true),
1982
+ sourceMessageIds: encodeCanonicalStringIds("claim.source_message_ids", row.sourceMessageIds, true)
1983
+ });
1984
+ if (normalized.sourceEpisodeIds.length === 0 && normalized.sourceMessageIds.length === 0) {
1985
+ throw new Error("world memory claim must have at least one source episode or message");
1986
+ }
1987
+ if (normalized.recordedToWriteSeq !== null
1988
+ && normalized.recordedToWriteSeq < normalized.recordedFromWriteSeq) {
1989
+ throw new Error("world memory claim recorded interval is inverted");
1990
+ }
1991
+ if (normalized.validToWorldSeconds !== null
1992
+ && normalized.validToWorldSeconds < normalized.validFromWorldSeconds) {
1993
+ throw new Error("world memory claim valid interval is inverted");
1994
+ }
1995
+ return normalized;
1996
+ }
1997
+ normalizeMemoryDerived(row) {
1998
+ const normalized = this.toMemoryDerived({
1999
+ ...row,
2000
+ coverageIds: encodeCanonicalStringIds("derived.coverage_ids", row.coverageIds, false),
2001
+ embedding: [...row.embedding]
2002
+ });
2003
+ if (normalized.authorityClass !== normalized.derivedKind) {
2004
+ throw new Error("world memory derived authority_class must equal derived_kind");
2005
+ }
2006
+ if (normalized.validToWorldSeconds !== null
2007
+ && normalized.validToWorldSeconds < normalized.validFromWorldSeconds) {
2008
+ throw new Error("world memory derived valid interval is inverted");
2009
+ }
2010
+ return normalized;
2011
+ }
2012
+ normalizeMemoryRelation(row) {
2013
+ const kind = assertMemoryRelationKind(row.kind);
2014
+ const sourceId = assertMemoryIdentifier("relation.source_id", row.sourceId);
2015
+ const targetId = assertMemoryIdentifier("relation.target_id", row.targetId);
2016
+ if ((kind === "WmClaimSupersedesClaim" || kind === "WmClaimContradictsClaim")
2017
+ && sourceId === targetId) {
2018
+ throw new Error("world memory claim relation must not be self-referential");
2019
+ }
2020
+ if (kind === "WmClaimContradictsClaim" && sourceId >= targetId) {
2021
+ throw new Error("world memory contradiction relation must use smaller claim id as source");
2022
+ }
2023
+ return Object.freeze({ kind, sourceId, targetId });
2024
+ }
2025
+ normalizeWorldMemorySnapshot(snapshot, requireComplete = true) {
2026
+ if (typeof snapshot !== "object" || snapshot === null) {
2027
+ throw new Error("world memory snapshot must be an object");
2028
+ }
2029
+ for (const field of ["episodes", "claimRoots", "claims", "derived", "relations"]) {
2030
+ if (!Array.isArray(snapshot[field])) {
2031
+ throw new Error(`world memory snapshot ${field} must be an array`);
2032
+ }
2033
+ }
2034
+ for (const field of [
2035
+ "episodeVectorIndexBuilt",
2036
+ "episodeFtsIndexBuilt",
2037
+ "derivedVectorIndexBuilt",
2038
+ "derivedFtsIndexBuilt"
2039
+ ]) {
2040
+ if (typeof snapshot[field] !== "boolean") {
2041
+ throw new Error(`world memory snapshot ${field} must be boolean`);
2042
+ }
2043
+ }
2044
+ const episodes = snapshot.episodes.map((row) => this.normalizeMemoryEpisode(row));
2045
+ const claimRoots = snapshot.claimRoots.map((row) => this.normalizeMemoryClaimRoot(row));
2046
+ const claims = snapshot.claims.map((row) => this.normalizeMemoryClaim(row));
2047
+ const derived = snapshot.derived.map((row) => this.normalizeMemoryDerived(row));
2048
+ const relations = snapshot.relations.map((row) => this.normalizeMemoryRelation(row));
2049
+ const unique = (label, rows) => {
2050
+ const ids = new Set();
2051
+ for (const row of rows) {
2052
+ if (ids.has(row.id))
2053
+ throw new Error(`world memory snapshot duplicate ${label} ${row.id}`);
2054
+ ids.add(row.id);
2055
+ }
2056
+ return ids;
2057
+ };
2058
+ const episodeIds = unique("episode", episodes);
2059
+ const claimRootIds = unique("claim root", claimRoots);
2060
+ const claimIds = unique("claim", claims);
2061
+ const derivedIds = unique("derived", derived);
2062
+ const episodeTurnBundles = new Set();
2063
+ for (const episode of episodes) {
2064
+ if (episode.recordedWriteSeq === 0) {
2065
+ throw new Error(`world memory snapshot episode write sequence is zero for ${episode.id}`);
2066
+ }
2067
+ if (episode.worldTimeTo !== null && episode.worldTimeTo < episode.worldTimeFrom) {
2068
+ throw new Error(`world memory snapshot episode interval inverted for ${episode.id}`);
2069
+ }
2070
+ if (episodeTurnBundles.has(episode.turnBundleId)) {
2071
+ throw new Error(`world memory snapshot duplicate turn bundle ${episode.turnBundleId}`);
2072
+ }
2073
+ episodeTurnBundles.add(episode.turnBundleId);
2074
+ }
2075
+ const claimsByRoot = new Map();
2076
+ const claimRootRevisions = new Set();
2077
+ for (const claim of claims) {
2078
+ if (requireComplete && !claimRootIds.has(claim.rootId)) {
2079
+ throw new Error(`world memory snapshot claim root missing for ${claim.id}`);
2080
+ }
2081
+ if (claim.status !== "active" && claim.status !== "retracted") {
2082
+ throw new Error(`world memory snapshot claim status invalid for ${claim.id}`);
2083
+ }
2084
+ if (claim.recordedFromWriteSeq === 0 || claim.revision === 0) {
2085
+ throw new Error(`world memory snapshot claim sequence or revision is zero for ${claim.id}`);
2086
+ }
2087
+ for (const episodeId of claim.sourceEpisodeIds) {
2088
+ if (requireComplete && !episodeIds.has(episodeId)) {
2089
+ throw new Error(`world memory snapshot claim source episode missing for ${claim.id}`);
2090
+ }
2091
+ }
2092
+ if (claim.sourceEpisodeIds.length === 0 && claim.sourceMessageIds.length === 0) {
2093
+ throw new Error(`world memory snapshot claim sources empty for ${claim.id}`);
2094
+ }
2095
+ const revisionKey = `${claim.rootId}\u0000${claim.revision}`;
2096
+ if (claimRootRevisions.has(revisionKey)) {
2097
+ throw new Error(`world memory snapshot duplicate claim revision ${claim.rootId}:${claim.revision}`);
2098
+ }
2099
+ claimRootRevisions.add(revisionKey);
2100
+ const rootClaims = claimsByRoot.get(claim.rootId) ?? [];
2101
+ rootClaims.push(claim);
2102
+ claimsByRoot.set(claim.rootId, rootClaims);
2103
+ }
2104
+ if (requireComplete) {
2105
+ for (const root of claimRoots) {
2106
+ const rootClaims = claimsByRoot.get(root.id) ?? [];
2107
+ const latestRevision = rootClaims.reduce((latest, claim) => Math.max(latest, claim.revision), 0);
2108
+ if (root.latestRevision !== latestRevision) {
2109
+ throw new Error(`world memory snapshot claim root latest revision mismatch for ${root.id}`);
2110
+ }
2111
+ if (root.currentClaimId !== null) {
2112
+ const current = rootClaims.find((claim) => claim.id === root.currentClaimId);
2113
+ if (current === undefined
2114
+ || current.status !== "active"
2115
+ || current.revision !== latestRevision) {
2116
+ throw new Error(`world memory snapshot claim root current claim invalid for ${root.id}`);
2117
+ }
2118
+ }
2119
+ }
2120
+ }
2121
+ for (const row of derived) {
2122
+ if (row.status !== "active" && row.status !== "retracted") {
2123
+ throw new Error(`world memory snapshot derived status invalid for ${row.id}`);
2124
+ }
2125
+ if (row.coverageIds.length === 0) {
2126
+ throw new Error(`world memory snapshot derived coverage empty for ${row.id}`);
2127
+ }
2128
+ if (row.recordedWriteSeq === 0) {
2129
+ throw new Error(`world memory snapshot derived write sequence is zero for ${row.id}`);
2130
+ }
2131
+ }
2132
+ const endpointExists = (kind, source, id) => {
2133
+ switch (kind) {
2134
+ case "WmEpisodeSupportsClaim": return source ? episodeIds.has(id) : claimIds.has(id);
2135
+ case "WmClaimSupersedesClaim":
2136
+ case "WmClaimContradictsClaim": return claimIds.has(id);
2137
+ case "WmDerivedFromEpisode": return source ? derivedIds.has(id) : episodeIds.has(id);
2138
+ case "WmDerivedFromClaim": return source ? derivedIds.has(id) : claimIds.has(id);
2139
+ case "WmEpisodeMentionsEntity": return source ? episodeIds.has(id) : true;
2140
+ case "WmDerivedMentionsEntity": return source ? derivedIds.has(id) : true;
2141
+ case "WmClaimAboutEntity": return source ? claimIds.has(id) : true;
2142
+ }
2143
+ };
2144
+ const relationKeys = new Set();
2145
+ for (const relation of relations) {
2146
+ if (requireComplete
2147
+ && (!endpointExists(relation.kind, true, relation.sourceId)
2148
+ || !endpointExists(relation.kind, false, relation.targetId))) {
2149
+ throw new Error(`world memory snapshot relation endpoint missing ${relation.kind}:${relation.sourceId}:${relation.targetId}`);
2150
+ }
2151
+ const key = `${relation.kind}\u0000${relation.sourceId}\u0000${relation.targetId}`;
2152
+ if (relationKeys.has(key))
2153
+ throw new Error(`world memory snapshot duplicate relation ${key}`);
2154
+ relationKeys.add(key);
2155
+ }
2156
+ const compareId = (left, right) => left.id.localeCompare(right.id, "und");
2157
+ const compareClaim = (left, right) => left.rootId.localeCompare(right.rootId, "und")
2158
+ || left.revision - right.revision
2159
+ || left.id.localeCompare(right.id, "und");
2160
+ const compareRelation = (left, right) => left.kind.localeCompare(right.kind, "und")
2161
+ || left.sourceId.localeCompare(right.sourceId, "und")
2162
+ || left.targetId.localeCompare(right.targetId, "und");
2163
+ return Object.freeze({
2164
+ episodes: Object.freeze([...episodes].sort(compareId)),
2165
+ claimRoots: Object.freeze([...claimRoots].sort(compareId)),
2166
+ claims: Object.freeze([...claims].sort(compareClaim)),
2167
+ derived: Object.freeze([...derived].sort(compareId)),
2168
+ relations: Object.freeze([...relations].sort(compareRelation)),
2169
+ episodeVectorIndexBuilt: snapshot.episodeVectorIndexBuilt,
2170
+ episodeFtsIndexBuilt: snapshot.episodeFtsIndexBuilt,
2171
+ derivedVectorIndexBuilt: snapshot.derivedVectorIndexBuilt,
2172
+ derivedFtsIndexBuilt: snapshot.derivedFtsIndexBuilt
2173
+ });
2174
+ }
2175
+ async executeWorldMemoryRun(statement, params, duringRollback = false) {
2176
+ return duringRollback
2177
+ ? this.runDuringRollback(statement, params)
2178
+ : this.runWorldMemory(statement, params);
2179
+ }
2180
+ async writeMemoryEpisode(row, duringRollback = false) {
2181
+ const normalized = this.normalizeMemoryEpisode(row);
2182
+ if (normalized.worldTimeTo !== null
2183
+ && normalized.worldTimeTo < normalized.worldTimeFrom) {
2184
+ throw new Error("world memory episode world-time interval is inverted");
2185
+ }
2186
+ const conflicts = await this.executeWorldMemoryRun(`MATCH (e:${WM_EPISODE_TABLE} {turnBundleId: $turnBundleId}) ` +
2187
+ "WHERE e.id <> $id RETURN e.id AS id LIMIT 1;", { turnBundleId: normalized.turnBundleId, id: normalized.id }, duringRollback);
2188
+ if (conflicts.length > 0)
2189
+ throw new Error("world memory turn already has a different episode");
2190
+ await this.executeWorldMemoryRun(`MERGE (e:${WM_EPISODE_TABLE} {id: $id}) SET ` +
2191
+ "e.sourceMessageIds = $sourceMessageIds, e.turnBundleId = $turnBundleId, " +
2192
+ "e.synopsis = $synopsis, e.searchText = $searchText, " +
2193
+ "e.embedding = $embedding, e.sourceDigest = $sourceDigest, " +
2194
+ "e.worldTimeFrom = $worldTimeFrom, e.worldTimeTo = $worldTimeTo, " +
2195
+ "e.recordedWriteSeq = $recordedWriteSeq, e.reachability = $reachability, " +
2196
+ "e.model = $model, e.provider = $provider, e.attemptId = $attemptId, " +
2197
+ "e.schemaVersion = $schemaVersion;", {
2198
+ ...normalized,
2199
+ sourceMessageIds: JSON.stringify(normalized.sourceMessageIds),
2200
+ searchText: buildIndexedSearchText(normalized.synopsis),
2201
+ embedding: [...normalized.embedding]
2202
+ }, duringRollback);
2203
+ }
2204
+ async writeMemoryClaimRoot(row, duringRollback = false) {
2205
+ const normalized = this.normalizeMemoryClaimRoot(row);
2206
+ if (normalized.currentClaimId !== null) {
2207
+ const linked = await this.executeWorldMemoryRun(`MATCH (c:${WM_CLAIM_TABLE} {id: $claimId, rootId: $rootId}) RETURN c.id AS id;`, { claimId: normalized.currentClaimId, rootId: normalized.id }, duringRollback);
2208
+ if (linked.length !== 1) {
2209
+ throw new Error("world memory claim root current_claim_id is not a claim in this root");
2210
+ }
2211
+ }
2212
+ await this.executeWorldMemoryRun(`MERGE (r:${WM_CLAIM_ROOT_TABLE} {id: $id}) SET ` +
2213
+ "r.subjectKind = $subjectKind, r.entityId = $entityId, r.attrPath = $attrPath, " +
2214
+ "r.relationKey = $relationKey, r.currentClaimId = $currentClaimId, " +
2215
+ "r.latestRevision = $latestRevision, r.schemaVersion = $schemaVersion;", { ...normalized }, duringRollback);
2216
+ }
2217
+ async writeMemoryClaim(row, duringRollback = false) {
2218
+ const normalized = this.normalizeMemoryClaim(row);
2219
+ const digest = await sha256HexUtf8(normalized.canonicalValue);
2220
+ if (digest !== normalized.canonicalValueSha256) {
2221
+ throw new Error("world memory canonical_value_sha256 mismatch");
2222
+ }
2223
+ const roots = await this.executeWorldMemoryRun(`MATCH (r:${WM_CLAIM_ROOT_TABLE} {id: $rootId}) RETURN r.id AS id;`, { rootId: normalized.rootId }, duringRollback);
2224
+ if (roots.length !== 1)
2225
+ throw new Error("world memory claim root does not exist");
2226
+ const revisionConflicts = await this.executeWorldMemoryRun(`MATCH (c:${WM_CLAIM_TABLE} {rootId: $rootId, revision: $revision}) ` +
2227
+ "WHERE c.id <> $id RETURN c.id AS id LIMIT 1;", { rootId: normalized.rootId, revision: normalized.revision, id: normalized.id }, duringRollback);
2228
+ if (revisionConflicts.length > 0) {
2229
+ throw new Error("world memory claim root already has a different claim at this revision");
2230
+ }
2231
+ await this.executeWorldMemoryRun(`MERGE (c:${WM_CLAIM_TABLE} {id: $id}) SET ` +
2232
+ "c.rootId = $rootId, c.authorityClass = $authorityClass, c.status = $status, " +
2233
+ "c.canonicalValue = $canonicalValue, c.canonicalValueSha256 = $canonicalValueSha256, " +
2234
+ "c.confidence = $confidence, c.recordedFromWriteSeq = $recordedFromWriteSeq, " +
2235
+ "c.recordedToWriteSeq = $recordedToWriteSeq, " +
2236
+ "c.validFromWorldSeconds = $validFromWorldSeconds, c.validToWorldSeconds = $validToWorldSeconds, " +
2237
+ "c.sourceDigest = $sourceDigest, c.sourceEpisodeIds = $sourceEpisodeIds, " +
2238
+ "c.sourceMessageIds = $sourceMessageIds, c.revision = $revision, " +
2239
+ "c.schemaVersion = $schemaVersion;", {
2240
+ ...normalized,
2241
+ sourceEpisodeIds: JSON.stringify(normalized.sourceEpisodeIds),
2242
+ sourceMessageIds: JSON.stringify(normalized.sourceMessageIds)
2243
+ }, duringRollback);
2244
+ }
2245
+ async writeMemoryDerived(row, duringRollback = false) {
2246
+ const normalized = this.normalizeMemoryDerived(row);
2247
+ await this.executeWorldMemoryRun(`MERGE (d:${WM_DERIVED_TABLE} {id: $id}) SET ` +
2248
+ "d.derivedKind = $derivedKind, d.text = $text, d.searchText = $searchText, " +
2249
+ "d.embedding = $embedding, " +
2250
+ "d.authorityClass = $authorityClass, d.status = $status, d.sourceDigest = $sourceDigest, " +
2251
+ "d.coverageIds = $coverageIds, d.summarizedThroughOpId = $summarizedThroughOpId, " +
2252
+ "d.validFromWorldSeconds = $validFromWorldSeconds, d.validToWorldSeconds = $validToWorldSeconds, " +
2253
+ "d.recordedWriteSeq = $recordedWriteSeq, d.model = $model, d.provider = $provider, " +
2254
+ "d.promptVersion = $promptVersion, d.tokenizerVersion = $tokenizerVersion, " +
2255
+ "d.schemaVersion = $schemaVersion;", {
2256
+ ...normalized,
2257
+ coverageIds: JSON.stringify(normalized.coverageIds),
2258
+ searchText: buildIndexedSearchText(normalized.text),
2259
+ embedding: [...normalized.embedding]
2260
+ }, duringRollback);
2261
+ }
2262
+ async writeMemoryRelation(row, duringRollback = false) {
2263
+ const { kind, sourceId, targetId } = this.normalizeMemoryRelation(row);
2264
+ const definition = MEMORY_RELATION_DEFINITIONS[kind];
2265
+ const rows = await this.executeWorldMemoryRun(`MATCH (s:${definition.sourceTable} {id: $sourceId}), ` +
2266
+ `(d:${definition.targetTable} {id: $targetId}) ` +
2267
+ `MERGE (s)-[r:${kind}]->(d) RETURN s.id AS sourceId, d.id AS targetId;`, { sourceId, targetId }, duringRollback);
2268
+ if (rows.length !== 1)
2269
+ throw new Error(`world memory ${kind} endpoint is missing`);
2270
+ }
2271
+ async assertWorldMemoryExternalEntityReferences(snapshot, duringRollback = false) {
2272
+ const requiredIds = new Set();
2273
+ for (const root of snapshot.claimRoots) {
2274
+ if (root.entityId !== null)
2275
+ requiredIds.add(root.entityId);
2276
+ }
2277
+ for (const relation of snapshot.relations) {
2278
+ if (relation.kind === "WmEpisodeMentionsEntity"
2279
+ || relation.kind === "WmDerivedMentionsEntity"
2280
+ || relation.kind === "WmClaimAboutEntity") {
2281
+ requiredIds.add(relation.targetId);
2282
+ }
2283
+ }
2284
+ if (requiredIds.size === 0)
2285
+ return;
2286
+ const ids = [...requiredIds].sort((left, right) => left.localeCompare(right, "und"));
2287
+ const statement = `UNWIND $ids AS id MATCH (e:${ENTITY_TABLE} {id: id}) RETURN e.id AS id ORDER BY id;`;
2288
+ const rows = duringRollback
2289
+ ? await this.runDuringRollback(statement, { ids })
2290
+ : await this.runInternal(statement, { ids });
2291
+ const found = new Set(rows.map((row) => asString(row.id)));
2292
+ const missing = ids.filter((id) => !found.has(id));
2293
+ if (missing.length > 0) {
2294
+ throw new Error(`world memory snapshot entity references missing: ${missing.join(",")}`);
2295
+ }
2296
+ }
2297
+ async replaceWorldMemorySnapshot(input, duringRollback = false, requireComplete = true) {
2298
+ const snapshot = this.normalizeWorldMemorySnapshot(input, requireComplete);
2299
+ await Promise.all(snapshot.claims.map(async (claim) => {
2300
+ const digest = await sha256HexUtf8(claim.canonicalValue);
2301
+ if (digest !== claim.canonicalValueSha256) {
2302
+ throw new Error(`world memory canonical_value_sha256 mismatch for ${claim.id}`);
2303
+ }
2304
+ }));
2305
+ await this.assertWorldMemoryExternalEntityReferences(snapshot, duringRollback);
2306
+ await this.dropMemoryEpisodeIndexes(duringRollback);
2307
+ await this.dropMemoryDerivedIndexes(duringRollback);
2308
+ const query = async (statement) => {
2309
+ if (duringRollback)
2310
+ await this.queryDuringRollback(statement);
2311
+ else
2312
+ await this.queryWorldMemory(statement);
2313
+ };
2314
+ const run = async (statement, params) => {
2315
+ if (duringRollback)
2316
+ await this.runDuringRollback(statement, params);
2317
+ else
2318
+ await this.runWorldMemory(statement, params);
2319
+ };
2320
+ await query(`MATCH (n:${WM_CLAIM_ROOT_TABLE}) DETACH DELETE n;`);
2321
+ await query(`MATCH (n:${WM_CLAIM_TABLE}) DETACH DELETE n;`);
2322
+ await query(`MATCH (n:${WM_EPISODE_TABLE}) DETACH DELETE n;`);
2323
+ await query(`MATCH (n:${WM_DERIVED_TABLE}) DETACH DELETE n;`);
2324
+ if (snapshot.episodes.length > 0) {
2325
+ await run(`UNWIND $rows AS row CREATE (e:${WM_EPISODE_TABLE} {` +
2326
+ "id: row.id, sourceMessageIds: row.sourceMessageIds, turnBundleId: row.turnBundleId, " +
2327
+ "synopsis: row.synopsis, searchText: row.searchText, " +
2328
+ "embedding: row.embedding, sourceDigest: row.sourceDigest, " +
2329
+ "worldTimeFrom: row.worldTimeFrom, worldTimeTo: row.worldTimeTo, " +
2330
+ "recordedWriteSeq: row.recordedWriteSeq, reachability: row.reachability, " +
2331
+ "model: row.model, provider: row.provider, attemptId: row.attemptId, " +
2332
+ "schemaVersion: row.schemaVersion});", {
2333
+ rows: snapshot.episodes.map((row) => ({
2334
+ ...row,
2335
+ sourceMessageIds: JSON.stringify(row.sourceMessageIds),
2336
+ searchText: buildIndexedSearchText(row.synopsis),
2337
+ embedding: [...row.embedding]
2338
+ }))
2339
+ });
2340
+ }
2341
+ if (snapshot.claimRoots.length > 0) {
2342
+ await run(`UNWIND $rows AS row CREATE (r:${WM_CLAIM_ROOT_TABLE} {` +
2343
+ "id: row.id, subjectKind: row.subjectKind, entityId: row.entityId, " +
2344
+ "attrPath: row.attrPath, relationKey: row.relationKey, currentClaimId: NULL, " +
2345
+ "latestRevision: row.latestRevision, schemaVersion: row.schemaVersion});", { rows: snapshot.claimRoots.map((row) => ({ ...row })) });
2346
+ }
2347
+ if (snapshot.claims.length > 0) {
2348
+ await run(`UNWIND $rows AS row CREATE (c:${WM_CLAIM_TABLE} {` +
2349
+ "id: row.id, rootId: row.rootId, authorityClass: row.authorityClass, status: row.status, " +
2350
+ "canonicalValue: row.canonicalValue, canonicalValueSha256: row.canonicalValueSha256, " +
2351
+ "confidence: row.confidence, recordedFromWriteSeq: row.recordedFromWriteSeq, " +
2352
+ "recordedToWriteSeq: row.recordedToWriteSeq, validFromWorldSeconds: row.validFromWorldSeconds, " +
2353
+ "validToWorldSeconds: row.validToWorldSeconds, sourceDigest: row.sourceDigest, " +
2354
+ "sourceEpisodeIds: row.sourceEpisodeIds, sourceMessageIds: row.sourceMessageIds, " +
2355
+ "revision: row.revision, schemaVersion: row.schemaVersion});", {
2356
+ rows: snapshot.claims.map((row) => ({
2357
+ ...row,
2358
+ sourceEpisodeIds: JSON.stringify(row.sourceEpisodeIds),
2359
+ sourceMessageIds: JSON.stringify(row.sourceMessageIds)
2360
+ }))
2361
+ });
2362
+ }
2363
+ if (snapshot.derived.length > 0) {
2364
+ await run(`UNWIND $rows AS row CREATE (d:${WM_DERIVED_TABLE} {` +
2365
+ "id: row.id, derivedKind: row.derivedKind, text: row.text, searchText: row.searchText, " +
2366
+ "embedding: row.embedding, " +
2367
+ "authorityClass: row.authorityClass, status: row.status, sourceDigest: row.sourceDigest, " +
2368
+ "coverageIds: row.coverageIds, summarizedThroughOpId: row.summarizedThroughOpId, " +
2369
+ "validFromWorldSeconds: row.validFromWorldSeconds, validToWorldSeconds: row.validToWorldSeconds, " +
2370
+ "recordedWriteSeq: row.recordedWriteSeq, model: row.model, provider: row.provider, " +
2371
+ "promptVersion: row.promptVersion, tokenizerVersion: row.tokenizerVersion, " +
2372
+ "schemaVersion: row.schemaVersion});", {
2373
+ rows: snapshot.derived.map((row) => ({
2374
+ ...row,
2375
+ searchText: buildIndexedSearchText(row.text),
2376
+ embedding: [...row.embedding],
2377
+ coverageIds: JSON.stringify(row.coverageIds)
2378
+ }))
2379
+ });
2380
+ }
2381
+ for (const [kind, definition] of Object.entries(MEMORY_RELATION_DEFINITIONS)) {
2382
+ const rows = snapshot.relations
2383
+ .filter((row) => row.kind === kind)
2384
+ .map((row) => ({ sourceId: row.sourceId, targetId: row.targetId }));
2385
+ if (rows.length === 0)
2386
+ continue;
2387
+ await run(`UNWIND $rows AS row MATCH (s:${definition.sourceTable} {id: row.sourceId}), ` +
2388
+ `(d:${definition.targetTable} {id: row.targetId}) ` +
2389
+ `CREATE (s)-[:${kind}]->(d);`, { rows });
2390
+ }
2391
+ if (snapshot.claimRoots.length > 0) {
2392
+ await run(`UNWIND $rows AS row MATCH (r:${WM_CLAIM_ROOT_TABLE} {id: row.id}) ` +
2393
+ "SET r.currentClaimId = row.currentClaimId;", { rows: snapshot.claimRoots.map((row) => ({ id: row.id, currentClaimId: row.currentClaimId })) });
2394
+ }
2395
+ if (snapshot.episodeVectorIndexBuilt) {
2396
+ await this.executeWorldMemoryQuery(`CALL CREATE_VECTOR_INDEX('${WM_EPISODE_TABLE}', '${WM_EPISODE_VECTOR_INDEX}', ` +
2397
+ "'embedding', metric := 'cosine');", duringRollback);
2398
+ this.memoryEpisodeVectorIndexBuilt = true;
2399
+ }
2400
+ if (snapshot.episodeFtsIndexBuilt) {
2401
+ await this.executeWorldMemoryQuery(`CALL CREATE_FTS_INDEX('${WM_EPISODE_TABLE}', '${WM_EPISODE_FTS_INDEX}', ['searchText'], ` +
2402
+ "stemmer := 'none', tokenizer := 'simple');", duringRollback);
2403
+ this.memoryEpisodeFtsIndexBuilt = true;
2404
+ }
2405
+ if (snapshot.derivedVectorIndexBuilt) {
2406
+ await this.executeWorldMemoryQuery(`CALL CREATE_VECTOR_INDEX('${WM_DERIVED_TABLE}', '${WM_DERIVED_VECTOR_INDEX}', ` +
2407
+ "'embedding', metric := 'cosine');", duringRollback);
2408
+ this.memoryDerivedVectorIndexBuilt = true;
2409
+ }
2410
+ if (snapshot.derivedFtsIndexBuilt) {
2411
+ await this.executeWorldMemoryQuery(`CALL CREATE_FTS_INDEX('${WM_DERIVED_TABLE}', '${WM_DERIVED_FTS_INDEX}', ['searchText'], ` +
2412
+ "stemmer := 'none', tokenizer := 'simple');", duringRollback);
2413
+ this.memoryDerivedFtsIndexBuilt = true;
2414
+ }
2415
+ return snapshot;
2416
+ }
2417
+ async restoreWorldMemorySnapshot(snapshot) {
2418
+ await this.replaceWorldMemorySnapshot(snapshot, true, false);
2419
+ }
2420
+ async restoreWorldMemorySnapshotInTransaction(snapshot) {
2421
+ if (this.tx === null || this.graphRollbackInProgress) {
2422
+ throw new Error("world memory transaction restore requires active graph transaction");
2423
+ }
2424
+ await this.recordWorldMemoryUndo();
2425
+ const expected = await this.replaceWorldMemorySnapshot(snapshot);
2426
+ const readback = await this.snapshotWorldMemory();
2427
+ if (JSON.stringify(readback) !== JSON.stringify(expected)) {
2428
+ throw new Error("world memory transaction snapshot immutable readback mismatch");
2429
+ }
2430
+ }
2431
+ async restoreWorldMemorySnapshotFromHost(snapshot) {
2432
+ if (this.tx !== null || this.graphRollbackInProgress) {
2433
+ throw new Error("world memory snapshot restore requires exclusive transaction ownership");
2434
+ }
2435
+ this.beginTransaction();
2436
+ try {
2437
+ await this.recordWorldMemoryUndo();
2438
+ const expected = await this.replaceWorldMemorySnapshot(snapshot);
2439
+ const readback = await this.snapshotWorldMemory();
2440
+ if (JSON.stringify(readback) !== JSON.stringify(expected)) {
2441
+ throw new Error("world memory snapshot immutable readback mismatch");
2442
+ }
2443
+ this.commitTransaction();
2444
+ }
2445
+ catch (cause) {
2446
+ try {
2447
+ await this.rollbackTransaction();
2448
+ }
2449
+ catch (rollbackCause) {
2450
+ throw new AggregateError([cause, rollbackCause], "world memory snapshot restore and rollback both failed");
2451
+ }
2452
+ throw cause;
2453
+ }
2454
+ }
2455
+ async upsertMemoryEpisode(row) {
2456
+ await this.recordWorldMemoryUndo();
2457
+ await this.dropMemoryEpisodeIndexes();
2458
+ await this.writeMemoryEpisode(row);
2459
+ await this.rebuildMemoryEpisodeIndexes();
2460
+ }
2461
+ async upsertMemoryClaimRoot(row) {
2462
+ await this.recordWorldMemoryUndo();
2463
+ await this.writeMemoryClaimRoot(row);
2464
+ }
2465
+ async upsertMemoryClaim(row) {
2466
+ await this.recordWorldMemoryUndo();
2467
+ await this.writeMemoryClaim(row);
2468
+ }
2469
+ async upsertMemoryDerived(row) {
2470
+ await this.recordWorldMemoryUndo();
2471
+ await this.dropMemoryDerivedIndexes();
2472
+ await this.writeMemoryDerived(row);
2473
+ await this.rebuildMemoryDerivedIndexes();
2474
+ }
2475
+ async upsertMemoryRelation(row) {
2476
+ await this.recordWorldMemoryUndo();
2477
+ await this.writeMemoryRelation(row);
2478
+ }
2479
+ async getClaimRoot(id) {
2480
+ const claimRootId = assertMemoryIdentifier("claim_root.id", id);
2481
+ const rows = await this.runWorldMemory(`MATCH (r:${WM_CLAIM_ROOT_TABLE} {id: $id}) RETURN ` +
2482
+ "r.id AS id, r.subjectKind AS subjectKind, r.entityId AS entityId, r.attrPath AS attrPath, " +
2483
+ "r.relationKey AS relationKey, r.currentClaimId AS currentClaimId, " +
2484
+ "r.latestRevision AS latestRevision, r.schemaVersion AS schemaVersion;", { id: claimRootId });
2485
+ if (rows.length > 1)
2486
+ throw new Error("world memory claim root primary key returned multiple rows");
2487
+ return rows.length === 0 ? null : this.toMemoryClaimRoot(rows[0]);
2488
+ }
2489
+ async getClaimsByRoot(rootId) {
2490
+ const id = assertMemoryIdentifier("claim.root_id", rootId);
2491
+ const rows = await this.runWorldMemory(`MATCH (c:${WM_CLAIM_TABLE} {rootId: $rootId}) ` +
2492
+ `RETURN ${this.memoryClaimProjection("c")} ORDER BY revision, id;`, { rootId: id });
2493
+ return rows.map((row) => this.toMemoryClaim(row));
2494
+ }
2495
+ async resolveClaimsForEntities(entityIds) {
2496
+ const ids = snapshotCanonicalStringIds("entity_ids", entityIds, true);
2497
+ if (ids.length === 0)
2498
+ return [];
2499
+ const rows = await this.runWorldMemory(`UNWIND $entityIds AS entityId MATCH (c:${WM_CLAIM_TABLE})-` +
2500
+ `[:WmClaimAboutEntity]->(e:${ENTITY_TABLE} {id: entityId}) ` +
2501
+ `RETURN ${this.memoryClaimProjection("c")} ORDER BY rootId, revision, id;`, { entityIds: [...ids] });
2502
+ const claims = new Map();
2503
+ for (const row of rows) {
2504
+ const claim = this.toMemoryClaim(row);
2505
+ claims.set(claim.id, claim);
2506
+ }
2507
+ return [...claims.values()];
2508
+ }
2509
+ async vectorSearchEpisodes(query, topK, config) {
2510
+ assertTopK(topK);
2511
+ const embedding = snapshotMemoryEmbedding(query);
2512
+ if (!this.memoryEpisodeVectorIndexBuilt) {
2513
+ throw new Error("world memory episode vector index is unavailable");
2514
+ }
2515
+ const efs = clampInt(config?.efs, HNSW_EFS_MIN, HNSW_EFS_MAX);
2516
+ const efsArg = efs !== undefined ? `, efs := ${efs}` : "";
2517
+ const rows = await this.runWorldMemory(`CALL QUERY_VECTOR_INDEX('${WM_EPISODE_TABLE}', '${WM_EPISODE_VECTOR_INDEX}', ` +
2518
+ `$query, $topK${efsArg}) YIELD node AS memory, distance ` +
2519
+ "RETURN memory.id AS id, distance ORDER BY distance;", { query: [...embedding], topK });
2520
+ return rows.map((row) => Object.freeze({
2521
+ id: asString(row.id),
2522
+ distance: asNumber(row.distance)
2523
+ }));
2524
+ }
2525
+ async fullTextSearchEpisodes(query, topK, config) {
2526
+ assertTopK(topK);
2527
+ assertMemoryIdentifier("episode.full_text_query", query);
2528
+ if (!this.memoryEpisodeFtsIndexBuilt) {
2529
+ throw new Error("world memory episode FTS index is unavailable");
2530
+ }
2531
+ const rows = await this.runWorldMemory(`CALL QUERY_FTS_INDEX('${WM_EPISODE_TABLE}', '${WM_EPISODE_FTS_INDEX}', ` +
2532
+ `$query${this.buildFtsQueryArgs(config)}) ` +
2533
+ "RETURN node.id AS id, score ORDER BY score DESC LIMIT $topK;", { query: buildIndexedSearchText(query), topK });
2534
+ return rows.map((row) => Object.freeze({ id: asString(row.id), score: asNumber(row.score) }));
2535
+ }
2536
+ async vectorSearchDerived(query, topK, config) {
2537
+ assertTopK(topK);
2538
+ const embedding = snapshotMemoryEmbedding(query);
2539
+ if (!this.memoryDerivedVectorIndexBuilt) {
2540
+ throw new Error("world memory derived vector index is unavailable");
2541
+ }
2542
+ const efs = clampInt(config?.efs, HNSW_EFS_MIN, HNSW_EFS_MAX);
2543
+ const efsArg = efs !== undefined ? `, efs := ${efs}` : "";
2544
+ const rows = await this.runWorldMemory(`CALL QUERY_VECTOR_INDEX('${WM_DERIVED_TABLE}', '${WM_DERIVED_VECTOR_INDEX}', ` +
2545
+ `$query, $topK${efsArg}) YIELD node AS memory, distance ` +
2546
+ "RETURN memory.id AS id, distance ORDER BY distance;", { query: [...embedding], topK });
2547
+ return rows.map((row) => Object.freeze({
2548
+ id: asString(row.id),
2549
+ distance: asNumber(row.distance)
2550
+ }));
2551
+ }
2552
+ async fullTextSearchDerived(query, topK, config) {
2553
+ assertTopK(topK);
2554
+ assertMemoryIdentifier("derived.full_text_query", query);
2555
+ if (!this.memoryDerivedFtsIndexBuilt) {
2556
+ throw new Error("world memory derived FTS index is unavailable");
2557
+ }
2558
+ const rows = await this.runWorldMemory(`CALL QUERY_FTS_INDEX('${WM_DERIVED_TABLE}', '${WM_DERIVED_FTS_INDEX}', ` +
2559
+ `$query${this.buildFtsQueryArgs(config)}) ` +
2560
+ "RETURN node.id AS id, score ORDER BY score DESC LIMIT $topK;", { query: buildIndexedSearchText(query), topK });
2561
+ return rows.map((row) => Object.freeze({ id: asString(row.id), score: asNumber(row.score) }));
2562
+ }
2563
+ async deleteMemoryClosure(closure) {
2564
+ const episodeIds = snapshotMemoryClosureIds("closure.episode_ids", closure.episodeIds);
2565
+ const claimRootIds = snapshotMemoryClosureIds("closure.claim_root_ids", closure.claimRootIds);
2566
+ const claimIds = snapshotMemoryClosureIds("closure.claim_ids", closure.claimIds);
2567
+ const derivedIds = snapshotMemoryClosureIds("closure.derived_ids", closure.derivedIds);
2568
+ if (episodeIds.length + claimRootIds.length + claimIds.length + derivedIds.length === 0) {
2569
+ return { deletedEpisodes: 0, deletedClaimRoots: 0, deletedClaims: 0, deletedDerived: 0 };
2570
+ }
2571
+ await this.recordWorldMemoryUndo();
2572
+ if (episodeIds.length > 0)
2573
+ await this.dropMemoryEpisodeIndexes();
2574
+ if (derivedIds.length > 0)
2575
+ await this.dropMemoryDerivedIndexes();
2576
+ const count = async (table, ids) => {
2577
+ if (ids.length === 0)
2578
+ return 0;
2579
+ const rows = await this.runWorldMemory(`UNWIND $ids AS id MATCH (n:${table} {id: id}) RETURN count(n) AS n;`, { ids: [...ids] });
2580
+ return rows.length === 0 ? 0 : asNumber(rows[0].n);
2581
+ };
2582
+ const deletedEpisodes = await count(WM_EPISODE_TABLE, episodeIds);
2583
+ const deletedClaimRoots = await count(WM_CLAIM_ROOT_TABLE, claimRootIds);
2584
+ const deletedClaims = await count(WM_CLAIM_TABLE, claimIds);
2585
+ const deletedDerived = await count(WM_DERIVED_TABLE, derivedIds);
2586
+ if (claimIds.length > 0) {
2587
+ await this.runWorldMemory(`UNWIND $ids AS id MATCH (r:${WM_CLAIM_ROOT_TABLE}) ` +
2588
+ "WHERE r.currentClaimId = id SET r.currentClaimId = NULL;", { ids: [...claimIds] });
2589
+ }
2590
+ for (const [table, ids] of [
2591
+ [WM_DERIVED_TABLE, derivedIds],
2592
+ [WM_CLAIM_TABLE, claimIds],
2593
+ [WM_CLAIM_ROOT_TABLE, claimRootIds],
2594
+ [WM_EPISODE_TABLE, episodeIds]
2595
+ ]) {
2596
+ if (ids.length === 0)
2597
+ continue;
2598
+ await this.runWorldMemory(`UNWIND $ids AS id MATCH (n:${table} {id: id}) DETACH DELETE n;`, { ids: [...ids] });
2599
+ }
2600
+ if (episodeIds.length > 0)
2601
+ await this.rebuildMemoryEpisodeIndexes();
2602
+ if (derivedIds.length > 0)
2603
+ await this.rebuildMemoryDerivedIndexes();
2604
+ return { deletedEpisodes, deletedClaimRoots, deletedClaims, deletedDerived };
2605
+ }
2606
+ async transcriptPk(scope, messageId) {
2607
+ return sha256HexUtf8(`ladybug-transcript-message-v1\u0000${JSON.stringify([
2608
+ scope.scenarioId,
2609
+ scope.saveId,
2610
+ scope.worldlineId,
2611
+ messageId
2612
+ ])}`);
2613
+ }
2614
+ toTranscriptMessage(row) {
2615
+ return validateTranscriptRecord({
2616
+ scenarioId: asString(row.scenarioId),
2617
+ saveId: asString(row.saveId),
2618
+ worldlineId: asString(row.worldlineId),
2619
+ messageId: asString(row.messageId),
2620
+ turnId: asString(row.turnId),
2621
+ role: assertTranscriptRole(asString(row.role)),
2622
+ content: asString(row.content),
2623
+ status: assertTranscriptStatus(asString(row.status)),
2624
+ createdAtMs: asNumber(row.createdAtMs),
2625
+ updatedAtMs: asNumber(row.updatedAtMs),
2626
+ sourceLedgerSeq: asNumber(row.sourceLedgerSeq)
2627
+ });
2628
+ }
2629
+ async writeTranscriptRecord(record, duringRollback = false) {
2630
+ const validated = validateTranscriptRecord(record);
2631
+ const pk = await this.transcriptPk(validated, validated.messageId);
2632
+ const statement = `MERGE (m:${TRANSCRIPT_MESSAGE_TABLE} {pk: $pk}) ` +
2633
+ "SET m.scenarioId = $scenarioId, m.saveId = $saveId, m.worldlineId = $worldlineId, " +
2634
+ "m.messageId = $messageId, m.turnId = $turnId, m.role = $role, m.content = $content, " +
2635
+ "m.searchText = $searchText, m.status = $status, m.createdAtMs = $createdAtMs, m.updatedAtMs = $updatedAtMs, " +
2636
+ "m.sourceLedgerSeq = $sourceLedgerSeq;";
2637
+ const params = { pk, ...validated, searchText: buildIndexedSearchText(validated.content) };
2638
+ if (duringRollback)
2639
+ await this.runDuringRollback(statement, params);
2640
+ else
2641
+ await this.runInternal(statement, params);
2642
+ }
2643
+ async readTranscriptByPk(pk) {
2644
+ const rows = await this.runInternal(`MATCH (m:${TRANSCRIPT_MESSAGE_TABLE} {pk: $pk}) ` +
2645
+ "RETURN m.scenarioId AS scenarioId, m.saveId AS saveId, m.worldlineId AS worldlineId, " +
2646
+ "m.messageId AS messageId, m.turnId AS turnId, m.role AS role, m.content AS content, " +
2647
+ "m.status AS status, m.createdAtMs AS createdAtMs, m.updatedAtMs AS updatedAtMs, " +
2648
+ "m.sourceLedgerSeq AS sourceLedgerSeq;", { pk });
2649
+ if (rows.length > 1)
2650
+ throw new Error("transcript primary key returned multiple rows");
2651
+ return rows.length === 0 ? null : this.toTranscriptMessage(rows[0]);
2652
+ }
2653
+ async listTranscriptScope(scope, duringRollback = false) {
2654
+ assertTranscriptScope(scope);
2655
+ const statement = `MATCH (m:${TRANSCRIPT_MESSAGE_TABLE} {scenarioId: $scenarioId, saveId: $saveId, worldlineId: $worldlineId}) ` +
2656
+ "RETURN m.scenarioId AS scenarioId, m.saveId AS saveId, m.worldlineId AS worldlineId, " +
2657
+ "m.messageId AS messageId, m.turnId AS turnId, m.role AS role, m.content AS content, " +
2658
+ "m.status AS status, m.createdAtMs AS createdAtMs, m.updatedAtMs AS updatedAtMs, " +
2659
+ "m.sourceLedgerSeq AS sourceLedgerSeq ORDER BY sourceLedgerSeq, messageId;";
2660
+ const rows = duringRollback
2661
+ ? await this.runDuringRollback(statement, { ...scope })
2662
+ : await this.runInternal(statement, { ...scope });
2663
+ const records = rows.map((row) => this.toTranscriptMessage(row));
2664
+ assertTranscriptRecordSetIdentities(records);
2665
+ return records;
2666
+ }
2667
+ async restoreTranscriptScope(scope, records, duringRollback = false) {
2668
+ const authorizedScope = snapshotTranscriptScope(scope);
2669
+ for (const record of records) {
2670
+ if (record.scenarioId !== authorizedScope.scenarioId
2671
+ || record.saveId !== authorizedScope.saveId
2672
+ || record.worldlineId !== authorizedScope.worldlineId) {
2673
+ throw new Error("transcript scope restore record mismatch");
2674
+ }
2675
+ }
2676
+ await this.invalidateTranscriptFtsIndex(duringRollback);
2677
+ const deleteStatement = `MATCH (m:${TRANSCRIPT_MESSAGE_TABLE} ` +
2678
+ "{scenarioId: $scenarioId, saveId: $saveId, worldlineId: $worldlineId}) DETACH DELETE m;";
2679
+ if (duringRollback)
2680
+ await this.runDuringRollback(deleteStatement, { ...authorizedScope });
2681
+ else
2682
+ await this.runInternal(deleteStatement, { ...authorizedScope });
2683
+ for (const record of records)
2684
+ await this.writeTranscriptRecord(record, duringRollback);
2685
+ const readback = await this.listTranscriptScope(authorizedScope, duringRollback);
2686
+ if (readback.length !== records.length
2687
+ || records.some((record) => !readback.some((candidate) => transcriptRecordMatches(candidate, record)))) {
2688
+ throw new Error("transcript scope restore readback mismatch");
2689
+ }
2690
+ }
2691
+ async invalidateTranscriptFtsIndex(duringRollback = false) {
2692
+ if (!this.transcriptFtsBuilt)
2693
+ return;
2694
+ const statement = `CALL DROP_FTS_INDEX('${TRANSCRIPT_MESSAGE_TABLE}', '${TRANSCRIPT_FTS_INDEX}');`;
2695
+ if (duringRollback)
2696
+ await this.queryDuringRollback(statement);
2697
+ else
2698
+ await this.queryInternal(statement);
2699
+ this.transcriptFtsBuilt = false;
2700
+ }
2701
+ async ensureTranscriptFtsIndex() {
2702
+ if (this.transcriptFtsBuilt)
2703
+ return;
2704
+ await this.queryInternal(`CALL CREATE_FTS_INDEX('${TRANSCRIPT_MESSAGE_TABLE}', '${TRANSCRIPT_FTS_INDEX}', ['searchText'], ` +
2705
+ "stemmer := 'none', tokenizer := 'simple');");
2706
+ this.transcriptFtsBuilt = true;
2707
+ }
2708
+ async upsertTranscriptMessage(request) {
2709
+ const result = await this.upsertTranscriptMessages({
2710
+ scope: request.scope,
2711
+ journal: request.journal,
2712
+ messages: [request.message]
2713
+ });
2714
+ const record = result.messages[0];
2715
+ if (!record)
2716
+ throw new Error("transcript single upsert returned no record");
2717
+ return record;
2718
+ }
2719
+ async upsertTranscriptMessages(request) {
2720
+ return this.mutateTranscriptMessagesBatch({
2721
+ scope: request.scope,
2722
+ journal: request.journal,
2723
+ messages: request.messages
2724
+ });
2725
+ }
2726
+ /**
2727
+ * Applies the pending journal's suffix operation and replacement/upsert cohort
2728
+ * under one bridge lock. A failure restores the exact pre-operation scope;
2729
+ * process loss restores the last SAV-backed in-memory database checkpoint.
2730
+ */
2731
+ async mutateTranscriptMessagesBatch(request) {
2732
+ const scope = snapshotTranscriptScope(request.scope);
2733
+ const journal = snapshotTranscriptJournalIdentity(request.journal);
2734
+ const records = snapshotTranscriptMutationRecords(scope, request.messages);
2735
+ if (records.length === 0 && request.suffix === undefined) {
2736
+ throw new Error("transcript mutation batch must not be empty");
2737
+ }
2738
+ const batchMessageIds = new Set();
2739
+ const batchLedgerSeqs = new Set();
2740
+ for (const record of records) {
2741
+ if (batchMessageIds.has(record.messageId)) {
2742
+ throw new Error("transcript mutation batch contains duplicate message_id");
2743
+ }
2744
+ if (batchLedgerSeqs.has(record.sourceLedgerSeq)) {
2745
+ throw new Error("transcript mutation batch contains duplicate source_ledger_seq");
2746
+ }
2747
+ batchMessageIds.add(record.messageId);
2748
+ batchLedgerSeqs.add(record.sourceLedgerSeq);
2749
+ }
2750
+ const suffix = request.suffix === undefined
2751
+ ? null
2752
+ : snapshotTranscriptSuffixSpec(request.suffix);
2753
+ return this.withTranscriptLock(async () => this.withTranscriptOperationIsolation(async () => {
2754
+ const mutationPayloadSha256 = await transcriptMutationPayloadSha256FromRecords(scope, records, suffix);
2755
+ const authorityBefore = await this.resolveTranscriptAuthority(scope, "mutation", journal, records.map((record) => record.messageId));
2756
+ if (authorityBefore.mutationPayloadSha256 !== mutationPayloadSha256) {
2757
+ throw new Error("transcript mutation payload does not match journal authority");
2758
+ }
2759
+ if (authorityBefore.requiredUpserts.length !== records.length
2760
+ || records.some((record, index) => {
2761
+ const required = authorityBefore.requiredUpserts[index];
2762
+ return required === undefined
2763
+ || required.messageId !== record.messageId
2764
+ || required.sourceLedgerSeq !== record.sourceLedgerSeq;
2765
+ })) {
2766
+ throw new Error("transcript mutation batch does not match journal-sealed upserts");
2767
+ }
2768
+ if (!sameTranscriptSuffixSpec(authorityBefore.suffix, suffix)) {
2769
+ throw new Error("transcript mutation suffix does not match journal authority");
2770
+ }
2771
+ const before = await this.listTranscriptScope(scope);
2772
+ await this.assertTranscriptReachabilityCoverage(scope, authorityBefore.activeReachableMessageIds, batchMessageIds, before);
2773
+ const authorizedMutationSet = new Set(authorityBefore.authorizedMutationMessageIds);
2774
+ const beforeByMessageId = new Map(before.map((record) => [record.messageId, record]));
2775
+ const beforeLedgerOwner = new Map(before.map((record) => [record.sourceLedgerSeq, record.messageId]));
2776
+ for (const record of records) {
2777
+ const original = beforeByMessageId.get(record.messageId);
2778
+ if (original !== undefined && !transcriptIdentityMatches(original, record)) {
2779
+ throw new Error("transcript immutable first-ledger identity mismatch");
2780
+ }
2781
+ const ledgerOwner = beforeLedgerOwner.get(record.sourceLedgerSeq);
2782
+ if (ledgerOwner !== undefined && ledgerOwner !== record.messageId) {
2783
+ throw new Error("transcript source_ledger_seq identity collision");
2784
+ }
2785
+ }
2786
+ let affected = [];
2787
+ if (suffix !== null) {
2788
+ if (suffix.afterSourceLedgerSeq > 0) {
2789
+ const pivotMatches = before.filter((record) => authorizedMutationSet.has(record.messageId)
2790
+ && record.sourceLedgerSeq === suffix.afterSourceLedgerSeq);
2791
+ if (pivotMatches.length !== 1) {
2792
+ throw new Error("transcript suffix pivot is not uniquely journal-authorized");
2793
+ }
2794
+ }
2795
+ affected = before.filter((record) => authorizedMutationSet.has(record.messageId)
2796
+ && record.sourceLedgerSeq > suffix.afterSourceLedgerSeq
2797
+ && (suffix.mode === "delete" || record.status !== "tombstoned"));
2798
+ if (affected.some((record) => suffix.updatedAtMs < record.updatedAtMs)) {
2799
+ throw new Error("transcript suffix updated_at_ms regression");
2800
+ }
2801
+ }
2802
+ const simulated = new Map(before.map((record) => [record.messageId, record]));
2803
+ if (suffix !== null) {
2804
+ for (const record of affected) {
2805
+ if (suffix.mode === "delete")
2806
+ simulated.delete(record.messageId);
2807
+ else
2808
+ simulated.set(record.messageId, Object.freeze({
2809
+ ...record,
2810
+ status: "tombstoned",
2811
+ updatedAtMs: suffix.updatedAtMs
2812
+ }));
2813
+ }
2814
+ }
2815
+ for (const record of records) {
2816
+ const existing = simulated.get(record.messageId);
2817
+ if (existing !== undefined) {
2818
+ if (!transcriptIdentityMatches(existing, record)) {
2819
+ throw new Error("transcript immutable first-ledger identity mismatch");
2820
+ }
2821
+ if (existing.status === "tombstoned" && record.status !== "tombstoned") {
2822
+ throw new Error("transcript tombstone resurrection forbidden");
2823
+ }
2824
+ if (record.updatedAtMs < existing.updatedAtMs) {
2825
+ throw new Error("transcript updated_at_ms regression");
2826
+ }
2827
+ if (record.updatedAtMs === existing.updatedAtMs && !transcriptRecordMatches(existing, record)) {
2828
+ throw new Error("transcript idempotency collision");
2829
+ }
2830
+ }
2831
+ simulated.set(record.messageId, record);
2832
+ }
2833
+ let mutationStarted = false;
2834
+ const scopeUndo = { kind: "transcript-restore-scope", scope, records: before };
2835
+ try {
2836
+ if (affected.length > 0 && suffix !== null) {
2837
+ mutationStarted = true;
2838
+ await this.invalidateTranscriptFtsIndex();
2839
+ if (suffix.mode === "tombstone") {
2840
+ await this.runInternal(`MATCH (m:${TRANSCRIPT_MESSAGE_TABLE} ` +
2841
+ "{scenarioId: $scenarioId, saveId: $saveId, worldlineId: $worldlineId}) " +
2842
+ "WHERE m.messageId IN $authorized AND m.sourceLedgerSeq > $pivot AND m.status <> 'tombstoned' " +
2843
+ "SET m.status = 'tombstoned', m.updatedAtMs = $updatedAtMs;", {
2844
+ ...scope,
2845
+ authorized: authorityBefore.authorizedMutationMessageIds,
2846
+ pivot: suffix.afterSourceLedgerSeq,
2847
+ updatedAtMs: suffix.updatedAtMs
2848
+ });
2849
+ }
2850
+ else {
2851
+ await this.runInternal(`MATCH (m:${TRANSCRIPT_MESSAGE_TABLE} ` +
2852
+ "{scenarioId: $scenarioId, saveId: $saveId, worldlineId: $worldlineId}) " +
2853
+ "WHERE m.messageId IN $authorized AND m.sourceLedgerSeq > $pivot DETACH DELETE m;", {
2854
+ ...scope,
2855
+ authorized: authorityBefore.authorizedMutationMessageIds,
2856
+ pivot: suffix.afterSourceLedgerSeq
2857
+ });
2858
+ }
2859
+ }
2860
+ if (records.length > 0) {
2861
+ if (!mutationStarted)
2862
+ await this.invalidateTranscriptFtsIndex();
2863
+ mutationStarted = true;
2864
+ for (const record of records) {
2865
+ const existing = await this.readTranscriptByPk(await this.transcriptPk(scope, record.messageId));
2866
+ if (existing === null || !transcriptRecordMatches(existing, record)) {
2867
+ await this.writeTranscriptRecord(record);
2868
+ }
2869
+ }
2870
+ }
2871
+ const readback = await this.listTranscriptScope(scope);
2872
+ await this.assertTranscriptReachabilityCoverage(scope, authorityBefore.activeReachableMessageIds, new Set(), readback);
2873
+ const resultMessages = records.map((record) => {
2874
+ const current = readback.find((candidate) => candidate.messageId === record.messageId);
2875
+ if (current === undefined || !transcriptRecordMatches(current, record)) {
2876
+ throw new Error("transcript batch upsert readback mismatch");
2877
+ }
2878
+ return current;
2879
+ });
2880
+ const affectedIds = affected.map((record) => record.messageId);
2881
+ const readbackById = new Map(readback.map((record) => [record.messageId, record]));
2882
+ const invalidSuffix = affectedIds.some((messageId) => {
2883
+ if (batchMessageIds.has(messageId))
2884
+ return false;
2885
+ const current = readbackById.get(messageId);
2886
+ return suffix?.mode === "delete" ? current !== undefined : current?.status !== "tombstoned";
2887
+ });
2888
+ if (invalidSuffix)
2889
+ throw new Error("transcript suffix mutation readback mismatch");
2890
+ const authorityAfter = await this.resolveTranscriptAuthority(scope, "mutation", journal, records.map((record) => record.messageId));
2891
+ if (!sameTranscriptAuthority(authorityBefore, authorityAfter)) {
2892
+ throw new Error("transcript authority changed during mutation");
2893
+ }
2894
+ const activeReachableMessageIds = Object.freeze([...authorityAfter.activeReachableMessageIds]);
2895
+ const upsertedMessageIds = Object.freeze(records.map((record) => record.messageId));
2896
+ const upserts = Object.freeze(records.map((record) => Object.freeze({
2897
+ messageId: record.messageId,
2898
+ sourceLedgerSeq: record.sourceLedgerSeq
2899
+ })));
2900
+ const suffixAffectedMessageIds = Object.freeze([...affectedIds]);
2901
+ const receiptImage = JSON.stringify([
2902
+ scope.scenarioId,
2903
+ scope.saveId,
2904
+ scope.worldlineId,
2905
+ journal.idempotencyKey,
2906
+ journal.payloadSha256,
2907
+ journal.phase,
2908
+ journal.phaseRevision,
2909
+ authorityAfter.lineageDigest,
2910
+ mutationPayloadSha256,
2911
+ activeReachableMessageIds,
2912
+ upserts,
2913
+ suffix === null
2914
+ ? null
2915
+ : [suffix.afterSourceLedgerSeq, suffix.mode, suffix.updatedAtMs],
2916
+ suffixAffectedMessageIds
2917
+ ]);
2918
+ const receipt = Object.freeze({
2919
+ journal,
2920
+ lineageDigest: authorityAfter.lineageDigest,
2921
+ mutationPayloadSha256,
2922
+ activeReachableMessageIds,
2923
+ upsertedMessageIds,
2924
+ upserts,
2925
+ suffix,
2926
+ suffixMode: suffix?.mode ?? null,
2927
+ suffixAffectedMessageIds,
2928
+ receiptSha256: await sha256HexUtf8(`ladybug-transcript-mutation-receipt-v1\u0000${receiptImage}`)
2929
+ });
2930
+ return Object.freeze({ messages: Object.freeze(resultMessages), receipt });
2931
+ }
2932
+ catch (error) {
2933
+ if (!mutationStarted)
2934
+ throw error;
2935
+ try {
2936
+ await this.restoreTranscriptScope(scopeUndo.scope, scopeUndo.records);
2937
+ }
2938
+ catch (recoveryError) {
2939
+ throw new AggregateError([error, recoveryError], "transcript batch mutation recovery failed");
2940
+ }
2941
+ throw error;
2942
+ }
2943
+ }));
2944
+ }
2945
+ async recallTranscriptMessages(request) {
2946
+ const scope = snapshotTranscriptScope(request.scope);
2947
+ const query = request.query.trim();
2948
+ if (query.length === 0)
2949
+ throw new Error("transcript recall query must not be empty");
2950
+ const searchQuery = buildIndexedSearchText(query);
2951
+ if (searchQuery.length === 0) {
2952
+ throw new Error(`transcript recall query has no searchable text: ${JSON.stringify(query)}`);
2953
+ }
2954
+ const topK = assertTopK(request.topK);
2955
+ return this.withTranscriptLock(async () => this.withTranscriptOperationIsolation(async () => {
2956
+ const authorityBefore = await this.resolveTranscriptAuthority(scope, "recall", null);
2957
+ const reachable = authorityBefore.activeReachableMessageIds;
2958
+ await this.assertTranscriptReachabilityCoverage(scope, reachable);
2959
+ if (reachable.length === 0) {
2960
+ const authorityAfter = await this.resolveTranscriptAuthority(scope, "recall", null);
2961
+ if (!sameTranscriptAuthority(authorityBefore, authorityAfter)) {
2962
+ throw new Error("transcript authority changed during recall");
2963
+ }
2964
+ return [];
2965
+ }
2966
+ await this.ensureTranscriptFtsIndex();
2967
+ const rows = await this.runInternal(`CALL QUERY_FTS_INDEX('${TRANSCRIPT_MESSAGE_TABLE}', '${TRANSCRIPT_FTS_INDEX}', $query) ` +
2968
+ "WHERE node.scenarioId = $scenarioId AND node.saveId = $saveId " +
2969
+ "AND node.worldlineId = $worldlineId AND node.messageId IN $reachable " +
2970
+ "AND node.status = 'done' " +
2971
+ "RETURN node.scenarioId AS scenarioId, node.saveId AS saveId, " +
2972
+ "node.worldlineId AS worldlineId, node.messageId AS messageId, node.turnId AS turnId, " +
2973
+ "node.role AS role, node.content AS content, node.status AS status, " +
2974
+ "node.createdAtMs AS createdAtMs, node.updatedAtMs AS updatedAtMs, " +
2975
+ "node.sourceLedgerSeq AS sourceLedgerSeq, score ORDER BY score DESC LIMIT $topK;", { query: searchQuery, topK, reachable, ...scope });
2976
+ const reachableSet = new Set(reachable);
2977
+ const hits = rows.map((row) => {
2978
+ const record = this.toTranscriptMessage(row);
2979
+ if (record.scenarioId !== scope.scenarioId
2980
+ || record.saveId !== scope.saveId
2981
+ || record.worldlineId !== scope.worldlineId
2982
+ || !reachableSet.has(record.messageId)
2983
+ || record.status !== "done") {
2984
+ throw new Error("transcript recall scope violation");
2985
+ }
2986
+ return { ...record, score: asNumber(row.score) };
2987
+ });
2988
+ const authorityAfter = await this.resolveTranscriptAuthority(scope, "recall", null);
2989
+ if (!sameTranscriptAuthority(authorityBefore, authorityAfter)) {
2990
+ throw new Error("transcript authority changed during recall");
2991
+ }
2992
+ return hits;
2993
+ }));
2994
+ }
2995
+ async mutateTranscriptSuffix(input) {
2996
+ const result = await this.mutateTranscriptMessagesBatch({
2997
+ scope: input.scope,
2998
+ journal: input.journal,
2999
+ messages: [],
3000
+ suffix: {
3001
+ afterSourceLedgerSeq: input.afterSourceLedgerSeq,
3002
+ mode: input.mode,
3003
+ updatedAtMs: input.updatedAtMs
3004
+ }
3005
+ });
3006
+ return Object.freeze({
3007
+ mode: input.mode,
3008
+ affectedMessageIds: result.receipt.suffixAffectedMessageIds,
3009
+ receipt: result.receipt
3010
+ });
3011
+ }
3012
+ latchPk(scenarioId, leafId) {
3013
+ return `${scenarioId}${leafId}`;
3014
+ }
3015
+ async latchLoad(scenarioId) {
3016
+ const rows = await this.runInternal(`MATCH (l:${LATCH_TABLE} {scenarioId: $sid}) ` +
3017
+ "RETURN l.scenarioId AS scenario_id, l.leafId AS leaf_id, l.state AS state, " +
3018
+ "l.firstFiredAtMs AS first_fired_at_ms, l.lastEvaluatedAtMs AS last_evaluated_at_ms, " +
3019
+ "l.fireCount AS fire_count, l.source AS source, l.confidence AS confidence;", { sid: scenarioId });
3020
+ return rows.map((row) => ({
3021
+ scenario_id: asString(row.scenario_id),
3022
+ leaf_id: asString(row.leaf_id),
3023
+ state: asLatchState(row.state),
3024
+ first_fired_at_ms: asNumber(row.first_fired_at_ms),
3025
+ last_evaluated_at_ms: asNumber(row.last_evaluated_at_ms),
3026
+ fire_count: asNumber(row.fire_count),
3027
+ source: asLatchSource(row.source),
3028
+ confidence: asNumber(row.confidence)
3029
+ }));
3030
+ }
3031
+ async latchCommitBatch(scenarioId, rows) {
3032
+ if (rows.length === 0)
3033
+ return 0;
3034
+ await this.recordLatchScenarioUndo(scenarioId);
3035
+ const payload = rows.map((row) => ({
3036
+ pk: this.latchPk(scenarioId, row.leaf_id),
3037
+ scenarioId,
3038
+ leafId: row.leaf_id,
3039
+ state: row.state,
3040
+ firstFiredAtMs: row.first_fired_at_ms,
3041
+ lastEvaluatedAtMs: row.last_evaluated_at_ms,
3042
+ fireCount: row.fire_count,
3043
+ source: row.source,
3044
+ confidence: row.confidence
3045
+ }));
3046
+ await this.runInternal(`UNWIND $rows AS row MERGE (l:${LATCH_TABLE} {pk: row.pk}) ` +
3047
+ "SET l.scenarioId = row.scenarioId, l.leafId = row.leafId, l.state = row.state, " +
3048
+ "l.firstFiredAtMs = row.firstFiredAtMs, l.lastEvaluatedAtMs = row.lastEvaluatedAtMs, " +
3049
+ "l.fireCount = row.fireCount, l.source = row.source, l.confidence = row.confidence;", { rows: payload });
3050
+ return rows.length;
3051
+ }
3052
+ async latchClear(scenarioId) {
3053
+ await this.recordLatchScenarioUndo(scenarioId);
3054
+ await this.runInternal(`MATCH (l:${LATCH_TABLE} {scenarioId: $sid}) DETACH DELETE l;`, { sid: scenarioId });
3055
+ }
3056
+ async latchRollback(scenarioId, leafIds) {
3057
+ if (leafIds.length === 0)
3058
+ return;
3059
+ await this.recordLatchScenarioUndo(scenarioId);
3060
+ const pks = leafIds.map((lid) => this.latchPk(scenarioId, lid));
3061
+ await this.runInternal(`UNWIND $pks AS p MATCH (l:${LATCH_TABLE} {pk: p}) DETACH DELETE l;`, { pks });
3062
+ }
3063
+ async upsertAttrHistory(rows) {
3064
+ if (rows.length === 0)
3065
+ return;
3066
+ const touched = new Set(rows.map((row) => `${row.entityId}\u0000${row.attrPath}`));
3067
+ for (const identity of touched) {
3068
+ const separator = identity.indexOf("\u0000");
3069
+ await this.recordAttrHistoryUndo(identity.slice(0, separator), identity.slice(separator + 1));
3070
+ }
3071
+ const payload = rows.map((row) => ({
3072
+ attrHistoryId: row.attrHistoryId,
3073
+ entityId: row.entityId,
3074
+ attrPath: row.attrPath,
3075
+ oldValueText: row.oldValueText,
3076
+ newValueText: row.newValueText,
3077
+ replacedAtMessageId: row.replacedAtMessageId,
3078
+ replacedAtTurnId: row.replacedAtTurnId,
3079
+ replacedAtWriteSeq: row.replacedAtWriteSeq,
3080
+ superseded: row.superseded
3081
+ }));
3082
+ await this.runInternal(`UNWIND $rows AS row MERGE (a:${ATTR_HISTORY_TABLE} {attrHistoryId: row.attrHistoryId}) ` +
3083
+ "SET a.entityId = row.entityId, a.attrPath = row.attrPath, " +
3084
+ "a.oldValueText = row.oldValueText, a.newValueText = row.newValueText, " +
3085
+ "a.replacedAtMessageId = row.replacedAtMessageId, a.replacedAtTurnId = row.replacedAtTurnId, " +
3086
+ "a.replacedAtWriteSeq = row.replacedAtWriteSeq, a.superseded = row.superseded;", { rows: payload });
3087
+ }
3088
+ async queryAttrHistory(entityId, attrPath) {
3089
+ const rows = await this.runInternal(`MATCH (a:${ATTR_HISTORY_TABLE} {entityId: $entityId, attrPath: $attrPath}) ` +
3090
+ "RETURN a.attrHistoryId AS attrHistoryId, a.entityId AS entityId, a.attrPath AS attrPath, " +
3091
+ "a.oldValueText AS oldValueText, a.newValueText AS newValueText, " +
3092
+ "a.replacedAtMessageId AS replacedAtMessageId, a.replacedAtTurnId AS replacedAtTurnId, " +
3093
+ "a.replacedAtWriteSeq AS replacedAtWriteSeq, a.superseded AS superseded " +
3094
+ "ORDER BY a.replacedAtWriteSeq;", { entityId, attrPath });
3095
+ return rows.map((row) => this.toAttrHistory(row));
3096
+ }
3097
+ async attrHistoryCheck(entityId, attrPath, valueText) {
3098
+ const rows = await this.runInternal(`MATCH (a:${ATTR_HISTORY_TABLE} {entityId: $entityId, attrPath: $attrPath}) ` +
3099
+ "WHERE a.oldValueText = $valueText OR (a.newValueText = $valueText AND a.superseded = true) " +
3100
+ "RETURN a.replacedAtMessageId AS supersededByMessageId, a.replacedAtWriteSeq AS writeSeq " +
3101
+ "ORDER BY a.replacedAtWriteSeq LIMIT 1;", { entityId, attrPath, valueText });
3102
+ if (rows.length === 0)
3103
+ return { superseded: false };
3104
+ return { superseded: true, supersededByMessageId: asString(rows[0].supersededByMessageId) };
3105
+ }
3106
+ async pruneAttrHistory(entityId, attrPath, maxRetain = ATTR_HISTORY_MAX_RETAIN) {
3107
+ const retain = this.assertMaxRetain(maxRetain);
3108
+ const stale = await this.runInternal(`MATCH (a:${ATTR_HISTORY_TABLE} {entityId: $entityId, attrPath: $attrPath}) ` +
3109
+ "RETURN a.attrHistoryId AS attrHistoryId " +
3110
+ `ORDER BY a.replacedAtWriteSeq DESC SKIP ${retain};`, { entityId, attrPath });
3111
+ if (stale.length === 0)
3112
+ return 0;
3113
+ await this.recordAttrHistoryUndo(entityId, attrPath);
3114
+ const ids = stale.map((row) => asString(row.attrHistoryId));
3115
+ await this.runInternal(`UNWIND $ids AS hid MATCH (a:${ATTR_HISTORY_TABLE} {attrHistoryId: hid}) DETACH DELETE a;`, { ids });
3116
+ return ids.length;
3117
+ }
3118
+ async rollbackAttrHistoryAfterMessage(messageId) {
3119
+ const pivotRows = await this.runInternal(`MATCH (a:${ATTR_HISTORY_TABLE}) WHERE a.replacedAtMessageId = $m ` +
3120
+ "RETURN max(a.replacedAtWriteSeq) AS pivot;", { m: messageId });
3121
+ const pivot = asPivot(pivotRows[0]?.pivot);
3122
+ if (pivot === null)
3123
+ return 0;
3124
+ await this.recordAllAttrHistoryUndo();
3125
+ const before = await this.attrHistoryCount();
3126
+ await this.runInternal(`MATCH (a:${ATTR_HISTORY_TABLE}) WHERE a.replacedAtWriteSeq > $p DETACH DELETE a;`, { p: pivot });
3127
+ return before - (await this.attrHistoryCount());
3128
+ }
3129
+ async attrHistoryCount() {
3130
+ const rows = await this.queryInternal(`MATCH (a:${ATTR_HISTORY_TABLE}) RETURN count(a) AS n;`);
3131
+ return rows.length === 0 ? 0 : asNumber(rows[0].n);
3132
+ }
3133
+ assertMaxRetain(maxRetain) {
3134
+ if (!Number.isInteger(maxRetain) || maxRetain < 0) {
3135
+ throw new Error(`maxRetain must be a non-negative integer got ${maxRetain}`);
3136
+ }
3137
+ return Math.min(maxRetain, ATTR_HISTORY_MAX_RETAIN);
3138
+ }
3139
+ toAttrHistory(row) {
3140
+ return {
3141
+ attrHistoryId: asString(row.attrHistoryId),
3142
+ entityId: asString(row.entityId),
3143
+ attrPath: asString(row.attrPath),
3144
+ oldValueText: asNullableString(row.oldValueText),
3145
+ newValueText: asString(row.newValueText),
3146
+ replacedAtMessageId: asString(row.replacedAtMessageId),
3147
+ replacedAtTurnId: asString(row.replacedAtTurnId),
3148
+ replacedAtWriteSeq: asNumber(row.replacedAtWriteSeq),
3149
+ superseded: row.superseded === true
3150
+ };
3151
+ }
3152
+ // The vector index must be (re)built after embeddings settle: ladybug rejects
3153
+ // SET on an indexed column ("Cannot set property ... used in one or more
3154
+ // indexes"). New CREATE auto-indexes, so the load flow is bulk upsert then
3155
+ // createVectorIndex once; an embedding change on an existing entity requires
3156
+ // dropVectorIndex first (editor flow).
3157
+ async createVectorIndex(config) {
3158
+ const embeddedRows = await this.queryInternal(`MATCH (e:${ENTITY_TABLE}) WHERE e.embedding IS NOT NULL RETURN count(e) AS n;`);
3159
+ const indexedCount = embeddedRows.length === 0 ? 0 : asNumber(embeddedRows[0].n);
3160
+ const args = this.buildVectorIndexArgs(config);
3161
+ await this.queryInternal(`CALL CREATE_VECTOR_INDEX('${ENTITY_TABLE}', '${VECTOR_INDEX}', 'embedding'${args});`);
3162
+ this.vectorIndexBuilt = true;
3163
+ this.vectorIndexedEntityCount = indexedCount;
3164
+ }
3165
+ async dropVectorIndex() {
3166
+ if (!this.vectorIndexBuilt)
3167
+ return;
3168
+ await this.queryInternal(`CALL DROP_VECTOR_INDEX('${ENTITY_TABLE}', '${VECTOR_INDEX}');`);
3169
+ this.vectorIndexBuilt = false;
3170
+ this.vectorIndexedEntityCount = 0;
3171
+ }
3172
+ async createFtsIndex(config) {
3173
+ const indexedRows = await this.queryInternal(`MATCH (p:${ENTITY_LEXICAL_PROJECTION_TABLE}) ` +
3174
+ "WHERE p.lexicalStatus = 'ready' RETURN count(p) AS n;");
3175
+ const indexedCount = indexedRows.length === 0 ? 0 : asNumber(indexedRows[0].n);
3176
+ const args = this.buildFtsIndexArgs(config);
3177
+ await this.queryInternal(`CALL CREATE_FTS_INDEX('${ENTITY_LEXICAL_PROJECTION_TABLE}', '${FTS_INDEX}', ` +
3178
+ `['lexicalText']${args});`);
3179
+ this.ftsIndexBuilt = true;
3180
+ this.ftsIndexedEntityCount = indexedCount;
3181
+ }
3182
+ buildVectorIndexArgs(config) {
3183
+ const parts = [];
3184
+ parts.push(`metric := '${pickEnum(config?.metric, VECTOR_METRIC_WHITELIST, FALLBACK_VECTOR_METRIC)}'`);
3185
+ const mu = clampInt(config?.mu, HNSW_MU_MIN, HNSW_MU_MAX);
3186
+ if (mu !== undefined)
3187
+ parts.push(`mu := ${mu}`);
3188
+ const ml = clampInt(config?.ml, HNSW_ML_MIN, HNSW_ML_MAX);
3189
+ if (ml !== undefined)
3190
+ parts.push(`ml := ${ml}`);
3191
+ const efc = clampInt(config?.efc, HNSW_EFC_MIN, HNSW_EFC_MAX);
3192
+ if (efc !== undefined)
3193
+ parts.push(`efc := ${efc}`);
3194
+ if (config?.cache_embeddings !== undefined) {
3195
+ parts.push(`cache_embeddings := ${config.cache_embeddings ? "true" : "false"}`);
3196
+ }
3197
+ return `, ${parts.join(", ")}`;
3198
+ }
3199
+ buildFtsIndexArgs(config) {
3200
+ if (config?.tokenizer !== undefined && config.tokenizer !== ENTITY_FTS_TOKENIZER) {
3201
+ throw new Error("entity FTS tokenizer is fixed to simple");
3202
+ }
3203
+ if (config?.stemmer !== undefined && config.stemmer !== FALLBACK_FTS_STEMMER) {
3204
+ throw new Error("entity FTS stemmer is fixed to none; stemming belongs to AnalyzerResult");
3205
+ }
3206
+ const parts = [
3207
+ `stemmer := '${FALLBACK_FTS_STEMMER}'`,
3208
+ `tokenizer := '${ENTITY_FTS_TOKENIZER}'`
3209
+ ];
3210
+ if (config?.stopwords !== undefined) {
3211
+ const cleaned = escapeStopwordsToken(config.stopwords);
3212
+ if (cleaned.length > 0)
3213
+ parts.push(`stopwords := '${cleaned}'`);
3214
+ }
3215
+ return `, ${parts.join(", ")}`;
3216
+ }
3217
+ async dropFtsIndex() {
3218
+ if (!this.ftsIndexBuilt)
3219
+ return;
3220
+ await this.queryInternal(`CALL DROP_FTS_INDEX('${ENTITY_LEXICAL_PROJECTION_TABLE}', '${FTS_INDEX}');`);
3221
+ this.ftsIndexBuilt = false;
3222
+ this.ftsIndexedEntityCount = 0;
3223
+ }
3224
+ hasVectorIndex() {
3225
+ return this.vectorIndexBuilt;
3226
+ }
3227
+ hasFtsIndex() {
3228
+ return this.ftsIndexBuilt;
3229
+ }
3230
+ /**
3231
+ * 只读运行时诊断。只返回数量与索引状态,不跨 wasm 边界暴露实体 props/embedding 明文。
3232
+ * `vectorIndexedEntityCount` 只在本 bridge 成功完成 CREATE_VECTOR_INDEX 后计数,
3233
+ * 因而可用于提交屏障确认 HNSW 已覆盖当前全部 embedding 行。
3234
+ */
3235
+ async diagnostics() {
3236
+ const entityRows = await this.queryInternal(`MATCH (e:${ENTITY_TABLE}) RETURN count(e) AS n;`);
3237
+ const relationRows = await this.queryInternal(`MATCH ()-[r:${RELATION_TABLE}]->() RETURN count(r) AS n;`);
3238
+ const embeddingRows = await this.queryInternal(`MATCH (e:${ENTITY_TABLE}) WHERE e.embedding IS NOT NULL RETURN count(e) AS n;`);
3239
+ const indexRows = await this.queryInternal("CALL SHOW_INDEXES() RETURN *;");
3240
+ const hasCatalogIndex = (table, name, type) => indexRows.some((row) => asString(row.table_name) === table &&
3241
+ asString(row.index_name) === name &&
3242
+ asString(row.index_type) === type);
3243
+ const vectorIndexBuilt = hasCatalogIndex(ENTITY_TABLE, VECTOR_INDEX, "HNSW");
3244
+ const ftsIndexBuilt = hasCatalogIndex(ENTITY_LEXICAL_PROJECTION_TABLE, FTS_INDEX, "FTS");
3245
+ if (vectorIndexBuilt !== this.vectorIndexBuilt || ftsIndexBuilt !== this.ftsIndexBuilt) {
3246
+ throw new Error("ladybug index catalog diverged from bridge state: " +
3247
+ `vector(catalog=${vectorIndexBuilt},bridge=${this.vectorIndexBuilt}) ` +
3248
+ `fts(catalog=${ftsIndexBuilt},bridge=${this.ftsIndexBuilt})`);
3249
+ }
3250
+ const entityCount = entityRows.length === 0 ? 0 : asNumber(entityRows[0].n);
3251
+ const relationCount = relationRows.length === 0 ? 0 : asNumber(relationRows[0].n);
3252
+ const embeddedEntityCount = embeddingRows.length === 0 ? 0 : asNumber(embeddingRows[0].n);
3253
+ return {
3254
+ entityCount,
3255
+ relationCount,
3256
+ embeddedEntityCount,
3257
+ vectorIndexedEntityCount: this.vectorIndexedEntityCount,
3258
+ ftsIndexedEntityCount: this.ftsIndexedEntityCount,
3259
+ vectorIndexBuilt,
3260
+ ftsIndexBuilt
3261
+ };
3262
+ }
3263
+ async upsertEntity(id, kind, props, embedding, messageId = "", turnId = "", analyzerProjection) {
3264
+ if (embedding && embedding.length !== this.dim) {
3265
+ throw new Error(`embedding dim mismatch expected ${this.dim} got ${embedding.length}`);
3266
+ }
3267
+ const projection = assertAnalyzerProjection(analyzerProjection);
3268
+ if (this.ftsIndexBuilt) {
3269
+ throw new Error("cannot change an entity analyzer projection while the FTS index is live — dropFtsIndex first");
3270
+ }
3271
+ const exists = (await this.runInternal(`MATCH (e:${ENTITY_TABLE} {id: $id}) RETURN 1 AS hit;`, { id })).length > 0;
3272
+ if (exists && embedding && this.vectorIndexBuilt) {
3273
+ throw new Error("cannot change embedding of an existing entity while the vector index is live — dropVectorIndex first");
3274
+ }
3275
+ await this.recordEntityUndoBeforeWrite(id);
3276
+ const writeSeq = this.nextSeq();
3277
+ if (embedding) {
3278
+ await this.runInternal(`MERGE (e:${ENTITY_TABLE} {id: $id}) ` +
3279
+ "SET e.kind = $kind, e.props = $props, e.embedding = $embedding, " +
3280
+ "e.messageId = $messageId, e.turnId = $turnId, e.writeSeq = $writeSeq;", {
3281
+ id,
3282
+ kind,
3283
+ props: encodeProps(props),
3284
+ embedding: [...embedding],
3285
+ messageId,
3286
+ turnId,
3287
+ writeSeq
3288
+ });
3289
+ await this.replaceEntityAnalyzerProjection(id, projection, writeSeq);
3290
+ return;
3291
+ }
3292
+ await this.runInternal(`MERGE (e:${ENTITY_TABLE} {id: $id}) ` +
3293
+ "SET e.kind = $kind, e.props = $props, " +
3294
+ "e.messageId = $messageId, e.turnId = $turnId, e.writeSeq = $writeSeq;", { id, kind, props: encodeProps(props), messageId, turnId, writeSeq });
3295
+ await this.replaceEntityAnalyzerProjection(id, projection, writeSeq);
3296
+ }
3297
+ async replaceEntityAnalyzerProjection(entityId, projection, writeSeq) {
3298
+ if (projection === null) {
3299
+ await this.runInternal(`MATCH (p:${ENTITY_LEXICAL_PROJECTION_TABLE} {entityId: $entityId}) DETACH DELETE p;`, { entityId });
3300
+ return;
3301
+ }
3302
+ await this.runInternal(`MERGE (p:${ENTITY_LEXICAL_PROJECTION_TABLE} {entityId: $entityId}) ` +
3303
+ "SET p.lexicalText = $lexicalText, p.projectionJson = $projectionJson, " +
3304
+ "p.analyzerVersion = $analyzerVersion, p.lexicalStatus = $lexicalStatus, " +
3305
+ "p.relationalStatus = $relationalStatus, p.writeSeq = $writeSeq;", {
3306
+ entityId,
3307
+ lexicalText: projection.lexicalText,
3308
+ projectionJson: encodeAnalyzerProjection(projection),
3309
+ analyzerVersion: projection.analyzerVersion,
3310
+ lexicalStatus: projection.lexicalStatus,
3311
+ relationalStatus: projection.relationalStatus,
3312
+ writeSeq
3313
+ });
3314
+ }
3315
+ async getEntityAnalyzerProjection(entityId) {
3316
+ const rows = await this.runInternal(`MATCH (p:${ENTITY_LEXICAL_PROJECTION_TABLE} {entityId: $entityId}) ` +
3317
+ "RETURN p.projectionJson AS projectionJson, p.lexicalText AS lexicalText, " +
3318
+ "p.analyzerVersion AS analyzerVersion, p.lexicalStatus AS lexicalStatus, " +
3319
+ "p.relationalStatus AS relationalStatus;", { entityId });
3320
+ if (rows.length === 0) {
3321
+ return {
3322
+ entityId,
3323
+ lexicalStatus: "reindex-needed",
3324
+ relationalStatus: "reindex-needed",
3325
+ lexicalArm: { status: "reindex-needed", analyzerVersion: null },
3326
+ relationalArm: { status: "reindex-needed", analyzerVersion: null },
3327
+ projection: null
3328
+ };
3329
+ }
3330
+ const projection = JSON.parse(asString(rows[0].projectionJson));
3331
+ assertAnalyzerProjection(projection);
3332
+ const { lexicalStatus, relationalStatus } = projection;
3333
+ if (!["ready", "unavailable", "failed"].includes(lexicalStatus) ||
3334
+ !["ready", "unavailable", "failed"].includes(relationalStatus)) {
3335
+ throw new Error("entity analyzer projection has invalid stored arm status");
3336
+ }
3337
+ if (asString(rows[0].lexicalText) !== projection.lexicalText ||
3338
+ asString(rows[0].analyzerVersion) !== projection.analyzerVersion ||
3339
+ asString(rows[0].lexicalStatus) !== lexicalStatus ||
3340
+ asString(rows[0].relationalStatus) !== relationalStatus) {
3341
+ throw new Error("entity analyzer projection sidecar integrity mismatch");
3342
+ }
3343
+ const analyzerVersion = projection.analyzerVersion;
3344
+ return {
3345
+ entityId,
3346
+ lexicalStatus,
3347
+ relationalStatus,
3348
+ lexicalArm: { status: lexicalStatus, analyzerVersion },
3349
+ relationalArm: { status: relationalStatus, analyzerVersion },
3350
+ projection
3351
+ };
3352
+ }
3353
+ // bulk_load 用 UNWIND 单次 prepare+execute 喂多行 ladybug 0.17.1 实测 entity
3354
+ // (with/without embedding) 和 relation (MATCH+MERGE) 均支持 空 rows UNWIND 会
3355
+ // 触发 "Cannot evaluate expression with type VARIABLE" 因此空数组走 early-return
3356
+ // 不再走 prepare 不可与 vector index 同时存在 (ladybug 拒绝 SET 索引列) 由
3357
+ // caller (bulk_load 流程) 保证先 dropVectorIndex
3358
+ async bulkUpsertEntities(entries) {
3359
+ if (entries.length === 0)
3360
+ return;
3361
+ if (this.vectorIndexBuilt) {
3362
+ throw new Error("cannot bulk upsert entities while the vector index is live — dropVectorIndex first");
3363
+ }
3364
+ if (this.ftsIndexBuilt) {
3365
+ throw new Error("cannot bulk upsert entities while the FTS index is live — dropFtsIndex first");
3366
+ }
3367
+ if (this.tx !== null) {
3368
+ for (const entry of entries) {
3369
+ await this.recordEntityUndoBeforeWrite(entry.id);
3370
+ }
3371
+ }
3372
+ const withEmbedding = [];
3373
+ const withoutEmbedding = [];
3374
+ const projections = [];
3375
+ for (const entry of entries) {
3376
+ const writeSeq = this.nextSeq();
3377
+ projections.push({
3378
+ entityId: entry.id,
3379
+ projection: assertAnalyzerProjection(entry.analyzerProjection),
3380
+ writeSeq
3381
+ });
3382
+ if (entry.embedding) {
3383
+ if (entry.embedding.length !== this.dim) {
3384
+ throw new Error(`embedding dim mismatch expected ${this.dim} got ${entry.embedding.length}`);
3385
+ }
3386
+ withEmbedding.push({
3387
+ id: entry.id,
3388
+ kind: entry.kind,
3389
+ props: encodeProps(entry.props),
3390
+ embedding: [...entry.embedding],
3391
+ messageId: entry.messageId,
3392
+ turnId: entry.turnId,
3393
+ writeSeq
3394
+ });
3395
+ }
3396
+ else {
3397
+ withoutEmbedding.push({
3398
+ id: entry.id,
3399
+ kind: entry.kind,
3400
+ props: encodeProps(entry.props),
3401
+ messageId: entry.messageId,
3402
+ turnId: entry.turnId,
3403
+ writeSeq
3404
+ });
3405
+ }
3406
+ }
3407
+ if (withEmbedding.length > 0) {
3408
+ await this.runInternal(`UNWIND $rows AS row MERGE (e:${ENTITY_TABLE} {id: row.id}) ` +
3409
+ "SET e.kind = row.kind, e.props = row.props, e.embedding = row.embedding, " +
3410
+ "e.messageId = row.messageId, e.turnId = row.turnId, e.writeSeq = row.writeSeq;", { rows: withEmbedding });
3411
+ }
3412
+ if (withoutEmbedding.length > 0) {
3413
+ await this.runInternal(`UNWIND $rows AS row MERGE (e:${ENTITY_TABLE} {id: row.id}) ` +
3414
+ "SET e.kind = row.kind, e.props = row.props, " +
3415
+ "e.messageId = row.messageId, e.turnId = row.turnId, e.writeSeq = row.writeSeq;", { rows: withoutEmbedding });
3416
+ }
3417
+ for (const row of projections) {
3418
+ await this.replaceEntityAnalyzerProjection(row.entityId, row.projection, row.writeSeq);
3419
+ }
3420
+ }
3421
+ async bulkUpsertRelations(entries) {
3422
+ if (entries.length === 0)
3423
+ return;
3424
+ if (this.tx !== null) {
3425
+ for (const entry of entries) {
3426
+ await this.recordRelationUndoBeforeWrite(entry.src, entry.dst, entry.kind);
3427
+ }
3428
+ }
3429
+ const rows = entries.map((entry) => ({
3430
+ src: entry.src,
3431
+ dst: entry.dst,
3432
+ kind: entry.kind,
3433
+ props: encodeProps(entry.props),
3434
+ messageId: entry.messageId,
3435
+ turnId: entry.turnId,
3436
+ writeSeq: this.nextSeq()
3437
+ }));
3438
+ await this.runInternal(`UNWIND $rows AS row MATCH (s:${ENTITY_TABLE} {id: row.src}), (d:${ENTITY_TABLE} {id: row.dst}) ` +
3439
+ `MERGE (s)-[r:${RELATION_TABLE} {kind: row.kind}]->(d) ` +
3440
+ "SET r.props = row.props, r.messageId = row.messageId, " +
3441
+ "r.turnId = row.turnId, r.writeSeq = row.writeSeq;", { rows });
3442
+ }
3443
+ async upsertRelation(src, dst, kind, props, messageId = "", turnId = "") {
3444
+ await this.recordRelationUndoBeforeWrite(src, dst, kind);
3445
+ const writeSeq = this.nextSeq();
3446
+ await this.runInternal(`MATCH (s:${ENTITY_TABLE} {id: $src}), (d:${ENTITY_TABLE} {id: $dst}) ` +
3447
+ `MERGE (s)-[r:${RELATION_TABLE} {kind: $kind}]->(d) ` +
3448
+ "SET r.props = $props, r.messageId = $messageId, " +
3449
+ "r.turnId = $turnId, r.writeSeq = $writeSeq;", { src, dst, kind, props: encodeProps(props), messageId, turnId, writeSeq });
3450
+ }
3451
+ async deleteEntity(id) {
3452
+ if (this.worldMemoryHostAuthority !== null && this.tx === null) {
3453
+ throw new Error("trusted WorldMemory bridge requires a transaction for entity deletion");
3454
+ }
3455
+ await this.recordWorldMemoryUndoIfTransaction();
3456
+ await this.recordEntityUndoBeforeDelete(id);
3457
+ await this.runInternal(`MATCH (p:${ENTITY_LEXICAL_PROJECTION_TABLE} {entityId: $id}) DETACH DELETE p;`, { id });
3458
+ await this.runInternal(`MATCH (e:${ENTITY_TABLE} {id: $id}) DETACH DELETE e;`, { id });
3459
+ }
3460
+ // Rollback 语义 找 message_id M 在 Entity+Relation 中的 max(writeSeq) 作 pivot
3461
+ // 删除 strictly writeSeq>pivot 的所有边和点 pivot 自身的写入保留 没命中 M 视 no-op
3462
+ // 用 DETACH DELETE 简化 此时残余挂在被删 Entity 上的边都是 writeSeq<=pivot
3463
+ // (因 writeSeq 全局单调 而 Entity 的入边只能在它存在后写入) 一并清掉
3464
+ // sentinel 防御: messageId 若命中 SNAPSHOT_PROVENANCE_MESSAGE_ID 直接视为 no-op 不当合法 pivot
3465
+ // 初始快照行不是任何一条对话消息的产物 没有"回滚到快照"这个语义 调用方传入 sentinel
3466
+ // 只可能是上游 bug (把快照当成了可寻址的消息) 拒绝掉避免把快照之后的真实游玩数据错误清空
3467
+ async rollbackAfterMessage(messageId) {
3468
+ if (messageId === SNAPSHOT_PROVENANCE_MESSAGE_ID) {
3469
+ return { deletedEntities: 0, deletedRelations: 0 };
3470
+ }
3471
+ if (this.worldMemoryHostAuthority !== null && this.tx === null) {
3472
+ throw new Error("trusted WorldMemory bridge requires a transaction for message rollback");
3473
+ }
3474
+ const entityPivotRows = await this.runInternal(`MATCH (e:${ENTITY_TABLE}) WHERE e.messageId = $m RETURN max(e.writeSeq) AS pivot;`, { m: messageId });
3475
+ const relationPivotRows = await this.runInternal(`MATCH ()-[r:${RELATION_TABLE}]->() WHERE r.messageId = $m RETURN max(r.writeSeq) AS pivot;`, { m: messageId });
3476
+ const ePivot = asPivot(entityPivotRows[0]?.pivot);
3477
+ const rPivot = asPivot(relationPivotRows[0]?.pivot);
3478
+ if (ePivot === null && rPivot === null) {
3479
+ return { deletedEntities: 0, deletedRelations: 0 };
3480
+ }
3481
+ const pivot = Math.max(ePivot ?? -1, rPivot ?? -1);
3482
+ await this.recordWorldMemoryUndoIfTransaction();
3483
+ await this.recordRollbackAfterMessageUndo(pivot);
3484
+ const beforeE = await this.entityCount();
3485
+ const beforeR = await this.relationCount();
3486
+ await this.runInternal(`MATCH ()-[r:${RELATION_TABLE}]->() WHERE r.writeSeq > $p DELETE r;`, { p: pivot });
3487
+ await this.runInternal(`MATCH (p:${ENTITY_LEXICAL_PROJECTION_TABLE}) WHERE p.writeSeq > $p DETACH DELETE p;`, { p: pivot });
3488
+ await this.runInternal(`MATCH (e:${ENTITY_TABLE}) WHERE e.writeSeq > $p DETACH DELETE e;`, { p: pivot });
3489
+ const afterE = await this.entityCount();
3490
+ const afterR = await this.relationCount();
3491
+ return {
3492
+ deletedEntities: beforeE - afterE,
3493
+ deletedRelations: beforeR - afterR
3494
+ };
3495
+ }
3496
+ async deleteRelation(src, dst) {
3497
+ await this.recordRelationUndoBeforeDelete(src, dst);
3498
+ await this.runInternal(`MATCH (s:${ENTITY_TABLE} {id: $src})-[r:${RELATION_TABLE}]->(d:${ENTITY_TABLE} {id: $dst}) DELETE r;`, { src, dst });
3499
+ }
3500
+ async getById(id) {
3501
+ const rows = await this.runInternal(`MATCH (e:${ENTITY_TABLE} {id: $id}) RETURN e.id AS id, e.kind AS kind, e.props AS props;`, { id });
3502
+ return rows.length === 0 ? null : this.toEntity(rows[0]);
3503
+ }
3504
+ async findByKind(kind) {
3505
+ const rows = await this.runInternal(`MATCH (e:${ENTITY_TABLE} {kind: $kind}) RETURN e.id AS id, e.kind AS kind, e.props AS props;`, { kind });
3506
+ return rows.map((row) => this.toEntity(row));
3507
+ }
3508
+ async allEntities() {
3509
+ const rows = await this.queryInternal(`MATCH (e:${ENTITY_TABLE}) ` +
3510
+ `OPTIONAL MATCH (p:${ENTITY_LEXICAL_PROJECTION_TABLE} {entityId: e.id}) ` +
3511
+ "RETURN e.id AS id, e.kind AS kind, e.props AS props, " +
3512
+ "p.projectionJson AS analyzerProjectionJson;");
3513
+ return rows.map((row) => {
3514
+ const entity = this.toEntity(row);
3515
+ if (row.analyzerProjectionJson === null || row.analyzerProjectionJson === undefined) {
3516
+ return entity;
3517
+ }
3518
+ const projection = JSON.parse(asString(row.analyzerProjectionJson));
3519
+ assertAnalyzerProjection(projection);
3520
+ if (typeof entity.props !== "object" || entity.props === null || Array.isArray(entity.props)) {
3521
+ throw new Error("entity props must be an object when attaching analyzer projection");
3522
+ }
3523
+ return {
3524
+ ...entity,
3525
+ props: {
3526
+ ...entity.props,
3527
+ __analyzerProjection: projection
3528
+ }
3529
+ };
3530
+ });
3531
+ }
3532
+ async allRelations() {
3533
+ const rows = await this.queryInternal(`MATCH (s:${ENTITY_TABLE})-[r:${RELATION_TABLE}]->(d:${ENTITY_TABLE}) ` +
3534
+ "RETURN s.id AS src, d.id AS dst, r.kind AS kind, r.props AS props;");
3535
+ return rows.map((row) => this.toRelation(row));
3536
+ }
3537
+ async neighbors(id, depth, options) {
3538
+ const safeDepth = assertDepth(depth);
3539
+ if (safeDepth === 0)
3540
+ return [];
3541
+ const entityKinds = (options?.entityKinds ?? []).filter((s) => s.length > 0);
3542
+ const relationKinds = (options?.relationKinds ?? []).filter((s) => s.length > 0);
3543
+ const whereClauses = [];
3544
+ const params = { id };
3545
+ if (entityKinds.length > 0) {
3546
+ // entity kind 支持 Cypher 参数绑定 直接 pushdown 到 kuzu 侧筛
3547
+ whereClauses.push("m.kind IN $entityKinds");
3548
+ params.entityKinds = [...entityKinds];
3549
+ }
3550
+ // relation kind 白名单 kuzu Cypher 不支持 list 参数绑定进入 all()/list_contains 谓词
3551
+ // (parsed_parameter_expression.h UNREACHABLE_CODE) 因此 TS 侧走 rel_kinds RETURN 后置过滤
3552
+ const whereFragment = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")} ` : "";
3553
+ const rows = await this.runInternal(`MATCH p = (s:${ENTITY_TABLE} {id: $id})-[r:${RELATION_TABLE}*1..${safeDepth}]->(m:${ENTITY_TABLE}) ` +
3554
+ whereFragment +
3555
+ "RETURN m.id AS id, m.kind AS kind, length(p) AS hop, " +
3556
+ "list_transform(rels(p), e -> e.kind) AS rel_kinds;", params);
3557
+ const mapped = rows.map((row) => ({
3558
+ row,
3559
+ relKinds: this.toStringList(row.rel_kinds),
3560
+ neighbor: this.toNeighbor(row)
3561
+ }));
3562
+ if (relationKinds.length === 0) {
3563
+ return mapped.map((m) => m.neighbor);
3564
+ }
3565
+ const relSet = new Set(relationKinds);
3566
+ return mapped
3567
+ .filter((m) => m.relKinds.every((k) => relSet.has(k)))
3568
+ .map((m) => m.neighbor);
3569
+ }
3570
+ async vectorSearch(query, topK, config) {
3571
+ assertTopK(topK);
3572
+ const efs = clampInt(config?.efs, HNSW_EFS_MIN, HNSW_EFS_MAX);
3573
+ const efsArg = efs !== undefined ? `, efs := ${efs}` : "";
3574
+ const rows = await this.runInternal(`CALL QUERY_VECTOR_INDEX('${ENTITY_TABLE}', '${VECTOR_INDEX}', $query, $topK${efsArg}) ` +
3575
+ "YIELD node AS seed, distance " +
3576
+ "RETURN seed.id AS id, distance ORDER BY distance;", { query: [...query], topK });
3577
+ return rows.map((row) => ({ id: asString(row.id), distance: asNumber(row.distance) }));
3578
+ }
3579
+ async fullTextSearch(query, topK, config) {
3580
+ assertTopK(topK);
3581
+ const ftsArgs = this.buildFtsQueryArgs(config);
3582
+ const rows = await this.runInternal(`CALL QUERY_FTS_INDEX('${ENTITY_LEXICAL_PROJECTION_TABLE}', '${FTS_INDEX}', ` +
3583
+ `$query${ftsArgs}) ` +
3584
+ "RETURN node.entityId AS id, score ORDER BY score DESC LIMIT $topK;", { query, topK });
3585
+ return rows.map((row) => asString(row.id));
3586
+ }
3587
+ async recallAndExpand(query, topK, depth, config) {
3588
+ assertTopK(topK);
3589
+ const safeDepth = assertDepth(depth);
3590
+ const efs = clampInt(config?.efs, HNSW_EFS_MIN, HNSW_EFS_MAX);
3591
+ const rows = await this.runInternal(this.recallCypher(safeDepth, efs), {
3592
+ query: [...query],
3593
+ topK
3594
+ });
3595
+ return this.rebuildGraphRag(rows);
3596
+ }
3597
+ buildFtsQueryArgs(config) {
3598
+ const parts = [];
3599
+ const k1 = clampDouble(config?.bm25_k1, BM25_K1_MIN, BM25_K1_MAX);
3600
+ if (k1 !== undefined)
3601
+ parts.push(`k1 := ${k1}`);
3602
+ const b = clampDouble(config?.bm25_b, BM25_B_MIN, BM25_B_MAX);
3603
+ if (b !== undefined)
3604
+ parts.push(`b := ${b}`);
3605
+ if (config?.conjunctive !== undefined) {
3606
+ parts.push(`conjunctive := ${config.conjunctive ? "true" : "false"}`);
3607
+ }
3608
+ return parts.length === 0 ? "" : `, ${parts.join(", ")}`;
3609
+ }
3610
+ recallCypher(depth, efs) {
3611
+ const efsArg = efs !== undefined ? `, efs := ${efs}` : "";
3612
+ const projection = "seed.id AS seed_id, seed.kind AS seed_kind, seed.props AS seed_props, distance";
3613
+ if (depth === 0) {
3614
+ return (`CALL QUERY_VECTOR_INDEX('${ENTITY_TABLE}', '${VECTOR_INDEX}', $query, $topK${efsArg}) ` +
3615
+ "YIELD node AS seed, distance " +
3616
+ `RETURN ${projection}, ` +
3617
+ "CAST(NULL AS STRING) AS m_id, CAST(NULL AS STRING) AS m_kind, CAST(NULL AS STRING) AS m_props, " +
3618
+ "CAST(NULL AS INT64) AS hop, [seed.id] AS path_ids, " +
3619
+ "CAST([] AS STRING[]) AS rel_kinds, CAST([] AS STRING[]) AS rel_props " +
3620
+ "ORDER BY distance;");
3621
+ }
3622
+ return (`CALL QUERY_VECTOR_INDEX('${ENTITY_TABLE}', '${VECTOR_INDEX}', $query, $topK${efsArg}) ` +
3623
+ "YIELD node AS seed, distance " +
3624
+ `OPTIONAL MATCH p = (seed)-[r:${RELATION_TABLE}*1..${depth}]->(m:${ENTITY_TABLE}) ` +
3625
+ `RETURN ${projection}, ` +
3626
+ "m.id AS m_id, m.kind AS m_kind, m.props AS m_props, length(p) AS hop, " +
3627
+ "list_transform(nodes(p), x -> x.id) AS path_ids, " +
3628
+ "list_transform(rels(p), e -> e.kind) AS rel_kinds, " +
3629
+ "list_transform(rels(p), e -> e.props) AS rel_props " +
3630
+ "ORDER BY distance, hop;");
3631
+ }
3632
+ rebuildGraphRag(rows) {
3633
+ const seeds = [];
3634
+ const seenSeed = new Set();
3635
+ const nodeOrder = [];
3636
+ const nodeEntities = new Map();
3637
+ const nodeProvenance = new Map();
3638
+ const provSeen = new Set();
3639
+ const edgeOrder = [];
3640
+ const edgeSeen = new Set();
3641
+ const addNode = (entity) => {
3642
+ if (nodeEntities.has(entity.id))
3643
+ return;
3644
+ nodeOrder.push(entity.id);
3645
+ nodeEntities.set(entity.id, entity);
3646
+ nodeProvenance.set(entity.id, []);
3647
+ };
3648
+ const addProvenance = (id, seedId, hop) => {
3649
+ const key = JSON.stringify([id, seedId, hop]);
3650
+ if (provSeen.has(key))
3651
+ return;
3652
+ provSeen.add(key);
3653
+ nodeProvenance.get(id)?.push({ seedId, hop });
3654
+ };
3655
+ const addEdge = (edge) => {
3656
+ const key = JSON.stringify([edge.src, edge.dst, edge.kind]);
3657
+ if (edgeSeen.has(key))
3658
+ return;
3659
+ edgeSeen.add(key);
3660
+ edgeOrder.push(edge);
3661
+ };
3662
+ for (const row of rows) {
3663
+ const seedId = asString(row.seed_id);
3664
+ if (!seenSeed.has(seedId)) {
3665
+ seenSeed.add(seedId);
3666
+ seeds.push({ id: seedId, distance: asNumber(row.distance) });
3667
+ }
3668
+ addNode({ id: seedId, kind: asString(row.seed_kind), props: parseProps(row.seed_props) });
3669
+ addProvenance(seedId, seedId, 0);
3670
+ const pathIds = this.toStringList(row.path_ids);
3671
+ const relKinds = this.toStringList(row.rel_kinds);
3672
+ const relProps = this.toStringList(row.rel_props);
3673
+ if (pathIds.length <= 1)
3674
+ continue;
3675
+ const mId = row.m_id == null ? null : asString(row.m_id);
3676
+ if (mId !== null) {
3677
+ addNode({ id: mId, kind: asString(row.m_kind), props: parseProps(row.m_props) });
3678
+ addProvenance(mId, seedId, asNumber(row.hop));
3679
+ }
3680
+ for (let i = 0; i + 1 < pathIds.length; i += 1) {
3681
+ addEdge({
3682
+ src: pathIds[i],
3683
+ dst: pathIds[i + 1],
3684
+ kind: relKinds[i] ?? "",
3685
+ props: parseProps(relProps[i])
3686
+ });
3687
+ }
3688
+ }
3689
+ const nodes = nodeOrder.map((id) => ({
3690
+ entity: nodeEntities.get(id),
3691
+ provenance: nodeProvenance.get(id) ?? []
3692
+ }));
3693
+ return { seeds, nodes, edges: edgeOrder };
3694
+ }
3695
+ toStringList(value) {
3696
+ return Array.isArray(value) ? value.map((item) => asString(item)) : [];
3697
+ }
3698
+ toEntity(row) {
3699
+ return { id: asString(row.id), kind: asString(row.kind), props: parseProps(row.props) };
3700
+ }
3701
+ toRelation(row) {
3702
+ return {
3703
+ src: asString(row.src),
3704
+ dst: asString(row.dst),
3705
+ kind: asString(row.kind),
3706
+ props: parseProps(row.props)
3707
+ };
3708
+ }
3709
+ toNeighbor(row) {
3710
+ const relKinds = this.toStringList(row.rel_kinds);
3711
+ return {
3712
+ id: asString(row.id),
3713
+ kind: asString(row.kind),
3714
+ edgeKind: relKinds.length > 0 ? relKinds[relKinds.length - 1] : "",
3715
+ depth: asNumber(row.hop)
3716
+ };
3717
+ }
3718
+ async entityCount() {
3719
+ const rows = await this.queryInternal(`MATCH (e:${ENTITY_TABLE}) RETURN count(e) AS n;`);
3720
+ return rows.length === 0 ? 0 : asNumber(rows[0].n);
3721
+ }
3722
+ async relationCount() {
3723
+ const rows = await this.queryInternal(`MATCH ()-[r:${RELATION_TABLE}]->() RETURN count(r) AS n;`);
3724
+ return rows.length === 0 ? 0 : asNumber(rows[0].n);
3725
+ }
3726
+ async hasEmbedding(id) {
3727
+ const rows = await this.runInternal(`MATCH (e:${ENTITY_TABLE} {id: $id}) RETURN e.embedding IS NOT NULL AS has;`, { id });
3728
+ return rows.length > 0 && rows[0].has === true;
3729
+ }
3730
+ async close() {
3731
+ if (this.closing) {
3732
+ throw new Error("ladybug bridge close is already in progress");
3733
+ }
3734
+ if (this.initializing) {
3735
+ throw new Error("ladybug bridge close cannot overlap initialization");
3736
+ }
3737
+ if (this.graphRollbackInProgress) {
3738
+ throw new Error("ladybug bridge close cannot overlap graph rollback replay");
3739
+ }
3740
+ if (this.transcriptOperationReservations > 0 || this.transcriptOperationActive) {
3741
+ throw new Error("ladybug bridge close cannot overlap a transcript operation");
3742
+ }
3743
+ if (this.worldMemoryOperationReservations > 0) {
3744
+ throw new Error("ladybug bridge close cannot overlap a world memory host operation");
3745
+ }
3746
+ if (this.tx !== null) {
3747
+ throw new Error("ladybug bridge close requires the active graph transaction to commit or rollback");
3748
+ }
3749
+ this.closing = true;
3750
+ this.ready = false;
3751
+ try {
3752
+ if (this.conn) {
3753
+ await this.conn.close();
3754
+ this.conn = null;
3755
+ }
3756
+ if (this.db) {
3757
+ await this.db.close();
3758
+ this.db = null;
3759
+ }
3760
+ this.vectorIndexBuilt = false;
3761
+ this.ftsIndexBuilt = false;
3762
+ this.vectorIndexedEntityCount = 0;
3763
+ this.ftsIndexedEntityCount = 0;
3764
+ this.memoryEpisodeVectorIndexBuilt = false;
3765
+ this.memoryEpisodeFtsIndexBuilt = false;
3766
+ this.memoryDerivedVectorIndexBuilt = false;
3767
+ this.memoryDerivedFtsIndexBuilt = false;
3768
+ this.transcriptFtsBuilt = false;
3769
+ this.transcriptQueue = Promise.resolve();
3770
+ this.transcriptOperationReservations = 0;
3771
+ this.transcriptOperationActive = false;
3772
+ this.worldMemoryQueue = Promise.resolve();
3773
+ this.worldMemoryOperationReservations = 0;
3774
+ this.worldMemoryRevision = 0;
3775
+ this._writeSeq = 0;
3776
+ }
3777
+ finally {
3778
+ this.closing = false;
3779
+ }
3780
+ }
3781
+ }