@remnic/core 9.3.761 → 9.3.763

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.
Files changed (134) hide show
  1. package/dist/access-admin-ops-surface.js +10 -9
  2. package/dist/access-boundary.js +11 -10
  3. package/dist/access-cli.js +45 -44
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.js +17 -16
  6. package/dist/access-identity-continuity-surface.js +10 -9
  7. package/dist/access-lcm-surface.js +10 -9
  8. package/dist/access-mcp.js +14 -13
  9. package/dist/access-observe-write-surface.js +10 -9
  10. package/dist/access-operations-batch.js +12 -11
  11. package/dist/access-operations.js +13 -12
  12. package/dist/access-recall-surface.js +10 -9
  13. package/dist/access-service.js +10 -9
  14. package/dist/adapters/index.js +4 -4
  15. package/dist/adapters/registry.js +2 -2
  16. package/dist/briefing.js +5 -4
  17. package/dist/causal-consolidation.js +6 -5
  18. package/dist/causal-consolidation.js.map +1 -1
  19. package/dist/{chunk-742MWWVL.js → chunk-2RLECZ6J.js} +2 -2
  20. package/dist/{chunk-CFHDI5CV.js → chunk-6YEENRE3.js} +4 -4
  21. package/dist/{chunk-JYH3XYWM.js → chunk-6ZFE2TPO.js} +3 -3
  22. package/dist/{chunk-LG2MCUBC.js → chunk-A4FMQIST.js} +2 -2
  23. package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
  24. package/dist/{chunk-OCPVA4TH.js → chunk-AS2NRB45.js} +2 -2
  25. package/dist/{chunk-W4ADKEIY.js → chunk-BAF7MBEH.js} +22 -22
  26. package/dist/{chunk-ICVNVSKN.js → chunk-BRYPUVJU.js} +2 -2
  27. package/dist/{chunk-IV4INTAU.js → chunk-CVINPTKG.js} +2 -2
  28. package/dist/{chunk-IRFHFRHP.js → chunk-D6YTILJO.js} +25 -25
  29. package/dist/chunk-DQC3ZZYM.js +35 -0
  30. package/dist/chunk-DQC3ZZYM.js.map +1 -0
  31. package/dist/{chunk-3DAIQSVS.js → chunk-DTTVX6EO.js} +23 -8
  32. package/dist/chunk-DTTVX6EO.js.map +1 -0
  33. package/dist/{chunk-P7UA66OG.js → chunk-IVFIYAIP.js} +2 -2
  34. package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
  35. package/dist/{chunk-Y3XEN5TL.js → chunk-K4EDULCP.js} +3 -3
  36. package/dist/{chunk-P7N2DQLC.js → chunk-KXG7HHIQ.js} +3 -3
  37. package/dist/{chunk-4WOXD56B.js → chunk-LDCFFJ67.js} +5 -5
  38. package/dist/{chunk-5AZQZFJG.js → chunk-N3QDLE2N.js} +5 -5
  39. package/dist/{chunk-2B3BTQN4.js → chunk-N4ZA2I7M.js} +2 -2
  40. package/dist/chunk-NDXJJKT2.js +118 -0
  41. package/dist/chunk-NDXJJKT2.js.map +1 -0
  42. package/dist/{chunk-HP23UD6W.js → chunk-NFWTMM2M.js} +2 -2
  43. package/dist/{chunk-3QLPBCFG.js → chunk-NKP6II6G.js} +4 -4
  44. package/dist/{chunk-HHCMZEUC.js → chunk-OX272PBW.js} +2 -2
  45. package/dist/{chunk-YU4CLTZY.js → chunk-QPTP6SKC.js} +2 -2
  46. package/dist/{chunk-6S2C5FQ4.js → chunk-R3K4CXPR.js} +2 -2
  47. package/dist/{chunk-XDFWMY4X.js → chunk-SFNQDXXL.js} +108 -14
  48. package/dist/chunk-SFNQDXXL.js.map +1 -0
  49. package/dist/{chunk-MB5RSUW6.js → chunk-SXN3LCUT.js} +114 -28
  50. package/dist/chunk-SXN3LCUT.js.map +1 -0
  51. package/dist/{chunk-3CBSKDRL.js → chunk-TIOXQPD7.js} +9 -9
  52. package/dist/{chunk-CXW6Y7SI.js → chunk-V5RVMULT.js} +4 -4
  53. package/dist/{chunk-DUU4VLOW.js → chunk-VKHZFF5P.js} +2 -2
  54. package/dist/{chunk-NPZTEHR7.js → chunk-X7HTU4HO.js} +2 -2
  55. package/dist/{chunk-ZAIU7ZEA.js → chunk-YDYC6PIS.js} +2 -2
  56. package/dist/cli.js +34 -33
  57. package/dist/compounding/engine.js +5 -4
  58. package/dist/connectors/codex-materialize-runner.js +5 -4
  59. package/dist/connectors/index.js +5 -4
  60. package/dist/entity-retrieval.js +5 -4
  61. package/dist/{forget-OEE5OELK.js → forget-WYLR4ZFV.js} +2 -2
  62. package/dist/index.js +47 -46
  63. package/dist/lcm/engine.js +2 -2
  64. package/dist/lcm/index.js +4 -4
  65. package/dist/maintenance/memory-governance.js +5 -4
  66. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +11 -1
  67. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -4
  68. package/dist/maintenance/rebuild-memory-projection.d.ts +22 -1
  69. package/dist/maintenance/rebuild-memory-projection.js +6 -5
  70. package/dist/memory-projection-store.d.ts +6 -2
  71. package/dist/memory-projection-store.js +9 -3
  72. package/dist/namespaces/migrate.js +9 -8
  73. package/dist/namespaces/search.js +3 -3
  74. package/dist/namespaces/storage.js +5 -4
  75. package/dist/operator-toolkit.js +12 -11
  76. package/dist/orchestration/maintenance.js +7 -6
  77. package/dist/orchestrator.js +45 -44
  78. package/dist/{purge-6ATBGT77.js → purge-IHRX4Z7X.js} +7 -1
  79. package/dist/purge-IHRX4Z7X.js.map +1 -0
  80. package/dist/schemas.d.ts +22 -22
  81. package/dist/search/factory.js +2 -2
  82. package/dist/search/index.js +4 -4
  83. package/dist/semantic-consolidation.js +6 -5
  84. package/dist/semantic-rule-promotion.js +5 -4
  85. package/dist/semantic-rule-verifier.js +5 -4
  86. package/dist/storage-guards.d.ts +9 -0
  87. package/dist/storage-guards.js +11 -0
  88. package/dist/storage-guards.js.map +1 -0
  89. package/dist/storage.js +4 -3
  90. package/dist/transfer/import-sqlite.js +2 -2
  91. package/dist/transfer/types.d.ts +12 -12
  92. package/dist/verified-recall.js +5 -4
  93. package/package.json +2 -2
  94. package/src/maintenance/purge.test.ts +76 -0
  95. package/src/maintenance/purge.ts +3 -0
  96. package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +59 -8
  97. package/src/maintenance/rebuild-memory-projection.ts +154 -10
  98. package/src/memory-projection-store.ts +143 -35
  99. package/src/storage-guards.ts +30 -0
  100. package/src/storage.ts +16 -16
  101. package/dist/chunk-3DAIQSVS.js.map +0 -1
  102. package/dist/chunk-GYJESQS5.js +0 -77
  103. package/dist/chunk-GYJESQS5.js.map +0 -1
  104. package/dist/chunk-MB5RSUW6.js.map +0 -1
  105. package/dist/chunk-XDFWMY4X.js.map +0 -1
  106. package/dist/purge-6ATBGT77.js.map +0 -1
  107. /package/dist/{chunk-742MWWVL.js.map → chunk-2RLECZ6J.js.map} +0 -0
  108. /package/dist/{chunk-CFHDI5CV.js.map → chunk-6YEENRE3.js.map} +0 -0
  109. /package/dist/{chunk-JYH3XYWM.js.map → chunk-6ZFE2TPO.js.map} +0 -0
  110. /package/dist/{chunk-LG2MCUBC.js.map → chunk-A4FMQIST.js.map} +0 -0
  111. /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
  112. /package/dist/{chunk-OCPVA4TH.js.map → chunk-AS2NRB45.js.map} +0 -0
  113. /package/dist/{chunk-W4ADKEIY.js.map → chunk-BAF7MBEH.js.map} +0 -0
  114. /package/dist/{chunk-ICVNVSKN.js.map → chunk-BRYPUVJU.js.map} +0 -0
  115. /package/dist/{chunk-IV4INTAU.js.map → chunk-CVINPTKG.js.map} +0 -0
  116. /package/dist/{chunk-IRFHFRHP.js.map → chunk-D6YTILJO.js.map} +0 -0
  117. /package/dist/{chunk-P7UA66OG.js.map → chunk-IVFIYAIP.js.map} +0 -0
  118. /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
  119. /package/dist/{chunk-Y3XEN5TL.js.map → chunk-K4EDULCP.js.map} +0 -0
  120. /package/dist/{chunk-P7N2DQLC.js.map → chunk-KXG7HHIQ.js.map} +0 -0
  121. /package/dist/{chunk-4WOXD56B.js.map → chunk-LDCFFJ67.js.map} +0 -0
  122. /package/dist/{chunk-5AZQZFJG.js.map → chunk-N3QDLE2N.js.map} +0 -0
  123. /package/dist/{chunk-2B3BTQN4.js.map → chunk-N4ZA2I7M.js.map} +0 -0
  124. /package/dist/{chunk-HP23UD6W.js.map → chunk-NFWTMM2M.js.map} +0 -0
  125. /package/dist/{chunk-3QLPBCFG.js.map → chunk-NKP6II6G.js.map} +0 -0
  126. /package/dist/{chunk-HHCMZEUC.js.map → chunk-OX272PBW.js.map} +0 -0
  127. /package/dist/{chunk-YU4CLTZY.js.map → chunk-QPTP6SKC.js.map} +0 -0
  128. /package/dist/{chunk-6S2C5FQ4.js.map → chunk-R3K4CXPR.js.map} +0 -0
  129. /package/dist/{chunk-3CBSKDRL.js.map → chunk-TIOXQPD7.js.map} +0 -0
  130. /package/dist/{chunk-CXW6Y7SI.js.map → chunk-V5RVMULT.js.map} +0 -0
  131. /package/dist/{chunk-DUU4VLOW.js.map → chunk-VKHZFF5P.js.map} +0 -0
  132. /package/dist/{chunk-NPZTEHR7.js.map → chunk-X7HTU4HO.js.map} +0 -0
  133. /package/dist/{chunk-ZAIU7ZEA.js.map → chunk-YDYC6PIS.js.map} +0 -0
  134. /package/dist/{forget-OEE5OELK.js.map → forget-WYLR4ZFV.js.map} +0 -0
