@remnic/plugin-openclaw 1.0.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.
@@ -0,0 +1,4242 @@
1
+ import {
2
+ log
3
+ } from "./chunk-DMGIUDBO.js";
4
+
5
+ // ../remnic-core/src/storage.ts
6
+ import { access, readdir, readFile as readFile2, stat as stat2, writeFile as writeFile2, mkdir as mkdir2, unlink, rename, appendFile } from "fs/promises";
7
+ import { appendFileSync, mkdirSync, statSync } from "fs";
8
+ import { createHash } from "crypto";
9
+ import path4 from "path";
10
+
11
+ // ../remnic-core/src/memory-cache.ts
12
+ var entityCacheByDir = /* @__PURE__ */ new Map();
13
+ function getCachedEntities(baseDir, currentVersion) {
14
+ if (currentVersion === 0) return null;
15
+ const entry = entityCacheByDir.get(baseDir);
16
+ if (!entry || entry.version !== currentVersion) return null;
17
+ return entry.entities;
18
+ }
19
+ function setCachedEntities(baseDir, entities, version) {
20
+ entityCacheByDir.set(baseDir, { entities, version, loadedAt: Date.now() });
21
+ }
22
+ var episodeMapByDir = /* @__PURE__ */ new Map();
23
+ var ruleMemoriesByDir = /* @__PURE__ */ new Map();
24
+ function getCachedEpisodeMap(baseDir, currentVersion) {
25
+ if (currentVersion === 0) return null;
26
+ const entry = episodeMapByDir.get(baseDir);
27
+ if (!entry || entry.sourceVersion !== currentVersion) return null;
28
+ return entry.data;
29
+ }
30
+ function setCachedEpisodeMap(baseDir, memories, version) {
31
+ const map = /* @__PURE__ */ new Map();
32
+ for (const m of memories) {
33
+ if (m.frontmatter.status === "archived") continue;
34
+ if (m.frontmatter.memoryKind !== "episode") continue;
35
+ map.set(m.frontmatter.id, m);
36
+ }
37
+ episodeMapByDir.set(baseDir, { data: map, sourceVersion: version });
38
+ return map;
39
+ }
40
+ function getCachedRuleMemories(baseDir, currentVersion) {
41
+ if (currentVersion === 0) return null;
42
+ const entry = ruleMemoriesByDir.get(baseDir);
43
+ if (!entry || entry.sourceVersion !== currentVersion) return null;
44
+ return entry.data;
45
+ }
46
+ function setCachedRuleMemories(baseDir, memories, version) {
47
+ const byId = /* @__PURE__ */ new Map();
48
+ const all = [];
49
+ for (const m of memories) {
50
+ byId.set(m.frontmatter.id, m);
51
+ if (m.frontmatter.category === "rule" && m.frontmatter.status !== "archived") {
52
+ all.push(m);
53
+ }
54
+ }
55
+ const result = { all, byId };
56
+ ruleMemoriesByDir.set(baseDir, { data: result, sourceVersion: version });
57
+ return result;
58
+ }
59
+ var QMD_CACHE_TTL_MS = 6e4;
60
+ var qmdSearchCache = /* @__PURE__ */ new Map();
61
+ function getCachedQmdSearch(cacheKey) {
62
+ const entry = qmdSearchCache.get(cacheKey);
63
+ if (!entry) return null;
64
+ if (Date.now() - entry.cachedAt > QMD_CACHE_TTL_MS) {
65
+ qmdSearchCache.delete(cacheKey);
66
+ return null;
67
+ }
68
+ return entry.results;
69
+ }
70
+ function setCachedQmdSearch(cacheKey, results) {
71
+ qmdSearchCache.set(cacheKey, { results, cachedAt: Date.now() });
72
+ if (qmdSearchCache.size > 200) {
73
+ const now = Date.now();
74
+ for (const [key, entry] of qmdSearchCache) {
75
+ if (now - entry.cachedAt > QMD_CACHE_TTL_MS) qmdSearchCache.delete(key);
76
+ }
77
+ }
78
+ }
79
+
80
+ // ../remnic-core/src/hygiene.ts
81
+ import { mkdir, readFile, stat, writeFile } from "fs/promises";
82
+ import path from "path";
83
+ function toSafeTimestamp(ts) {
84
+ return ts.toISOString().replace(/[:.]/g, "");
85
+ }
86
+ async function lintWorkspaceFiles(opts) {
87
+ const warnings = [];
88
+ const warnAtBytes = Math.floor(opts.budgetBytes * opts.warnRatio);
89
+ for (const p of opts.paths) {
90
+ const abs = path.isAbsolute(p) ? p : path.join(opts.workspaceDir, p);
91
+ try {
92
+ const st = await stat(abs);
93
+ if (!st.isFile()) continue;
94
+ const bytes = st.size;
95
+ if (bytes >= warnAtBytes) {
96
+ warnings.push({
97
+ path: p,
98
+ bytes,
99
+ budgetBytes: opts.budgetBytes,
100
+ warnAtBytes,
101
+ message: `Bootstrap file '${p}' is approaching its budget (${bytes} bytes >= ${warnAtBytes} bytes). Consider splitting/archiving it to avoid silent truncation.`
102
+ });
103
+ }
104
+ } catch {
105
+ }
106
+ }
107
+ return warnings;
108
+ }
109
+ async function rotateMarkdownFileToArchive(opts) {
110
+ const existing = await readFile(opts.filePath, "utf-8");
111
+ const ts = toSafeTimestamp(/* @__PURE__ */ new Date());
112
+ const archiveName = `${opts.archivePrefix}-${ts}.md`;
113
+ await mkdir(opts.archiveDir, { recursive: true });
114
+ const archivedPath = path.join(opts.archiveDir, archiveName);
115
+ await writeFile(archivedPath, existing, "utf-8");
116
+ const tail = opts.keepTailChars > 0 && existing.length > opts.keepTailChars ? existing.slice(-opts.keepTailChars) : existing;
117
+ const relLink = path.relative(path.dirname(opts.filePath), archivedPath);
118
+ const newContent = [
119
+ "# Index",
120
+ "",
121
+ "This file is kept intentionally small to reduce the risk of silent truncation when OpenClaw bootstraps workspace files into the prompt.",
122
+ "",
123
+ "## Archives",
124
+ `- [${archiveName}](${relLink})`,
125
+ "",
126
+ "## Recent Tail (for continuity)",
127
+ "",
128
+ "```md",
129
+ tail.trim(),
130
+ "```",
131
+ ""
132
+ ].join("\n");
133
+ return { archivedPath, newContent };
134
+ }
135
+
136
+ // ../remnic-core/src/sanitize.ts
137
+ var INJECTION_PATTERNS = [
138
+ /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?|context)/i,
139
+ /forget\s+(everything|all|previous|what)/i,
140
+ /new\s+(system\s+)?prompt:/i,
141
+ /\[system\]/i,
142
+ /<\s*system\s*>/i,
143
+ /you\s+are\s+now\s+(?!called|named)/i,
144
+ /disregard\s+(all\s+)?(previous|prior)/i,
145
+ /override\s+(previous\s+)?(instructions?|prompt)/i,
146
+ /act\s+as\s+(?:an?\s+)?(?:AI|assistant|ChatGPT|GPT|Claude|LLM)\s+(?:without|that\s+ignores)/i,
147
+ /do\s+not\s+(?:follow|obey)\s+(?:previous|prior|your)\s+instructions/i,
148
+ /pretend\s+(?:you\s+)?(?:have\s+no|you\s+don.?t\s+have)\s+(restrictions|guidelines|rules)/i
149
+ ];
150
+ var REDACTED_PLACEHOLDER = "[content removed: possible prompt injection]";
151
+ function sanitizeMemoryContent(text) {
152
+ const source = typeof text === "string" ? text : "";
153
+ const violations = [];
154
+ for (const pattern of INJECTION_PATTERNS) {
155
+ if (pattern.test(source)) {
156
+ violations.push(pattern.source);
157
+ }
158
+ }
159
+ if (violations.length === 0) {
160
+ return { clean: true, text: source, violations: [] };
161
+ }
162
+ return {
163
+ clean: false,
164
+ text: REDACTED_PLACEHOLDER,
165
+ violations
166
+ };
167
+ }
168
+
169
+ // ../remnic-core/src/types.ts
170
+ function confidenceTier(score) {
171
+ if (score >= 0.95) return "explicit";
172
+ if (score >= 0.7) return "implied";
173
+ if (score >= 0.4) return "inferred";
174
+ return "speculative";
175
+ }
176
+ var SPECULATIVE_TTL_DAYS = 30;
177
+
178
+ // ../remnic-core/src/memory-projection-store.ts
179
+ import path2 from "path";
180
+ import { readFileSync } from "fs";
181
+
182
+ // ../remnic-core/src/runtime/better-sqlite.ts
183
+ import { createRequire } from "module";
184
+ var cachedCtor = null;
185
+ function loadBetterSqlite3() {
186
+ if (cachedCtor) return cachedCtor;
187
+ const require2 = createRequire(import.meta.url);
188
+ try {
189
+ const loaded = require2("better-sqlite3");
190
+ const ctor = typeof loaded === "function" ? loaded : loaded.default;
191
+ if (typeof ctor !== "function") {
192
+ throw new Error("module did not export a constructor");
193
+ }
194
+ cachedCtor = ctor;
195
+ return ctor;
196
+ } catch (error) {
197
+ const detail = error instanceof Error && error.message.length > 0 ? ` (${error.message})` : "";
198
+ throw new Error(
199
+ "better-sqlite3 is unavailable. Rebuild it in the plugin install with `npm rebuild better-sqlite3 --build-from-source` before using SQLite-backed Engram features" + detail,
200
+ { cause: error instanceof Error ? error : void 0 }
201
+ );
202
+ }
203
+ }
204
+ function openBetterSqlite3(file, options) {
205
+ const Database = loadBetterSqlite3();
206
+ return new Database(file, options);
207
+ }
208
+
209
+ // ../remnic-core/src/memory-projection-store.ts
210
+ var MEMORY_PROJECTION_SCHEMA_VERSION = 2;
211
+ function getMemoryProjectionPath(memoryDir) {
212
+ return path2.join(memoryDir, "state", "memory-projection.sqlite");
213
+ }
214
+ function listTableColumns(db, tableName) {
215
+ try {
216
+ const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
217
+ return new Set(rows.map((row) => row.name).filter((name) => typeof name === "string"));
218
+ } catch {
219
+ return /* @__PURE__ */ new Set();
220
+ }
221
+ }
222
+ function migrateMemoryCurrentTable(db) {
223
+ const columns = listTableColumns(db, "memory_current");
224
+ if (columns.size === 0) return;
225
+ if (!columns.has("tags_json")) {
226
+ db.exec(`ALTER TABLE memory_current ADD COLUMN tags_json TEXT NOT NULL DEFAULT '[]'`);
227
+ }
228
+ if (!columns.has("preview_text")) {
229
+ db.exec(`ALTER TABLE memory_current ADD COLUMN preview_text TEXT NOT NULL DEFAULT ''`);
230
+ }
231
+ }
232
+ function memoryCurrentRequiresMigration(db) {
233
+ const columns = listTableColumns(db, "memory_current");
234
+ return columns.size > 0 && (!columns.has("tags_json") || !columns.has("preview_text"));
235
+ }
236
+ function migrateProjectionSchemaIfNeeded(memoryDir) {
237
+ const dbPath = getMemoryProjectionPath(memoryDir);
238
+ try {
239
+ const db = openBetterSqlite3(dbPath, { fileMustExist: true });
240
+ try {
241
+ if (!memoryCurrentRequiresMigration(db)) return;
242
+ initializeMemoryProjectionDb(db);
243
+ } finally {
244
+ db.close();
245
+ }
246
+ } catch {
247
+ }
248
+ }
249
+ function memoryCurrentSelectExpressions(db) {
250
+ const columns = listTableColumns(db, "memory_current");
251
+ return {
252
+ tagsJson: columns.has("tags_json") ? "tags_json" : `'[]' AS tags_json`,
253
+ previewText: columns.has("preview_text") ? "preview_text" : `'' AS preview_text`
254
+ };
255
+ }
256
+ function initializeMemoryProjectionDb(db) {
257
+ db.exec(`
258
+ CREATE TABLE IF NOT EXISTS meta (
259
+ key TEXT PRIMARY KEY,
260
+ value TEXT NOT NULL
261
+ );
262
+
263
+ CREATE TABLE IF NOT EXISTS memory_current (
264
+ memory_id TEXT PRIMARY KEY,
265
+ category TEXT NOT NULL,
266
+ status TEXT NOT NULL,
267
+ lifecycle_state TEXT,
268
+ path_rel TEXT NOT NULL,
269
+ created_at TEXT NOT NULL,
270
+ updated_at TEXT NOT NULL,
271
+ archived_at TEXT,
272
+ superseded_at TEXT,
273
+ entity_ref TEXT,
274
+ source TEXT NOT NULL,
275
+ confidence REAL NOT NULL,
276
+ confidence_tier TEXT NOT NULL,
277
+ memory_kind TEXT,
278
+ access_count INTEGER,
279
+ last_accessed TEXT,
280
+ tags_json TEXT NOT NULL DEFAULT '[]',
281
+ preview_text TEXT NOT NULL DEFAULT ''
282
+ );
283
+
284
+ CREATE INDEX IF NOT EXISTS idx_memory_current_status
285
+ ON memory_current(status);
286
+
287
+ CREATE INDEX IF NOT EXISTS idx_memory_current_category
288
+ ON memory_current(category);
289
+
290
+ CREATE INDEX IF NOT EXISTS idx_memory_current_updated
291
+ ON memory_current(updated_at DESC);
292
+
293
+ CREATE TABLE IF NOT EXISTS memory_timeline (
294
+ event_id TEXT PRIMARY KEY,
295
+ memory_id TEXT NOT NULL,
296
+ event_type TEXT NOT NULL,
297
+ timestamp TEXT NOT NULL,
298
+ event_order INTEGER NOT NULL,
299
+ actor TEXT NOT NULL,
300
+ reason_code TEXT,
301
+ rule_version TEXT NOT NULL,
302
+ related_memory_ids_json TEXT,
303
+ before_json TEXT,
304
+ after_json TEXT,
305
+ correlation_id TEXT
306
+ );
307
+
308
+ CREATE INDEX IF NOT EXISTS idx_memory_timeline_memory_ts
309
+ ON memory_timeline(memory_id, timestamp, event_order);
310
+
311
+ CREATE TABLE IF NOT EXISTS memory_entity_mentions (
312
+ memory_id TEXT NOT NULL,
313
+ entity_ref TEXT NOT NULL,
314
+ mention_source TEXT NOT NULL,
315
+ created_at TEXT NOT NULL,
316
+ updated_at TEXT NOT NULL,
317
+ PRIMARY KEY (memory_id, entity_ref, mention_source)
318
+ );
319
+
320
+ CREATE INDEX IF NOT EXISTS idx_memory_entity_mentions_entity
321
+ ON memory_entity_mentions(entity_ref, updated_at DESC);
322
+
323
+ CREATE TABLE IF NOT EXISTS native_knowledge_chunks (
324
+ chunk_id TEXT PRIMARY KEY,
325
+ source_path TEXT NOT NULL,
326
+ title TEXT NOT NULL,
327
+ source_kind TEXT NOT NULL,
328
+ start_line INTEGER NOT NULL,
329
+ end_line INTEGER NOT NULL,
330
+ derived_date TEXT,
331
+ session_key TEXT,
332
+ workflow_key TEXT,
333
+ author TEXT,
334
+ agent TEXT,
335
+ namespace TEXT,
336
+ privacy_class TEXT,
337
+ source_hash TEXT,
338
+ preview_text TEXT NOT NULL
339
+ );
340
+
341
+ CREATE INDEX IF NOT EXISTS idx_native_knowledge_source_kind
342
+ ON native_knowledge_chunks(source_kind);
343
+
344
+ CREATE INDEX IF NOT EXISTS idx_native_knowledge_namespace
345
+ ON native_knowledge_chunks(namespace);
346
+
347
+ CREATE TABLE IF NOT EXISTS memory_review_runs (
348
+ run_id TEXT PRIMARY KEY,
349
+ created_at TEXT NOT NULL,
350
+ mode TEXT NOT NULL,
351
+ summary_json TEXT NOT NULL,
352
+ metrics_json TEXT NOT NULL,
353
+ applied_actions_json TEXT NOT NULL,
354
+ report_markdown TEXT NOT NULL
355
+ );
356
+
357
+ CREATE INDEX IF NOT EXISTS idx_memory_review_runs_created
358
+ ON memory_review_runs(created_at DESC);
359
+
360
+ CREATE TABLE IF NOT EXISTS memory_review_queue (
361
+ entry_id TEXT PRIMARY KEY,
362
+ run_id TEXT NOT NULL,
363
+ memory_id TEXT NOT NULL,
364
+ path TEXT NOT NULL,
365
+ reason_code TEXT NOT NULL,
366
+ severity TEXT NOT NULL,
367
+ suggested_action TEXT NOT NULL,
368
+ suggested_status TEXT,
369
+ related_memory_ids_json TEXT NOT NULL
370
+ );
371
+
372
+ CREATE INDEX IF NOT EXISTS idx_memory_review_queue_run
373
+ ON memory_review_queue(run_id, reason_code, memory_id);
374
+
375
+ CREATE TABLE IF NOT EXISTS memory_review_actions (
376
+ row_key TEXT PRIMARY KEY,
377
+ run_id TEXT NOT NULL,
378
+ action TEXT NOT NULL,
379
+ memory_id TEXT NOT NULL,
380
+ reason_code TEXT NOT NULL,
381
+ before_status TEXT NOT NULL,
382
+ after_status TEXT,
383
+ original_path TEXT NOT NULL,
384
+ current_path TEXT NOT NULL
385
+ );
386
+
387
+ CREATE INDEX IF NOT EXISTS idx_memory_review_actions_run
388
+ ON memory_review_actions(run_id, memory_id);
389
+ `);
390
+ migrateMemoryCurrentTable(db);
391
+ db.prepare("INSERT OR REPLACE INTO meta(key, value) VALUES (?, ?)").run("schemaVersion", String(MEMORY_PROJECTION_SCHEMA_VERSION));
392
+ }
393
+ function openProjectionReadonly(memoryDir) {
394
+ const dbPath = getMemoryProjectionPath(memoryDir);
395
+ try {
396
+ return openBetterSqlite3(dbPath, { readonly: true, fileMustExist: true });
397
+ } catch {
398
+ return null;
399
+ }
400
+ }
401
+ function withProjectionReadonly(memoryDir, reader) {
402
+ const db = openProjectionReadonly(memoryDir);
403
+ if (!db) return null;
404
+ let needsMigration = false;
405
+ try {
406
+ needsMigration = memoryCurrentRequiresMigration(db);
407
+ return reader(db);
408
+ } catch {
409
+ return null;
410
+ } finally {
411
+ db.close();
412
+ if (needsMigration) {
413
+ migrateProjectionSchemaIfNeeded(memoryDir);
414
+ }
415
+ }
416
+ }
417
+ function parseStringArray(value) {
418
+ if (typeof value !== "string" || value.length === 0) return [];
419
+ try {
420
+ const parsed = JSON.parse(value);
421
+ return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : [];
422
+ } catch {
423
+ return [];
424
+ }
425
+ }
426
+ function parseJsonObject(value) {
427
+ if (typeof value !== "string" || value.length === 0) return void 0;
428
+ try {
429
+ return JSON.parse(value);
430
+ } catch {
431
+ return void 0;
432
+ }
433
+ }
434
+ function parseCurrentRow(memoryDir, row) {
435
+ if (!row) return null;
436
+ if (typeof row.memory_id !== "string" || typeof row.category !== "string" || typeof row.status !== "string" || typeof row.path_rel !== "string" || typeof row.created_at !== "string" || typeof row.updated_at !== "string" || typeof row.source !== "string" || typeof row.confidence !== "number" || typeof row.confidence_tier !== "string") {
437
+ return null;
438
+ }
439
+ return {
440
+ memoryId: row.memory_id,
441
+ category: row.category,
442
+ status: row.status,
443
+ lifecycleState: typeof row.lifecycle_state === "string" ? row.lifecycle_state : void 0,
444
+ path: path2.join(memoryDir, row.path_rel),
445
+ pathRel: row.path_rel,
446
+ created: row.created_at,
447
+ updated: row.updated_at,
448
+ archivedAt: typeof row.archived_at === "string" ? row.archived_at : void 0,
449
+ supersededAt: typeof row.superseded_at === "string" ? row.superseded_at : void 0,
450
+ entityRef: typeof row.entity_ref === "string" ? row.entity_ref : void 0,
451
+ source: row.source,
452
+ confidence: row.confidence,
453
+ confidenceTier: row.confidence_tier,
454
+ memoryKind: typeof row.memory_kind === "string" ? row.memory_kind : void 0,
455
+ accessCount: typeof row.access_count === "number" ? row.access_count : void 0,
456
+ lastAccessed: typeof row.last_accessed === "string" ? row.last_accessed : void 0,
457
+ tags: parseStringArray(row.tags_json),
458
+ preview: typeof row.preview_text === "string" ? row.preview_text : ""
459
+ };
460
+ }
461
+ function parseTimelineRows(rows) {
462
+ const out = [];
463
+ for (const row of rows) {
464
+ if (typeof row.event_id !== "string" || typeof row.memory_id !== "string" || typeof row.event_type !== "string" || typeof row.timestamp !== "string" || typeof row.actor !== "string" || typeof row.rule_version !== "string") {
465
+ continue;
466
+ }
467
+ out.push({
468
+ eventId: row.event_id,
469
+ memoryId: row.memory_id,
470
+ eventType: row.event_type,
471
+ timestamp: row.timestamp,
472
+ actor: row.actor,
473
+ reasonCode: typeof row.reason_code === "string" ? row.reason_code : void 0,
474
+ ruleVersion: row.rule_version,
475
+ relatedMemoryIds: parseStringArray(row.related_memory_ids_json),
476
+ before: parseJsonObject(row.before_json),
477
+ after: parseJsonObject(row.after_json),
478
+ correlationId: typeof row.correlation_id === "string" ? row.correlation_id : void 0
479
+ });
480
+ }
481
+ return out;
482
+ }
483
+ function readProjectedMemoryState(memoryDir, memoryId) {
484
+ return withProjectionReadonly(memoryDir, (db) => {
485
+ const currentSelect = memoryCurrentSelectExpressions(db);
486
+ const row = db.prepare(
487
+ `
488
+ SELECT
489
+ memory_id,
490
+ category,
491
+ status,
492
+ lifecycle_state,
493
+ path_rel,
494
+ created_at,
495
+ updated_at,
496
+ archived_at,
497
+ superseded_at,
498
+ entity_ref,
499
+ source,
500
+ confidence,
501
+ confidence_tier,
502
+ memory_kind,
503
+ access_count,
504
+ last_accessed,
505
+ ${currentSelect.tagsJson},
506
+ ${currentSelect.previewText}
507
+ FROM memory_current
508
+ WHERE memory_id = ?
509
+ `
510
+ ).get(memoryId);
511
+ return parseCurrentRow(memoryDir, row);
512
+ });
513
+ }
514
+ function readProjectedMemoryTimeline(memoryDir, memoryId, limit) {
515
+ const db = openProjectionReadonly(memoryDir);
516
+ if (!db) return null;
517
+ try {
518
+ const rows = db.prepare(
519
+ `
520
+ SELECT * FROM (
521
+ SELECT
522
+ event_id,
523
+ memory_id,
524
+ event_type,
525
+ timestamp,
526
+ event_order,
527
+ actor,
528
+ reason_code,
529
+ rule_version,
530
+ related_memory_ids_json,
531
+ before_json,
532
+ after_json,
533
+ correlation_id
534
+ FROM memory_timeline
535
+ WHERE memory_id = ?
536
+ ORDER BY timestamp DESC, event_order DESC
537
+ LIMIT ?
538
+ )
539
+ ORDER BY timestamp ASC, event_order ASC
540
+ `
541
+ ).all(memoryId, limit);
542
+ if (rows.length === 0) return null;
543
+ return parseTimelineRows(rows);
544
+ } catch {
545
+ return null;
546
+ } finally {
547
+ db.close();
548
+ }
549
+ }
550
+ function readProjectedMemoryBrowse(memoryDir, options) {
551
+ return withProjectionReadonly(memoryDir, (db) => {
552
+ const normalizedQuery = options.query?.trim().toLowerCase() ?? "";
553
+ const currentSelect = memoryCurrentSelectExpressions(db);
554
+ const whereClauses = [];
555
+ const params = [];
556
+ if (options.status) {
557
+ whereClauses.push("status = ?");
558
+ params.push(options.status);
559
+ }
560
+ if (options.category) {
561
+ whereClauses.push("category = ?");
562
+ params.push(options.category);
563
+ }
564
+ const sort = options.sort ?? "updated_desc";
565
+ const orderBySql = (() => {
566
+ switch (sort) {
567
+ case "updated_asc":
568
+ return "updated_at ASC, created_at ASC, memory_id ASC";
569
+ case "created_desc":
570
+ return "created_at DESC, updated_at DESC, memory_id ASC";
571
+ case "created_asc":
572
+ return "created_at ASC, updated_at ASC, memory_id ASC";
573
+ case "updated_desc":
574
+ default:
575
+ return "updated_at DESC, created_at DESC, memory_id ASC";
576
+ }
577
+ })();
578
+ const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
579
+ if (normalizedQuery) {
580
+ const allRows = db.prepare(`
581
+ SELECT
582
+ memory_id,
583
+ path_rel,
584
+ category,
585
+ status,
586
+ created_at,
587
+ updated_at,
588
+ entity_ref,
589
+ ${currentSelect.tagsJson},
590
+ ${currentSelect.previewText}
591
+ FROM memory_current
592
+ ${whereSql}
593
+ ORDER BY ${orderBySql}
594
+ `).all(...params);
595
+ const filtered = allRows.filter((row) => {
596
+ if (typeof row.memory_id !== "string" || typeof row.path_rel !== "string") return false;
597
+ const preview = typeof row.preview_text === "string" ? row.preview_text.toLowerCase() : "";
598
+ const category = typeof row.category === "string" ? row.category.toLowerCase() : "";
599
+ const entityRef = typeof row.entity_ref === "string" ? row.entity_ref.toLowerCase() : "";
600
+ const tags = typeof row.tags_json === "string" ? row.tags_json.toLowerCase() : "";
601
+ if (preview.includes(normalizedQuery) || category.includes(normalizedQuery) || entityRef.includes(normalizedQuery) || tags.includes(normalizedQuery)) {
602
+ return true;
603
+ }
604
+ try {
605
+ const filePath = path2.join(memoryDir, row.path_rel);
606
+ const content = readFileSync(filePath, "utf-8").toLowerCase();
607
+ return content.includes(normalizedQuery);
608
+ } catch {
609
+ return false;
610
+ }
611
+ });
612
+ const pageRows = filtered.slice(options.offset, options.offset + options.limit);
613
+ return {
614
+ total: filtered.length,
615
+ memories: pageRows.filter(
616
+ (row) => typeof row.memory_id === "string" && typeof row.path_rel === "string" && typeof row.category === "string" && typeof row.status === "string"
617
+ ).map((row) => ({
618
+ id: row.memory_id,
619
+ path: path2.join(memoryDir, row.path_rel),
620
+ category: row.category,
621
+ status: row.status,
622
+ created: typeof row.created_at === "string" ? row.created_at : void 0,
623
+ updated: typeof row.updated_at === "string" ? row.updated_at : void 0,
624
+ tags: parseStringArray(row.tags_json),
625
+ entityRef: typeof row.entity_ref === "string" ? row.entity_ref : void 0,
626
+ preview: typeof row.preview_text === "string" ? row.preview_text : ""
627
+ }))
628
+ };
629
+ }
630
+ const totalRow = db.prepare(`SELECT COUNT(*) AS total FROM memory_current ${whereSql}`).get(...params);
631
+ const rows = db.prepare(`
632
+ SELECT
633
+ memory_id,
634
+ path_rel,
635
+ category,
636
+ status,
637
+ created_at,
638
+ updated_at,
639
+ entity_ref,
640
+ ${currentSelect.tagsJson},
641
+ ${currentSelect.previewText}
642
+ FROM memory_current
643
+ ${whereSql}
644
+ ORDER BY ${orderBySql}
645
+ LIMIT ? OFFSET ?
646
+ `).all(...params, options.limit, options.offset);
647
+ return {
648
+ total: typeof totalRow?.total === "number" ? totalRow.total : 0,
649
+ memories: rows.filter(
650
+ (row) => typeof row.memory_id === "string" && typeof row.path_rel === "string" && typeof row.category === "string" && typeof row.status === "string"
651
+ ).map((row) => ({
652
+ id: row.memory_id,
653
+ path: path2.join(memoryDir, row.path_rel),
654
+ category: row.category,
655
+ status: row.status,
656
+ created: typeof row.created_at === "string" ? row.created_at : void 0,
657
+ updated: typeof row.updated_at === "string" ? row.updated_at : void 0,
658
+ tags: parseStringArray(row.tags_json),
659
+ entityRef: typeof row.entity_ref === "string" ? row.entity_ref : void 0,
660
+ preview: typeof row.preview_text === "string" ? row.preview_text : ""
661
+ }))
662
+ };
663
+ });
664
+ }
665
+ function readProjectedEntityMentions(memoryDir, memoryIds) {
666
+ const db = openProjectionReadonly(memoryDir);
667
+ if (!db) return null;
668
+ try {
669
+ const rows = db.prepare(`
670
+ SELECT
671
+ memory_id,
672
+ entity_ref,
673
+ mention_source,
674
+ created_at,
675
+ updated_at
676
+ FROM memory_entity_mentions
677
+ ORDER BY entity_ref ASC, updated_at DESC, memory_id ASC
678
+ `).all();
679
+ return rows.filter(
680
+ (row) => typeof row.memory_id === "string" && typeof row.entity_ref === "string" && typeof row.mention_source === "string" && typeof row.created_at === "string" && typeof row.updated_at === "string" && (!memoryIds || memoryIds.has(row.memory_id))
681
+ ).map((row) => ({
682
+ memoryId: row.memory_id,
683
+ entityRef: row.entity_ref,
684
+ mentionSource: row.mention_source,
685
+ created: row.created_at,
686
+ updated: row.updated_at
687
+ }));
688
+ } catch {
689
+ return null;
690
+ } finally {
691
+ db.close();
692
+ }
693
+ }
694
+ function readProjectedNativeKnowledgeChunks(memoryDir) {
695
+ const db = openProjectionReadonly(memoryDir);
696
+ if (!db) return null;
697
+ try {
698
+ const rows = db.prepare(`
699
+ SELECT
700
+ chunk_id,
701
+ source_path,
702
+ title,
703
+ source_kind,
704
+ start_line,
705
+ end_line,
706
+ derived_date,
707
+ session_key,
708
+ workflow_key,
709
+ author,
710
+ agent,
711
+ namespace,
712
+ privacy_class,
713
+ source_hash,
714
+ preview_text
715
+ FROM native_knowledge_chunks
716
+ ORDER BY source_kind ASC, source_path ASC, start_line ASC
717
+ `).all();
718
+ return rows.filter(
719
+ (row) => typeof row.chunk_id === "string" && typeof row.source_path === "string" && typeof row.title === "string" && typeof row.source_kind === "string" && typeof row.start_line === "number" && typeof row.end_line === "number" && typeof row.preview_text === "string"
720
+ ).map((row) => ({
721
+ chunkId: row.chunk_id,
722
+ sourcePath: row.source_path,
723
+ title: row.title,
724
+ sourceKind: row.source_kind,
725
+ startLine: row.start_line,
726
+ endLine: row.end_line,
727
+ derivedDate: typeof row.derived_date === "string" ? row.derived_date : void 0,
728
+ sessionKey: typeof row.session_key === "string" ? row.session_key : void 0,
729
+ workflowKey: typeof row.workflow_key === "string" ? row.workflow_key : void 0,
730
+ author: typeof row.author === "string" ? row.author : void 0,
731
+ agent: typeof row.agent === "string" ? row.agent : void 0,
732
+ namespace: typeof row.namespace === "string" ? row.namespace : void 0,
733
+ privacyClass: typeof row.privacy_class === "string" ? row.privacy_class : void 0,
734
+ sourceHash: typeof row.source_hash === "string" ? row.source_hash : void 0,
735
+ preview: row.preview_text
736
+ }));
737
+ } catch {
738
+ return null;
739
+ } finally {
740
+ db.close();
741
+ }
742
+ }
743
+ function readProjectedLatestReviewQueue(memoryDir) {
744
+ const db = openProjectionReadonly(memoryDir);
745
+ if (!db) return null;
746
+ try {
747
+ const latestRunId = db.prepare(`SELECT value FROM meta WHERE key = 'latestGovernanceRunId'`).get()?.value ?? db.prepare(`SELECT run_id AS value FROM memory_review_runs ORDER BY created_at DESC LIMIT 1`).get()?.value;
748
+ if (!latestRunId) {
749
+ return { found: false };
750
+ }
751
+ const runRow = db.prepare(`
752
+ SELECT
753
+ run_id,
754
+ summary_json,
755
+ metrics_json,
756
+ applied_actions_json,
757
+ report_markdown
758
+ FROM memory_review_runs
759
+ WHERE run_id = ?
760
+ `).get(latestRunId);
761
+ if (!runRow || typeof runRow.run_id !== "string") {
762
+ return { found: false };
763
+ }
764
+ const queueRows = db.prepare(`
765
+ SELECT
766
+ entry_id,
767
+ memory_id,
768
+ path,
769
+ reason_code,
770
+ severity,
771
+ suggested_action,
772
+ suggested_status,
773
+ related_memory_ids_json
774
+ FROM memory_review_queue
775
+ WHERE run_id = ?
776
+ ORDER BY reason_code ASC, memory_id ASC
777
+ `).all(latestRunId);
778
+ const actionRows = db.prepare(`
779
+ SELECT
780
+ row_key,
781
+ action,
782
+ memory_id,
783
+ reason_code,
784
+ before_status,
785
+ after_status,
786
+ original_path,
787
+ current_path
788
+ FROM memory_review_actions
789
+ WHERE run_id = ?
790
+ ORDER BY memory_id ASC, action ASC
791
+ `).all(latestRunId);
792
+ const reviewQueue = queueRows.filter(
793
+ (row) => typeof row.entry_id === "string" && typeof row.memory_id === "string" && typeof row.path === "string" && typeof row.reason_code === "string" && typeof row.severity === "string" && typeof row.suggested_action === "string"
794
+ ).map((row) => ({
795
+ entryId: row.entry_id,
796
+ memoryId: row.memory_id,
797
+ path: row.path,
798
+ reasonCode: row.reason_code,
799
+ severity: row.severity,
800
+ suggestedAction: row.suggested_action,
801
+ suggestedStatus: typeof row.suggested_status === "string" ? row.suggested_status : void 0,
802
+ relatedMemoryIds: parseStringArray(row.related_memory_ids_json)
803
+ }));
804
+ return {
805
+ found: true,
806
+ runId: latestRunId,
807
+ summary: parseJsonObject(runRow.summary_json),
808
+ metrics: parseJsonObject(runRow.metrics_json),
809
+ reviewQueue,
810
+ appliedActions: actionRows.length > 0 ? actionRows.filter(
811
+ (row) => typeof row.action === "string" && typeof row.memory_id === "string" && typeof row.reason_code === "string" && typeof row.before_status === "string" && typeof row.original_path === "string" && typeof row.current_path === "string"
812
+ ).map((row) => ({
813
+ action: row.action,
814
+ memoryId: row.memory_id,
815
+ reasonCode: row.reason_code,
816
+ beforeStatus: row.before_status,
817
+ afterStatus: typeof row.after_status === "string" ? row.after_status : void 0,
818
+ originalPath: row.original_path,
819
+ currentPath: row.current_path
820
+ })) : parseJsonObject(runRow.applied_actions_json) ?? [],
821
+ report: typeof runRow.report_markdown === "string" ? runRow.report_markdown : void 0
822
+ };
823
+ } catch {
824
+ return null;
825
+ } finally {
826
+ db.close();
827
+ }
828
+ }
829
+ function readProjectedGovernanceRecord(memoryDir) {
830
+ const snapshot = readProjectedLatestReviewQueue(memoryDir);
831
+ if (!snapshot?.found || !snapshot.runId) return null;
832
+ return {
833
+ runId: snapshot.runId,
834
+ summary: snapshot.summary ?? {},
835
+ metrics: snapshot.metrics ?? {},
836
+ reviewQueueRows: (snapshot.reviewQueue ?? []).map((entry) => ({
837
+ runId: snapshot.runId,
838
+ entryId: entry.entryId,
839
+ memoryId: entry.memoryId,
840
+ path: entry.path,
841
+ reasonCode: entry.reasonCode,
842
+ severity: entry.severity,
843
+ suggestedAction: entry.suggestedAction,
844
+ suggestedStatus: entry.suggestedStatus,
845
+ relatedMemoryIds: [...entry.relatedMemoryIds]
846
+ })),
847
+ appliedActionRows: (snapshot.appliedActions ?? []).map((action) => ({
848
+ runId: snapshot.runId,
849
+ rowKey: [
850
+ action.action,
851
+ action.memoryId,
852
+ action.reasonCode,
853
+ action.originalPath,
854
+ action.currentPath
855
+ ].join("::"),
856
+ action: action.action,
857
+ memoryId: action.memoryId,
858
+ reasonCode: action.reasonCode,
859
+ beforeStatus: action.beforeStatus,
860
+ afterStatus: action.afterStatus,
861
+ originalPath: action.originalPath,
862
+ currentPath: action.currentPath
863
+ })),
864
+ report: snapshot.report ?? ""
865
+ };
866
+ }
867
+
868
+ // ../remnic-core/src/memory-lifecycle-ledger-utils.ts
869
+ import path3 from "path";
870
+ var MEMORY_LIFECYCLE_RULE_VERSION = "memory-lifecycle-ledger.v1";
871
+ var MEMORY_LIFECYCLE_EVENT_SORT_ORDER = {
872
+ created: 0,
873
+ updated: 1,
874
+ promoted: 2,
875
+ explicit_capture_accepted: 3,
876
+ explicit_capture_queued: 4,
877
+ imported: 5,
878
+ merged: 6,
879
+ restored: 7,
880
+ superseded: 8,
881
+ rejected: 9,
882
+ archived: 10
883
+ };
884
+ function toMemoryPathRel(baseDir, filePath) {
885
+ if (!baseDir) return filePath.split(path3.sep).join("/");
886
+ return path3.relative(baseDir, filePath).split(path3.sep).join("/");
887
+ }
888
+ function isArchivedMemoryPath(pathRel) {
889
+ return pathRel === "archive" || pathRel.startsWith("archive/");
890
+ }
891
+ function inferMemoryStatus(frontmatter, pathRel, fallbackStatus = "active") {
892
+ if (frontmatter.status && frontmatter.status !== "active") return frontmatter.status;
893
+ if (frontmatter.archivedAt) return "archived";
894
+ if (isArchivedMemoryPath(pathRel)) return "archived";
895
+ if (frontmatter.status) return frontmatter.status;
896
+ return fallbackStatus;
897
+ }
898
+ function summarizeMemoryLifecycleState(memory) {
899
+ return {
900
+ category: memory.frontmatter.category,
901
+ path: memory.path,
902
+ status: memory.frontmatter.status ?? "active",
903
+ lifecycleState: memory.frontmatter.lifecycleState
904
+ };
905
+ }
906
+ function makeRebuiltMemoryLifecycleEvent(memory, eventType, timestamp) {
907
+ return {
908
+ eventId: `rebuild-${memory.frontmatter.id}-${eventType}-${timestamp}`,
909
+ memoryId: memory.frontmatter.id,
910
+ eventType,
911
+ timestamp,
912
+ actor: "maintenance.rebuildMemoryLifecycleLedger",
913
+ ruleVersion: MEMORY_LIFECYCLE_RULE_VERSION,
914
+ after: summarizeMemoryLifecycleState(memory),
915
+ relatedMemoryIds: [
916
+ ...memory.frontmatter.supersededBy ? [memory.frontmatter.supersededBy] : [],
917
+ ...memory.frontmatter.supersedes ? [memory.frontmatter.supersedes] : [],
918
+ ...(memory.frontmatter.lineage ?? []).filter(Boolean)
919
+ ]
920
+ };
921
+ }
922
+ function buildLifecycleEventsForMemory(memory) {
923
+ const events = [];
924
+ const created = memory.frontmatter.created;
925
+ const updated = memory.frontmatter.updated;
926
+ const archivedAt = memory.frontmatter.archivedAt;
927
+ const supersededAt = memory.frontmatter.supersededAt;
928
+ const effectiveArchivedAt = archivedAt ?? (memory.frontmatter.status === "archived" && updated ? updated : void 0);
929
+ events.push(makeRebuiltMemoryLifecycleEvent(memory, "created", created));
930
+ if (updated && updated !== created && updated !== effectiveArchivedAt && updated !== supersededAt) {
931
+ events.push(makeRebuiltMemoryLifecycleEvent(memory, "updated", updated));
932
+ }
933
+ if (supersededAt) {
934
+ events.push(makeRebuiltMemoryLifecycleEvent(memory, "superseded", supersededAt));
935
+ }
936
+ if (effectiveArchivedAt) {
937
+ events.push(makeRebuiltMemoryLifecycleEvent(memory, "archived", effectiveArchivedAt));
938
+ }
939
+ return events;
940
+ }
941
+ function sortMemoryLifecycleEvents(events) {
942
+ return [...events].sort((a, b) => {
943
+ if (a.memoryId !== b.memoryId) return a.memoryId.localeCompare(b.memoryId);
944
+ if (a.timestamp !== b.timestamp) return a.timestamp.localeCompare(b.timestamp);
945
+ return MEMORY_LIFECYCLE_EVENT_SORT_ORDER[a.eventType] - MEMORY_LIFECYCLE_EVENT_SORT_ORDER[b.eventType];
946
+ });
947
+ }
948
+
949
+ // ../remnic-core/src/memory-projection-format.ts
950
+ function normalizeProjectionPreview(content, maxChars = 180) {
951
+ return content.replace(/\s+/g, " ").trim().slice(0, maxChars);
952
+ }
953
+ function normalizeProjectionTags(tags) {
954
+ return [...new Set(
955
+ (tags ?? []).map((value) => value.trim()).filter((value) => value.length > 0)
956
+ )].sort();
957
+ }
958
+
959
+ // ../remnic-core/src/identity-continuity.ts
960
+ function parseFrontmatterValue(raw) {
961
+ try {
962
+ return JSON.parse(raw);
963
+ } catch {
964
+ return raw;
965
+ }
966
+ }
967
+ function parseFrontmatter(raw) {
968
+ const parsed = {};
969
+ for (const line of raw.split("\n")) {
970
+ const idx = line.indexOf(":");
971
+ if (idx <= 0) continue;
972
+ const key = line.slice(0, idx).trim();
973
+ const value = line.slice(idx + 1).trim();
974
+ parsed[key] = parseFrontmatterValue(value);
975
+ }
976
+ return parsed;
977
+ }
978
+ function emitSection(lines, title, value) {
979
+ if (!value || value.trim().length === 0) return;
980
+ lines.push(`## ${title}`, "", value.trim(), "");
981
+ }
982
+ function parseSection(body, title) {
983
+ const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
984
+ const re = new RegExp(`## ${escaped}\\n\\n([\\s\\S]*?)(?=\\n## |$)`);
985
+ const match = body.match(re);
986
+ if (!match) return void 0;
987
+ const value = match[1].trim();
988
+ return value.length > 0 ? value : void 0;
989
+ }
990
+ function serializeContinuityIncident(incident) {
991
+ const lines = [
992
+ "---",
993
+ `id: ${JSON.stringify(incident.id)}`,
994
+ `state: ${JSON.stringify(incident.state)}`,
995
+ `openedAt: ${JSON.stringify(incident.openedAt)}`,
996
+ `updatedAt: ${JSON.stringify(incident.updatedAt)}`
997
+ ];
998
+ if (incident.closedAt) lines.push(`closedAt: ${JSON.stringify(incident.closedAt)}`);
999
+ if (incident.triggerWindow) lines.push(`triggerWindow: ${JSON.stringify(incident.triggerWindow)}`);
1000
+ lines.push("---", "");
1001
+ emitSection(lines, "Symptom", incident.symptom);
1002
+ emitSection(lines, "Suspected Cause", incident.suspectedCause);
1003
+ emitSection(lines, "Fix Applied", incident.fixApplied);
1004
+ emitSection(lines, "Verification Result", incident.verificationResult);
1005
+ emitSection(lines, "Preventive Rule", incident.preventiveRule);
1006
+ return lines.join("\n").trimEnd() + "\n";
1007
+ }
1008
+ function parseContinuityIncident(raw) {
1009
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
1010
+ if (!match) return null;
1011
+ const frontmatter = parseFrontmatter(match[1]);
1012
+ const body = match[2] ?? "";
1013
+ const id = typeof frontmatter.id === "string" ? frontmatter.id : "";
1014
+ const stateRaw = frontmatter.state;
1015
+ const state = stateRaw === "closed" ? "closed" : "open";
1016
+ const openedAt = typeof frontmatter.openedAt === "string" ? frontmatter.openedAt : "";
1017
+ const updatedAt = typeof frontmatter.updatedAt === "string" ? frontmatter.updatedAt : openedAt;
1018
+ const symptom = parseSection(body, "Symptom");
1019
+ if (!id || !openedAt || !updatedAt || !symptom) return null;
1020
+ return {
1021
+ id,
1022
+ state,
1023
+ openedAt,
1024
+ updatedAt,
1025
+ triggerWindow: typeof frontmatter.triggerWindow === "string" ? frontmatter.triggerWindow : void 0,
1026
+ symptom,
1027
+ suspectedCause: parseSection(body, "Suspected Cause"),
1028
+ fixApplied: parseSection(body, "Fix Applied"),
1029
+ verificationResult: parseSection(body, "Verification Result"),
1030
+ preventiveRule: parseSection(body, "Preventive Rule"),
1031
+ closedAt: typeof frontmatter.closedAt === "string" ? frontmatter.closedAt : void 0
1032
+ };
1033
+ }
1034
+ function createContinuityIncidentRecord(id, input, nowIso) {
1035
+ return {
1036
+ id,
1037
+ state: "open",
1038
+ openedAt: nowIso,
1039
+ updatedAt: nowIso,
1040
+ triggerWindow: input.triggerWindow?.trim() || void 0,
1041
+ symptom: input.symptom.trim(),
1042
+ suspectedCause: input.suspectedCause?.trim() || void 0
1043
+ };
1044
+ }
1045
+ function closeContinuityIncidentRecord(incident, closure, nowIso) {
1046
+ return {
1047
+ ...incident,
1048
+ state: "closed",
1049
+ updatedAt: nowIso,
1050
+ closedAt: nowIso,
1051
+ fixApplied: closure.fixApplied.trim(),
1052
+ verificationResult: closure.verificationResult.trim(),
1053
+ preventiveRule: closure.preventiveRule?.trim() || incident.preventiveRule
1054
+ };
1055
+ }
1056
+ var LOOP_HEADER = "# Continuity Improvement Loops";
1057
+ var LOOP_CADENCES = /* @__PURE__ */ new Set(["daily", "weekly", "monthly", "quarterly"]);
1058
+ var LOOP_STATUSES = /* @__PURE__ */ new Set(["active", "paused", "retired"]);
1059
+ var STALE_LAST_REVIEWED_FALLBACK = "1970-01-01T00:00:00.000Z";
1060
+ function normalizeLoopField(value) {
1061
+ if (typeof value !== "string") return void 0;
1062
+ const trimmed = value.trim();
1063
+ if (trimmed.length === 0) return void 0;
1064
+ return trimmed.replace(/\s+/g, " ");
1065
+ }
1066
+ function isValidIso(value) {
1067
+ const ts = Date.parse(value);
1068
+ return Number.isFinite(ts);
1069
+ }
1070
+ function normalizeContinuityLoop(input, nowIso) {
1071
+ const id = normalizeLoopField(input.id);
1072
+ const cadence = normalizeLoopField(input.cadence);
1073
+ const status = normalizeLoopField(input.status);
1074
+ const purpose = normalizeLoopField(input.purpose);
1075
+ const killCondition = normalizeLoopField(input.killCondition);
1076
+ const notes = normalizeLoopField(input.notes);
1077
+ const lastReviewedRaw = "lastReviewed" in input && typeof input.lastReviewed === "string" ? input.lastReviewed : void 0;
1078
+ const lastReviewed = normalizeLoopField(lastReviewedRaw) ?? nowIso;
1079
+ if (!id || !cadence || !status || !purpose || !killCondition) return null;
1080
+ if (!LOOP_CADENCES.has(cadence)) return null;
1081
+ if (!LOOP_STATUSES.has(status)) return null;
1082
+ if (!isValidIso(lastReviewed)) return null;
1083
+ return {
1084
+ id,
1085
+ cadence,
1086
+ purpose,
1087
+ status,
1088
+ killCondition,
1089
+ lastReviewed,
1090
+ notes
1091
+ };
1092
+ }
1093
+ function serializeContinuityLoopSection(loop) {
1094
+ const lines = [
1095
+ `## ${loop.id}`,
1096
+ `cadence: ${loop.cadence}`,
1097
+ `purpose: ${loop.purpose}`,
1098
+ `status: ${loop.status}`,
1099
+ `killCondition: ${loop.killCondition}`,
1100
+ `lastReviewed: ${loop.lastReviewed}`
1101
+ ];
1102
+ if (loop.notes) lines.push(`notes: ${loop.notes}`);
1103
+ return lines.join("\n");
1104
+ }
1105
+ function splitLoopMarkdown(raw) {
1106
+ const text = (raw ?? "").replace(/\r/g, "");
1107
+ const lines = text.split("\n");
1108
+ const headerLines = [];
1109
+ const sections = [];
1110
+ let current = null;
1111
+ for (const line of lines) {
1112
+ const sectionMatch = line.match(/^##\s+(.+?)\s*$/);
1113
+ if (sectionMatch) {
1114
+ if (current) sections.push({ title: current.title, body: current.body.trimEnd() });
1115
+ current = { title: sectionMatch[1].trim(), body: "" };
1116
+ continue;
1117
+ }
1118
+ if (!current) {
1119
+ headerLines.push(line);
1120
+ continue;
1121
+ }
1122
+ current.body += current.body.length > 0 ? `
1123
+ ${line}` : line;
1124
+ }
1125
+ if (current) sections.push({ title: current.title, body: current.body.trimEnd() });
1126
+ const headerRaw = headerLines.join("\n").trim();
1127
+ const header = headerRaw.length > 0 ? headerRaw : LOOP_HEADER;
1128
+ return { header, sections };
1129
+ }
1130
+ function parseLoopFromSection(section, nowIso) {
1131
+ const fields = {};
1132
+ for (const line of section.body.split("\n")) {
1133
+ const kv = line.match(/^([A-Za-z][A-Za-z0-9_]*):\s*(.+?)\s*$/);
1134
+ if (!kv) continue;
1135
+ fields[kv[1]] = kv[2];
1136
+ }
1137
+ const parsedLastReviewed = normalizeLoopField(fields.lastReviewed);
1138
+ const safeLastReviewed = parsedLastReviewed && isValidIso(parsedLastReviewed) ? parsedLastReviewed : STALE_LAST_REVIEWED_FALLBACK;
1139
+ return normalizeContinuityLoop(
1140
+ {
1141
+ id: section.title,
1142
+ cadence: fields.cadence ?? "",
1143
+ purpose: fields.purpose ?? "",
1144
+ status: fields.status ?? "",
1145
+ killCondition: fields.killCondition ?? "",
1146
+ lastReviewed: safeLastReviewed,
1147
+ notes: fields.notes
1148
+ },
1149
+ nowIso
1150
+ );
1151
+ }
1152
+ function joinLoopMarkdown(header, sections) {
1153
+ const lines = [header.trim(), ""];
1154
+ for (const section of sections) {
1155
+ lines.push(`## ${section.title}`);
1156
+ if (section.body.trim().length > 0) {
1157
+ lines.push(section.body.trimEnd());
1158
+ }
1159
+ lines.push("");
1160
+ }
1161
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
1162
+ }
1163
+ function parseContinuityImprovementLoops(raw) {
1164
+ const parsed = splitLoopMarkdown(raw);
1165
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
1166
+ return parsed.sections.map((section) => parseLoopFromSection(section, nowIso)).filter((loop) => loop !== null);
1167
+ }
1168
+ function upsertContinuityLoopInMarkdown(raw, input, nowIso) {
1169
+ const normalized = normalizeContinuityLoop(input, nowIso);
1170
+ if (!normalized) {
1171
+ throw new Error("Invalid continuity loop input");
1172
+ }
1173
+ const parsed = splitLoopMarkdown(raw);
1174
+ let replaced = false;
1175
+ const nextSections = parsed.sections.map((section) => {
1176
+ if (normalizeLoopField(section.title) !== normalized.id) return section;
1177
+ replaced = true;
1178
+ return { title: normalized.id, body: serializeContinuityLoopSection(normalized).split("\n").slice(1).join("\n") };
1179
+ });
1180
+ if (!replaced) {
1181
+ nextSections.push({
1182
+ title: normalized.id,
1183
+ body: serializeContinuityLoopSection(normalized).split("\n").slice(1).join("\n")
1184
+ });
1185
+ }
1186
+ return { markdown: joinLoopMarkdown(parsed.header, nextSections), loop: normalized };
1187
+ }
1188
+ function reviewContinuityLoopInMarkdown(raw, id, input, nowIso) {
1189
+ const parsed = splitLoopMarkdown(raw);
1190
+ const normalizedId = normalizeLoopField(id);
1191
+ if (!normalizedId) {
1192
+ return { markdown: joinLoopMarkdown(parsed.header, parsed.sections), loop: null };
1193
+ }
1194
+ let updatedLoop = null;
1195
+ const nextSections = parsed.sections.map((section) => {
1196
+ if (normalizeLoopField(section.title) !== normalizedId) return section;
1197
+ const existing = parseLoopFromSection(section, nowIso);
1198
+ if (!existing) return section;
1199
+ const reviewed = applyContinuityLoopReview(existing, input, nowIso);
1200
+ updatedLoop = reviewed;
1201
+ return { title: reviewed.id, body: serializeContinuityLoopSection(reviewed).split("\n").slice(1).join("\n") };
1202
+ });
1203
+ return { markdown: joinLoopMarkdown(parsed.header, nextSections), loop: updatedLoop };
1204
+ }
1205
+ function applyContinuityLoopReview(existing, input, nowIso) {
1206
+ const nextStatus = normalizeLoopField(input.status);
1207
+ const nextNotes = normalizeLoopField(input.notes);
1208
+ const reviewedAt = normalizeLoopField(input.reviewedAt) ?? nowIso;
1209
+ return {
1210
+ ...existing,
1211
+ status: nextStatus && LOOP_STATUSES.has(nextStatus) ? nextStatus : existing.status,
1212
+ notes: nextNotes ?? existing.notes,
1213
+ lastReviewed: isValidIso(reviewedAt) ? reviewedAt : nowIso
1214
+ };
1215
+ }
1216
+
1217
+ // ../remnic-core/src/storage.ts
1218
+ var ARTIFACT_SEARCH_STOPWORDS = /* @__PURE__ */ new Set([
1219
+ "a",
1220
+ "an",
1221
+ "and",
1222
+ "are",
1223
+ "as",
1224
+ "at",
1225
+ "be",
1226
+ "but",
1227
+ "by",
1228
+ "for",
1229
+ "from",
1230
+ "has",
1231
+ "have",
1232
+ "i",
1233
+ "in",
1234
+ "is",
1235
+ "it",
1236
+ "of",
1237
+ "on",
1238
+ "or",
1239
+ "that",
1240
+ "the",
1241
+ "this",
1242
+ "to",
1243
+ "was",
1244
+ "were",
1245
+ "with"
1246
+ ]);
1247
+ function tokenizeArtifactSearchText(input) {
1248
+ return input.toLowerCase().split(/[^a-z0-9]+/i).map((t) => t.trim()).filter((t) => t.length >= 2).filter((t) => !ARTIFACT_SEARCH_STOPWORDS.has(t));
1249
+ }
1250
+ function serializeFrontmatter(fm) {
1251
+ const lines = [
1252
+ "---",
1253
+ `id: ${fm.id}`,
1254
+ `category: ${fm.category}`,
1255
+ `created: ${fm.created}`,
1256
+ `updated: ${fm.updated}`,
1257
+ `source: ${fm.source}`,
1258
+ `confidence: ${fm.confidence}`,
1259
+ `confidenceTier: ${fm.confidenceTier}`,
1260
+ `tags: [${fm.tags.map((t) => `"${t}"`).join(", ")}]`
1261
+ ];
1262
+ if (fm.entityRef) lines.push(`entityRef: ${fm.entityRef}`);
1263
+ if (fm.supersedes) lines.push(`supersedes: ${fm.supersedes}`);
1264
+ if (fm.expiresAt) lines.push(`expiresAt: ${fm.expiresAt}`);
1265
+ if (fm.lineage && fm.lineage.length > 0) {
1266
+ lines.push(`lineage: [${fm.lineage.map((l) => `"${l}"`).join(", ")}]`);
1267
+ }
1268
+ if (fm.status && fm.status !== "active") lines.push(`status: ${fm.status}`);
1269
+ if (fm.supersededBy) lines.push(`supersededBy: ${fm.supersededBy}`);
1270
+ if (fm.supersededAt) lines.push(`supersededAt: ${fm.supersededAt}`);
1271
+ if (fm.archivedAt) lines.push(`archivedAt: ${fm.archivedAt}`);
1272
+ if (fm.lifecycleState) lines.push(`lifecycleState: ${fm.lifecycleState}`);
1273
+ if (fm.verificationState) lines.push(`verificationState: ${fm.verificationState}`);
1274
+ if (fm.policyClass) lines.push(`policyClass: ${fm.policyClass}`);
1275
+ if (fm.lastValidatedAt) lines.push(`lastValidatedAt: ${fm.lastValidatedAt}`);
1276
+ if (fm.decayScore !== void 0) lines.push(`decayScore: ${fm.decayScore}`);
1277
+ if (fm.heatScore !== void 0) lines.push(`heatScore: ${fm.heatScore}`);
1278
+ if (fm.accessCount !== void 0 && fm.accessCount > 0) {
1279
+ lines.push(`accessCount: ${fm.accessCount}`);
1280
+ }
1281
+ if (fm.lastAccessed) lines.push(`lastAccessed: ${fm.lastAccessed}`);
1282
+ if (fm.importance) {
1283
+ lines.push(`importanceScore: ${fm.importance.score}`);
1284
+ lines.push(`importanceLevel: ${fm.importance.level}`);
1285
+ if (fm.importance.reasons.length > 0) {
1286
+ lines.push(
1287
+ `importanceReasons: [${fm.importance.reasons.map((r) => `"${r.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`).join(", ")}]`
1288
+ );
1289
+ }
1290
+ if (fm.importance.keywords.length > 0) {
1291
+ lines.push(`importanceKeywords: [${fm.importance.keywords.map((k) => `"${k}"`).join(", ")}]`);
1292
+ }
1293
+ }
1294
+ if (fm.parentId) lines.push(`parentId: ${fm.parentId}`);
1295
+ if (fm.chunkIndex !== void 0) lines.push(`chunkIndex: ${fm.chunkIndex}`);
1296
+ if (fm.chunkTotal !== void 0) lines.push(`chunkTotal: ${fm.chunkTotal}`);
1297
+ if (fm.links && fm.links.length > 0) {
1298
+ lines.push("links:");
1299
+ for (const link of fm.links) {
1300
+ lines.push(` - targetId: ${link.targetId}`);
1301
+ lines.push(` linkType: ${link.linkType}`);
1302
+ lines.push(` strength: ${link.strength}`);
1303
+ if (link.reason) lines.push(` reason: ${JSON.stringify(link.reason)}`);
1304
+ }
1305
+ }
1306
+ if (fm.intentGoal) lines.push(`intentGoal: ${fm.intentGoal}`);
1307
+ if (fm.intentActionType) lines.push(`intentActionType: ${fm.intentActionType}`);
1308
+ if (fm.intentEntityTypes && fm.intentEntityTypes.length > 0) {
1309
+ lines.push(`intentEntityTypes: [${fm.intentEntityTypes.map((t) => `"${t}"`).join(", ")}]`);
1310
+ }
1311
+ if (fm.artifactType) lines.push(`artifactType: ${fm.artifactType}`);
1312
+ if (fm.sourceMemoryId) lines.push(`sourceMemoryId: ${fm.sourceMemoryId}`);
1313
+ if (fm.sourceTurnId) lines.push(`sourceTurnId: ${fm.sourceTurnId}`);
1314
+ if (fm.memoryKind) lines.push(`memoryKind: ${fm.memoryKind}`);
1315
+ if (fm.structuredAttributes && Object.keys(fm.structuredAttributes).length > 0) {
1316
+ lines.push(`structuredAttributes: ${JSON.stringify(fm.structuredAttributes)}`);
1317
+ }
1318
+ lines.push("---");
1319
+ return lines.join("\n");
1320
+ }
1321
+ function parseStructuredAttributes(raw) {
1322
+ if (!raw || !raw.trim()) return void 0;
1323
+ try {
1324
+ const parsed = JSON.parse(raw);
1325
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1326
+ const result = {};
1327
+ for (const [k, v] of Object.entries(parsed)) {
1328
+ if (typeof k === "string" && typeof v === "string") {
1329
+ result[k] = v;
1330
+ }
1331
+ }
1332
+ return Object.keys(result).length > 0 ? result : void 0;
1333
+ }
1334
+ } catch {
1335
+ }
1336
+ return void 0;
1337
+ }
1338
+ function parseLinkReasonValue(rawValue) {
1339
+ const legacyValue = rawValue.replace(/\\"/g, '"');
1340
+ const looksLikeLegacyPath = !rawValue.includes("\\\\") && (/[A-Za-z]:\\[A-Za-z0-9._ -]+(?:\\[A-Za-z0-9._ -]+)*/.test(rawValue) || /\\[A-Za-z0-9._ -]+\\[A-Za-z0-9._ -]+/.test(rawValue));
1341
+ if (looksLikeLegacyPath) {
1342
+ return legacyValue;
1343
+ }
1344
+ try {
1345
+ return JSON.parse(`"${rawValue}"`);
1346
+ } catch {
1347
+ return legacyValue;
1348
+ }
1349
+ }
1350
+ function parseFrontmatter2(raw) {
1351
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
1352
+ if (!match) return null;
1353
+ const fmBlock = match[1];
1354
+ const content = match[2].trim();
1355
+ const fm = {};
1356
+ for (const line of fmBlock.split("\n")) {
1357
+ const colonIdx = line.indexOf(":");
1358
+ if (colonIdx === -1) continue;
1359
+ const key = line.slice(0, colonIdx).trim();
1360
+ const value = line.slice(colonIdx + 1).trim();
1361
+ fm[key] = value;
1362
+ }
1363
+ let tags = [];
1364
+ const tagsStr = fm.tags ?? "";
1365
+ const tagMatch = tagsStr.match(/\[(.*)]/);
1366
+ if (tagMatch) {
1367
+ tags = tagMatch[1].split(",").map((t) => t.trim().replace(/^"|"$/g, "")).filter(Boolean);
1368
+ }
1369
+ let intentEntityTypes;
1370
+ const intentEntityTypesStr = fm.intentEntityTypes ?? "";
1371
+ const intentEntityTypesMatch = intentEntityTypesStr.match(/\[(.*)]/);
1372
+ if (intentEntityTypesMatch) {
1373
+ intentEntityTypes = intentEntityTypesMatch[1].split(",").map((t) => t.trim().replace(/^"|"$/g, "")).filter(Boolean);
1374
+ }
1375
+ const conf = parseFloat(fm.confidence ?? "0.8");
1376
+ let lineage;
1377
+ const lineageStr = fm.lineage ?? "";
1378
+ const lineageMatch = lineageStr.match(/\[(.*)]/);
1379
+ if (lineageMatch) {
1380
+ lineage = lineageMatch[1].split(",").map((l) => l.trim().replace(/^"|"$/g, "")).filter(Boolean);
1381
+ }
1382
+ const accessCount = fm.accessCount ? parseInt(fm.accessCount, 10) : void 0;
1383
+ const decayScore = fm.decayScore !== void 0 ? parseFloat(fm.decayScore) : void 0;
1384
+ const heatScore = fm.heatScore !== void 0 ? parseFloat(fm.heatScore) : void 0;
1385
+ let importance;
1386
+ if (fm.importanceScore) {
1387
+ const score = parseFloat(fm.importanceScore);
1388
+ const level = fm.importanceLevel || "normal";
1389
+ let reasons = [];
1390
+ const reasonsStr = fm.importanceReasons ?? "";
1391
+ if (reasonsStr.trim().startsWith("[") && reasonsStr.trim().endsWith("]")) {
1392
+ const reasonMatches = reasonsStr.matchAll(/"((?:\\.|[^"\\])*)"/g);
1393
+ for (const match2 of reasonMatches) {
1394
+ const reason = parseLinkReasonValue(match2[1]);
1395
+ if (reason.length > 0) {
1396
+ reasons.push(reason);
1397
+ }
1398
+ }
1399
+ }
1400
+ let keywords = [];
1401
+ const keywordsStr = fm.importanceKeywords ?? "";
1402
+ const keywordsMatch = keywordsStr.match(/\[(.*)]/);
1403
+ if (keywordsMatch) {
1404
+ keywords = keywordsMatch[1].split(",").map((k) => k.trim().replace(/^"|"$/g, "")).filter(Boolean);
1405
+ }
1406
+ importance = { score, level, reasons, keywords };
1407
+ }
1408
+ const result = {
1409
+ frontmatter: {
1410
+ id: fm.id ?? "",
1411
+ category: fm.category ?? "fact",
1412
+ created: fm.created ?? (/* @__PURE__ */ new Date()).toISOString(),
1413
+ updated: fm.updated ?? (/* @__PURE__ */ new Date()).toISOString(),
1414
+ source: fm.source ?? "unknown",
1415
+ confidence: conf,
1416
+ confidenceTier: fm.confidenceTier || confidenceTier(conf),
1417
+ tags,
1418
+ entityRef: fm.entityRef || void 0,
1419
+ supersedes: fm.supersedes || void 0,
1420
+ expiresAt: fm.expiresAt || void 0,
1421
+ lineage: lineage && lineage.length > 0 ? lineage : void 0,
1422
+ // Status management
1423
+ status: fm.status || "active",
1424
+ supersededBy: fm.supersededBy || void 0,
1425
+ supersededAt: fm.supersededAt || void 0,
1426
+ archivedAt: fm.archivedAt || void 0,
1427
+ lifecycleState: fm.lifecycleState || void 0,
1428
+ verificationState: fm.verificationState || void 0,
1429
+ policyClass: fm.policyClass || void 0,
1430
+ lastValidatedAt: fm.lastValidatedAt || void 0,
1431
+ decayScore: Number.isFinite(decayScore) ? decayScore : void 0,
1432
+ heatScore: Number.isFinite(heatScore) ? heatScore : void 0,
1433
+ // Access tracking
1434
+ accessCount: accessCount && accessCount > 0 ? accessCount : void 0,
1435
+ lastAccessed: fm.lastAccessed || void 0,
1436
+ // Importance scoring
1437
+ importance,
1438
+ // Chunking
1439
+ parentId: fm.parentId || void 0,
1440
+ chunkIndex: fm.chunkIndex ? parseInt(fm.chunkIndex, 10) : void 0,
1441
+ chunkTotal: fm.chunkTotal ? parseInt(fm.chunkTotal, 10) : void 0,
1442
+ // Links are parsed separately below
1443
+ intentGoal: fm.intentGoal || void 0,
1444
+ intentActionType: fm.intentActionType || void 0,
1445
+ intentEntityTypes: intentEntityTypes && intentEntityTypes.length > 0 ? intentEntityTypes : void 0,
1446
+ artifactType: fm.artifactType || void 0,
1447
+ sourceMemoryId: fm.sourceMemoryId || void 0,
1448
+ sourceTurnId: fm.sourceTurnId || void 0,
1449
+ // v8.0 Phase 2B: HiMem episode/note classification
1450
+ memoryKind: fm.memoryKind || void 0,
1451
+ // Structured attributes (JSON on a single line)
1452
+ structuredAttributes: parseStructuredAttributes(fm.structuredAttributes)
1453
+ },
1454
+ content
1455
+ };
1456
+ if (fmBlock.includes("links:")) {
1457
+ const links = [];
1458
+ const linkMatches = fmBlock.matchAll(
1459
+ /- targetId: (\S+)\s+linkType: (\S+)\s+strength: ([\d.]+)(?:\s+reason: "((?:\\.|[^"\\])*)")?/g
1460
+ );
1461
+ for (const match2 of linkMatches) {
1462
+ links.push({
1463
+ targetId: match2[1],
1464
+ linkType: match2[2],
1465
+ strength: parseFloat(match2[3]),
1466
+ reason: match2[4] ? parseLinkReasonValue(match2[4]) : void 0
1467
+ });
1468
+ }
1469
+ if (links.length > 0) {
1470
+ result.frontmatter.links = links;
1471
+ }
1472
+ }
1473
+ return result;
1474
+ }
1475
+ function normalizeFrontmatterForPath(frontmatter, pathRel) {
1476
+ if (isArchivedMemoryPath(pathRel) && (!frontmatter.status || frontmatter.status === "active")) {
1477
+ return {
1478
+ ...frontmatter,
1479
+ status: "archived"
1480
+ };
1481
+ }
1482
+ return frontmatter;
1483
+ }
1484
+ function inferCurrentStateStatus(frontmatter, pathRel, fallbackStatus) {
1485
+ return inferMemoryStatus(frontmatter, pathRel, fallbackStatus);
1486
+ }
1487
+ var userAliases = {};
1488
+ var BUILTIN_ALIASES = {
1489
+ openclaw: "openclaw",
1490
+ "open-claw": "openclaw"
1491
+ };
1492
+ function normalizeEntityName(raw, type) {
1493
+ const rawStr = typeof raw === "string" ? raw : "";
1494
+ const typeStr = typeof type === "string" && type.trim().length > 0 ? type : "entity";
1495
+ let name = rawStr.toLowerCase().trim();
1496
+ const typePrefix = `${typeStr.toLowerCase()}-`;
1497
+ if (name.startsWith(typePrefix)) {
1498
+ name = name.slice(typePrefix.length);
1499
+ }
1500
+ let normalized = name.replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1501
+ if (userAliases[normalized]) {
1502
+ normalized = userAliases[normalized];
1503
+ } else if (BUILTIN_ALIASES[normalized]) {
1504
+ normalized = BUILTIN_ALIASES[normalized];
1505
+ }
1506
+ return `${typeStr.toLowerCase()}-${normalized}`;
1507
+ }
1508
+ function levenshtein(a, b) {
1509
+ const m = a.length;
1510
+ const n = b.length;
1511
+ if (m === 0) return n;
1512
+ if (n === 0) return m;
1513
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
1514
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
1515
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
1516
+ for (let i = 1; i <= m; i++) {
1517
+ for (let j = 1; j <= n; j++) {
1518
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
1519
+ }
1520
+ }
1521
+ return dp[m][n];
1522
+ }
1523
+ function dehyphenate(s) {
1524
+ return s.replace(/-/g, "");
1525
+ }
1526
+ var ContentHashIndex = class _ContentHashIndex {
1527
+ hashes = /* @__PURE__ */ new Set();
1528
+ dirty = false;
1529
+ filePath;
1530
+ constructor(stateDir) {
1531
+ this.filePath = path4.join(stateDir, "fact-hashes.txt");
1532
+ }
1533
+ /** Load existing hashes from disk. Safe to call multiple times. */
1534
+ async load() {
1535
+ try {
1536
+ const raw = await readFile2(this.filePath, "utf-8");
1537
+ for (const line of raw.split("\n")) {
1538
+ const trimmed = line.trim();
1539
+ if (trimmed.length > 0) {
1540
+ this.hashes.add(trimmed);
1541
+ }
1542
+ }
1543
+ log.debug(`content-hash index: loaded ${this.hashes.size} hashes`);
1544
+ } catch {
1545
+ log.debug("content-hash index: no existing index \u2014 starting fresh");
1546
+ }
1547
+ }
1548
+ /** Check if content already exists in the index. */
1549
+ has(content) {
1550
+ return this.hashes.has(_ContentHashIndex.computeHash(content));
1551
+ }
1552
+ /** Add content hash to the index. */
1553
+ add(content) {
1554
+ const hash = _ContentHashIndex.computeHash(content);
1555
+ if (!this.hashes.has(hash)) {
1556
+ this.hashes.add(hash);
1557
+ this.dirty = true;
1558
+ }
1559
+ }
1560
+ get size() {
1561
+ return this.hashes.size;
1562
+ }
1563
+ /** Persist index to disk if changed. */
1564
+ async save() {
1565
+ if (!this.dirty) return;
1566
+ await mkdir2(path4.dirname(this.filePath), { recursive: true });
1567
+ await writeFile2(this.filePath, [...this.hashes].join("\n") + "\n", "utf-8");
1568
+ this.dirty = false;
1569
+ log.debug(`content-hash index: saved ${this.hashes.size} hashes`);
1570
+ }
1571
+ /** Remove a hash from the index (used when archiving/deleting). */
1572
+ remove(content) {
1573
+ const hash = _ContentHashIndex.computeHash(content);
1574
+ if (this.hashes.delete(hash)) {
1575
+ this.dirty = true;
1576
+ }
1577
+ }
1578
+ /** Normalize content and compute SHA-256 hash. */
1579
+ static normalizeContent(content) {
1580
+ return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
1581
+ }
1582
+ /** Normalize content and compute SHA-256 hash. */
1583
+ static computeHash(content) {
1584
+ const normalized = _ContentHashIndex.normalizeContent(content);
1585
+ return createHash("sha256").update(normalized).digest("hex");
1586
+ }
1587
+ };
1588
+ function parseEntityFile(content) {
1589
+ const lines = content.split("\n");
1590
+ let name = "";
1591
+ let type = "other";
1592
+ let updated = "";
1593
+ let summary;
1594
+ const facts = [];
1595
+ const relationships = [];
1596
+ const activity = [];
1597
+ const aliases = [];
1598
+ const headingLine = lines.find((l) => l.startsWith("# "));
1599
+ if (headingLine) name = headingLine.slice(2).trim();
1600
+ const typeLine = lines.find((l) => l.startsWith("**Type:**"));
1601
+ if (typeLine) type = typeLine.replace("**Type:**", "").trim();
1602
+ const updatedLine = lines.find((l) => l.startsWith("**Updated:**"));
1603
+ if (updatedLine) updated = updatedLine.replace("**Updated:**", "").trim();
1604
+ let section = "";
1605
+ for (const line of lines) {
1606
+ if (line.startsWith("## ")) {
1607
+ section = line.slice(3).trim().toLowerCase();
1608
+ continue;
1609
+ }
1610
+ if (!line.startsWith("- ")) continue;
1611
+ const bullet = line.slice(2).trim();
1612
+ if (!bullet) continue;
1613
+ switch (section) {
1614
+ case "facts":
1615
+ facts.push(bullet);
1616
+ break;
1617
+ case "summary":
1618
+ break;
1619
+ case "connected to": {
1620
+ const relMatch = bullet.match(/^\[\[([^\]]+)\]\]\s*[—–-]\s*(.+)$/);
1621
+ if (relMatch) {
1622
+ relationships.push({ target: relMatch[1].trim(), label: relMatch[2].trim() });
1623
+ }
1624
+ break;
1625
+ }
1626
+ case "activity": {
1627
+ const actMatch = bullet.match(/^(\d{4}-\d{2}-\d{2}):\s*(.+)$/);
1628
+ if (actMatch) {
1629
+ activity.push({ date: actMatch[1], note: actMatch[2].trim() });
1630
+ }
1631
+ break;
1632
+ }
1633
+ case "aliases":
1634
+ aliases.push(bullet);
1635
+ break;
1636
+ }
1637
+ }
1638
+ const summaryIdx = lines.findIndex((l) => l.startsWith("## Summary"));
1639
+ if (summaryIdx !== -1) {
1640
+ const summaryLines = [];
1641
+ for (let i = summaryIdx + 1; i < lines.length; i++) {
1642
+ if (lines[i].startsWith("## ")) break;
1643
+ const trimmed = lines[i].trim();
1644
+ if (trimmed) summaryLines.push(trimmed);
1645
+ }
1646
+ if (summaryLines.length > 0) summary = summaryLines.join(" ");
1647
+ }
1648
+ return { name, type, updated, facts, summary, relationships, activity, aliases };
1649
+ }
1650
+ function serializeEntityFile(entity) {
1651
+ const lines = [
1652
+ `# ${entity.name}`,
1653
+ "",
1654
+ `**Type:** ${entity.type}`,
1655
+ `**Updated:** ${entity.updated || (/* @__PURE__ */ new Date()).toISOString()}`,
1656
+ ""
1657
+ ];
1658
+ if (entity.summary) {
1659
+ lines.push("## Summary", "", entity.summary, "");
1660
+ }
1661
+ lines.push("## Facts", "");
1662
+ for (const f of entity.facts) {
1663
+ lines.push(`- ${f}`);
1664
+ }
1665
+ lines.push("");
1666
+ if (entity.relationships.length > 0) {
1667
+ lines.push("## Connected to", "");
1668
+ for (const rel of entity.relationships) {
1669
+ lines.push(`- [[${rel.target}]] \u2014 ${rel.label}`);
1670
+ }
1671
+ lines.push("");
1672
+ }
1673
+ if (entity.activity.length > 0) {
1674
+ lines.push("## Activity", "");
1675
+ for (const act of entity.activity) {
1676
+ lines.push(`- ${act.date}: ${act.note}`);
1677
+ }
1678
+ lines.push("");
1679
+ }
1680
+ if (entity.aliases.length > 0) {
1681
+ lines.push("## Aliases", "");
1682
+ for (const alias of entity.aliases) {
1683
+ lines.push(`- ${alias}`);
1684
+ }
1685
+ lines.push("");
1686
+ }
1687
+ return lines.join("\n");
1688
+ }
1689
+ var StorageManager = class _StorageManager {
1690
+ constructor(baseDir) {
1691
+ this.baseDir = baseDir;
1692
+ }
1693
+ baseDir;
1694
+ knowledgeIndexCache = null;
1695
+ static KNOWLEDGE_INDEX_CACHE_TTL_MS = 6e5;
1696
+ // 10 minutes (entity mutations invalidate)
1697
+ artifactIndexCache = null;
1698
+ static ARTIFACT_INDEX_CACHE_TTL_MS = 6e4;
1699
+ // 1 minute
1700
+ static artifactWriteVersionByDir = /* @__PURE__ */ new Map();
1701
+ static memoryStatusVersionByDir = /* @__PURE__ */ new Map();
1702
+ // Module-level cache for readAllMemories() keyed by base directory.
1703
+ // Shared across all StorageManager instances to avoid duplicate I/O when
1704
+ // multiple concurrent callers (e.g. verifiedRecall + verifiedRules) read the
1705
+ // same directory simultaneously. In-flight deduplication prevents multiple
1706
+ // concurrent reads of the same directory.
1707
+ //
1708
+ // Stale-while-revalidate: once the cache has a value, subsequent reads after
1709
+ // TTL expiry return the stale cached data immediately and kick off a background
1710
+ // refresh. This eliminates the 13-60 s cold-scan penalty that would otherwise
1711
+ // block recall requests every 5 minutes on large memory collections (80k+ files).
1712
+ static allMemoriesInFlight = /* @__PURE__ */ new Map();
1713
+ // Cache for readQuestions() — avoids serially re-reading tens of thousands of
1714
+ // question files on every recall. 60-second TTL is intentionally short so that
1715
+ // newly written questions surface quickly.
1716
+ static QUESTIONS_CACHE_TTL_MS = 6e4;
1717
+ // 1 minute
1718
+ static questionsCache = /* @__PURE__ */ new Map();
1719
+ factHashIndex = null;
1720
+ factHashIndexLoadPromise = null;
1721
+ factHashIndexAuthoritative = null;
1722
+ factHashIndexAuthoritativePromise = null;
1723
+ /** The root directory of this storage instance. */
1724
+ get dir() {
1725
+ return this.baseDir;
1726
+ }
1727
+ identityFilePath(workspaceDir, namespace) {
1728
+ const rawNamespace = typeof namespace === "string" ? namespace.trim() : "";
1729
+ if (!rawNamespace) return path4.join(workspaceDir, "IDENTITY.md");
1730
+ const safeNamespace = rawNamespace.replace(/[^a-zA-Z0-9._-]/g, "-");
1731
+ return path4.join(workspaceDir, `IDENTITY.${safeNamespace}.md`);
1732
+ }
1733
+ versionFilePath(kind) {
1734
+ const fileName = kind === "memory-status" ? ".memory-status-version.log" : ".artifact-write-version.log";
1735
+ return path4.join(this.stateDir, fileName);
1736
+ }
1737
+ bumpSharedVersion(kind, fallbackMap) {
1738
+ const filePath = this.versionFilePath(kind);
1739
+ try {
1740
+ mkdirSync(this.stateDir, { recursive: true });
1741
+ appendFileSync(filePath, "x");
1742
+ const next = statSync(filePath).size;
1743
+ fallbackMap.set(this.baseDir, next);
1744
+ return next;
1745
+ } catch {
1746
+ const next = (fallbackMap.get(this.baseDir) ?? 0) + 1;
1747
+ fallbackMap.set(this.baseDir, next);
1748
+ return next;
1749
+ }
1750
+ }
1751
+ readSharedVersion(kind, fallbackMap) {
1752
+ const filePath = this.versionFilePath(kind);
1753
+ try {
1754
+ return statSync(filePath).size;
1755
+ } catch {
1756
+ return fallbackMap.get(this.baseDir) ?? 0;
1757
+ }
1758
+ }
1759
+ bumpMemoryStatusVersion() {
1760
+ this.bumpSharedVersion("memory-status", _StorageManager.memoryStatusVersionByDir);
1761
+ }
1762
+ getMemoryStatusVersion() {
1763
+ return this.readSharedVersion("memory-status", _StorageManager.memoryStatusVersionByDir);
1764
+ }
1765
+ bumpArtifactWriteVersion() {
1766
+ return this.bumpSharedVersion("artifact-write", _StorageManager.artifactWriteVersionByDir);
1767
+ }
1768
+ getArtifactWriteVersion() {
1769
+ return this.readSharedVersion("artifact-write", _StorageManager.artifactWriteVersionByDir);
1770
+ }
1771
+ get factsDir() {
1772
+ return path4.join(this.baseDir, "facts");
1773
+ }
1774
+ get correctionsDir() {
1775
+ return path4.join(this.baseDir, "corrections");
1776
+ }
1777
+ get entitiesDir() {
1778
+ return path4.join(this.baseDir, "entities");
1779
+ }
1780
+ get stateDir() {
1781
+ return path4.join(this.baseDir, "state");
1782
+ }
1783
+ get factHashIndexReadyPath() {
1784
+ return path4.join(this.stateDir, "fact-hashes.ready");
1785
+ }
1786
+ async getFactHashIndex() {
1787
+ if (this.factHashIndex) {
1788
+ return this.factHashIndex;
1789
+ }
1790
+ if (!this.factHashIndexLoadPromise) {
1791
+ const index = new ContentHashIndex(this.stateDir);
1792
+ this.factHashIndexLoadPromise = index.load().then(() => {
1793
+ this.factHashIndex = index;
1794
+ return index;
1795
+ }).catch((err) => {
1796
+ this.factHashIndexLoadPromise = null;
1797
+ throw err;
1798
+ });
1799
+ }
1800
+ return this.factHashIndexLoadPromise;
1801
+ }
1802
+ async ensureFactHashIndexAuthoritative() {
1803
+ if (this.factHashIndexAuthoritative === true) {
1804
+ return;
1805
+ }
1806
+ if (this.factHashIndexAuthoritativePromise) {
1807
+ await this.factHashIndexAuthoritativePromise;
1808
+ return;
1809
+ }
1810
+ this.factHashIndexAuthoritativePromise = (async () => {
1811
+ try {
1812
+ await access(this.factHashIndexReadyPath);
1813
+ this.factHashIndexAuthoritative = true;
1814
+ return;
1815
+ } catch {
1816
+ }
1817
+ const factHashIndex = await this.getFactHashIndex();
1818
+ const existing = await this.readAllMemories();
1819
+ for (const memory of existing) {
1820
+ if (memory.frontmatter.category !== "fact") continue;
1821
+ if (inferMemoryStatus(memory.frontmatter, memory.path) !== "active") continue;
1822
+ factHashIndex.add(memory.content);
1823
+ }
1824
+ await factHashIndex.save();
1825
+ await mkdir2(path4.dirname(this.factHashIndexReadyPath), { recursive: true });
1826
+ await writeFile2(this.factHashIndexReadyPath, "v1\n", "utf-8");
1827
+ this.factHashIndexAuthoritative = true;
1828
+ })().finally(() => {
1829
+ this.factHashIndexAuthoritativePromise = null;
1830
+ });
1831
+ await this.factHashIndexAuthoritativePromise;
1832
+ }
1833
+ get questionsDir() {
1834
+ return path4.join(this.baseDir, "questions");
1835
+ }
1836
+ get artifactsDir() {
1837
+ return path4.join(this.baseDir, "artifacts");
1838
+ }
1839
+ get identityDir() {
1840
+ return path4.join(this.baseDir, "identity");
1841
+ }
1842
+ get identityAnchorPath() {
1843
+ return path4.join(this.identityDir, "identity-anchor.md");
1844
+ }
1845
+ get identityIncidentsDir() {
1846
+ return path4.join(this.identityDir, "incidents");
1847
+ }
1848
+ get identityAuditsWeeklyDir() {
1849
+ return path4.join(this.identityDir, "audits", "weekly");
1850
+ }
1851
+ get identityAuditsMonthlyDir() {
1852
+ return path4.join(this.identityDir, "audits", "monthly");
1853
+ }
1854
+ get identityImprovementLoopsPath() {
1855
+ return path4.join(this.identityDir, "improvement-loops.md");
1856
+ }
1857
+ get identityReflectionsPath() {
1858
+ return path4.join(this.identityDir, "reflections.md");
1859
+ }
1860
+ get profilePath() {
1861
+ return path4.join(this.baseDir, "profile.md");
1862
+ }
1863
+ get memoryActionsPath() {
1864
+ return path4.join(this.stateDir, "memory-actions.jsonl");
1865
+ }
1866
+ get memoryLifecycleLedgerPath() {
1867
+ return path4.join(this.stateDir, "memory-lifecycle-ledger.jsonl");
1868
+ }
1869
+ get compressionGuidelinesPath() {
1870
+ return path4.join(this.stateDir, "compression-guidelines.md");
1871
+ }
1872
+ get compressionGuidelineDraftPath() {
1873
+ return path4.join(this.stateDir, "compression-guidelines.draft.md");
1874
+ }
1875
+ get compressionGuidelineStatePath() {
1876
+ return path4.join(this.stateDir, "compression-guideline-state.json");
1877
+ }
1878
+ get compressionGuidelineDraftStatePath() {
1879
+ return path4.join(this.stateDir, "compression-guideline-draft-state.json");
1880
+ }
1881
+ get behaviorSignalsPath() {
1882
+ return path4.join(this.stateDir, "behavior-signals.jsonl");
1883
+ }
1884
+ /**
1885
+ * Load user-defined entity aliases from config/aliases.json in the memory store.
1886
+ * File format: { "variant": "canonical", "variant2": "canonical", ... }
1887
+ * Call this once at startup (e.g. from orchestrator.initialize()).
1888
+ */
1889
+ async loadAliases() {
1890
+ const aliasPath = path4.join(this.baseDir, "config", "aliases.json");
1891
+ try {
1892
+ const raw = await readFile2(aliasPath, "utf-8");
1893
+ const parsed = JSON.parse(raw);
1894
+ if (typeof parsed === "object" && parsed !== null) {
1895
+ userAliases = parsed;
1896
+ log.debug(`loaded ${Object.keys(userAliases).length} entity aliases from ${aliasPath}`);
1897
+ }
1898
+ } catch {
1899
+ log.debug("no config/aliases.json found \u2014 using built-in aliases only");
1900
+ }
1901
+ }
1902
+ async ensureDirectories() {
1903
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1904
+ await mkdir2(path4.join(this.factsDir, today), { recursive: true });
1905
+ await mkdir2(this.correctionsDir, { recursive: true });
1906
+ await mkdir2(this.entitiesDir, { recursive: true });
1907
+ await mkdir2(this.stateDir, { recursive: true });
1908
+ await mkdir2(this.questionsDir, { recursive: true });
1909
+ await mkdir2(this.artifactsDir, { recursive: true });
1910
+ await mkdir2(this.identityDir, { recursive: true });
1911
+ await mkdir2(this.identityIncidentsDir, { recursive: true });
1912
+ await mkdir2(this.identityAuditsWeeklyDir, { recursive: true });
1913
+ await mkdir2(this.identityAuditsMonthlyDir, { recursive: true });
1914
+ await mkdir2(path4.join(this.baseDir, "config"), { recursive: true });
1915
+ }
1916
+ async writeMemory(category, content, options = {}) {
1917
+ await this.ensureDirectories();
1918
+ const now = /* @__PURE__ */ new Date();
1919
+ const today = now.toISOString().slice(0, 10);
1920
+ const id = `${category}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
1921
+ const conf = options.confidence ?? 0.8;
1922
+ const tier = confidenceTier(conf);
1923
+ let expiresAt;
1924
+ if (typeof options.expiresAt === "string" && options.expiresAt.length > 0) {
1925
+ expiresAt = options.expiresAt;
1926
+ } else if (tier === "speculative") {
1927
+ const expiry = new Date(now.getTime() + SPECULATIVE_TTL_DAYS * 24 * 60 * 60 * 1e3);
1928
+ expiresAt = expiry.toISOString();
1929
+ }
1930
+ const fm = {
1931
+ id,
1932
+ category,
1933
+ created: now.toISOString(),
1934
+ updated: now.toISOString(),
1935
+ source: options.source ?? "extraction",
1936
+ confidence: conf,
1937
+ confidenceTier: tier,
1938
+ tags: options.tags ?? [],
1939
+ entityRef: options.entityRef,
1940
+ supersedes: options.supersedes,
1941
+ expiresAt,
1942
+ lineage: options.lineage,
1943
+ importance: options.importance,
1944
+ links: options.links,
1945
+ intentGoal: options.intentGoal,
1946
+ intentActionType: options.intentActionType,
1947
+ intentEntityTypes: options.intentEntityTypes,
1948
+ artifactType: options.artifactType,
1949
+ sourceMemoryId: options.sourceMemoryId,
1950
+ sourceTurnId: options.sourceTurnId,
1951
+ memoryKind: options.memoryKind,
1952
+ structuredAttributes: options.structuredAttributes
1953
+ };
1954
+ let enrichedContent = content;
1955
+ if (options.structuredAttributes && Object.keys(options.structuredAttributes).length > 0) {
1956
+ const attrLines = Object.entries(options.structuredAttributes).map(([k, v]) => `${k}: ${v}`).join("; ");
1957
+ enrichedContent = `${content}
1958
+ [Attributes: ${attrLines}]`;
1959
+ }
1960
+ const sanitized = sanitizeMemoryContent(enrichedContent);
1961
+ if (!sanitized.clean) {
1962
+ log.warn(`memory content sanitized for ${id}; violations=${sanitized.violations.join(", ")}`);
1963
+ }
1964
+ const fileContent = `${serializeFrontmatter(fm)}
1965
+
1966
+ ${sanitized.text}
1967
+ `;
1968
+ let filePath;
1969
+ if (category === "correction") {
1970
+ filePath = path4.join(this.correctionsDir, `${id}.md`);
1971
+ } else {
1972
+ filePath = path4.join(this.factsDir, today, `${id}.md`);
1973
+ }
1974
+ await writeFile2(filePath, fileContent, "utf-8");
1975
+ this.invalidateAllMemoriesCache();
1976
+ await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.writeMemory", {
1977
+ memoryId: id,
1978
+ eventType: "created",
1979
+ timestamp: fm.created,
1980
+ actor: options.actor ?? "storage.writeMemory",
1981
+ after: this.summarizeLifecycleState(fm, filePath),
1982
+ relatedMemoryIds: [
1983
+ ...options.supersedes ? [options.supersedes] : [],
1984
+ ...(options.lineage ?? []).filter(Boolean)
1985
+ ]
1986
+ });
1987
+ if (category === "fact") {
1988
+ try {
1989
+ const factHashIndex = await this.getFactHashIndex();
1990
+ factHashIndex.add(sanitized.text);
1991
+ await factHashIndex.save();
1992
+ } catch (err) {
1993
+ log.warn(`storage.writeMemory completed but failed to update fact hash index: ${err}`);
1994
+ }
1995
+ }
1996
+ log.debug(`wrote memory ${id} to ${filePath}`);
1997
+ return id;
1998
+ }
1999
+ async hasFactContentHash(content) {
2000
+ await this.ensureFactHashIndexAuthoritative();
2001
+ const factHashIndex = await this.getFactHashIndex();
2002
+ const sanitized = sanitizeMemoryContent(content);
2003
+ return factHashIndex.has(sanitized.text);
2004
+ }
2005
+ async isFactContentHashAuthoritative() {
2006
+ await this.ensureFactHashIndexAuthoritative();
2007
+ return true;
2008
+ }
2009
+ async writeArtifact(quote, options = {}) {
2010
+ await this.ensureDirectories();
2011
+ const now = /* @__PURE__ */ new Date();
2012
+ const day = now.toISOString().slice(0, 10);
2013
+ const dir = path4.join(this.artifactsDir, day);
2014
+ await mkdir2(dir, { recursive: true });
2015
+ const id = `artifact-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
2016
+ const fm = {
2017
+ id,
2018
+ category: "fact",
2019
+ created: now.toISOString(),
2020
+ updated: now.toISOString(),
2021
+ source: "artifact",
2022
+ confidence: options.confidence ?? 0.9,
2023
+ confidenceTier: confidenceTier(options.confidence ?? 0.9),
2024
+ tags: options.tags ?? [],
2025
+ artifactType: options.artifactType ?? "fact",
2026
+ sourceMemoryId: options.sourceMemoryId,
2027
+ sourceTurnId: options.sourceTurnId,
2028
+ intentGoal: options.intentGoal,
2029
+ intentActionType: options.intentActionType,
2030
+ intentEntityTypes: options.intentEntityTypes
2031
+ };
2032
+ const sanitized = sanitizeMemoryContent(quote);
2033
+ if (!sanitized.clean) {
2034
+ log.warn(`artifact content rejected for ${id}; violations=${sanitized.violations.join(", ")}`);
2035
+ return "";
2036
+ }
2037
+ const filePath = path4.join(dir, `${id}.md`);
2038
+ await writeFile2(filePath, `${serializeFrontmatter(fm)}
2039
+
2040
+ ${sanitized.text}
2041
+ `, "utf-8");
2042
+ const actor = typeof options.actor === "string" && options.actor.length > 0 ? options.actor : "storage.writeArtifact";
2043
+ await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.writeArtifact", {
2044
+ memoryId: id,
2045
+ eventType: "created",
2046
+ timestamp: fm.created,
2047
+ actor,
2048
+ after: this.summarizeLifecycleState(fm, filePath),
2049
+ relatedMemoryIds: options.sourceMemoryId ? [options.sourceMemoryId] : []
2050
+ });
2051
+ this.bumpArtifactWriteVersion();
2052
+ this.artifactIndexCache = null;
2053
+ return id;
2054
+ }
2055
+ async readAllArtifactsCached() {
2056
+ if (this.artifactIndexCache && Date.now() - this.artifactIndexCache.loadedAtMs <= _StorageManager.ARTIFACT_INDEX_CACHE_TTL_MS && this.artifactIndexCache.writeVersion === this.getArtifactWriteVersion()) {
2057
+ return this.artifactIndexCache.memories;
2058
+ }
2059
+ const scanArtifacts = async () => {
2060
+ const artifacts = [];
2061
+ const readDir = async (dir) => {
2062
+ try {
2063
+ const entries = await readdir(dir, { withFileTypes: true });
2064
+ for (const entry of entries) {
2065
+ const fullPath = path4.join(dir, entry.name);
2066
+ if (entry.isDirectory()) {
2067
+ await readDir(fullPath);
2068
+ continue;
2069
+ }
2070
+ if (!entry.name.endsWith(".md")) continue;
2071
+ const memory = await this.readMemoryByPath(fullPath);
2072
+ if (!memory) continue;
2073
+ artifacts.push(memory);
2074
+ }
2075
+ } catch {
2076
+ }
2077
+ };
2078
+ await readDir(this.artifactsDir);
2079
+ return artifacts;
2080
+ };
2081
+ const MAX_REBUILD_RETRIES = 2;
2082
+ let latestArtifacts = [];
2083
+ for (let attempt = 0; attempt <= MAX_REBUILD_RETRIES; attempt += 1) {
2084
+ const versionBefore = this.getArtifactWriteVersion();
2085
+ const artifacts = await scanArtifacts();
2086
+ const versionAfter = this.getArtifactWriteVersion();
2087
+ latestArtifacts = artifacts;
2088
+ if (versionAfter === versionBefore) {
2089
+ this.artifactIndexCache = { memories: artifacts, loadedAtMs: Date.now(), writeVersion: versionAfter };
2090
+ return artifacts;
2091
+ }
2092
+ }
2093
+ this.artifactIndexCache = null;
2094
+ return latestArtifacts;
2095
+ }
2096
+ async searchArtifacts(query, maxResults) {
2097
+ const tokens = tokenizeArtifactSearchText(query);
2098
+ if (tokens.length === 0) return [];
2099
+ const artifacts = await this.readAllArtifactsCached();
2100
+ const hits = [];
2101
+ for (const memory of artifacts) {
2102
+ const indexedTokens = new Set(
2103
+ tokenizeArtifactSearchText(`${memory.content} ${(memory.frontmatter.tags ?? []).join(" ")}`)
2104
+ );
2105
+ const score = tokens.reduce((sum, t) => sum + (indexedTokens.has(t) ? 1 : 0), 0);
2106
+ if (score > 0) {
2107
+ hits.push({ score, memory });
2108
+ }
2109
+ }
2110
+ hits.sort((a, b) => b.score - a.score);
2111
+ return hits.slice(0, maxResults).map((h) => h.memory);
2112
+ }
2113
+ async writeEntity(name, type, facts) {
2114
+ await this.ensureDirectories();
2115
+ if (typeof name !== "string" || !name.trim() || typeof type !== "string" || !type.trim()) {
2116
+ log.warn("writeEntity: invalid entity payload, skipping", {
2117
+ nameType: typeof name,
2118
+ typeType: typeof type
2119
+ });
2120
+ return "";
2121
+ }
2122
+ const safeFacts = Array.isArray(facts) ? facts.filter((f) => typeof f === "string") : [];
2123
+ let normalized = normalizeEntityName(name, type);
2124
+ const match = await this.findMatchingEntity(name, type);
2125
+ if (match && match !== normalized) {
2126
+ log.debug(`fuzzy match: "${normalized}" \u2192 existing "${match}"`);
2127
+ normalized = match;
2128
+ }
2129
+ const filePath = path4.join(this.entitiesDir, `${normalized}.md`);
2130
+ let entity = {
2131
+ name,
2132
+ type,
2133
+ updated: (/* @__PURE__ */ new Date()).toISOString(),
2134
+ facts: [],
2135
+ summary: void 0,
2136
+ relationships: [],
2137
+ activity: [],
2138
+ aliases: []
2139
+ };
2140
+ try {
2141
+ const existing = await readFile2(filePath, "utf-8");
2142
+ entity = parseEntityFile(existing);
2143
+ } catch {
2144
+ }
2145
+ entity.facts = [.../* @__PURE__ */ new Set([...entity.facts, ...safeFacts])];
2146
+ entity.name = name;
2147
+ entity.type = type;
2148
+ entity.updated = (/* @__PURE__ */ new Date()).toISOString();
2149
+ await writeFile2(filePath, serializeEntityFile(entity), "utf-8");
2150
+ this.invalidateKnowledgeIndexCache();
2151
+ this.bumpMemoryStatusVersion();
2152
+ log.debug(`wrote entity ${normalized}`);
2153
+ return normalized;
2154
+ }
2155
+ async readProfile() {
2156
+ try {
2157
+ return await readFile2(this.profilePath, "utf-8");
2158
+ } catch {
2159
+ return "";
2160
+ }
2161
+ }
2162
+ async writeProfile(content) {
2163
+ await this.ensureDirectories();
2164
+ await writeFile2(this.profilePath, content, "utf-8");
2165
+ log.debug("updated profile.md");
2166
+ }
2167
+ /**
2168
+ * Normalize a string for fuzzy profile dedup: lowercase, strip punctuation, collapse whitespace.
2169
+ */
2170
+ static normalizeForDedup(s) {
2171
+ if (typeof s !== "string") return "";
2172
+ return s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
2173
+ }
2174
+ /**
2175
+ * Check if a new bullet is a fuzzy duplicate of any existing bullet.
2176
+ * Returns true if the new bullet should be skipped.
2177
+ */
2178
+ static isFuzzyDuplicate(newNorm, existingNorms) {
2179
+ for (const existing of existingNorms) {
2180
+ if (newNorm === existing) return true;
2181
+ const shorter = newNorm.length <= existing.length ? newNorm : existing;
2182
+ const longer = newNorm.length > existing.length ? newNorm : existing;
2183
+ if (shorter.length > 20 && shorter.length / longer.length > 0.6 && longer.includes(shorter)) {
2184
+ return true;
2185
+ }
2186
+ }
2187
+ return false;
2188
+ }
2189
+ async appendToProfile(updates) {
2190
+ updates = updates.filter((u) => typeof u === "string" && u.trim().length > 0);
2191
+ if (updates.length === 0) return;
2192
+ const existing = await this.readProfile();
2193
+ const lines = existing ? existing.split("\n") : [];
2194
+ const existingBulletRaw = lines.filter((l) => l.startsWith("- ")).map((l) => l.slice(2).trim());
2195
+ const existingNorms = existingBulletRaw.map(_StorageManager.normalizeForDedup);
2196
+ const newBullets = updates.filter((u) => {
2197
+ const norm = _StorageManager.normalizeForDedup(u);
2198
+ return !_StorageManager.isFuzzyDuplicate(norm, existingNorms);
2199
+ });
2200
+ if (newBullets.length === 0) return;
2201
+ if (!existing) {
2202
+ const content = [
2203
+ "# Behavioral Profile",
2204
+ "",
2205
+ `*Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}*`,
2206
+ "",
2207
+ ...newBullets.map((b) => `- ${b}`),
2208
+ ""
2209
+ ].join("\n");
2210
+ await this.writeProfile(content);
2211
+ } else {
2212
+ const updatedTimestamp = existing.replace(
2213
+ /\*Last updated:.*\*/,
2214
+ `*Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}*`
2215
+ );
2216
+ const withBullets = updatedTimestamp.trimEnd() + "\n" + newBullets.map((b) => `- ${b}`).join("\n") + "\n";
2217
+ await this.writeProfile(withBullets);
2218
+ }
2219
+ }
2220
+ /** Check if profile.md exceeds the max line cap and needs LLM consolidation */
2221
+ async profileNeedsConsolidation(triggerLines) {
2222
+ const profile = await this.readProfile();
2223
+ if (!profile) return false;
2224
+ const lineCount = profile.split("\n").length;
2225
+ const threshold = typeof triggerLines === "number" ? Math.max(0, Math.floor(triggerLines)) : _StorageManager.PROFILE_MAX_LINES;
2226
+ return lineCount > threshold;
2227
+ }
2228
+ async readAllMemories() {
2229
+ const inFlight = _StorageManager.allMemoriesInFlight.get(this.baseDir);
2230
+ if (inFlight) return inFlight;
2231
+ const readPromise = this._readAllMemoriesFromDisk();
2232
+ _StorageManager.allMemoriesInFlight.set(this.baseDir, readPromise);
2233
+ try {
2234
+ return await readPromise;
2235
+ } finally {
2236
+ if (_StorageManager.allMemoriesInFlight.get(this.baseDir) === readPromise) {
2237
+ _StorageManager.allMemoriesInFlight.delete(this.baseDir);
2238
+ }
2239
+ }
2240
+ }
2241
+ /** Invalidate the readAllMemories() cache after writes that add/remove memories. */
2242
+ /** Public cache invalidation for callers that need authoritative disk reads
2243
+ * (e.g. projection verify/rebuild). */
2244
+ invalidateAllMemoriesCacheForDir() {
2245
+ this.invalidateAllMemoriesCache();
2246
+ }
2247
+ /** Clear ALL static caches. Use in tests that write files directly
2248
+ * (bypassing StorageManager.writeMemory) to avoid stale reads. */
2249
+ static clearAllStaticCaches() {
2250
+ _StorageManager.allMemoriesInFlight.clear();
2251
+ _StorageManager.questionsCache.clear();
2252
+ }
2253
+ /** Cancel any in-flight concurrent read so the next readAllMemories()
2254
+ * starts a fresh disk scan and sees the just-written data. */
2255
+ invalidateAllMemoriesCache() {
2256
+ _StorageManager.allMemoriesInFlight.delete(this.baseDir);
2257
+ }
2258
+ normalizeMemoryReadBatchSize(batchSize) {
2259
+ if (typeof batchSize !== "number" || !Number.isFinite(batchSize)) {
2260
+ return 50;
2261
+ }
2262
+ return Math.max(1, Math.floor(batchSize));
2263
+ }
2264
+ async collectActiveMemoryPaths() {
2265
+ const filePaths = [];
2266
+ const collectPaths = async (dir) => {
2267
+ try {
2268
+ const entries = await readdir(dir, { withFileTypes: true });
2269
+ const subdirs = [];
2270
+ for (const entry of entries) {
2271
+ const fullPath = path4.join(dir, entry.name);
2272
+ if (entry.isDirectory()) {
2273
+ subdirs.push(fullPath);
2274
+ } else if (entry.name.endsWith(".md")) {
2275
+ filePaths.push(fullPath);
2276
+ }
2277
+ }
2278
+ for (const subdir of subdirs) {
2279
+ await collectPaths(subdir);
2280
+ }
2281
+ } catch {
2282
+ }
2283
+ };
2284
+ await collectPaths(this.factsDir);
2285
+ await collectPaths(this.correctionsDir);
2286
+ return filePaths;
2287
+ }
2288
+ async readParsedMemoriesFromPaths(filePaths, batchSize) {
2289
+ if (filePaths.length === 0) return [];
2290
+ const normalizedBatchSize = this.normalizeMemoryReadBatchSize(batchSize);
2291
+ const memories = [];
2292
+ for (let i = 0; i < filePaths.length; i += normalizedBatchSize) {
2293
+ const batch = filePaths.slice(i, i + normalizedBatchSize);
2294
+ const results = await Promise.all(
2295
+ batch.map(async (fullPath) => {
2296
+ try {
2297
+ const raw = await readFile2(fullPath, "utf-8");
2298
+ const parsed = parseFrontmatter2(raw);
2299
+ if (!parsed) return null;
2300
+ return {
2301
+ path: fullPath,
2302
+ frontmatter: normalizeFrontmatterForPath(
2303
+ parsed.frontmatter,
2304
+ toMemoryPathRel(this.baseDir, fullPath)
2305
+ ),
2306
+ content: parsed.content
2307
+ };
2308
+ } catch {
2309
+ return null;
2310
+ }
2311
+ })
2312
+ );
2313
+ for (const memory of results) {
2314
+ if (memory !== null) memories.push(memory);
2315
+ }
2316
+ }
2317
+ return memories;
2318
+ }
2319
+ async readWindowUpdatedMs(filePath) {
2320
+ try {
2321
+ const raw = await readFile2(filePath, "utf-8");
2322
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?/);
2323
+ if (!match) return null;
2324
+ const frontmatterBlock = match[1];
2325
+ const rawUpdated = frontmatterBlock.match(/^updated:\s*"?([^"\n]*)"?/m)?.[1] ?? frontmatterBlock.match(/^created:\s*"?([^"\n]*)"?/m)?.[1] ?? null;
2326
+ const updatedMs = rawUpdated ? Date.parse(rawUpdated) : Number.NaN;
2327
+ return Number.isFinite(updatedMs) ? updatedMs : null;
2328
+ } catch {
2329
+ return null;
2330
+ }
2331
+ }
2332
+ async filterWindowPathsByUpdatedAfter(filePaths, updatedAfterMs) {
2333
+ const results = await Promise.all(filePaths.map(async (filePath) => {
2334
+ const updatedMs = await this.readWindowUpdatedMs(filePath);
2335
+ if (updatedMs !== null) {
2336
+ return updatedMs >= updatedAfterMs ? filePath : null;
2337
+ }
2338
+ try {
2339
+ const fileStat = await stat2(filePath);
2340
+ return fileStat.mtimeMs >= updatedAfterMs ? filePath : null;
2341
+ } catch {
2342
+ return filePath;
2343
+ }
2344
+ }));
2345
+ return results.filter((filePath) => filePath !== null);
2346
+ }
2347
+ orderWindowPaths(filePaths) {
2348
+ const correctionPaths = [];
2349
+ const factPaths = [];
2350
+ for (const filePath of filePaths) {
2351
+ if (filePath === this.correctionsDir || filePath.startsWith(`${this.correctionsDir}${path4.sep}`)) {
2352
+ correctionPaths.push(filePath);
2353
+ } else {
2354
+ factPaths.push(filePath);
2355
+ }
2356
+ }
2357
+ correctionPaths.sort((left, right) => right.localeCompare(left));
2358
+ factPaths.sort((left, right) => right.localeCompare(left));
2359
+ if (correctionPaths.length === 0) return factPaths;
2360
+ if (factPaths.length === 0) return correctionPaths;
2361
+ const ordered = [];
2362
+ const maxLength = Math.max(correctionPaths.length, factPaths.length);
2363
+ for (let i = 0; i < maxLength; i += 1) {
2364
+ const correctionPath = correctionPaths[i];
2365
+ if (correctionPath) ordered.push(correctionPath);
2366
+ const factPath = factPaths[i];
2367
+ if (factPath) ordered.push(factPath);
2368
+ }
2369
+ return ordered;
2370
+ }
2371
+ async readWindowBoundedBatch(candidateBatchPaths, remainingSlots, remainingInspectionBudget, readBatchSize) {
2372
+ const memories = [];
2373
+ const filePaths = [];
2374
+ const normalizedReadBatchSize = this.normalizeMemoryReadBatchSize(readBatchSize);
2375
+ for (let index = 0; index < candidateBatchPaths.length; ) {
2376
+ if (memories.length >= remainingSlots || filePaths.length >= remainingInspectionBudget) break;
2377
+ const availableSlots = remainingSlots - memories.length;
2378
+ const availableInspectionBudget = remainingInspectionBudget - filePaths.length;
2379
+ const parallelWindow = availableSlots >= 4 && availableInspectionBudget >= 4 ? Math.min(normalizedReadBatchSize, 4) : 1;
2380
+ const candidatePaths = candidateBatchPaths.slice(
2381
+ index,
2382
+ index + Math.min(parallelWindow, availableInspectionBudget)
2383
+ );
2384
+ index += candidatePaths.length;
2385
+ if (candidatePaths.length === 0) break;
2386
+ filePaths.push(...candidatePaths);
2387
+ const parsedMemories = await this.readParsedMemoriesFromPaths(candidatePaths, candidatePaths.length);
2388
+ if (parsedMemories.length === 0) continue;
2389
+ memories.push(...parsedMemories.slice(0, availableSlots));
2390
+ }
2391
+ return { memories, filePaths };
2392
+ }
2393
+ async readMemoriesWindow(options = {}) {
2394
+ const allPaths = await this.collectActiveMemoryPaths();
2395
+ const sortedPaths = this.orderWindowPaths(allPaths);
2396
+ const maxMemories = typeof options.maxMemories === "number" && Number.isFinite(options.maxMemories) ? Math.max(1, Math.floor(options.maxMemories)) : void 0;
2397
+ const maxCandidatePaths = maxMemories === void 0 ? void 0 : maxMemories * 2;
2398
+ const updatedAfterMs = options.updatedAfter?.getTime();
2399
+ const normalizedBatchSize = this.normalizeMemoryReadBatchSize(options.batchSize);
2400
+ const memories = [];
2401
+ const selectedPaths = [];
2402
+ for (let i = 0; i < sortedPaths.length; i += normalizedBatchSize) {
2403
+ if (maxMemories !== void 0 && (memories.length >= maxMemories || maxCandidatePaths !== void 0 && selectedPaths.length >= maxCandidatePaths)) {
2404
+ return { memories, filePaths: selectedPaths };
2405
+ }
2406
+ const batchPaths = sortedPaths.slice(i, i + normalizedBatchSize);
2407
+ const candidateBatchPaths = updatedAfterMs === void 0 ? batchPaths : await this.filterWindowPathsByUpdatedAfter(batchPaths, updatedAfterMs);
2408
+ const remainingSlots = maxMemories === void 0 ? void 0 : Math.max(0, maxMemories - memories.length);
2409
+ const remainingInspectionBudget = maxCandidatePaths === void 0 ? void 0 : Math.max(0, maxCandidatePaths - selectedPaths.length);
2410
+ const { memories: batchMemories, filePaths: parsedCandidatePaths } = remainingSlots === void 0 ? {
2411
+ memories: await this.readParsedMemoriesFromPaths(candidateBatchPaths, normalizedBatchSize),
2412
+ filePaths: candidateBatchPaths
2413
+ } : await this.readWindowBoundedBatch(
2414
+ candidateBatchPaths,
2415
+ remainingSlots,
2416
+ remainingInspectionBudget ?? remainingSlots,
2417
+ normalizedBatchSize
2418
+ );
2419
+ selectedPaths.push(...parsedCandidatePaths);
2420
+ for (const memory of batchMemories) {
2421
+ memories.push(memory);
2422
+ if (maxMemories !== void 0 && memories.length >= maxMemories) {
2423
+ return { memories, filePaths: selectedPaths };
2424
+ }
2425
+ }
2426
+ }
2427
+ return { memories, filePaths: selectedPaths };
2428
+ }
2429
+ async _readAllMemoriesFromDisk() {
2430
+ const filePaths = await this.collectActiveMemoryPaths();
2431
+ return this.readParsedMemoriesFromPaths(filePaths, 50);
2432
+ }
2433
+ /**
2434
+ * Read archived memory markdown files under archive/.
2435
+ * Used by long-term recall fallback when hot recall has no hits.
2436
+ */
2437
+ async readArchivedMemories() {
2438
+ const memories = [];
2439
+ const root = this.archiveDir;
2440
+ const readDir = async (dir) => {
2441
+ try {
2442
+ const entries = await readdir(dir, { withFileTypes: true });
2443
+ for (const entry of entries) {
2444
+ const fullPath = path4.join(dir, entry.name);
2445
+ if (entry.isDirectory()) {
2446
+ await readDir(fullPath);
2447
+ } else if (entry.name.endsWith(".md")) {
2448
+ try {
2449
+ const raw = await readFile2(fullPath, "utf-8");
2450
+ const parsed = parseFrontmatter2(raw);
2451
+ if (parsed) {
2452
+ memories.push({
2453
+ path: fullPath,
2454
+ frontmatter: normalizeFrontmatterForPath(
2455
+ parsed.frontmatter,
2456
+ toMemoryPathRel(this.baseDir, fullPath)
2457
+ ),
2458
+ content: parsed.content
2459
+ });
2460
+ }
2461
+ } catch {
2462
+ }
2463
+ }
2464
+ }
2465
+ } catch {
2466
+ }
2467
+ };
2468
+ await readDir(root);
2469
+ return memories;
2470
+ }
2471
+ /** Read a single memory file by its absolute path. Returns null if unreadable. */
2472
+ async readMemoryByPath(filePath) {
2473
+ try {
2474
+ const raw = await readFile2(filePath, "utf-8");
2475
+ const parsed = parseFrontmatter2(raw);
2476
+ if (parsed) {
2477
+ return {
2478
+ path: filePath,
2479
+ frontmatter: normalizeFrontmatterForPath(
2480
+ parsed.frontmatter,
2481
+ toMemoryPathRel(this.baseDir, filePath)
2482
+ ),
2483
+ content: parsed.content
2484
+ };
2485
+ }
2486
+ const normalizedPath = filePath.split(path4.sep).join("/");
2487
+ if (normalizedPath.includes("/entities/") && filePath.endsWith(".md")) {
2488
+ const entity = parseEntityFile(raw);
2489
+ if (!entity.name) return null;
2490
+ const nameWithoutExt = path4.basename(filePath, ".md");
2491
+ const fileMtime = entity.updated || await stat2(filePath).then((s) => s.mtime.toISOString()).catch(() => (/* @__PURE__ */ new Date(0)).toISOString());
2492
+ return {
2493
+ path: filePath,
2494
+ frontmatter: {
2495
+ id: nameWithoutExt,
2496
+ category: "entity",
2497
+ created: fileMtime,
2498
+ updated: fileMtime,
2499
+ source: "entity_extraction",
2500
+ confidence: 0.9,
2501
+ confidenceTier: confidenceTier(0.9),
2502
+ tags: entity.type ? [entity.type] : []
2503
+ },
2504
+ content: raw
2505
+ };
2506
+ }
2507
+ return null;
2508
+ } catch {
2509
+ return null;
2510
+ }
2511
+ }
2512
+ resolveTierRootDir(tier) {
2513
+ return tier === "cold" ? path4.join(this.baseDir, "cold") : this.baseDir;
2514
+ }
2515
+ resolveMemoryDateDir(memory) {
2516
+ const preferred = memory.frontmatter.created || memory.frontmatter.updated;
2517
+ const dateToken = (preferred ?? "").slice(0, 10);
2518
+ return /^\d{4}-\d{2}-\d{2}$/.test(dateToken) ? dateToken : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2519
+ }
2520
+ isArtifactMemory(memory) {
2521
+ if (memory.frontmatter.source === "artifact") return true;
2522
+ if (memory.frontmatter.artifactType !== void 0) return true;
2523
+ return /[\\/]artifacts[\\/]/.test(memory.path);
2524
+ }
2525
+ buildTierMemoryPath(memory, tier) {
2526
+ const root = this.resolveTierRootDir(tier);
2527
+ if (this.isArtifactMemory(memory)) {
2528
+ return path4.join(root, "artifacts", this.resolveMemoryDateDir(memory), `${memory.frontmatter.id}.md`);
2529
+ }
2530
+ if (memory.frontmatter.category === "correction") {
2531
+ return path4.join(root, "corrections", `${memory.frontmatter.id}.md`);
2532
+ }
2533
+ return path4.join(root, "facts", this.resolveMemoryDateDir(memory), `${memory.frontmatter.id}.md`);
2534
+ }
2535
+ async writeMemoryFileAtomic(targetPath, memory) {
2536
+ const fileContent = `${serializeFrontmatter(memory.frontmatter)}
2537
+
2538
+ ${memory.content}
2539
+ `;
2540
+ await mkdir2(path4.dirname(targetPath), { recursive: true });
2541
+ const tempPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
2542
+ try {
2543
+ await writeFile2(tempPath, fileContent, "utf-8");
2544
+ await rename(tempPath, targetPath);
2545
+ this.invalidateAllMemoriesCache();
2546
+ } catch (err) {
2547
+ try {
2548
+ await unlink(tempPath);
2549
+ } catch {
2550
+ }
2551
+ throw err;
2552
+ }
2553
+ }
2554
+ async moveMemoryToPath(memory, targetPath) {
2555
+ await this.writeMemoryFileAtomic(targetPath, memory);
2556
+ const sourcePath = path4.resolve(memory.path);
2557
+ const destPath = path4.resolve(targetPath);
2558
+ if (sourcePath !== destPath) {
2559
+ try {
2560
+ await unlink(memory.path);
2561
+ } catch (err) {
2562
+ const message = err instanceof Error ? err.message : String(err);
2563
+ if (!message.includes("ENOENT")) {
2564
+ throw err;
2565
+ }
2566
+ }
2567
+ this.invalidateAllMemoriesCache();
2568
+ }
2569
+ }
2570
+ async migrateMemoryToTier(memory, targetTier) {
2571
+ const targetPath = this.buildTierMemoryPath(memory, targetTier);
2572
+ const sourcePath = path4.resolve(memory.path);
2573
+ const destPath = path4.resolve(targetPath);
2574
+ if (sourcePath === destPath) {
2575
+ return { changed: false, targetPath };
2576
+ }
2577
+ const existing = await this.readMemoryByPath(targetPath);
2578
+ if (existing?.frontmatter.id === memory.frontmatter.id) {
2579
+ try {
2580
+ await unlink(memory.path);
2581
+ } catch (err) {
2582
+ const message = err instanceof Error ? err.message : String(err);
2583
+ if (!message.includes("ENOENT")) {
2584
+ throw err;
2585
+ }
2586
+ }
2587
+ this.bumpMemoryStatusVersion();
2588
+ return { changed: false, targetPath };
2589
+ }
2590
+ await this.moveMemoryToPath(memory, targetPath);
2591
+ this.invalidateAllMemoriesCache();
2592
+ this.bumpMemoryStatusVersion();
2593
+ return { changed: true, targetPath };
2594
+ }
2595
+ get archiveDir() {
2596
+ return path4.join(this.baseDir, "archive");
2597
+ }
2598
+ /**
2599
+ * Archive a memory by moving it from facts/ to archive/YYYY-MM-DD/.
2600
+ * Updates frontmatter with archived status before moving.
2601
+ * Returns the new file path on success, null on failure.
2602
+ */
2603
+ async archiveMemory(memory, lifecycle) {
2604
+ try {
2605
+ const now = lifecycle?.at ?? /* @__PURE__ */ new Date();
2606
+ const today = now.toISOString().slice(0, 10);
2607
+ const destDir = path4.join(this.archiveDir, today);
2608
+ await mkdir2(destDir, { recursive: true });
2609
+ const updatedFm = {
2610
+ ...memory.frontmatter,
2611
+ status: "archived",
2612
+ archivedAt: now.toISOString(),
2613
+ updated: now.toISOString()
2614
+ };
2615
+ const fileContent = `${serializeFrontmatter(updatedFm)}
2616
+
2617
+ ${memory.content}
2618
+ `;
2619
+ const destPath = path4.join(destDir, path4.basename(memory.path));
2620
+ await writeFile2(destPath, fileContent, "utf-8");
2621
+ await unlink(memory.path);
2622
+ this.invalidateAllMemoriesCache();
2623
+ await this.appendGeneratedMemoryLifecycleEventFailOpen(
2624
+ "storage.archiveMemory",
2625
+ {
2626
+ memoryId: memory.frontmatter.id,
2627
+ eventType: "archived",
2628
+ timestamp: updatedFm.archivedAt ?? updatedFm.updated,
2629
+ actor: lifecycle?.actor ?? "storage.archiveMemory",
2630
+ reasonCode: lifecycle?.reasonCode,
2631
+ before: this.summarizeLifecycleState(memory.frontmatter, memory.path),
2632
+ after: this.summarizeLifecycleState(updatedFm, destPath),
2633
+ relatedMemoryIds: lifecycle?.relatedMemoryIds,
2634
+ correlationId: lifecycle?.correlationId
2635
+ },
2636
+ lifecycle?.ruleVersion
2637
+ );
2638
+ this.bumpMemoryStatusVersion();
2639
+ log.debug(`archived memory ${memory.frontmatter.id} \u2192 ${destPath}`);
2640
+ return destPath;
2641
+ } catch (err) {
2642
+ log.warn(`failed to archive memory ${memory.frontmatter.id}: ${err}`);
2643
+ return null;
2644
+ }
2645
+ }
2646
+ async readEntities() {
2647
+ try {
2648
+ const entries = await readdir(this.entitiesDir);
2649
+ return entries.filter((e) => e.endsWith(".md")).map((e) => e.replace(".md", ""));
2650
+ } catch {
2651
+ return [];
2652
+ }
2653
+ }
2654
+ async readEntity(name) {
2655
+ try {
2656
+ return await readFile2(path4.join(this.entitiesDir, `${name}.md`), "utf-8");
2657
+ } catch {
2658
+ return "";
2659
+ }
2660
+ }
2661
+ /** Return sorted list of entity filenames (without .md extension) */
2662
+ async listEntityNames() {
2663
+ try {
2664
+ const entries = await readdir(this.entitiesDir);
2665
+ return entries.filter((e) => e.endsWith(".md")).map((e) => e.replace(".md", "")).sort();
2666
+ } catch {
2667
+ return [];
2668
+ }
2669
+ }
2670
+ /**
2671
+ * Find an existing entity that fuzzy-matches the proposed name.
2672
+ * Returns the existing entity filename (without .md) or null if no match.
2673
+ *
2674
+ * Matching priority:
2675
+ * 1. Exact normalized match (handled by normalizeEntityName already)
2676
+ * 2. Dehyphenated match: "jane-doe" vs "janedoe"
2677
+ * 3. Substring containment: "handle-janedoe" contains "janedoe"
2678
+ * 4. Levenshtein ≤ 2 on dehyphenated names
2679
+ */
2680
+ async findMatchingEntity(proposedName, type) {
2681
+ const existing = await this.listEntityNames();
2682
+ if (existing.length === 0) return null;
2683
+ const typePrefix = `${type.toLowerCase()}-`;
2684
+ const proposedFull = normalizeEntityName(proposedName, type);
2685
+ const proposedNamePart = proposedFull.startsWith(typePrefix) ? proposedFull.slice(typePrefix.length) : proposedFull;
2686
+ const proposedDehyph = dehyphenate(proposedNamePart);
2687
+ const sameType = existing.filter((e) => e.startsWith(typePrefix));
2688
+ for (const entity of sameType) {
2689
+ const entityNamePart = entity.slice(typePrefix.length);
2690
+ const entityDehyph = dehyphenate(entityNamePart);
2691
+ if (entity === proposedFull) return entity;
2692
+ if (entityDehyph === proposedDehyph) return entity;
2693
+ const shorter = proposedDehyph.length <= entityDehyph.length ? proposedDehyph : entityDehyph;
2694
+ const longer = proposedDehyph.length > entityDehyph.length ? proposedDehyph : entityDehyph;
2695
+ if (shorter.length > 3 && shorter.length / longer.length > 0.6 && longer.includes(shorter)) {
2696
+ return entity;
2697
+ }
2698
+ if (proposedDehyph.length >= 4 && entityDehyph.length >= 4) {
2699
+ const dist = levenshtein(proposedDehyph, entityDehyph);
2700
+ if (dist <= 2) return entity;
2701
+ }
2702
+ }
2703
+ return null;
2704
+ }
2705
+ async invalidateMemory(id) {
2706
+ const memories = await this.readAllMemories();
2707
+ const memory = memories.find((m) => m.frontmatter.id === id);
2708
+ if (!memory) return false;
2709
+ try {
2710
+ await unlink(memory.path);
2711
+ this.invalidateAllMemoriesCache();
2712
+ this.bumpMemoryStatusVersion();
2713
+ log.debug(`invalidated memory ${id}`);
2714
+ return true;
2715
+ } catch {
2716
+ return false;
2717
+ }
2718
+ }
2719
+ async updateMemory(id, newContent, options) {
2720
+ const memories = await this.readAllMemories();
2721
+ const memory = memories.find((m) => m.frontmatter.id === id);
2722
+ if (!memory) return false;
2723
+ const mergedLineage = [
2724
+ ...memory.frontmatter.lineage ?? [],
2725
+ ...options?.lineage ?? []
2726
+ ].filter((v, i, a) => a.indexOf(v) === i);
2727
+ const updated = {
2728
+ ...memory.frontmatter,
2729
+ updated: (/* @__PURE__ */ new Date()).toISOString(),
2730
+ supersedes: options?.supersedes ?? memory.frontmatter.supersedes,
2731
+ lineage: mergedLineage.length > 0 ? mergedLineage : void 0
2732
+ };
2733
+ const sanitized = sanitizeMemoryContent(newContent);
2734
+ if (!sanitized.clean) {
2735
+ log.warn(`updated memory content sanitized for ${id}; violations=${sanitized.violations.join(", ")}`);
2736
+ }
2737
+ const fileContent = `${serializeFrontmatter(updated)}
2738
+
2739
+ ${sanitized.text}
2740
+ `;
2741
+ await writeFile2(memory.path, fileContent, "utf-8");
2742
+ this.invalidateAllMemoriesCache();
2743
+ await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.updateMemory", {
2744
+ memoryId: id,
2745
+ eventType: "updated",
2746
+ timestamp: updated.updated,
2747
+ actor: options?.actor ?? "storage.updateMemory",
2748
+ before: this.summarizeLifecycleState(memory.frontmatter, memory.path),
2749
+ after: this.summarizeLifecycleState(updated, memory.path),
2750
+ relatedMemoryIds: [
2751
+ ...updated.supersedes ? [updated.supersedes] : [],
2752
+ ...(updated.lineage ?? []).filter(Boolean)
2753
+ ]
2754
+ });
2755
+ log.debug(`updated memory ${id}`);
2756
+ return true;
2757
+ }
2758
+ /**
2759
+ * Update frontmatter fields without changing memory content.
2760
+ * Returns false when the memory is not found.
2761
+ */
2762
+ async writeMemoryFrontmatter(memory, patch, lifecycle) {
2763
+ const beforeStatus = memory.frontmatter.status ?? "active";
2764
+ const updated = {
2765
+ ...memory.frontmatter,
2766
+ ...patch
2767
+ };
2768
+ const afterStatus = updated.status ?? "active";
2769
+ const fileContent = `${serializeFrontmatter(updated)}
2770
+
2771
+ ${memory.content}
2772
+ `;
2773
+ await writeFile2(memory.path, fileContent, "utf-8");
2774
+ this.invalidateAllMemoriesCache();
2775
+ await this.appendGeneratedMemoryLifecycleEventFailOpen(
2776
+ "storage.writeMemoryFrontmatter",
2777
+ {
2778
+ memoryId: updated.id,
2779
+ eventType: this.frontmatterPatchEventType(memory.frontmatter, updated),
2780
+ timestamp: updated.updated ?? (/* @__PURE__ */ new Date()).toISOString(),
2781
+ actor: lifecycle?.actor ?? "storage.writeMemoryFrontmatter",
2782
+ reasonCode: lifecycle?.reasonCode,
2783
+ before: this.summarizeLifecycleState(memory.frontmatter, memory.path),
2784
+ after: this.summarizeLifecycleState(updated, memory.path),
2785
+ relatedMemoryIds: [
2786
+ ...lifecycle?.relatedMemoryIds ?? [],
2787
+ ...updated.supersededBy ? [updated.supersededBy] : [],
2788
+ ...updated.supersedes ? [updated.supersedes] : []
2789
+ ],
2790
+ correlationId: lifecycle?.correlationId
2791
+ },
2792
+ lifecycle?.ruleVersion
2793
+ );
2794
+ if (beforeStatus !== afterStatus) {
2795
+ this.bumpMemoryStatusVersion();
2796
+ }
2797
+ return true;
2798
+ }
2799
+ /**
2800
+ * Update frontmatter by memory ID.
2801
+ * Prefer writeMemoryFrontmatter(memory, patch) in batch loops to avoid full-corpus rescans.
2802
+ */
2803
+ async updateMemoryFrontmatter(id, patch) {
2804
+ const memories = await this.readAllMemories();
2805
+ const memory = memories.find((m) => m.frontmatter.id === id);
2806
+ if (!memory) return false;
2807
+ return this.writeMemoryFrontmatter(memory, patch);
2808
+ }
2809
+ /** Remove memories past their TTL expiresAt date */
2810
+ async cleanExpiredTTL() {
2811
+ const memories = await this.readAllMemories();
2812
+ const now = Date.now();
2813
+ const deleted = [];
2814
+ for (const m of memories) {
2815
+ if (!m.frontmatter.expiresAt) continue;
2816
+ const expiresAt = new Date(m.frontmatter.expiresAt).getTime();
2817
+ if (expiresAt < now) {
2818
+ try {
2819
+ await unlink(m.path);
2820
+ deleted.push(m);
2821
+ log.debug(`cleaned expired memory ${m.frontmatter.id} (TTL expired)`);
2822
+ } catch {
2823
+ }
2824
+ }
2825
+ }
2826
+ if (deleted.length > 0) {
2827
+ this.invalidateAllMemoriesCache();
2828
+ this.bumpMemoryStatusVersion();
2829
+ }
2830
+ return deleted;
2831
+ }
2832
+ async loadBuffer() {
2833
+ const bufferPath = path4.join(this.stateDir, "buffer.json");
2834
+ try {
2835
+ const raw = await readFile2(bufferPath, "utf-8");
2836
+ return JSON.parse(raw);
2837
+ } catch {
2838
+ return { turns: [], lastExtractionAt: null, extractionCount: 0 };
2839
+ }
2840
+ }
2841
+ async saveBuffer(state) {
2842
+ await this.ensureDirectories();
2843
+ const bufferPath = path4.join(this.stateDir, "buffer.json");
2844
+ await writeFile2(bufferPath, JSON.stringify(state, null, 2), "utf-8");
2845
+ }
2846
+ async loadMeta() {
2847
+ const metaPath = path4.join(this.stateDir, "meta.json");
2848
+ try {
2849
+ const raw = await readFile2(metaPath, "utf-8");
2850
+ return JSON.parse(raw);
2851
+ } catch {
2852
+ return {
2853
+ extractionCount: 0,
2854
+ lastExtractionAt: null,
2855
+ lastConsolidationAt: null,
2856
+ totalMemories: 0,
2857
+ totalEntities: 0
2858
+ };
2859
+ }
2860
+ }
2861
+ async saveMeta(state) {
2862
+ await this.ensureDirectories();
2863
+ const metaPath = path4.join(this.stateDir, "meta.json");
2864
+ await writeFile2(metaPath, JSON.stringify(state, null, 2), "utf-8");
2865
+ }
2866
+ async appendMemoryActionEvents(events) {
2867
+ if (events.length === 0) return 0;
2868
+ await this.ensureDirectories();
2869
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2870
+ const payload = events.map((event) => {
2871
+ const normalized = {
2872
+ ...event,
2873
+ timestamp: event.timestamp && event.timestamp.length > 0 ? event.timestamp : nowIso
2874
+ };
2875
+ return `${JSON.stringify(normalized)}
2876
+ `;
2877
+ }).join("");
2878
+ await appendFile(this.memoryActionsPath, payload, "utf-8");
2879
+ return events.length;
2880
+ }
2881
+ async appendMemoryLifecycleEvents(events) {
2882
+ if (events.length === 0) return 0;
2883
+ await this.ensureDirectories();
2884
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2885
+ const payload = events.map((event) => {
2886
+ const normalized = {
2887
+ ...event,
2888
+ timestamp: event.timestamp && event.timestamp.length > 0 ? event.timestamp : nowIso
2889
+ };
2890
+ return `${JSON.stringify(normalized)}
2891
+ `;
2892
+ }).join("");
2893
+ await appendFile(this.memoryLifecycleLedgerPath, payload, "utf-8");
2894
+ return events.length;
2895
+ }
2896
+ async appendBehaviorSignals(events) {
2897
+ if (events.length === 0) return 0;
2898
+ await this.ensureDirectories();
2899
+ let existingKeys = /* @__PURE__ */ new Set();
2900
+ try {
2901
+ const raw = await readFile2(this.behaviorSignalsPath, "utf-8");
2902
+ const lines = raw.split("\n");
2903
+ for (const line of lines) {
2904
+ const row = line.trim();
2905
+ if (!row) continue;
2906
+ try {
2907
+ const parsed = JSON.parse(row);
2908
+ if (typeof parsed.memoryId === "string" && typeof parsed.signalHash === "string") {
2909
+ existingKeys.add(`${parsed.memoryId}:${parsed.signalHash}`);
2910
+ }
2911
+ } catch {
2912
+ }
2913
+ }
2914
+ } catch {
2915
+ existingKeys = /* @__PURE__ */ new Set();
2916
+ }
2917
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2918
+ const deduped = [];
2919
+ for (const event of events) {
2920
+ const key = `${event.memoryId}:${event.signalHash}`;
2921
+ if (existingKeys.has(key)) continue;
2922
+ existingKeys.add(key);
2923
+ deduped.push({
2924
+ ...event,
2925
+ timestamp: event.timestamp && event.timestamp.length > 0 ? event.timestamp : nowIso
2926
+ });
2927
+ }
2928
+ if (deduped.length === 0) return 0;
2929
+ const payload = deduped.map((event) => `${JSON.stringify(event)}
2930
+ `).join("");
2931
+ await appendFile(this.behaviorSignalsPath, payload, "utf-8");
2932
+ return deduped.length;
2933
+ }
2934
+ async appendReextractJobs(events) {
2935
+ if (events.length === 0) return 0;
2936
+ await this.ensureDirectories();
2937
+ const filePath = path4.join(this.stateDir, "reextract-jobs.jsonl");
2938
+ const lines = events.map((event) => JSON.stringify(event)).join("\n") + "\n";
2939
+ try {
2940
+ await appendFile(filePath, lines, "utf-8");
2941
+ return events.length;
2942
+ } catch {
2943
+ return 0;
2944
+ }
2945
+ }
2946
+ async readReextractJobs(limit = 200) {
2947
+ const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.min(1e3, Math.floor(limit))) : 200;
2948
+ const filePath = path4.join(this.stateDir, "reextract-jobs.jsonl");
2949
+ try {
2950
+ const raw = await readFile2(filePath, "utf-8");
2951
+ const lines = raw.split("\n").filter((line) => line.trim().length > 0);
2952
+ const parsed = [];
2953
+ for (const line of lines) {
2954
+ try {
2955
+ const record = JSON.parse(line);
2956
+ if (typeof record.memoryId !== "string" || record.memoryId.length === 0 || typeof record.model !== "string" || record.model.length === 0 || typeof record.requestedAt !== "string" || record.requestedAt.length === 0 || record.source !== "cli-migrate") {
2957
+ continue;
2958
+ }
2959
+ parsed.push({
2960
+ memoryId: record.memoryId,
2961
+ model: record.model,
2962
+ requestedAt: record.requestedAt,
2963
+ source: "cli-migrate"
2964
+ });
2965
+ } catch {
2966
+ continue;
2967
+ }
2968
+ }
2969
+ return parsed.slice(-safeLimit);
2970
+ } catch {
2971
+ return [];
2972
+ }
2973
+ }
2974
+ async readBehaviorSignals(limit = 200) {
2975
+ const cappedLimit = Math.max(0, Math.floor(limit));
2976
+ if (cappedLimit === 0) return [];
2977
+ try {
2978
+ const raw = await readFile2(this.behaviorSignalsPath, "utf-8");
2979
+ const out = [];
2980
+ const lines = raw.split("\n");
2981
+ for (let i = lines.length - 1; i >= 0 && out.length < cappedLimit; i -= 1) {
2982
+ const row = lines[i]?.trim();
2983
+ if (!row) continue;
2984
+ try {
2985
+ const parsed = JSON.parse(row);
2986
+ if (typeof parsed.timestamp === "string" && typeof parsed.namespace === "string" && typeof parsed.memoryId === "string" && typeof parsed.category === "string" && typeof parsed.signalType === "string" && typeof parsed.direction === "string" && typeof parsed.confidence === "number" && typeof parsed.signalHash === "string" && typeof parsed.source === "string") {
2987
+ out.push(parsed);
2988
+ }
2989
+ } catch {
2990
+ }
2991
+ }
2992
+ return out.reverse();
2993
+ } catch {
2994
+ return [];
2995
+ }
2996
+ }
2997
+ async readMemoryActionEvents(limit = 200) {
2998
+ const cappedLimit = Math.max(0, Math.floor(limit));
2999
+ if (cappedLimit === 0) return [];
3000
+ try {
3001
+ const raw = await readFile2(this.memoryActionsPath, "utf-8");
3002
+ const out = [];
3003
+ const lines = raw.split("\n");
3004
+ for (let i = lines.length - 1; i >= 0 && out.length < cappedLimit; i -= 1) {
3005
+ const line = lines[i]?.trim();
3006
+ if (!line) continue;
3007
+ try {
3008
+ const parsed = JSON.parse(line);
3009
+ if (typeof parsed.timestamp === "string" && typeof parsed.action === "string" && typeof parsed.outcome === "string") {
3010
+ out.push(parsed);
3011
+ }
3012
+ } catch {
3013
+ }
3014
+ }
3015
+ return out.reverse();
3016
+ } catch {
3017
+ return [];
3018
+ }
3019
+ }
3020
+ async readAllMemoryLifecycleEvents() {
3021
+ try {
3022
+ const raw = await readFile2(this.memoryLifecycleLedgerPath, "utf-8");
3023
+ const out = [];
3024
+ const lines = raw.split("\n");
3025
+ for (const line of lines) {
3026
+ const row = line.trim();
3027
+ if (!row) continue;
3028
+ try {
3029
+ const parsed = JSON.parse(row);
3030
+ if (typeof parsed.eventId === "string" && typeof parsed.memoryId === "string" && typeof parsed.eventType === "string" && typeof parsed.timestamp === "string" && typeof parsed.actor === "string" && typeof parsed.ruleVersion === "string") {
3031
+ out.push(parsed);
3032
+ }
3033
+ } catch {
3034
+ }
3035
+ }
3036
+ return sortMemoryLifecycleEvents(out);
3037
+ } catch {
3038
+ return [];
3039
+ }
3040
+ }
3041
+ async readMemoryLifecycleEvents(limit = 200) {
3042
+ const cappedLimit = Math.max(0, Math.floor(limit));
3043
+ if (cappedLimit === 0) return [];
3044
+ const events = await this.readAllMemoryLifecycleEvents();
3045
+ return events.slice(-cappedLimit);
3046
+ }
3047
+ async writeCompressionGuidelines(content) {
3048
+ await this.ensureDirectories();
3049
+ await writeFile2(this.compressionGuidelinesPath, content, "utf-8");
3050
+ }
3051
+ async readCompressionGuidelines() {
3052
+ try {
3053
+ return await readFile2(this.compressionGuidelinesPath, "utf-8");
3054
+ } catch {
3055
+ return null;
3056
+ }
3057
+ }
3058
+ async writeCompressionGuidelineDraft(content) {
3059
+ await this.ensureDirectories();
3060
+ await writeFile2(this.compressionGuidelineDraftPath, content, "utf-8");
3061
+ }
3062
+ async readCompressionGuidelineDraft() {
3063
+ try {
3064
+ return await readFile2(this.compressionGuidelineDraftPath, "utf-8");
3065
+ } catch {
3066
+ return null;
3067
+ }
3068
+ }
3069
+ async writeCompressionGuidelineOptimizerState(state) {
3070
+ await this.ensureDirectories();
3071
+ await writeFile2(this.compressionGuidelineStatePath, `${JSON.stringify(state, null, 2)}
3072
+ `, "utf-8");
3073
+ }
3074
+ async writeCompressionGuidelineDraftState(state) {
3075
+ await this.ensureDirectories();
3076
+ await writeFile2(this.compressionGuidelineDraftStatePath, `${JSON.stringify(state, null, 2)}
3077
+ `, "utf-8");
3078
+ }
3079
+ async readCompressionGuidelineOptimizerState() {
3080
+ return this.readCompressionGuidelineStateFile(this.compressionGuidelineStatePath);
3081
+ }
3082
+ async readCompressionGuidelineDraftState() {
3083
+ return this.readCompressionGuidelineStateFile(this.compressionGuidelineDraftStatePath);
3084
+ }
3085
+ async activateCompressionGuidelineDraft(options) {
3086
+ const [draftContent, draftState] = await Promise.all([
3087
+ this.readCompressionGuidelineDraft(),
3088
+ this.readCompressionGuidelineDraftState()
3089
+ ]);
3090
+ if (!draftContent || !draftState) return false;
3091
+ if (typeof options?.expectedContentHash === "string" && options.expectedContentHash.length > 0 && draftState.contentHash !== options.expectedContentHash) {
3092
+ return false;
3093
+ }
3094
+ if (typeof options?.expectedGuidelineVersion === "number" && Number.isFinite(options.expectedGuidelineVersion) && draftState.guidelineVersion !== options.expectedGuidelineVersion) {
3095
+ return false;
3096
+ }
3097
+ if (draftState.contentHash) {
3098
+ const contentHash = createHash("sha256").update(draftContent).digest("hex");
3099
+ if (contentHash !== draftState.contentHash) return false;
3100
+ }
3101
+ await this.writeCompressionGuidelines(draftContent);
3102
+ await this.writeCompressionGuidelineOptimizerState({
3103
+ ...draftState,
3104
+ activationState: "active"
3105
+ });
3106
+ await Promise.all([
3107
+ unlink(this.compressionGuidelineDraftPath).catch(() => void 0),
3108
+ unlink(this.compressionGuidelineDraftStatePath).catch(() => void 0)
3109
+ ]);
3110
+ return true;
3111
+ }
3112
+ async readCompressionGuidelineStateFile(filePath) {
3113
+ const isFiniteNonNegativeInteger = (value) => typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
3114
+ const isValidActionSummary = (value) => {
3115
+ if (!value || typeof value !== "object") return false;
3116
+ const summary = value;
3117
+ return typeof summary.action === "string" && isFiniteNonNegativeInteger(summary.total) && summary.outcomes !== null && typeof summary.outcomes === "object" && isFiniteNonNegativeInteger(summary.outcomes.applied) && isFiniteNonNegativeInteger(summary.outcomes.skipped) && isFiniteNonNegativeInteger(summary.outcomes.failed) && summary.quality !== null && typeof summary.quality === "object" && isFiniteNonNegativeInteger(summary.quality.good) && isFiniteNonNegativeInteger(summary.quality.poor) && isFiniteNonNegativeInteger(summary.quality.unknown);
3118
+ };
3119
+ const isValidRuleUpdate = (value) => {
3120
+ if (!value || typeof value !== "object") return false;
3121
+ const rule = value;
3122
+ return typeof rule.action === "string" && typeof rule.delta === "number" && Number.isFinite(rule.delta) && (rule.direction === "increase" || rule.direction === "decrease" || rule.direction === "hold") && (rule.confidence === "low" || rule.confidence === "medium" || rule.confidence === "high") && Array.isArray(rule.notes) && rule.notes.every((note) => typeof note === "string");
3123
+ };
3124
+ try {
3125
+ const raw = await readFile2(filePath, "utf-8");
3126
+ const parsed = JSON.parse(raw);
3127
+ const sourceWindow = parsed?.sourceWindow;
3128
+ const eventCounts = parsed?.eventCounts;
3129
+ const activationState = parsed?.activationState === "draft" || parsed?.activationState === "active" ? parsed.activationState : void 0;
3130
+ const contentHash = typeof parsed?.contentHash === "string" && parsed.contentHash.length > 0 ? parsed.contentHash : void 0;
3131
+ const actionSummaries = Array.isArray(parsed?.actionSummaries) ? parsed.actionSummaries.filter(isValidActionSummary) : void 0;
3132
+ const ruleUpdates = Array.isArray(parsed?.ruleUpdates) ? parsed.ruleUpdates.filter(isValidRuleUpdate) : void 0;
3133
+ if (!isFiniteNonNegativeInteger(parsed?.version) || typeof parsed?.updatedAt !== "string" || parsed.updatedAt.length === 0 || !sourceWindow || typeof sourceWindow.from !== "string" || sourceWindow.from.length === 0 || typeof sourceWindow.to !== "string" || sourceWindow.to.length === 0 || !eventCounts || !isFiniteNonNegativeInteger(eventCounts.total) || !isFiniteNonNegativeInteger(eventCounts.applied) || !isFiniteNonNegativeInteger(eventCounts.skipped) || !isFiniteNonNegativeInteger(eventCounts.failed) || !isFiniteNonNegativeInteger(parsed?.guidelineVersion)) {
3134
+ return null;
3135
+ }
3136
+ return {
3137
+ version: parsed.version,
3138
+ updatedAt: parsed.updatedAt,
3139
+ sourceWindow: {
3140
+ from: sourceWindow.from,
3141
+ to: sourceWindow.to
3142
+ },
3143
+ eventCounts: {
3144
+ total: eventCounts.total,
3145
+ applied: eventCounts.applied,
3146
+ skipped: eventCounts.skipped,
3147
+ failed: eventCounts.failed
3148
+ },
3149
+ guidelineVersion: parsed.guidelineVersion,
3150
+ ...contentHash ? { contentHash } : {},
3151
+ ...activationState ? { activationState } : {},
3152
+ ...actionSummaries ? { actionSummaries } : {},
3153
+ ...ruleUpdates ? { ruleUpdates } : {}
3154
+ };
3155
+ } catch {
3156
+ return null;
3157
+ }
3158
+ }
3159
+ async writeIdentityAnchor(content) {
3160
+ await this.ensureDirectories();
3161
+ await writeFile2(this.identityAnchorPath, content, "utf-8");
3162
+ }
3163
+ async readIdentityAnchor() {
3164
+ try {
3165
+ return await readFile2(this.identityAnchorPath, "utf-8");
3166
+ } catch {
3167
+ return null;
3168
+ }
3169
+ }
3170
+ async appendContinuityIncident(input) {
3171
+ await this.ensureDirectories();
3172
+ const now = /* @__PURE__ */ new Date();
3173
+ const nowIso = now.toISOString();
3174
+ const date = nowIso.slice(0, 10);
3175
+ const id = this.generateId("incident");
3176
+ const incident = createContinuityIncidentRecord(id, input, nowIso);
3177
+ const filePath = path4.join(this.identityIncidentsDir, `${date}-${id}.md`);
3178
+ await writeFile2(filePath, serializeContinuityIncident(incident), "utf-8");
3179
+ return { ...incident, filePath };
3180
+ }
3181
+ async readContinuityIncidents(limit = 200, state = "all") {
3182
+ const normalizedLimit = Number.isFinite(limit) ? Math.floor(limit) : 0;
3183
+ const cappedLimit = Math.max(0, normalizedLimit);
3184
+ if (cappedLimit === 0) return [];
3185
+ try {
3186
+ const candidates = await this.readContinuityIncidentFileNames();
3187
+ const incidents = [];
3188
+ for (const file of candidates) {
3189
+ if (incidents.length >= cappedLimit) break;
3190
+ const filePath = path4.join(this.identityIncidentsDir, file);
3191
+ try {
3192
+ const raw = await readFile2(filePath, "utf-8");
3193
+ const parsed = parseContinuityIncident(raw);
3194
+ if (!parsed) continue;
3195
+ if (state !== "all" && parsed.state !== state) continue;
3196
+ incidents.push({ ...parsed, filePath });
3197
+ } catch {
3198
+ }
3199
+ }
3200
+ return incidents;
3201
+ } catch {
3202
+ return [];
3203
+ }
3204
+ }
3205
+ async closeContinuityIncident(id, closure) {
3206
+ const directFilePath = await this.findContinuityIncidentFilePathById(id);
3207
+ const target = directFilePath ? await this.readContinuityIncidentFile(directFilePath) : null;
3208
+ if (!target || !directFilePath) return null;
3209
+ if (target.state === "closed") return target;
3210
+ const closed = closeContinuityIncidentRecord(target, closure, (/* @__PURE__ */ new Date()).toISOString());
3211
+ await writeFile2(directFilePath, serializeContinuityIncident(closed), "utf-8");
3212
+ return { ...closed, filePath: directFilePath };
3213
+ }
3214
+ async writeIdentityAudit(period, key, content) {
3215
+ await this.ensureDirectories();
3216
+ const safeKey = this.sanitizeIdentityAuditKey(key);
3217
+ const dir = period === "weekly" ? this.identityAuditsWeeklyDir : this.identityAuditsMonthlyDir;
3218
+ const filePath = path4.join(dir, `${safeKey}.md`);
3219
+ await writeFile2(filePath, content, "utf-8");
3220
+ return filePath;
3221
+ }
3222
+ async readIdentityAudit(period, key) {
3223
+ try {
3224
+ const safeKey = this.sanitizeIdentityAuditKey(key);
3225
+ const dir = period === "weekly" ? this.identityAuditsWeeklyDir : this.identityAuditsMonthlyDir;
3226
+ return await readFile2(path4.join(dir, `${safeKey}.md`), "utf-8");
3227
+ } catch {
3228
+ return null;
3229
+ }
3230
+ }
3231
+ async writeIdentityImprovementLoops(content) {
3232
+ await this.ensureDirectories();
3233
+ await writeFile2(this.identityImprovementLoopsPath, content, "utf-8");
3234
+ }
3235
+ async readIdentityImprovementLoops() {
3236
+ try {
3237
+ return await readFile2(this.identityImprovementLoopsPath, "utf-8");
3238
+ } catch {
3239
+ return null;
3240
+ }
3241
+ }
3242
+ async readIdentityImprovementLoopRegister() {
3243
+ const raw = await this.readIdentityImprovementLoops();
3244
+ if (!raw) return [];
3245
+ return parseContinuityImprovementLoops(raw);
3246
+ }
3247
+ async upsertIdentityImprovementLoop(input) {
3248
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
3249
+ const raw = await this.readIdentityImprovementLoops();
3250
+ const { markdown, loop } = upsertContinuityLoopInMarkdown(raw, input, nowIso);
3251
+ await this.writeIdentityImprovementLoops(markdown);
3252
+ return loop;
3253
+ }
3254
+ async reviewIdentityImprovementLoop(id, input) {
3255
+ const raw = await this.readIdentityImprovementLoops();
3256
+ const { markdown, loop } = reviewContinuityLoopInMarkdown(raw, id, input, (/* @__PURE__ */ new Date()).toISOString());
3257
+ if (!loop) return null;
3258
+ await this.writeIdentityImprovementLoops(markdown);
3259
+ return loop;
3260
+ }
3261
+ // ---------------------------------------------------------------------------
3262
+ // Question storage
3263
+ // ---------------------------------------------------------------------------
3264
+ generateId(prefix = "m") {
3265
+ const ts = Date.now().toString(36);
3266
+ const rand = Math.random().toString(36).slice(2, 4);
3267
+ return `${prefix}-${ts}-${rand}`;
3268
+ }
3269
+ async readContinuityIncidentFileNames() {
3270
+ const files = await readdir(this.identityIncidentsDir);
3271
+ return files.filter((file) => file.endsWith(".md")).sort().reverse();
3272
+ }
3273
+ async readContinuityIncidentFile(filePath) {
3274
+ try {
3275
+ const raw = await readFile2(filePath, "utf-8");
3276
+ const parsed = parseContinuityIncident(raw);
3277
+ return parsed ? { ...parsed, filePath } : null;
3278
+ } catch {
3279
+ return null;
3280
+ }
3281
+ }
3282
+ async findContinuityIncidentFilePathById(id) {
3283
+ const fileNames = await this.readContinuityIncidentFileNames();
3284
+ const directMatch = fileNames.find((name) => name.endsWith(`-${id}.md`));
3285
+ if (directMatch) {
3286
+ const directPath = path4.join(this.identityIncidentsDir, directMatch);
3287
+ const parsed = await this.readContinuityIncidentFile(directPath);
3288
+ if (parsed?.id === id) return directPath;
3289
+ }
3290
+ for (const fileName of fileNames) {
3291
+ const filePath = path4.join(this.identityIncidentsDir, fileName);
3292
+ const parsed = await this.readContinuityIncidentFile(filePath);
3293
+ if (parsed?.id === id) return filePath;
3294
+ }
3295
+ return null;
3296
+ }
3297
+ sanitizeIdentityAuditKey(key) {
3298
+ const trimmed = key.trim();
3299
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(trimmed) || trimmed.includes("..")) {
3300
+ throw new Error("Invalid identity audit key");
3301
+ }
3302
+ return trimmed;
3303
+ }
3304
+ async writeQuestion(question, context, priority) {
3305
+ await mkdir2(this.questionsDir, { recursive: true });
3306
+ const id = this.generateId("q");
3307
+ const frontmatter = {
3308
+ id,
3309
+ created: (/* @__PURE__ */ new Date()).toISOString(),
3310
+ priority,
3311
+ resolved: false
3312
+ };
3313
+ const content = `---
3314
+ ${Object.entries(frontmatter).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join("\n")}
3315
+ ---
3316
+
3317
+ ${question}
3318
+
3319
+ **Context:** ${context}
3320
+ `;
3321
+ const filePath = path4.join(this.questionsDir, `${id}.md`);
3322
+ await writeFile2(filePath, content, "utf-8");
3323
+ log.debug(`wrote question ${id} to ${filePath}`);
3324
+ this.invalidateQuestionsCache();
3325
+ return id;
3326
+ }
3327
+ async readQuestions(opts) {
3328
+ const cacheKey = this.questionsDir;
3329
+ const cached = _StorageManager.questionsCache.get(cacheKey);
3330
+ if (cached && Date.now() - cached.loadedAt < _StorageManager.QUESTIONS_CACHE_TTL_MS) {
3331
+ try {
3332
+ const dirStat = await stat2(this.questionsDir);
3333
+ if (dirStat.mtimeMs <= cached.loadedAt) {
3334
+ const all = cached.questions;
3335
+ return opts?.unresolvedOnly ? all.filter((q) => !q.resolved) : all;
3336
+ }
3337
+ } catch {
3338
+ }
3339
+ }
3340
+ try {
3341
+ const files = await readdir(this.questionsDir);
3342
+ const questions = [];
3343
+ for (const file of files) {
3344
+ if (!file.endsWith(".md")) continue;
3345
+ const filePath = path4.join(this.questionsDir, file);
3346
+ const raw = await readFile2(filePath, "utf-8");
3347
+ const parsed = this.parseQuestionFile(raw, filePath);
3348
+ if (parsed) {
3349
+ questions.push(parsed);
3350
+ }
3351
+ }
3352
+ const sorted = questions.sort((a, b) => b.priority - a.priority);
3353
+ _StorageManager.questionsCache.set(cacheKey, { questions: sorted, loadedAt: Date.now() });
3354
+ return opts?.unresolvedOnly ? sorted.filter((q) => !q.resolved) : sorted;
3355
+ } catch {
3356
+ return [];
3357
+ }
3358
+ }
3359
+ /** Invalidate the questions cache (call after writing a question). */
3360
+ invalidateQuestionsCache() {
3361
+ _StorageManager.questionsCache.delete(this.questionsDir);
3362
+ }
3363
+ parseQuestionFile(raw, filePath) {
3364
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n\n([\s\S]*)$/);
3365
+ if (!match) return null;
3366
+ const frontmatterStr = match[1];
3367
+ const body = match[2].trim();
3368
+ const id = this.extractFrontmatterValue(frontmatterStr, "id") ?? path4.basename(filePath, ".md");
3369
+ const created = this.extractFrontmatterValue(frontmatterStr, "created") ?? "";
3370
+ const priority = parseFloat(
3371
+ this.extractFrontmatterValue(frontmatterStr, "priority") ?? "0.5"
3372
+ );
3373
+ const resolved = this.extractFrontmatterValue(frontmatterStr, "resolved") === "true";
3374
+ const contextMatch = body.match(/\*\*Context:\*\*\s*(.*)/);
3375
+ const question = contextMatch ? body.slice(0, contextMatch.index).trim() : body;
3376
+ const context = contextMatch ? contextMatch[1].trim() : "";
3377
+ return { id, question, context, priority, resolved, created, filePath };
3378
+ }
3379
+ extractFrontmatterValue(frontmatter, key) {
3380
+ const match = frontmatter.match(
3381
+ new RegExp(`^${key}:\\s*"?([^"\\n]*)"?`, "m")
3382
+ );
3383
+ return match ? match[1] : null;
3384
+ }
3385
+ async resolveQuestion(id) {
3386
+ const questions = await this.readQuestions();
3387
+ const q = questions.find((q2) => q2.id === id);
3388
+ if (!q) return false;
3389
+ let raw = await readFile2(q.filePath, "utf-8");
3390
+ raw = raw.replace(/resolved: false/, "resolved: true");
3391
+ raw = raw.replace(
3392
+ /---\n\n/,
3393
+ `resolvedAt: "${(/* @__PURE__ */ new Date()).toISOString()}"
3394
+ ---
3395
+
3396
+ `
3397
+ );
3398
+ await writeFile2(q.filePath, raw, "utf-8");
3399
+ log.debug(`resolved question ${id}`);
3400
+ return true;
3401
+ }
3402
+ // ---------------------------------------------------------------------------
3403
+ // Identity file
3404
+ // ---------------------------------------------------------------------------
3405
+ async readIdentity(workspaceDir, namespace) {
3406
+ const identityPath = this.identityFilePath(workspaceDir, namespace);
3407
+ try {
3408
+ return await readFile2(identityPath, "utf-8");
3409
+ } catch {
3410
+ return "";
3411
+ }
3412
+ }
3413
+ async writeIdentity(workspaceDir, content, namespace) {
3414
+ const identityPath = this.identityFilePath(workspaceDir, namespace);
3415
+ await writeFile2(identityPath, content, "utf-8");
3416
+ log.debug(`wrote consolidated IDENTITY.md (${content.length} chars)`);
3417
+ }
3418
+ /** Max size for IDENTITY.md before we stop appending reflections (15KB leaves room under 20KB gateway limit) */
3419
+ static IDENTITY_MAX_BYTES = 15e3;
3420
+ /** Minimum interval between reflections (1 hour) */
3421
+ static REFLECTION_COOLDOWN_MS = 60 * 60 * 1e3;
3422
+ async appendToIdentity(workspaceDir, reflection, opts) {
3423
+ const identityPath = this.identityFilePath(workspaceDir, opts?.namespace);
3424
+ let existing = "";
3425
+ try {
3426
+ existing = await readFile2(identityPath, "utf-8");
3427
+ } catch {
3428
+ }
3429
+ const hygiene = opts?.hygiene;
3430
+ const rotateEnabled = hygiene?.enabled === true && hygiene.rotateEnabled === true && Array.isArray(hygiene.rotatePaths) && hygiene.rotatePaths.includes(path4.basename(identityPath));
3431
+ if (rotateEnabled) {
3432
+ const maxBytes = hygiene.rotateMaxBytes;
3433
+ if (existing.length > maxBytes) {
3434
+ const archiveDir = path4.join(workspaceDir, hygiene.archiveDir);
3435
+ const { newContent } = await rotateMarkdownFileToArchive({
3436
+ filePath: identityPath,
3437
+ archiveDir,
3438
+ archivePrefix: "IDENTITY",
3439
+ keepTailChars: hygiene.rotateKeepTailChars
3440
+ });
3441
+ await writeFile2(identityPath, newContent, "utf-8");
3442
+ existing = newContent;
3443
+ log.info(
3444
+ `rotated IDENTITY.md to archive (size=${existing.length} chars, maxBytes=${maxBytes})`
3445
+ );
3446
+ }
3447
+ } else {
3448
+ if (existing.length > _StorageManager.IDENTITY_MAX_BYTES) {
3449
+ log.debug(`IDENTITY.md is ${existing.length} chars (limit ${_StorageManager.IDENTITY_MAX_BYTES}); skipping reflection`);
3450
+ return;
3451
+ }
3452
+ }
3453
+ const lastMatch = existing.match(/## Reflection — (\S+)\s*$/m);
3454
+ if (lastMatch) {
3455
+ const allMatches = [...existing.matchAll(/## Reflection — (\S+)/g)];
3456
+ if (allMatches.length > 0) {
3457
+ const lastTimestamp = allMatches[allMatches.length - 1][1];
3458
+ const elapsed = Date.now() - new Date(lastTimestamp).getTime();
3459
+ if (elapsed < _StorageManager.REFLECTION_COOLDOWN_MS) {
3460
+ log.debug(`reflection cooldown: ${Math.round(elapsed / 1e3)}s since last (need ${_StorageManager.REFLECTION_COOLDOWN_MS / 1e3}s)`);
3461
+ return;
3462
+ }
3463
+ }
3464
+ }
3465
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3466
+ const section = `
3467
+
3468
+ ## Reflection \u2014 ${timestamp}
3469
+
3470
+ ${reflection}
3471
+ `;
3472
+ await writeFile2(identityPath, existing + section, "utf-8");
3473
+ log.debug(`appended reflection to ${identityPath}`);
3474
+ }
3475
+ async readIdentityReflections() {
3476
+ try {
3477
+ return await readFile2(this.identityReflectionsPath, "utf-8");
3478
+ } catch {
3479
+ return null;
3480
+ }
3481
+ }
3482
+ async writeIdentityReflections(content) {
3483
+ await mkdir2(this.identityDir, { recursive: true });
3484
+ await writeFile2(this.identityReflectionsPath, content, "utf-8");
3485
+ }
3486
+ async appendIdentityReflection(reflection) {
3487
+ let existing = "";
3488
+ try {
3489
+ existing = await readFile2(this.identityReflectionsPath, "utf-8");
3490
+ } catch {
3491
+ }
3492
+ if (existing.length > _StorageManager.IDENTITY_MAX_BYTES) {
3493
+ log.debug(
3494
+ `identity/reflections.md is ${existing.length} chars (limit ${_StorageManager.IDENTITY_MAX_BYTES}); skipping reflection`
3495
+ );
3496
+ return;
3497
+ }
3498
+ const allMatches = [...existing.matchAll(/## Reflection — (\S+)/g)];
3499
+ if (allMatches.length > 0) {
3500
+ const lastTimestamp = allMatches[allMatches.length - 1][1];
3501
+ const elapsed = Date.now() - new Date(lastTimestamp).getTime();
3502
+ if (elapsed < _StorageManager.REFLECTION_COOLDOWN_MS) {
3503
+ log.debug(
3504
+ `reflection cooldown: ${Math.round(elapsed / 1e3)}s since last (need ${_StorageManager.REFLECTION_COOLDOWN_MS / 1e3}s)`
3505
+ );
3506
+ return;
3507
+ }
3508
+ }
3509
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3510
+ const section = `${existing.trimEnd().length > 0 ? "\n\n" : ""}## Reflection \u2014 ${timestamp}
3511
+
3512
+ ${reflection}
3513
+ `;
3514
+ await mkdir2(this.identityDir, { recursive: true });
3515
+ await writeFile2(this.identityReflectionsPath, `${existing.trimEnd()}${section}`, "utf-8");
3516
+ log.debug(`appended namespace-local reflection to ${this.identityReflectionsPath}`);
3517
+ }
3518
+ // ---------------------------------------------------------------------------
3519
+ // Entity mutation helpers (Knowledge Graph v7.0)
3520
+ // ---------------------------------------------------------------------------
3521
+ /**
3522
+ * Add a relationship to an entity file.
3523
+ * Deduplicates by target+label.
3524
+ */
3525
+ async addEntityRelationship(name, rel) {
3526
+ const filePath = path4.join(this.entitiesDir, `${name}.md`);
3527
+ let entity;
3528
+ try {
3529
+ const content = await readFile2(filePath, "utf-8");
3530
+ entity = parseEntityFile(content);
3531
+ } catch {
3532
+ log.debug(`addEntityRelationship: entity file ${name}.md not found`);
3533
+ return;
3534
+ }
3535
+ const exists = entity.relationships.some(
3536
+ (r) => r.target === rel.target && r.label === rel.label
3537
+ );
3538
+ if (exists) return;
3539
+ entity.relationships.push(rel);
3540
+ entity.updated = (/* @__PURE__ */ new Date()).toISOString();
3541
+ await writeFile2(filePath, serializeEntityFile(entity), "utf-8");
3542
+ this.invalidateKnowledgeIndexCache();
3543
+ }
3544
+ /**
3545
+ * Add an activity entry to an entity file.
3546
+ * Prepends to the beginning, prunes oldest entries beyond maxEntries.
3547
+ */
3548
+ async addEntityActivity(name, entry, maxEntries) {
3549
+ const filePath = path4.join(this.entitiesDir, `${name}.md`);
3550
+ let entity;
3551
+ try {
3552
+ const content = await readFile2(filePath, "utf-8");
3553
+ entity = parseEntityFile(content);
3554
+ } catch {
3555
+ log.debug(`addEntityActivity: entity file ${name}.md not found`);
3556
+ return;
3557
+ }
3558
+ entity.activity.unshift(entry);
3559
+ if (entity.activity.length > maxEntries) {
3560
+ entity.activity = entity.activity.slice(0, maxEntries);
3561
+ }
3562
+ entity.updated = (/* @__PURE__ */ new Date()).toISOString();
3563
+ await writeFile2(filePath, serializeEntityFile(entity), "utf-8");
3564
+ this.invalidateKnowledgeIndexCache();
3565
+ }
3566
+ /**
3567
+ * Add an alias to an entity file. Deduplicates.
3568
+ */
3569
+ async addEntityAlias(name, alias) {
3570
+ const filePath = path4.join(this.entitiesDir, `${name}.md`);
3571
+ let entity;
3572
+ try {
3573
+ const content = await readFile2(filePath, "utf-8");
3574
+ entity = parseEntityFile(content);
3575
+ } catch {
3576
+ log.debug(`addEntityAlias: entity file ${name}.md not found`);
3577
+ return;
3578
+ }
3579
+ if (entity.aliases.includes(alias)) return;
3580
+ entity.aliases.push(alias);
3581
+ entity.updated = (/* @__PURE__ */ new Date()).toISOString();
3582
+ await writeFile2(filePath, serializeEntityFile(entity), "utf-8");
3583
+ this.invalidateKnowledgeIndexCache();
3584
+ }
3585
+ /**
3586
+ * Set or update the summary of an entity file.
3587
+ */
3588
+ async updateEntitySummary(name, summary) {
3589
+ const filePath = path4.join(this.entitiesDir, `${name}.md`);
3590
+ let entity;
3591
+ try {
3592
+ const content = await readFile2(filePath, "utf-8");
3593
+ entity = parseEntityFile(content);
3594
+ } catch {
3595
+ log.debug(`updateEntitySummary: entity file ${name}.md not found`);
3596
+ return;
3597
+ }
3598
+ entity.summary = summary;
3599
+ entity.updated = (/* @__PURE__ */ new Date()).toISOString();
3600
+ await writeFile2(filePath, serializeEntityFile(entity), "utf-8");
3601
+ this.invalidateKnowledgeIndexCache();
3602
+ this.bumpMemoryStatusVersion();
3603
+ }
3604
+ // ---------------------------------------------------------------------------
3605
+ // Scoring + Knowledge Index (Knowledge Graph v7.0)
3606
+ // ---------------------------------------------------------------------------
3607
+ /**
3608
+ * Read all entity files and return lightweight EntityFile objects.
3609
+ * Parsing is fast (~50-100ms for ~1,800 files) since entity files are small.
3610
+ */
3611
+ async readAllEntityFiles() {
3612
+ const currentVersion = this.getMemoryStatusVersion();
3613
+ const cached = getCachedEntities(this.baseDir, currentVersion);
3614
+ if (cached) return cached;
3615
+ try {
3616
+ const entries = await readdir(this.entitiesDir);
3617
+ const mdFiles = entries.filter((e) => e.endsWith(".md"));
3618
+ if (mdFiles.length === 0) return [];
3619
+ const BATCH_SIZE = 100;
3620
+ const entities = [];
3621
+ for (let i = 0; i < mdFiles.length; i += BATCH_SIZE) {
3622
+ const batch = mdFiles.slice(i, i + BATCH_SIZE);
3623
+ const results = await Promise.all(
3624
+ batch.map(
3625
+ (entry) => readFile2(path4.join(this.entitiesDir, entry), "utf-8").catch(() => null)
3626
+ )
3627
+ );
3628
+ for (const content of results) {
3629
+ if (content !== null) entities.push(parseEntityFile(content));
3630
+ }
3631
+ }
3632
+ setCachedEntities(this.baseDir, entities, currentVersion);
3633
+ return entities;
3634
+ } catch {
3635
+ return [];
3636
+ }
3637
+ }
3638
+ /**
3639
+ * Score an entity based on recency, frequency, activity, type priority,
3640
+ * and relationship density.
3641
+ *
3642
+ * score = recency*0.40 + frequency*0.25 + activity*0.15 + typePriority*0.10 + relationshipDensity*0.10
3643
+ */
3644
+ static scoreEntity(entity, now) {
3645
+ const updated = entity.updated ? new Date(entity.updated).getTime() : 0;
3646
+ const daysSince = Math.max(0, (now.getTime() - updated) / (1e3 * 60 * 60 * 24));
3647
+ const recency = 1 / (1 + daysSince / 7);
3648
+ const frequency = Math.min(entity.facts.length / 20, 1);
3649
+ const activityScore = Math.min(entity.activity.length / 10, 1);
3650
+ const TYPE_PRIORITY = {
3651
+ person: 1,
3652
+ project: 0.8,
3653
+ company: 0.7,
3654
+ tool: 0.6,
3655
+ place: 0.5,
3656
+ other: 0.3
3657
+ };
3658
+ const typePriority = TYPE_PRIORITY[entity.type.toLowerCase()] ?? 0.3;
3659
+ const relDensity = Math.min(entity.relationships.length / 8, 1);
3660
+ return recency * 0.4 + frequency * 0.25 + activityScore * 0.15 + typePriority * 0.1 + relDensity * 0.1;
3661
+ }
3662
+ /**
3663
+ * Build the Knowledge Index: a compact markdown table of top-scored entities.
3664
+ * Respects maxEntities and maxChars limits from config.
3665
+ */
3666
+ async buildKnowledgeIndex(config, overrides) {
3667
+ const useDefaultLimits = overrides?.maxEntities === void 0 && overrides?.maxChars === void 0;
3668
+ if (useDefaultLimits && this.knowledgeIndexCache && Date.now() - this.knowledgeIndexCache.builtAt < _StorageManager.KNOWLEDGE_INDEX_CACHE_TTL_MS) {
3669
+ return { result: this.knowledgeIndexCache.result, cached: true };
3670
+ }
3671
+ const entities = await this.readAllEntityFiles();
3672
+ if (entities.length === 0) {
3673
+ if (useDefaultLimits) this.knowledgeIndexCache = { result: "", builtAt: Date.now() };
3674
+ return { result: "", cached: false };
3675
+ }
3676
+ const now = /* @__PURE__ */ new Date();
3677
+ const scored = entities.map((e) => ({
3678
+ name: e.name,
3679
+ type: e.type,
3680
+ score: _StorageManager.scoreEntity(e, now),
3681
+ factCount: e.facts.length,
3682
+ summary: e.summary,
3683
+ topRelationships: e.relationships.slice(0, 3).map((r) => r.target)
3684
+ }));
3685
+ scored.sort((a, b) => b.score - a.score);
3686
+ const maxEntities = typeof overrides?.maxEntities === "number" ? Math.max(0, Math.floor(overrides.maxEntities)) : config.knowledgeIndexMaxEntities;
3687
+ const topN = scored.slice(0, maxEntities);
3688
+ if (topN.length === 0) {
3689
+ if (useDefaultLimits) this.knowledgeIndexCache = { result: "", builtAt: Date.now() };
3690
+ return { result: "", cached: false };
3691
+ }
3692
+ const header = "## Knowledge Index\n\n| Entity | Type | Summary | Connected to |\n|--------|------|---------|-------------|";
3693
+ const rows = [];
3694
+ let totalChars = header.length;
3695
+ const maxChars = typeof overrides?.maxChars === "number" ? Math.max(0, Math.floor(overrides.maxChars)) : config.knowledgeIndexMaxChars;
3696
+ for (const entity of topN) {
3697
+ const summary = entity.summary || `${entity.factCount} facts`;
3698
+ const connected = entity.topRelationships.length > 0 ? entity.topRelationships.join(", ") : "\u2014";
3699
+ const row = `| ${entity.name} | ${entity.type} | ${summary} | ${connected} |`;
3700
+ if (totalChars + row.length + 1 > maxChars) break;
3701
+ rows.push(row);
3702
+ totalChars += row.length + 1;
3703
+ }
3704
+ const result = rows.length === 0 ? "" : `${header}
3705
+ ${rows.join("\n")}
3706
+ `;
3707
+ if (useDefaultLimits) this.knowledgeIndexCache = { result, builtAt: Date.now() };
3708
+ return { result, cached: false };
3709
+ }
3710
+ /** Invalidate the Knowledge Index cache (call after entity mutations). */
3711
+ invalidateKnowledgeIndexCache() {
3712
+ this.knowledgeIndexCache = null;
3713
+ }
3714
+ // ---------------------------------------------------------------------------
3715
+ // Commitment decay
3716
+ // ---------------------------------------------------------------------------
3717
+ /** Max lines for profile.md before LLM consolidation triggers */
3718
+ static PROFILE_MAX_LINES = 300;
3719
+ /**
3720
+ * Merge fragmented entity files that resolve to the same canonical name.
3721
+ * Preserves relationships, activity, aliases, and summary from all fragments.
3722
+ * Returns count of files merged.
3723
+ */
3724
+ async mergeFragmentedEntities() {
3725
+ let merged = 0;
3726
+ try {
3727
+ const entries = await readdir(this.entitiesDir);
3728
+ const mdFiles = entries.filter((e) => e.endsWith(".md"));
3729
+ const groups = /* @__PURE__ */ new Map();
3730
+ for (const file of mdFiles) {
3731
+ const baseName = file.replace(".md", "");
3732
+ const dashIdx = baseName.indexOf("-");
3733
+ if (dashIdx === -1) continue;
3734
+ const type = baseName.slice(0, dashIdx);
3735
+ const restOfName = baseName.slice(dashIdx + 1);
3736
+ const canonical = normalizeEntityName(restOfName, type);
3737
+ if (!groups.has(canonical)) groups.set(canonical, []);
3738
+ groups.get(canonical).push(file);
3739
+ }
3740
+ for (const [canonical, files] of groups) {
3741
+ if (files.length <= 1) continue;
3742
+ const mergedEntity = {
3743
+ name: "",
3744
+ type: "other",
3745
+ updated: "",
3746
+ facts: [],
3747
+ summary: void 0,
3748
+ relationships: [],
3749
+ activity: [],
3750
+ aliases: []
3751
+ };
3752
+ for (const file of files) {
3753
+ const filePath = path4.join(this.entitiesDir, file);
3754
+ try {
3755
+ const content = await readFile2(filePath, "utf-8");
3756
+ const parsed = parseEntityFile(content);
3757
+ if (!mergedEntity.type || mergedEntity.type === "other") {
3758
+ mergedEntity.type = parsed.type;
3759
+ }
3760
+ if (!mergedEntity.updated || parsed.updated > mergedEntity.updated) {
3761
+ mergedEntity.updated = parsed.updated;
3762
+ }
3763
+ if (parsed.name.length > mergedEntity.name.length) {
3764
+ mergedEntity.name = parsed.name;
3765
+ }
3766
+ if (!mergedEntity.summary && parsed.summary) {
3767
+ mergedEntity.summary = parsed.summary;
3768
+ }
3769
+ mergedEntity.facts.push(...parsed.facts);
3770
+ mergedEntity.relationships.push(...parsed.relationships);
3771
+ mergedEntity.activity.push(...parsed.activity);
3772
+ mergedEntity.aliases.push(...parsed.aliases);
3773
+ } catch {
3774
+ }
3775
+ }
3776
+ mergedEntity.facts = [...new Set(mergedEntity.facts)];
3777
+ const relKeys = /* @__PURE__ */ new Set();
3778
+ mergedEntity.relationships = mergedEntity.relationships.filter((r) => {
3779
+ const key = `${r.target}::${r.label}`;
3780
+ if (relKeys.has(key)) return false;
3781
+ relKeys.add(key);
3782
+ return true;
3783
+ });
3784
+ const actKeys = /* @__PURE__ */ new Set();
3785
+ mergedEntity.activity = mergedEntity.activity.filter((a) => {
3786
+ const key = `${a.date}::${a.note}`;
3787
+ if (actKeys.has(key)) return false;
3788
+ actKeys.add(key);
3789
+ return true;
3790
+ }).sort((a, b) => b.date.localeCompare(a.date));
3791
+ mergedEntity.aliases = [...new Set(mergedEntity.aliases)];
3792
+ if (!mergedEntity.name) {
3793
+ const dashIdx = canonical.indexOf("-");
3794
+ mergedEntity.name = dashIdx !== -1 ? canonical.slice(dashIdx + 1) : canonical;
3795
+ }
3796
+ mergedEntity.updated = mergedEntity.updated || (/* @__PURE__ */ new Date()).toISOString();
3797
+ const canonicalPath = path4.join(this.entitiesDir, `${canonical}.md`);
3798
+ await writeFile2(canonicalPath, serializeEntityFile(mergedEntity), "utf-8");
3799
+ for (const file of files) {
3800
+ const filePath = path4.join(this.entitiesDir, file);
3801
+ if (filePath !== canonicalPath) {
3802
+ try {
3803
+ await unlink(filePath);
3804
+ merged++;
3805
+ log.debug(`merged entity ${file} \u2192 ${canonical}.md`);
3806
+ } catch {
3807
+ }
3808
+ }
3809
+ }
3810
+ }
3811
+ } catch {
3812
+ }
3813
+ return merged;
3814
+ }
3815
+ async cleanExpiredCommitments(decayDays) {
3816
+ const memories = await this.readAllMemories();
3817
+ const cutoff = Date.now() - decayDays * 24 * 60 * 60 * 1e3;
3818
+ const deleted = [];
3819
+ for (const m of memories) {
3820
+ if (m.frontmatter.category !== "commitment") continue;
3821
+ const isResolved = m.frontmatter.tags.some(
3822
+ (t) => t === "fulfilled" || t === "expired"
3823
+ );
3824
+ if (!isResolved) continue;
3825
+ const updatedAt = new Date(m.frontmatter.updated).getTime();
3826
+ if (updatedAt < cutoff) {
3827
+ try {
3828
+ await unlink(m.path);
3829
+ deleted.push(m);
3830
+ log.debug(`cleaned expired commitment ${m.frontmatter.id}`);
3831
+ } catch {
3832
+ }
3833
+ }
3834
+ }
3835
+ if (deleted.length > 0) {
3836
+ this.bumpMemoryStatusVersion();
3837
+ }
3838
+ return deleted;
3839
+ }
3840
+ // ---------------------------------------------------------------------------
3841
+ // Access Tracking (Phase 1A)
3842
+ // ---------------------------------------------------------------------------
3843
+ /**
3844
+ * Flush batched access tracking updates to disk.
3845
+ * Called during consolidation or when buffer exceeds max size.
3846
+ */
3847
+ async flushAccessTracking(entries) {
3848
+ if (entries.length === 0) return 0;
3849
+ const memories = await this.readAllMemories();
3850
+ const memoryMap = new Map(memories.map((m) => [m.frontmatter.id, m]));
3851
+ let updated = 0;
3852
+ for (const entry of entries) {
3853
+ const memory = memoryMap.get(entry.memoryId);
3854
+ if (!memory) continue;
3855
+ const newFm = {
3856
+ ...memory.frontmatter,
3857
+ accessCount: entry.newCount,
3858
+ lastAccessed: entry.lastAccessed
3859
+ };
3860
+ const fileContent = `${serializeFrontmatter(newFm)}
3861
+
3862
+ ${memory.content}
3863
+ `;
3864
+ try {
3865
+ await writeFile2(memory.path, fileContent, "utf-8");
3866
+ updated++;
3867
+ } catch (err) {
3868
+ log.debug(`failed to update access tracking for ${entry.memoryId}: ${err}`);
3869
+ }
3870
+ }
3871
+ if (updated > 0) {
3872
+ log.debug(`flushed access tracking for ${updated} memories`);
3873
+ }
3874
+ return updated;
3875
+ }
3876
+ /**
3877
+ * Get a memory by its ID.
3878
+ */
3879
+ async getMemoryById(id) {
3880
+ const memories = await this.readAllMemories();
3881
+ return memories.find((m) => m.frontmatter.id === id) ?? null;
3882
+ }
3883
+ async getProjectedMemoryState(id) {
3884
+ const projected = readProjectedMemoryState(this.baseDir, id);
3885
+ if (projected) return projected;
3886
+ const active = await this.getMemoryById(id);
3887
+ if (active) return this.toProjectedCurrentState(active, "active");
3888
+ const archived = (await this.readArchivedMemories()).find((memory) => memory.frontmatter.id === id);
3889
+ if (!archived) return null;
3890
+ return this.toProjectedCurrentState(archived, "archived");
3891
+ }
3892
+ async browseProjectedMemories(options) {
3893
+ return readProjectedMemoryBrowse(this.baseDir, options);
3894
+ }
3895
+ async getProjectedGovernanceRecord() {
3896
+ return readProjectedGovernanceRecord(this.baseDir);
3897
+ }
3898
+ toProjectedCurrentState(memory, fallbackStatus) {
3899
+ const pathRel = toMemoryPathRel(this.baseDir, memory.path);
3900
+ return {
3901
+ memoryId: memory.frontmatter.id,
3902
+ category: memory.frontmatter.category,
3903
+ status: inferCurrentStateStatus(memory.frontmatter, pathRel, fallbackStatus),
3904
+ lifecycleState: memory.frontmatter.lifecycleState,
3905
+ path: memory.path,
3906
+ pathRel,
3907
+ created: memory.frontmatter.created,
3908
+ updated: memory.frontmatter.updated,
3909
+ archivedAt: memory.frontmatter.archivedAt,
3910
+ supersededAt: memory.frontmatter.supersededAt,
3911
+ entityRef: memory.frontmatter.entityRef,
3912
+ source: memory.frontmatter.source,
3913
+ confidence: memory.frontmatter.confidence,
3914
+ confidenceTier: memory.frontmatter.confidenceTier,
3915
+ memoryKind: memory.frontmatter.memoryKind,
3916
+ accessCount: memory.frontmatter.accessCount,
3917
+ lastAccessed: memory.frontmatter.lastAccessed,
3918
+ tags: normalizeProjectionTags(memory.frontmatter.tags),
3919
+ preview: normalizeProjectionPreview(memory.content)
3920
+ };
3921
+ }
3922
+ async getMemoryTimeline(memoryId, limit = 200) {
3923
+ const cappedLimit = Math.max(0, Math.floor(limit));
3924
+ if (cappedLimit === 0) return [];
3925
+ const projected = readProjectedMemoryTimeline(this.baseDir, memoryId, cappedLimit);
3926
+ if (projected && projected.length > 0) return projected;
3927
+ const events = await this.readAllMemoryLifecycleEvents();
3928
+ return events.filter((event) => event.memoryId === memoryId).slice(-cappedLimit);
3929
+ }
3930
+ // ---------------------------------------------------------------------------
3931
+ // Chunking (Phase 2A)
3932
+ // ---------------------------------------------------------------------------
3933
+ /**
3934
+ * Write a memory chunk with parent reference.
3935
+ * Chunk IDs follow format: {parentId}-chunk-{index}
3936
+ */
3937
+ async writeChunk(parentId, chunkIndex, chunkTotal, category, content, options = {}) {
3938
+ await this.ensureDirectories();
3939
+ const now = /* @__PURE__ */ new Date();
3940
+ const today = now.toISOString().slice(0, 10);
3941
+ const id = `${parentId}-chunk-${chunkIndex}`;
3942
+ const conf = options.confidence ?? 0.8;
3943
+ const tier = confidenceTier(conf);
3944
+ const fm = {
3945
+ id,
3946
+ category,
3947
+ created: now.toISOString(),
3948
+ updated: now.toISOString(),
3949
+ source: options.source ?? "chunking",
3950
+ confidence: conf,
3951
+ confidenceTier: tier,
3952
+ tags: options.tags ?? [],
3953
+ entityRef: options.entityRef,
3954
+ importance: options.importance,
3955
+ parentId,
3956
+ chunkIndex,
3957
+ chunkTotal,
3958
+ intentGoal: options.intentGoal,
3959
+ intentActionType: options.intentActionType,
3960
+ intentEntityTypes: options.intentEntityTypes,
3961
+ memoryKind: options.memoryKind
3962
+ };
3963
+ const sanitized = sanitizeMemoryContent(content);
3964
+ if (!sanitized.clean) {
3965
+ log.warn(`chunk content sanitized for ${id}; violations=${sanitized.violations.join(", ")}`);
3966
+ }
3967
+ const fileContent = `${serializeFrontmatter(fm)}
3968
+
3969
+ ${sanitized.text}
3970
+ `;
3971
+ let filePath;
3972
+ if (category === "correction") {
3973
+ filePath = path4.join(this.correctionsDir, `${id}.md`);
3974
+ } else {
3975
+ filePath = path4.join(this.factsDir, today, `${id}.md`);
3976
+ }
3977
+ await writeFile2(filePath, fileContent, "utf-8");
3978
+ log.debug(`wrote chunk ${id} (${chunkIndex + 1}/${chunkTotal}) to ${filePath}`);
3979
+ return id;
3980
+ }
3981
+ /**
3982
+ * Get all chunks for a given parent memory ID.
3983
+ * Returns chunks sorted by chunkIndex.
3984
+ */
3985
+ async getChunksForParent(parentId) {
3986
+ const memories = await this.readAllMemories();
3987
+ return memories.filter((m) => m.frontmatter.parentId === parentId).sort((a, b) => (a.frontmatter.chunkIndex ?? 0) - (b.frontmatter.chunkIndex ?? 0));
3988
+ }
3989
+ // ---------------------------------------------------------------------------
3990
+ // Contradiction Detection (Phase 2B)
3991
+ // ---------------------------------------------------------------------------
3992
+ /**
3993
+ * Mark a memory as superseded by another.
3994
+ * Updates the old memory's status and adds the supersededBy link.
3995
+ */
3996
+ async supersedeMemory(oldMemoryId, newMemoryId, reason) {
3997
+ const memories = await this.readAllMemories();
3998
+ const oldMemory = memories.find((m) => m.frontmatter.id === oldMemoryId);
3999
+ if (!oldMemory) return false;
4000
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4001
+ const updatedFm = {
4002
+ ...oldMemory.frontmatter,
4003
+ status: "superseded",
4004
+ supersededBy: newMemoryId,
4005
+ supersededAt: now,
4006
+ updated: now
4007
+ };
4008
+ const fileContent = `${serializeFrontmatter(updatedFm)}
4009
+
4010
+ ${oldMemory.content}
4011
+ `;
4012
+ try {
4013
+ await writeFile2(oldMemory.path, fileContent, "utf-8");
4014
+ await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.supersedeMemory", {
4015
+ memoryId: oldMemoryId,
4016
+ eventType: "superseded",
4017
+ timestamp: now,
4018
+ actor: "storage.supersedeMemory",
4019
+ reasonCode: reason,
4020
+ before: this.summarizeLifecycleState(oldMemory.frontmatter, oldMemory.path),
4021
+ after: this.summarizeLifecycleState(updatedFm, oldMemory.path),
4022
+ relatedMemoryIds: [newMemoryId]
4023
+ });
4024
+ this.bumpMemoryStatusVersion();
4025
+ log.debug(`superseded memory ${oldMemoryId} by ${newMemoryId}: ${reason}`);
4026
+ await this.writeMemory("correction", `Superseded: ${oldMemory.content}
4027
+
4028
+ Reason: ${reason}`, {
4029
+ confidence: 1,
4030
+ tags: ["supersession", "auto-resolved"],
4031
+ source: "contradiction-detection",
4032
+ lineage: [oldMemoryId, newMemoryId]
4033
+ });
4034
+ return true;
4035
+ } catch (err) {
4036
+ log.error(`failed to supersede memory ${oldMemoryId}:`, err);
4037
+ return false;
4038
+ }
4039
+ }
4040
+ // ---------------------------------------------------------------------------
4041
+ // Memory Summarization (Phase 4A)
4042
+ // ---------------------------------------------------------------------------
4043
+ get summariesDir() {
4044
+ return path4.join(this.baseDir, "summaries");
4045
+ }
4046
+ /**
4047
+ * Write a memory summary.
4048
+ */
4049
+ async writeSummary(summary) {
4050
+ await mkdir2(this.summariesDir, { recursive: true });
4051
+ const filePath = path4.join(this.summariesDir, `${summary.id}.json`);
4052
+ await writeFile2(filePath, JSON.stringify(summary, null, 2), "utf-8");
4053
+ log.debug(`wrote summary ${summary.id}`);
4054
+ }
4055
+ /**
4056
+ * Get all summaries.
4057
+ */
4058
+ async readSummaries() {
4059
+ try {
4060
+ const files = await readdir(this.summariesDir);
4061
+ const summaries = [];
4062
+ for (const file of files) {
4063
+ if (!file.endsWith(".json")) continue;
4064
+ const filePath = path4.join(this.summariesDir, file);
4065
+ const raw = await readFile2(filePath, "utf-8");
4066
+ summaries.push(JSON.parse(raw));
4067
+ }
4068
+ return summaries;
4069
+ } catch {
4070
+ return [];
4071
+ }
4072
+ }
4073
+ /**
4074
+ * Archive memories (mark as archived, not delete).
4075
+ */
4076
+ async archiveMemories(memoryIds, summaryId) {
4077
+ const memories = await this.readAllMemories();
4078
+ const memoryMap = new Map(memories.map((m) => [m.frontmatter.id, m]));
4079
+ let archived = 0;
4080
+ for (const id of memoryIds) {
4081
+ const memory = memoryMap.get(id);
4082
+ if (!memory) continue;
4083
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4084
+ const updatedFm = {
4085
+ ...memory.frontmatter,
4086
+ status: "archived",
4087
+ archivedAt: now,
4088
+ updated: now
4089
+ };
4090
+ const fileContent = `${serializeFrontmatter(updatedFm)}
4091
+
4092
+ ${memory.content}
4093
+ `;
4094
+ try {
4095
+ await writeFile2(memory.path, fileContent, "utf-8");
4096
+ await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.archiveMemories", {
4097
+ memoryId: id,
4098
+ eventType: "archived",
4099
+ timestamp: updatedFm.archivedAt ?? updatedFm.updated,
4100
+ actor: "storage.archiveMemories",
4101
+ reasonCode: `summary:${summaryId}`,
4102
+ before: this.summarizeLifecycleState(memory.frontmatter, memory.path),
4103
+ after: this.summarizeLifecycleState(updatedFm, memory.path),
4104
+ relatedMemoryIds: [summaryId]
4105
+ });
4106
+ archived++;
4107
+ } catch {
4108
+ }
4109
+ }
4110
+ if (archived > 0) {
4111
+ this.bumpMemoryStatusVersion();
4112
+ log.debug(`archived ${archived} memories for summary ${summaryId}`);
4113
+ }
4114
+ return archived;
4115
+ }
4116
+ // ---------------------------------------------------------------------------
4117
+ // Topic Extraction (Phase 4B)
4118
+ // ---------------------------------------------------------------------------
4119
+ /**
4120
+ * Save topic scores to meta.json.
4121
+ */
4122
+ async saveTopics(topics) {
4123
+ const metaPath = path4.join(this.stateDir, "topics.json");
4124
+ await mkdir2(this.stateDir, { recursive: true });
4125
+ await writeFile2(metaPath, JSON.stringify({ topics, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), "utf-8");
4126
+ log.debug(`saved ${topics.length} topic scores`);
4127
+ }
4128
+ /**
4129
+ * Load topic scores from meta.json.
4130
+ */
4131
+ async loadTopics() {
4132
+ const metaPath = path4.join(this.stateDir, "topics.json");
4133
+ try {
4134
+ const raw = await readFile2(metaPath, "utf-8");
4135
+ return JSON.parse(raw);
4136
+ } catch {
4137
+ return { topics: [], updatedAt: null };
4138
+ }
4139
+ }
4140
+ /**
4141
+ * Add links to an existing memory.
4142
+ */
4143
+ async addLinksToMemory(memoryId, links, lifecycle) {
4144
+ const memories = await this.readAllMemories();
4145
+ const memory = memories.find((m) => m.frontmatter.id === memoryId);
4146
+ if (!memory) return false;
4147
+ const existingLinks = memory.frontmatter.links ?? [];
4148
+ const mergedLinks = [...existingLinks];
4149
+ for (const link of links) {
4150
+ if (!mergedLinks.some((l) => l.targetId === link.targetId && l.linkType === link.linkType)) {
4151
+ mergedLinks.push(link);
4152
+ }
4153
+ }
4154
+ try {
4155
+ await this.writeMemoryFrontmatter(
4156
+ memory,
4157
+ {
4158
+ links: mergedLinks,
4159
+ updated: (/* @__PURE__ */ new Date()).toISOString()
4160
+ },
4161
+ lifecycle
4162
+ );
4163
+ log.debug(`added ${links.length} links to memory ${memoryId}`);
4164
+ return true;
4165
+ } catch (err) {
4166
+ log.error(`failed to add links to memory ${memoryId}:`, err);
4167
+ return false;
4168
+ }
4169
+ }
4170
+ summarizeLifecycleState(frontmatter, filePath) {
4171
+ return {
4172
+ category: frontmatter.category,
4173
+ path: filePath,
4174
+ status: frontmatter.status ?? "active",
4175
+ lifecycleState: frontmatter.lifecycleState
4176
+ };
4177
+ }
4178
+ frontmatterPatchEventType(before, after) {
4179
+ const beforeStatus = before.status ?? "active";
4180
+ const afterStatus = after.status ?? "active";
4181
+ if (beforeStatus !== "archived" && afterStatus === "archived") return "archived";
4182
+ if (beforeStatus !== "superseded" && afterStatus === "superseded") return "superseded";
4183
+ if (beforeStatus !== "rejected" && afterStatus === "rejected") return "rejected";
4184
+ if (beforeStatus !== "active" && afterStatus === "active") {
4185
+ return "restored";
4186
+ }
4187
+ return "updated";
4188
+ }
4189
+ async appendGeneratedMemoryLifecycleEvent(input, ruleVersion = "memory-lifecycle-ledger.v1") {
4190
+ await this.appendMemoryLifecycleEvents([
4191
+ {
4192
+ ...input,
4193
+ eventId: this.generateId("mle"),
4194
+ ruleVersion
4195
+ }
4196
+ ]);
4197
+ }
4198
+ async appendGeneratedMemoryLifecycleEventFailOpen(operation, input, ruleVersion) {
4199
+ try {
4200
+ await this.appendGeneratedMemoryLifecycleEvent(input, ruleVersion);
4201
+ } catch (appendErr) {
4202
+ log.warn(`${operation} completed but failed to append lifecycle event: ${appendErr}`);
4203
+ }
4204
+ }
4205
+ };
4206
+
4207
+ export {
4208
+ sanitizeMemoryContent,
4209
+ getCachedEpisodeMap,
4210
+ setCachedEpisodeMap,
4211
+ getCachedRuleMemories,
4212
+ setCachedRuleMemories,
4213
+ getCachedQmdSearch,
4214
+ setCachedQmdSearch,
4215
+ lintWorkspaceFiles,
4216
+ rotateMarkdownFileToArchive,
4217
+ confidenceTier,
4218
+ openBetterSqlite3,
4219
+ MEMORY_PROJECTION_SCHEMA_VERSION,
4220
+ getMemoryProjectionPath,
4221
+ memoryCurrentSelectExpressions,
4222
+ initializeMemoryProjectionDb,
4223
+ parseCurrentRow,
4224
+ parseTimelineRows,
4225
+ readProjectedEntityMentions,
4226
+ readProjectedNativeKnowledgeChunks,
4227
+ readProjectedGovernanceRecord,
4228
+ MEMORY_LIFECYCLE_EVENT_SORT_ORDER,
4229
+ toMemoryPathRel,
4230
+ inferMemoryStatus,
4231
+ buildLifecycleEventsForMemory,
4232
+ sortMemoryLifecycleEvents,
4233
+ normalizeProjectionPreview,
4234
+ normalizeProjectionTags,
4235
+ parseContinuityIncident,
4236
+ parseContinuityImprovementLoops,
4237
+ normalizeEntityName,
4238
+ ContentHashIndex,
4239
+ parseEntityFile,
4240
+ serializeEntityFile,
4241
+ StorageManager
4242
+ };