@@ -17,7 +17,7 @@ import {
17
17
  type BetterSqlite3Database,
18
18
  } from "./runtime/better-sqlite.js";
19
19
 
20
- export const MEMORY_PROJECTION_SCHEMA_VERSION = 2;
20
+ export const MEMORY_PROJECTION_SCHEMA_VERSION = 3;
21
21
 
22
22
  export interface ProjectedMemoryBrowseOptions {
23
23
  query?: string;
@@ -153,11 +153,13 @@ function migrateProjectionSchemaIfNeeded(memoryDir: string): void {
153
153
  export function memoryCurrentSelectExpressions(db: BetterSqlite3Database): {
154
154
  tagsJson: string;
155
155
  previewText: string;
156
+ hasPathValid: boolean;
156
157
  } {
157
158
  const columns = listTableColumns(db, "memory_current");
158
159
  return {
159
160
  tagsJson: columns.has("tags_json") ? "tags_json" : `'[]' AS tags_json`,
160
161
  previewText: columns.has("preview_text") ? "preview_text" : `'' AS preview_text`,
162
+ hasPathValid: columns.has("path_valid"),
161
163
  };
162
164
  }
163
165
 
@@ -174,6 +176,7 @@ export function initializeMemoryProjectionDb(db: BetterSqlite3Database): void {
174
176
  status TEXT NOT NULL,
175
177
  lifecycle_state TEXT,
176
178
  path_rel TEXT NOT NULL,
179
+ path_valid INTEGER NOT NULL DEFAULT 0 CHECK (path_valid IN (0, 1)),
177
180
  created_at TEXT NOT NULL,
178
181
  updated_at TEXT NOT NULL,
179
182
  archived_at TEXT,
@@ -195,8 +198,6 @@ export function initializeMemoryProjectionDb(db: BetterSqlite3Database): void {
195
198
  CREATE INDEX IF NOT EXISTS idx_memory_current_category
196
199
  ON memory_current(category);
197
200
 
198
- CREATE INDEX IF NOT EXISTS idx_memory_current_updated
199
- ON memory_current(updated_at DESC);
200
201
 
201
202
  CREATE TABLE IF NOT EXISTS memory_timeline (
202
203
  event_id TEXT PRIMARY KEY,
@@ -297,6 +298,18 @@ export function initializeMemoryProjectionDb(db: BetterSqlite3Database): void {
297
298
  `);
298
299
 
299
300
  migrateMemoryCurrentTable(db);
301
+ if (listTableColumns(db, "memory_current").has("path_valid")) {
302
+ db.exec(`
303
+ CREATE INDEX IF NOT EXISTS idx_memory_current_browse_updated_desc
304
+ ON memory_current(path_valid, updated_at DESC, created_at DESC, memory_id ASC);
305
+ CREATE INDEX IF NOT EXISTS idx_memory_current_browse_updated_asc
306
+ ON memory_current(path_valid, updated_at ASC, created_at ASC, memory_id ASC);
307
+ CREATE INDEX IF NOT EXISTS idx_memory_current_browse_created_desc
308
+ ON memory_current(path_valid, created_at DESC, updated_at DESC, memory_id ASC);
309
+ CREATE INDEX IF NOT EXISTS idx_memory_current_browse_created_asc
310
+ ON memory_current(path_valid, created_at ASC, updated_at ASC, memory_id ASC);
311
+ `);
312
+ }
300
313
  db.prepare("INSERT OR REPLACE INTO meta(key, value) VALUES (?, ?)")
301
314
  .run("schemaVersion", String(MEMORY_PROJECTION_SCHEMA_VERSION));
302
315
  }
@@ -309,6 +322,42 @@ function openProjectionReadonly(memoryDir: string): BetterSqlite3Database | null
309
322
  return null;
310
323
  }
311
324
  }
325
+ function updateProjectionBestEffort(
326
+ memoryDir: string,
327
+ sql: string,
328
+ params: unknown[],
329
+ ): void {
330
+ let db: BetterSqlite3Database | null = null;
331
+ try {
332
+ db = openBetterSqlite3(getMemoryProjectionPath(memoryDir), { fileMustExist: true });
333
+ db.prepare(sql).run(...params);
334
+ } catch {
335
+ // Projection updates must never block the canonical filesystem mutation.
336
+ } finally {
337
+ db?.close();
338
+ }
339
+ }
340
+
341
+ export function markProjectedMemoryPathInvalid(memoryDir: string, memoryId: string): void {
342
+ updateProjectionBestEffort(
343
+ memoryDir,
344
+ "UPDATE memory_current SET path_valid = 0 WHERE memory_id = ?",
345
+ [memoryId],
346
+ );
347
+ }
348
+
349
+ export function updateProjectedMemoryPath(
350
+ memoryDir: string,
351
+ memoryId: string,
352
+ pathRel: string,
353
+ ): void {
354
+ const pathValid = isProjectedMemoryPathValid(memoryDir, pathRel) ? 1 : 0;
355
+ updateProjectionBestEffort(
356
+ memoryDir,
357
+ "UPDATE memory_current SET path_rel = ?, path_valid = ? WHERE memory_id = ?",
358
+ [pathRel, pathValid, memoryId],
359
+ );
360
+ }
312
361
 
313
362
  function withProjectionReadonly<T>(
314
363
  memoryDir: string,
@@ -363,7 +412,11 @@ function isPathInsideRoot(candidatePath: string, rootPath: string): boolean {
363
412
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
364
413
  }
365
414
 
366
- function resolveProjectedMemoryPath(memoryDir: string, pathRel: string): string | null {
415
+ function resolveProjectedMemoryPath(
416
+ memoryDir: string,
417
+ pathRel: string,
418
+ requireExisting = false,
419
+ ): string | null {
367
420
  if (path.isAbsolute(pathRel)) return null;
368
421
  const root = resolveMemoryDirRoot(memoryDir);
369
422
  const candidate = path.resolve(root, pathRel);
@@ -372,14 +425,21 @@ function resolveProjectedMemoryPath(memoryDir: string, pathRel: string): string
372
425
  const realCandidate = fs.realpathSync(candidate);
373
426
  if (!isPathInsideRoot(realCandidate, root)) return null;
374
427
  } catch {
428
+ if (requireExisting) return null;
375
429
  // Missing files are handled by callers. Still keep the lexical guard above.
376
430
  }
377
431
  return candidate;
378
432
  }
379
433
 
434
+ export function isProjectedMemoryPathValid(memoryDir: string, pathRel: string): boolean {
435
+ return resolveProjectedMemoryPath(memoryDir, pathRel) !== null;
436
+ }
437
+
380
438
  function projectedBrowseRowFromCurrentRow(
381
439
  memoryDir: string,
382
440
  row: Record<string, unknown>,
441
+ resolvedFilePath?: string,
442
+ requireExisting = false,
383
443
  ): ProjectedMemoryBrowseRow | null {
384
444
  if (
385
445
  typeof row.memory_id !== "string" ||
@@ -389,7 +449,7 @@ function projectedBrowseRowFromCurrentRow(
389
449
  ) {
390
450
  return null;
391
451
  }
392
- const filePath = resolveProjectedMemoryPath(memoryDir, row.path_rel);
452
+ const filePath = resolvedFilePath ?? resolveProjectedMemoryPath(memoryDir, row.path_rel, requireExisting);
393
453
  if (!filePath) return null;
394
454
  return {
395
455
  id: row.memory_id,
@@ -613,8 +673,15 @@ export function readProjectedMemoryBrowse(
613
673
  const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
614
674
 
615
675
  if (normalizedQuery) {
616
- // Query-based browse: fetch all matching rows, filter by full file content, then paginate in JS
617
- const allRows = db
676
+ // Full-content search still examines every eligible row so totals and
677
+ // matches stay exact, but iteration keeps memory use proportional to the page.
678
+ const queryWhereClauses = currentSelect.hasPathValid
679
+ ? [...whereClauses, ...projectedBrowsePathSqlClauses(), "path_valid = 1"]
680
+ : whereClauses;
681
+ const queryWhereSql = queryWhereClauses.length > 0
682
+ ? `WHERE ${queryWhereClauses.join(" AND ")}`
683
+ : "";
684
+ const rows = db
618
685
  .prepare(`
619
686
  SELECT
620
687
  memory_id,
@@ -627,47 +694,88 @@ export function readProjectedMemoryBrowse(
627
694
  ${currentSelect.tagsJson},
628
695
  ${currentSelect.previewText}
629
696
  FROM memory_current
630
- ${whereSql}
697
+ ${queryWhereSql}
631
698
  ORDER BY ${orderBySql}
632
699
  `)
633
- .all(...params) as Array<Record<string, unknown>>;
700
+ .iterate(...params) as Iterable<Record<string, unknown>>;
701
+ const pageRows: ProjectedMemoryBrowseRow[] = [];
702
+ let total = 0;
634
703
 
635
- const filtered = allRows.filter((row) => {
636
- if (typeof row.memory_id !== "string" || typeof row.path_rel !== "string") return false;
637
- const filePath = resolveProjectedMemoryPath(memoryDir, row.path_rel);
638
- if (!filePath) return false;
639
- // Check preview, category, entity_ref, tags first (fast)
704
+ for (const row of rows) {
705
+ if (typeof row.memory_id !== "string" || typeof row.path_rel !== "string") continue;
706
+ const filePath = resolveProjectedMemoryPath(
707
+ memoryDir,
708
+ row.path_rel,
709
+ currentSelect.hasPathValid,
710
+ );
711
+ if (!filePath) continue;
640
712
  const preview = typeof row.preview_text === "string" ? row.preview_text.toLowerCase() : "";
641
713
  const category = typeof row.category === "string" ? row.category.toLowerCase() : "";
642
714
  const entityRef = typeof row.entity_ref === "string" ? row.entity_ref.toLowerCase() : "";
643
715
  const tags = typeof row.tags_json === "string" ? row.tags_json.toLowerCase() : "";
644
- if (preview.includes(normalizedQuery) || category.includes(normalizedQuery) ||
645
- entityRef.includes(normalizedQuery) || tags.includes(normalizedQuery)) {
646
- return true;
716
+ let matches = preview.includes(normalizedQuery) || category.includes(normalizedQuery) ||
717
+ entityRef.includes(normalizedQuery) || tags.includes(normalizedQuery);
718
+ if (!matches) {
719
+ try {
720
+ matches = fs.readFileSync(filePath, "utf-8").toLowerCase().includes(normalizedQuery);
721
+ } catch {
722
+ matches = false;
723
+ }
647
724
  }
648
- // Fall back to reading full file content from disk
649
- try {
650
- const content = fs.readFileSync(filePath, "utf-8").toLowerCase();
651
- return content.includes(normalizedQuery);
652
- } catch {
653
- return false;
725
+ if (!matches) continue;
726
+ if (total >= options.offset && pageRows.length < options.limit) {
727
+ const browseRow = projectedBrowseRowFromCurrentRow(memoryDir, row, filePath);
728
+ if (browseRow) pageRows.push(browseRow);
654
729
  }
655
- });
656
-
657
- const pageRows = filtered.slice(options.offset, options.offset + options.limit);
658
- return {
659
- total: filtered.length,
660
- memories: pageRows
661
- .map((row) => projectedBrowseRowFromCurrentRow(memoryDir, row))
662
- .filter((row): row is ProjectedMemoryBrowseRow => row !== null),
663
- };
730
+ total += 1;
731
+ }
732
+
733
+ return { total, memories: pageRows };
664
734
  }
665
735
 
666
- // No query: push lexical path safety into SQL, then count through the same
667
- // realpath-aware row parser used for returned rows so symlink escapes do not
668
- // inflate totals.
669
736
  const browseWhereClauses = [...whereClauses, ...projectedBrowsePathSqlClauses()];
737
+ if (currentSelect.hasPathValid) {
738
+ browseWhereClauses.push("path_valid = 1");
739
+ }
670
740
  const browseWhereSql = `WHERE ${browseWhereClauses.join(" AND ")}`;
741
+
742
+ if (currentSelect.hasPathValid) {
743
+ const rows = db
744
+ .prepare(`
745
+ SELECT
746
+ memory_id,
747
+ path_rel,
748
+ category,
749
+ status,
750
+ created_at,
751
+ updated_at,
752
+ entity_ref,
753
+ ${currentSelect.tagsJson},
754
+ ${currentSelect.previewText}
755
+ FROM memory_current
756
+ ${browseWhereSql}
757
+ ORDER BY ${orderBySql}
758
+ `)
759
+ .iterate(...params) as Iterable<Record<string, unknown>>;
760
+ const pageRows: ProjectedMemoryBrowseRow[] = [];
761
+ let validRowsSeen = 0;
762
+ for (const row of rows) {
763
+ const browseRow = projectedBrowseRowFromCurrentRow(memoryDir, row, undefined, true);
764
+ if (!browseRow) continue;
765
+ if (validRowsSeen >= options.offset) {
766
+ pageRows.push(browseRow);
767
+ if (pageRows.length >= options.limit) break;
768
+ }
769
+ validRowsSeen += 1;
770
+ }
771
+ const totalRow = db
772
+ .prepare(`SELECT COUNT(*) AS total FROM memory_current ${browseWhereSql}`)
773
+ .get(...params) as { total: number };
774
+ return { total: totalRow.total, memories: pageRows };
775
+ }
776
+
777
+ // Legacy projections have no materialized path validity. Preserve the
778
+ // realpath-aware full scan rather than trusting rows from an older writer.
671
779
  const pageRows: ProjectedMemoryBrowseRow[] = [];
672
780
  const fetchSize = Math.max(options.limit * 2, 50);
673
781
  let validRowsSeen = 0;
@@ -0,0 +1,30 @@
1
+ import { log } from "./logger.js";
2
+
3
+ const PROJECTION_FALLBACK_WARN_INTERVAL_MS = 5 * 60_000;
4
+ const projectionFallbackWarnedAt = new Map<string, number>();
5
+
6
+ export function assertMemoryFrontmatterId(
7
+ fm: { id: string; category?: string; created?: string; entityRef?: string },
8
+ ): void {
9
+ if (typeof fm.id !== "string" || fm.id.trim().length === 0) {
10
+ const context = [
11
+ fm.category ? `category=${fm.category}` : null,
12
+ fm.created ? `created=${fm.created}` : null,
13
+ fm.entityRef ? `entityRef=${fm.entityRef}` : null,
14
+ ].filter(Boolean).join(" ");
15
+ throw new Error(
16
+ `memory frontmatter id must not be blank${context ? ` (${context})` : ""}`,
17
+ );
18
+ }
19
+ }
20
+
21
+ export function warnProjectionFallback(memoryDir: string, consumer: string): null {
22
+ const key = `${memoryDir}\0${consumer}`;
23
+ const now = Date.now();
24
+ const warnedAt = projectionFallbackWarnedAt.get(key) ?? 0;
25
+ if (now - warnedAt >= PROJECTION_FALLBACK_WARN_INTERVAL_MS) {
26
+ projectionFallbackWarnedAt.set(key, now);
27
+ log.warn(`storage.${consumer}: memory projection absent or empty; falling back to full corpus`);
28
+ }
29
+ return null;
30
+ }
package/src/storage.ts CHANGED
@@ -4,6 +4,7 @@ import { createHash } from "node:crypto"
4
4
  import { normalizeContent, computeContentHash } from "./content-hash.js";;
5
5
  import path from "node:path";
6
6
  import { log } from "./logger.js";
7
+ import { assertMemoryFrontmatterId, warnProjectionFallback } from "./storage-guards.js";
7
8
  import { MemoryReadStore } from "./storage/memory-read-store.js";
8
9
  import {
9
10
  readMaybeEncryptedLines,
@@ -102,11 +103,11 @@ import type {
102
103
  import { confidenceTier, SPECULATIVE_TTL_DAYS } from "./types.js";
103
104
  import {
104
105
  type ProjectedMemoryBrowseOptions,
105
- type ProjectedMemoryBrowsePage,
106
+ type ProjectedMemoryBrowsePage, markProjectedMemoryPathInvalid,
106
107
  readProjectedMemoryState,
107
108
  readProjectedMemoryBrowse,
108
109
  readProjectedGovernanceRecord,
109
- readProjectedMemoryTimeline,
110
+ readProjectedMemoryTimeline, updateProjectedMemoryPath,
110
111
  } from "./memory-projection-store.js";
111
112
  import {
112
113
  inferMemoryStatus,
@@ -308,6 +309,7 @@ function stripCitationMarkersForHashRemoval(value: string, template: string): st
308
309
  }
309
310
 
310
311
  function serializeFrontmatter(fm: MemoryFrontmatter): string {
312
+ assertMemoryFrontmatterId(fm);
311
313
  const lines = [
312
314
  "---",
313
315
  `id: ${fm.id}`,
@@ -4564,14 +4566,12 @@ export class StorageManager {
4564
4566
  const dir = categoryDirName(memory.frontmatter.category);
4565
4567
  return path.join(root, dir, this.resolveMemoryDateDir(memory), `${memory.frontmatter.id}.md`);
4566
4568
  }
4567
-
4568
4569
  private async writeMemoryFileAtomic(targetPath: string, memory: MemoryFile): Promise<void> {
4569
4570
  const fileContent = `${serializeFrontmatter(memory.frontmatter)}\n\n${memory.content}\n`;
4570
4571
  await writeMaybeEncryptedFile(targetPath, fileContent, this.resolveWriteKey(), {}, this.baseDir);
4571
4572
  this.invalidateAllMemoriesCache();
4572
4573
  this.notifyCatalogWrite();
4573
4574
  }
4574
-
4575
4575
  async moveMemoryToPath(memory: MemoryFile, targetPath: string): Promise<void> {
4576
4576
  await this.writeMemoryFileAtomic(targetPath, memory);
4577
4577
  const sourcePath = path.resolve(memory.path);
@@ -4589,6 +4589,7 @@ export class StorageManager {
4589
4589
  // invalidated, but a concurrent readAllMemories() may have re-populated
4590
4590
  // the cache between the write and the unlink.
4591
4591
  this.invalidateAllMemoriesCache();
4592
+ updateProjectedMemoryPath(this.baseDir, memory.frontmatter.id, toMemoryPathRel(this.baseDir, targetPath));
4592
4593
  }
4593
4594
  }
4594
4595
 
@@ -4613,6 +4614,7 @@ export class StorageManager {
4613
4614
  throw err;
4614
4615
  }
4615
4616
  }
4617
+ updateProjectedMemoryPath(this.baseDir, memory.frontmatter.id, toMemoryPathRel(this.baseDir, targetPath));
4616
4618
  this.bumpMemoryStatusVersion();
4617
4619
  return { changed: false, targetPath };
4618
4620
  }
@@ -4660,7 +4662,7 @@ export class StorageManager {
4660
4662
 
4661
4663
  // Write to archive location first (encrypted if applicable), then remove original.
4662
4664
  await this.writeStorageSecureFile(destPath, fileContent);
4663
- await unlink(memory.path);
4665
+ await unlink(memory.path); markProjectedMemoryPathInvalid(this.baseDir, memory.frontmatter.id);
4664
4666
  this.invalidateAllMemoriesCache();
4665
4667
  await this.appendGeneratedMemoryLifecycleEventFailOpen(
4666
4668
  "storage.archiveMemory",
@@ -4765,7 +4767,7 @@ export class StorageManager {
4765
4767
  if (!memory) return false;
4766
4768
 
4767
4769
  try {
4768
- await unlink(memory.path);
4770
+ await unlink(memory.path); markProjectedMemoryPathInvalid(this.baseDir, id);
4769
4771
  this.invalidateAllMemoriesCache();
4770
4772
  this.bumpMemoryStatusVersion();
4771
4773
  log.debug(`invalidated memory ${id}`);
@@ -4904,7 +4906,7 @@ export class StorageManager {
4904
4906
  const expiresAt = new Date(m.frontmatter.expiresAt).getTime();
4905
4907
  if (expiresAt < now) {
4906
4908
  try {
4907
- await unlink(m.path);
4909
+ await unlink(m.path); markProjectedMemoryPathInvalid(this.baseDir, m.frontmatter.id);
4908
4910
  deleted.push(m);
4909
4911
  log.debug(`cleaned expired memory ${m.frontmatter.id} (TTL expired)`);
4910
4912
  } catch {
@@ -5979,7 +5981,7 @@ export class StorageManager {
5979
5981
  if (updatedAt < cutoff) {
5980
5982
  // Remove the file
5981
5983
  try {
5982
- await unlink(m.path);
5984
+ await unlink(m.path); markProjectedMemoryPathInvalid(this.baseDir, m.frontmatter.id);
5983
5985
  deleted.push(m);
5984
5986
  log.debug(`cleaned expired commitment ${m.frontmatter.id}`);
5985
5987
  } catch {
@@ -6020,8 +6022,8 @@ export class StorageManager {
6020
6022
  lastAccessed: entry.lastAccessed,
6021
6023
  };
6022
6024
 
6023
- const fileContent = `${serializeFrontmatter(newFm)}\n\n${memory.content}\n`;
6024
6025
  try {
6026
+ const fileContent = `${serializeFrontmatter(newFm)}\n\n${memory.content}\n`;
6025
6027
  await this.writeStorageSecureFile(memory.path, fileContent);
6026
6028
  updated++;
6027
6029
  } catch (err) {
@@ -6070,8 +6072,7 @@ export class StorageManager {
6070
6072
 
6071
6073
  async getProjectedMemoryState(id: string): Promise<MemoryProjectionCurrentState | null> {
6072
6074
  const projected = readProjectedMemoryState(this.baseDir, id);
6073
- if (projected) return projected;
6074
-
6075
+ if (projected) return projected; warnProjectionFallback(this.baseDir, "getProjectedMemoryState");
6075
6076
  const active = await this.getMemoryById(id);
6076
6077
  if (active) return this.toProjectedCurrentState(active, "active");
6077
6078
 
@@ -6084,7 +6085,8 @@ export class StorageManager {
6084
6085
  async browseProjectedMemories(
6085
6086
  options: ProjectedMemoryBrowseOptions,
6086
6087
  ): Promise<ProjectedMemoryBrowsePage | null> {
6087
- return readProjectedMemoryBrowse(this.baseDir, options);
6088
+ return readProjectedMemoryBrowse(this.baseDir, options)
6089
+ ?? warnProjectionFallback(this.baseDir, "browseProjectedMemories");
6088
6090
  }
6089
6091
 
6090
6092
  async getProjectedGovernanceRecord(): Promise<ReturnType<typeof readProjectedGovernanceRecord>> {
@@ -6124,8 +6126,7 @@ export class StorageManager {
6124
6126
  if (cappedLimit === 0) return [];
6125
6127
 
6126
6128
  const projected = readProjectedMemoryTimeline(this.baseDir, memoryId, cappedLimit);
6127
- if (projected && projected.length > 0) return projected;
6128
-
6129
+ if (projected && projected.length > 0) return projected; warnProjectionFallback(this.baseDir, "getMemoryTimeline");
6129
6130
  const events = await this.readAllMemoryLifecycleEvents();
6130
6131
  return events.filter((event) => event.memoryId === memoryId).slice(-cappedLimit);
6131
6132
  }
@@ -6382,9 +6383,8 @@ export class StorageManager {
6382
6383
  updated: now,
6383
6384
  };
6384
6385
 
6385
- const fileContent = `${serializeFrontmatter(updatedFm)}\n\n${memory.content}\n`;
6386
-
6387
6386
  try {
6387
+ const fileContent = `${serializeFrontmatter(updatedFm)}\n\n${memory.content}\n`;
6388
6388
  await this.writeStorageSecureFile(memory.path, fileContent);
6389
6389
  await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.archiveMemories", {
6390
6390
  memoryId: id,