@remnic/core 9.3.760 → 9.3.762

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 (160) hide show
  1. package/dist/access-admin-ops-surface.d.ts +3 -3
  2. package/dist/access-admin-ops-surface.js +11 -10
  3. package/dist/access-boundary.d.ts +3 -3
  4. package/dist/access-boundary.js +12 -11
  5. package/dist/access-cli.js +46 -45
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-http.d.ts +3 -3
  8. package/dist/access-http.js +18 -17
  9. package/dist/access-identity-continuity-surface.d.ts +2 -2
  10. package/dist/access-identity-continuity-surface.js +11 -10
  11. package/dist/access-lcm-surface.d.ts +3 -3
  12. package/dist/access-lcm-surface.js +11 -10
  13. package/dist/access-mcp.d.ts +3 -3
  14. package/dist/access-mcp.js +15 -14
  15. package/dist/access-observe-write-surface.d.ts +3 -3
  16. package/dist/access-observe-write-surface.js +11 -10
  17. package/dist/access-operations-batch.js +13 -12
  18. package/dist/access-operations.d.ts +7 -7
  19. package/dist/access-operations.js +14 -13
  20. package/dist/access-recall-surface.d.ts +3 -3
  21. package/dist/access-recall-surface.js +11 -10
  22. package/dist/access-schema.d.ts +68 -68
  23. package/dist/{access-service-BW-gb3DL.d.ts → access-service-CaWJP_f6.d.ts} +2 -2
  24. package/dist/access-service.d.ts +3 -3
  25. package/dist/access-service.js +11 -10
  26. package/dist/access-surface-catalog.d.ts +3 -3
  27. package/dist/adapters/index.js +4 -4
  28. package/dist/adapters/registry.js +2 -2
  29. package/dist/bootstrap.d.ts +2 -2
  30. package/dist/briefing.js +4 -3
  31. package/dist/{catalog-DaKAFbSJ.d.ts → catalog-bflYNWsM.d.ts} +8 -4
  32. package/dist/causal-consolidation.js +5 -4
  33. package/dist/causal-consolidation.js.map +1 -1
  34. package/dist/{chunk-OLEBPUZ7.js → chunk-2KYXXMTE.js} +6 -6
  35. package/dist/{chunk-6CUUFFU7.js → chunk-2PGB63VU.js} +131 -39
  36. package/dist/chunk-2PGB63VU.js.map +1 -0
  37. package/dist/{chunk-FYJWEGYP.js → chunk-4HYJETVO.js} +2 -2
  38. package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
  39. package/dist/{chunk-W3M34XT4.js → chunk-BAVVI5GP.js} +2 -2
  40. package/dist/{chunk-XQT5GSWU.js → chunk-BRYPUVJU.js} +3 -3
  41. package/dist/{chunk-2XE4F3VZ.js → chunk-DB647OZ4.js} +24 -24
  42. package/dist/{chunk-3JBW3GHN.js → chunk-DPZ5GJMD.js} +2 -2
  43. package/dist/chunk-DQC3ZZYM.js +35 -0
  44. package/dist/chunk-DQC3ZZYM.js.map +1 -0
  45. package/dist/{chunk-SMLMH7S5.js → chunk-EDC7RZCW.js} +24 -24
  46. package/dist/{chunk-ZFXCQPNO.js → chunk-EGGE52UU.js} +2 -2
  47. package/dist/chunk-EGGE52UU.js.map +1 -0
  48. package/dist/{chunk-IOZZG75J.js → chunk-ES3GAMSQ.js} +2 -2
  49. package/dist/{chunk-QUGPXPFJ.js → chunk-ESDBHQY6.js} +5 -5
  50. package/dist/{chunk-ILVF6TIY.js → chunk-HDFS6RWQ.js} +2 -2
  51. package/dist/{chunk-SN2EWIAI.js → chunk-HDYFNXVO.js} +2 -2
  52. package/dist/{chunk-ZEQVS6AZ.js → chunk-HMUEAKSD.js} +2 -2
  53. package/dist/{chunk-P26O6R3Q.js → chunk-IIURIWJG.js} +102 -12
  54. package/dist/chunk-IIURIWJG.js.map +1 -0
  55. package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
  56. package/dist/{chunk-QQJMVF6P.js → chunk-JU6BKZHX.js} +3 -3
  57. package/dist/{chunk-SLAZZXBU.js → chunk-KZWUJC7Y.js} +2 -2
  58. package/dist/{chunk-IZLGHKWA.js → chunk-LAJIXABF.js} +10 -10
  59. package/dist/{chunk-3QLPBCFG.js → chunk-NKP6II6G.js} +4 -4
  60. package/dist/{chunk-WIXQ3F5P.js → chunk-OQ6F3WBX.js} +2 -2
  61. package/dist/{chunk-YP23FQPT.js → chunk-P7W4CDX6.js} +92 -41
  62. package/dist/chunk-P7W4CDX6.js.map +1 -0
  63. package/dist/{chunk-EGQ3AVZV.js → chunk-PNFN7SIN.js} +3 -3
  64. package/dist/{chunk-M2AAYJVI.js → chunk-QPFO54VC.js} +4 -4
  65. package/dist/{chunk-7HWOEYHW.js → chunk-QWIE43BS.js} +2 -2
  66. package/dist/chunk-TEKTCF2V.js +118 -0
  67. package/dist/chunk-TEKTCF2V.js.map +1 -0
  68. package/dist/{chunk-QOM3F6YW.js → chunk-TIWRP5NW.js} +5 -5
  69. package/dist/{chunk-CXW6Y7SI.js → chunk-V5RVMULT.js} +4 -4
  70. package/dist/{chunk-DTSOB2YF.js → chunk-VHYFY3JI.js} +2 -2
  71. package/dist/{chunk-AZSSKOG6.js → chunk-WAIMHMO3.js} +3 -3
  72. package/dist/{chunk-PKRRBRY2.js → chunk-ZTBH2OHD.js} +2 -2
  73. package/dist/{cli-D551dnva.d.ts → cli-CvWpIbMD.d.ts} +2 -2
  74. package/dist/cli.d.ts +4 -4
  75. package/dist/cli.js +35 -34
  76. package/dist/compounding/engine.js +4 -3
  77. package/dist/connectors/codex-materialize-runner.js +4 -3
  78. package/dist/connectors/index.js +4 -3
  79. package/dist/entity-retrieval.js +4 -3
  80. package/dist/explicit-capture.d.ts +2 -2
  81. package/dist/{forget-OEE5OELK.js → forget-WYLR4ZFV.js} +2 -2
  82. package/dist/index.d.ts +5 -5
  83. package/dist/index.js +48 -47
  84. package/dist/lcm/engine.js +2 -2
  85. package/dist/lcm/index.js +4 -4
  86. package/dist/maintenance/memory-governance.js +4 -3
  87. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +11 -1
  88. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -3
  89. package/dist/maintenance/rebuild-memory-projection.d.ts +22 -1
  90. package/dist/maintenance/rebuild-memory-projection.js +5 -4
  91. package/dist/mcp-memory-inspector-app.d.ts +3 -3
  92. package/dist/namespaces/migrate.d.ts +1 -1
  93. package/dist/namespaces/migrate.js +9 -8
  94. package/dist/namespaces/search.js +4 -4
  95. package/dist/namespaces/storage.d.ts +1 -1
  96. package/dist/namespaces/storage.js +5 -4
  97. package/dist/operator-toolkit.js +13 -12
  98. package/dist/orchestration/maintenance.d.ts +1 -1
  99. package/dist/orchestration/maintenance.js +8 -7
  100. package/dist/{orchestrator-BnM187cb.d.ts → orchestrator-C04Q4Zao.d.ts} +1 -1
  101. package/dist/orchestrator.d.ts +2 -2
  102. package/dist/orchestrator.js +46 -45
  103. package/dist/schemas.d.ts +84 -84
  104. package/dist/search/factory.js +2 -2
  105. package/dist/search/index.js +4 -4
  106. package/dist/semantic-consolidation.js +5 -4
  107. package/dist/semantic-rule-promotion.js +4 -3
  108. package/dist/semantic-rule-verifier.js +4 -3
  109. package/dist/shared-context/manager.d.ts +8 -8
  110. package/dist/storage-guards.d.ts +9 -0
  111. package/dist/storage-guards.js +11 -0
  112. package/dist/storage-guards.js.map +1 -0
  113. package/dist/storage.js +3 -2
  114. package/dist/transfer/import-sqlite.js +2 -2
  115. package/dist/transfer/types.d.ts +66 -66
  116. package/dist/verified-recall.js +4 -3
  117. package/package.json +2 -2
  118. package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +59 -8
  119. package/src/maintenance/rebuild-memory-projection.ts +149 -9
  120. package/src/namespaces/catalog.test.ts +133 -1
  121. package/src/namespaces/catalog.ts +163 -48
  122. package/src/namespaces/identity.ts +2 -2
  123. package/src/storage/secure-line-reader.ts +79 -0
  124. package/src/storage-guards.ts +30 -0
  125. package/src/storage-memory-lifecycle-ledger.test.ts +130 -0
  126. package/src/storage.ts +24 -41
  127. package/dist/chunk-6CUUFFU7.js.map +0 -1
  128. package/dist/chunk-LLVKDFNU.js +0 -77
  129. package/dist/chunk-LLVKDFNU.js.map +0 -1
  130. package/dist/chunk-P26O6R3Q.js.map +0 -1
  131. package/dist/chunk-YP23FQPT.js.map +0 -1
  132. package/dist/chunk-ZFXCQPNO.js.map +0 -1
  133. /package/dist/{chunk-OLEBPUZ7.js.map → chunk-2KYXXMTE.js.map} +0 -0
  134. /package/dist/{chunk-FYJWEGYP.js.map → chunk-4HYJETVO.js.map} +0 -0
  135. /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
  136. /package/dist/{chunk-W3M34XT4.js.map → chunk-BAVVI5GP.js.map} +0 -0
  137. /package/dist/{chunk-XQT5GSWU.js.map → chunk-BRYPUVJU.js.map} +0 -0
  138. /package/dist/{chunk-2XE4F3VZ.js.map → chunk-DB647OZ4.js.map} +0 -0
  139. /package/dist/{chunk-3JBW3GHN.js.map → chunk-DPZ5GJMD.js.map} +0 -0
  140. /package/dist/{chunk-SMLMH7S5.js.map → chunk-EDC7RZCW.js.map} +0 -0
  141. /package/dist/{chunk-IOZZG75J.js.map → chunk-ES3GAMSQ.js.map} +0 -0
  142. /package/dist/{chunk-QUGPXPFJ.js.map → chunk-ESDBHQY6.js.map} +0 -0
  143. /package/dist/{chunk-ILVF6TIY.js.map → chunk-HDFS6RWQ.js.map} +0 -0
  144. /package/dist/{chunk-SN2EWIAI.js.map → chunk-HDYFNXVO.js.map} +0 -0
  145. /package/dist/{chunk-ZEQVS6AZ.js.map → chunk-HMUEAKSD.js.map} +0 -0
  146. /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
  147. /package/dist/{chunk-QQJMVF6P.js.map → chunk-JU6BKZHX.js.map} +0 -0
  148. /package/dist/{chunk-SLAZZXBU.js.map → chunk-KZWUJC7Y.js.map} +0 -0
  149. /package/dist/{chunk-IZLGHKWA.js.map → chunk-LAJIXABF.js.map} +0 -0
  150. /package/dist/{chunk-3QLPBCFG.js.map → chunk-NKP6II6G.js.map} +0 -0
  151. /package/dist/{chunk-WIXQ3F5P.js.map → chunk-OQ6F3WBX.js.map} +0 -0
  152. /package/dist/{chunk-EGQ3AVZV.js.map → chunk-PNFN7SIN.js.map} +0 -0
  153. /package/dist/{chunk-M2AAYJVI.js.map → chunk-QPFO54VC.js.map} +0 -0
  154. /package/dist/{chunk-7HWOEYHW.js.map → chunk-QWIE43BS.js.map} +0 -0
  155. /package/dist/{chunk-QOM3F6YW.js.map → chunk-TIWRP5NW.js.map} +0 -0
  156. /package/dist/{chunk-CXW6Y7SI.js.map → chunk-V5RVMULT.js.map} +0 -0
  157. /package/dist/{chunk-DTSOB2YF.js.map → chunk-VHYFY3JI.js.map} +0 -0
  158. /package/dist/{chunk-AZSSKOG6.js.map → chunk-WAIMHMO3.js.map} +0 -0
  159. /package/dist/{chunk-PKRRBRY2.js.map → chunk-ZTBH2OHD.js.map} +0 -0
  160. /package/dist/{forget-OEE5OELK.js.map → forget-WYLR4ZFV.js.map} +0 -0
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { stat } from "node:fs/promises";
3
3
  import { StorageManager } from "../storage.js";
4
+ import { log } from "../logger.js";
4
5
  import type { MemoryLifecycleEvent } from "../types.js";
5
6
  import { toBackupStamp } from "./backup-stamp.js";
6
7
  import { writeFileAtomically } from "./atomic-file.js";
@@ -15,12 +16,24 @@ export interface RebuildMemoryLifecycleLedgerOptions {
15
16
  now?: Date;
16
17
  }
17
18
 
19
+ export interface SkippedLifecycleBlankIdMemory {
20
+ path: string;
21
+ }
22
+
23
+ export interface SkippedDuplicateLifecycleEvent {
24
+ eventId: string;
25
+ keptPath: string;
26
+ skippedPath: string;
27
+ }
28
+
18
29
  export interface RebuildMemoryLifecycleLedgerResult {
19
30
  dryRun: boolean;
20
31
  scannedMemories: number;
21
32
  rebuiltRows: number;
22
33
  outputPath: string;
23
34
  backupPath?: string;
35
+ skippedBlankIdMemories: SkippedLifecycleBlankIdMemory[];
36
+ skippedDuplicateEvents: SkippedDuplicateLifecycleEvent[];
24
37
  }
25
38
 
26
39
  function isNodeError(err: unknown): err is NodeJS.ErrnoException {
@@ -59,16 +72,52 @@ export async function rebuildMemoryLifecycleLedger(
59
72
  const now = options.now ?? new Date();
60
73
  const outputPath = path.join(options.memoryDir, "state", "memory-lifecycle-ledger.jsonl");
61
74
  const storage = new StorageManager(options.memoryDir);
62
- const allMemories = [
63
- ...await storage.readAllMemories(),
64
- ...await storage.readAllColdMemories(),
65
- ...await storage.readArchivedMemories(),
66
- ]
67
- .sort((a, b) => a.frontmatter.id.localeCompare(b.frontmatter.id));
75
+ const tiers = [
76
+ await storage.readAllMemories(),
77
+ await storage.readAllColdMemories(),
78
+ await storage.readArchivedMemories(),
79
+ ];
80
+ const allMemories = tiers.flat();
81
+ const skippedBlankIdMemories: SkippedLifecycleBlankIdMemory[] = [];
82
+ const eventCandidates: Array<{ event: MemoryLifecycleEvent; path: string }> = [];
83
+ for (const tier of tiers) {
84
+ for (const memory of [...tier].sort((left, right) => left.path.localeCompare(right.path))) {
85
+ if (!memory.frontmatter.id.trim()) {
86
+ skippedBlankIdMemories.push({ path: memory.path });
87
+ log.warn(`lifecycle ledger rebuild skipped blank memory id: path=${memory.path}`);
88
+ continue;
89
+ }
90
+ for (const event of buildLifecycleEventsForMemory(memory)) {
91
+ eventCandidates.push({ event, path: memory.path });
92
+ }
93
+ }
94
+ }
68
95
 
69
- const events: MemoryLifecycleEvent[] = sortMemoryLifecycleEvents(
70
- allMemories.flatMap((memory) => buildLifecycleEventsForMemory(memory)),
96
+ const orderedEvents = sortMemoryLifecycleEvents(eventCandidates.map((candidate) => candidate.event));
97
+ const candidateByEvent = new Map(
98
+ eventCandidates.map((candidate) => [candidate.event, candidate]),
71
99
  );
100
+ const keptByEventId = new Map<string, { event: MemoryLifecycleEvent; path: string }>();
101
+ const events: MemoryLifecycleEvent[] = [];
102
+ const skippedDuplicateEvents: SkippedDuplicateLifecycleEvent[] = [];
103
+ for (const event of orderedEvents) {
104
+ const candidate = candidateByEvent.get(event)!;
105
+ const kept = keptByEventId.get(event.eventId);
106
+ if (kept) {
107
+ skippedDuplicateEvents.push({
108
+ eventId: event.eventId,
109
+ keptPath: kept.path,
110
+ skippedPath: candidate.path,
111
+ });
112
+ log.warn(
113
+ `lifecycle ledger rebuild skipped duplicate event_id=${event.eventId}: `
114
+ + `kept=${kept.path} skipped=${candidate.path}`,
115
+ );
116
+ continue;
117
+ }
118
+ keptByEventId.set(event.eventId, candidate);
119
+ events.push(event);
120
+ }
72
121
 
73
122
  let backupPath: string | undefined;
74
123
  if (!dryRun) {
@@ -87,5 +136,7 @@ export async function rebuildMemoryLifecycleLedger(
87
136
  rebuiltRows: events.length,
88
137
  outputPath,
89
138
  backupPath,
139
+ skippedBlankIdMemories,
140
+ skippedDuplicateEvents,
90
141
  };
91
142
  }
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { mkdir, rm, stat } from "node:fs/promises";
3
3
  import { StorageManager } from "../storage.js";
4
+ import { log } from "../logger.js";
4
5
  import {
5
6
  listMemoryGovernanceRuns,
6
7
  readMemoryGovernanceRunArtifact,
@@ -51,6 +52,27 @@ export interface RebuildMemoryProjectionOptions {
51
52
  updatedBefore?: string;
52
53
  }
53
54
 
55
+ export interface SkippedDuplicateMemory {
56
+ memoryId: string;
57
+ keptPath: string;
58
+ skippedPath: string;
59
+ }
60
+
61
+ export interface SkippedBlankIdMemory {
62
+ path: string;
63
+ }
64
+
65
+ export interface SkippedDuplicateTimelineEvent {
66
+ eventId: string;
67
+ keptPath: string;
68
+ skippedPath: string;
69
+ }
70
+
71
+ export interface SkippedBlankIdTimelineEvent {
72
+ eventId: string;
73
+ path: string;
74
+ }
75
+
54
76
  export interface RebuildMemoryProjectionResult {
55
77
  dryRun: boolean;
56
78
  scannedMemories: number;
@@ -62,6 +84,10 @@ export interface RebuildMemoryProjectionResult {
62
84
  outputPath: string;
63
85
  backupPath?: string;
64
86
  usedLifecycleLedger: boolean;
87
+ skippedDuplicateMemories: SkippedDuplicateMemory[];
88
+ skippedBlankIdMemories: SkippedBlankIdMemory[];
89
+ skippedDuplicateTimelineEvents: SkippedDuplicateTimelineEvent[];
90
+ skippedBlankIdTimelineEvents: SkippedBlankIdTimelineEvent[];
65
91
  scope: {
66
92
  updatedAfter: string | null;
67
93
  updatedBefore: string | null;
@@ -435,6 +461,91 @@ function serializeGovernanceReviewQueueRow(
435
461
  });
436
462
  }
437
463
 
464
+ function selectProjectionMemories(
465
+ tiers: MemoryFile[][],
466
+ ): {
467
+ allMemories: MemoryFile[];
468
+ memories: MemoryFile[];
469
+ skippedDuplicateMemories: SkippedDuplicateMemory[];
470
+ skippedBlankIdMemories: SkippedBlankIdMemory[];
471
+ } {
472
+ const allMemories = tiers.flat();
473
+ const memories: MemoryFile[] = [];
474
+ const keptById = new Map<string, MemoryFile>();
475
+ const skippedDuplicateMemories: SkippedDuplicateMemory[] = [];
476
+ const skippedBlankIdMemories: SkippedBlankIdMemory[] = [];
477
+
478
+ // Tier order is authoritative: hot wins over cold, and cold wins over archive.
479
+ // Paths break ties within a tier so the winner does not depend on readdir order.
480
+ for (const tier of tiers) {
481
+ for (const memory of [...tier].sort((left, right) => left.path.localeCompare(right.path))) {
482
+ const memoryId = memory.frontmatter.id.trim();
483
+ if (!memoryId) {
484
+ skippedBlankIdMemories.push({ path: memory.path });
485
+ log.warn(`projection rebuild skipped blank memory id: path=${memory.path}`);
486
+ continue;
487
+ }
488
+ const kept = keptById.get(memoryId);
489
+ if (kept) {
490
+ skippedDuplicateMemories.push({
491
+ memoryId,
492
+ keptPath: kept.path,
493
+ skippedPath: memory.path,
494
+ });
495
+ log.warn(
496
+ `projection rebuild skipped duplicate memory id=${memoryId}: kept=${kept.path} skipped=${memory.path}`,
497
+ );
498
+ continue;
499
+ }
500
+ keptById.set(memoryId, memory);
501
+ memories.push(memory);
502
+ }
503
+ }
504
+
505
+ return { allMemories, memories, skippedDuplicateMemories, skippedBlankIdMemories };
506
+ }
507
+
508
+ function deduplicateTimelineEvents(
509
+ events: MemoryLifecycleEvent[],
510
+ sourcePathFor: (event: MemoryLifecycleEvent) => string,
511
+ ): {
512
+ events: MemoryLifecycleEvent[];
513
+ skipped: SkippedDuplicateTimelineEvent[];
514
+ skippedBlankIds: SkippedBlankIdTimelineEvent[];
515
+ } {
516
+ const keptById = new Map<string, { event: MemoryLifecycleEvent; path: string }>();
517
+ const unique: MemoryLifecycleEvent[] = [];
518
+ const skipped: SkippedDuplicateTimelineEvent[] = [];
519
+ const skippedBlankIds: SkippedBlankIdTimelineEvent[] = [];
520
+ for (const event of sortMemoryLifecycleEvents(events)) {
521
+ const sourcePath = sourcePathFor(event);
522
+ if (!event.memoryId.trim()) {
523
+ skippedBlankIds.push({ eventId: event.eventId, path: sourcePath });
524
+ log.warn(
525
+ `projection rebuild skipped timeline event with blank memory id: `
526
+ + `event_id=${event.eventId} path=${sourcePath}`,
527
+ );
528
+ continue;
529
+ }
530
+ const kept = keptById.get(event.eventId);
531
+ if (kept) {
532
+ skipped.push({
533
+ eventId: event.eventId,
534
+ keptPath: kept.path,
535
+ skippedPath: sourcePath,
536
+ });
537
+ log.warn(
538
+ `projection rebuild skipped duplicate timeline event_id=${event.eventId}: `
539
+ + `kept=${kept.path} skipped=${sourcePath}`,
540
+ );
541
+ continue;
542
+ }
543
+ keptById.set(event.eventId, { event, path: sourcePath });
544
+ unique.push(event);
545
+ }
546
+ return { events: unique, skipped, skippedBlankIds };
547
+ }
548
+
438
549
  async function loadAuthoritativeProjectionSnapshot(options: {
439
550
  memoryDir: string;
440
551
  defaultNamespace?: string;
@@ -464,22 +575,43 @@ async function loadAuthoritativeProjectionSnapshot(options: {
464
575
  updatedAfter: string | null;
465
576
  updatedBefore: string | null;
466
577
  };
578
+ skippedDuplicateMemories: SkippedDuplicateMemory[];
579
+ skippedBlankIdMemories: SkippedBlankIdMemory[];
580
+ skippedDuplicateTimelineEvents: SkippedDuplicateTimelineEvent[];
581
+ skippedBlankIdTimelineEvents: SkippedBlankIdTimelineEvent[];
467
582
  }> {
468
583
  const storage = new StorageManager(options.memoryDir);
469
584
  // Force a fresh disk read — projection verify/rebuild must see the true
470
585
  // on-disk state, not a potentially stale in-process cache.
471
586
  storage.invalidateAllMemoriesCacheForDir();
472
- const allMemories = [
473
- ...await storage.readAllMemories(),
474
- ...await storage.readAllColdMemories(),
475
- ...await storage.readArchivedMemories(),
476
- ]
477
- .sort((a, b) => a.frontmatter.id.localeCompare(b.frontmatter.id));
587
+ const selected = selectProjectionMemories([
588
+ await storage.readAllMemories(),
589
+ await storage.readAllColdMemories(),
590
+ await storage.readArchivedMemories(),
591
+ ]);
592
+ const allMemories = selected.allMemories;
593
+ const projectionMemories = selected.memories;
478
594
  const lifecycleEvents = await storage.readAllMemoryLifecycleEvents();
479
- const { events, usedLifecycleLedger } = loadTimelineEvents(allMemories, lifecycleEvents);
480
- const currentRows = allMemories.map((memory) => toCurrentStateRow(options.memoryDir, memory));
595
+ const timeline = loadTimelineEvents(projectionMemories, lifecycleEvents);
596
+ const lifecycleLedgerPath = path.join(
597
+ options.memoryDir,
598
+ "state",
599
+ "memory-lifecycle-ledger.jsonl",
600
+ );
601
+ const memoryPathById = new Map(
602
+ projectionMemories.map((memory) => [memory.frontmatter.id, memory.path]),
603
+ );
604
+ const deduplicatedTimeline = deduplicateTimelineEvents(
605
+ timeline.events,
606
+ (event) => timeline.usedLifecycleLedger
607
+ ? lifecycleLedgerPath
608
+ : memoryPathById.get(event.memoryId) ?? lifecycleLedgerPath,
609
+ );
610
+ const events = deduplicatedTimeline.events;
611
+ const usedLifecycleLedger = timeline.usedLifecycleLedger;
612
+ const currentRows = projectionMemories.map((memory) => toCurrentStateRow(options.memoryDir, memory));
481
613
  const scope = normalizeProjectionScope(options);
482
- const scopedMemories = filterMemoriesForProjectionScope(allMemories, scope);
614
+ const scopedMemories = filterMemoriesForProjectionScope(projectionMemories, scope);
483
615
  const scopedMemoryIds = new Set(scopedMemories.map((memory) => memory.frontmatter.id));
484
616
  const nativeKnowledgeRows = await loadPersistedNativeKnowledgeChunks({
485
617
  memoryDir: options.memoryDir,
@@ -516,6 +648,10 @@ async function loadAuthoritativeProjectionSnapshot(options: {
516
648
  governance,
517
649
  usedLifecycleLedger,
518
650
  scope,
651
+ skippedDuplicateMemories: selected.skippedDuplicateMemories,
652
+ skippedBlankIdMemories: selected.skippedBlankIdMemories,
653
+ skippedDuplicateTimelineEvents: deduplicatedTimeline.skipped,
654
+ skippedBlankIdTimelineEvents: deduplicatedTimeline.skippedBlankIds,
519
655
  };
520
656
  }
521
657
 
@@ -1040,6 +1176,10 @@ export async function rebuildMemoryProjection(
1040
1176
  outputPath,
1041
1177
  backupPath,
1042
1178
  usedLifecycleLedger: snapshot.usedLifecycleLedger,
1179
+ skippedDuplicateMemories: snapshot.skippedDuplicateMemories,
1180
+ skippedBlankIdMemories: snapshot.skippedBlankIdMemories,
1181
+ skippedDuplicateTimelineEvents: snapshot.skippedDuplicateTimelineEvents,
1182
+ skippedBlankIdTimelineEvents: snapshot.skippedBlankIdTimelineEvents,
1043
1183
  scope: snapshot.scope,
1044
1184
  };
1045
1185
  }
@@ -1,5 +1,16 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdir, mkdtemp, readFile, realpath, rm, stat, symlink, utimes, writeFile } from "node:fs/promises";
2
+ import {
3
+ appendFile,
4
+ mkdir,
5
+ mkdtemp,
6
+ readFile,
7
+ realpath,
8
+ rm,
9
+ stat,
10
+ symlink,
11
+ utimes,
12
+ writeFile,
13
+ } from "node:fs/promises";
3
14
  import os from "node:os";
4
15
  import path from "node:path";
5
16
  import test from "node:test";
@@ -32,6 +43,21 @@ async function mkMemoryDir(): Promise<string> {
32
43
  return await mkdtemp(path.join(os.tmpdir(), "remnic-ns-catalog-"));
33
44
  }
34
45
 
46
+ class CountingNamespaceCatalog extends NamespaceCatalog {
47
+ catalogReadCount = 0;
48
+
49
+ constructor(config: PluginConfig) {
50
+ super(config);
51
+ this.onCatalogReadForTest = () => {
52
+ this.catalogReadCount++;
53
+ };
54
+ }
55
+
56
+ setAfterAppendHook(hook: () => Promise<void>): void {
57
+ this.onAfterCatalogAppendForTest = hook;
58
+ }
59
+ }
60
+
35
61
  test("markWrite registers a dynamic project namespace in the catalog", async () => {
36
62
  const memoryDir = await mkMemoryDir();
37
63
  try {
@@ -50,6 +76,85 @@ test("markWrite registers a dynamic project namespace in the catalog", async ()
50
76
  }
51
77
  });
52
78
 
79
+ test("touch reloads the catalog after an external append", async () => {
80
+ const memoryDir = await mkMemoryDir();
81
+ try {
82
+ const catalog = new NamespaceCatalog(makeConfig(memoryDir));
83
+ await catalog.markWrite("alpha", { discoveredBy: "write" });
84
+
85
+ const catalogPath = path.join(memoryDir, "state", "namespaces.jsonl");
86
+ const alpha = JSON.parse((await readFile(catalogPath, "utf8")).trim());
87
+ const externalNamespace = "external";
88
+ await appendFile(
89
+ catalogPath,
90
+ `${JSON.stringify({
91
+ ...alpha,
92
+ namespace: externalNamespace,
93
+ identityToken: namespaceIdentityToken(externalNamespace),
94
+ storageDir: path.join(memoryDir, "namespaces", namespaceIdentityToken(externalNamespace)),
95
+ })}\n`,
96
+ "utf8",
97
+ );
98
+
99
+ await catalog.markRead("alpha");
100
+
101
+ assert.ok(
102
+ (await catalog.listNamespaces()).some((record) => record.namespace === externalNamespace),
103
+ "the touch must fold in a row appended through another file descriptor",
104
+ );
105
+ } finally {
106
+ await rm(memoryDir, { recursive: true, force: true });
107
+ }
108
+ });
109
+
110
+ test("repeated touches do not re-read an unchanged catalog", async () => {
111
+ const memoryDir = await mkMemoryDir();
112
+ try {
113
+ const catalog = new CountingNamespaceCatalog(makeConfig(memoryDir));
114
+
115
+ await catalog.markWrite("alpha", { discoveredBy: "write" });
116
+ await catalog.markRead("alpha");
117
+ await catalog.markMaintenance("alpha", "test");
118
+
119
+ assert.equal(catalog.catalogReadCount, 1);
120
+ } finally {
121
+ await rm(memoryDir, { recursive: true, force: true });
122
+ }
123
+ });
124
+
125
+ test("a foreign append between append and stat invalidates the cache", async () => {
126
+ const memoryDir = await mkMemoryDir();
127
+ try {
128
+ const catalog = new CountingNamespaceCatalog(makeConfig(memoryDir));
129
+ await catalog.markWrite("alpha", { discoveredBy: "write" });
130
+
131
+ const catalogPath = path.join(memoryDir, "state", "namespaces.jsonl");
132
+ const alpha = JSON.parse((await readFile(catalogPath, "utf8")).trim());
133
+ const externalNamespace = "racing-external";
134
+ catalog.setAfterAppendHook(async () => {
135
+ await appendFile(
136
+ catalogPath,
137
+ `${JSON.stringify({
138
+ ...alpha,
139
+ namespace: externalNamespace,
140
+ identityToken: namespaceIdentityToken(externalNamespace),
141
+ storageDir: path.join(memoryDir, "namespaces", namespaceIdentityToken(externalNamespace)),
142
+ })}\n`,
143
+ "utf8",
144
+ );
145
+ });
146
+
147
+ await catalog.markRead("alpha");
148
+ const readsBeforeReload = catalog.catalogReadCount;
149
+ const records = await catalog.listNamespaces();
150
+
151
+ assert.equal(catalog.catalogReadCount, readsBeforeReload + 1);
152
+ assert.ok(records.some((record) => record.namespace === externalNamespace));
153
+ } finally {
154
+ await rm(memoryDir, { recursive: true, force: true });
155
+ }
156
+ });
157
+
53
158
  test("configured default and shared namespaces appear in the catalog after register", async () => {
54
159
  const memoryDir = await mkMemoryDir();
55
160
  try {
@@ -89,6 +194,19 @@ test("rebuildFromDisk finds existing tokenized namespace directories", async ()
89
194
  }
90
195
  });
91
196
 
197
+ test("rebuildFromDisk accepts config without sharedNamespace", async () => {
198
+ const memoryDir = await mkMemoryDir();
199
+ try {
200
+ const config = makeConfig(memoryDir);
201
+ delete (config as Partial<PluginConfig>).sharedNamespace;
202
+ const catalog = new NamespaceCatalog(config);
203
+
204
+ await assert.doesNotReject(catalog.rebuildFromDisk());
205
+ } finally {
206
+ await rm(memoryDir, { recursive: true, force: true });
207
+ }
208
+ });
209
+
92
210
  test("rebuildFromDisk dry-run does not write the catalog file", async () => {
93
211
  const memoryDir = await mkMemoryDir();
94
212
  try {
@@ -405,6 +523,20 @@ test("catalog tolerates corrupt / non-object JSONL lines on read", async () => {
405
523
  }
406
524
  });
407
525
 
526
+ test("catalog read fails open when the catalog path is a directory", async () => {
527
+ const memoryDir = await mkMemoryDir();
528
+ try {
529
+ await mkdir(path.join(memoryDir, "state", "namespaces.jsonl"), { recursive: true });
530
+ const catalog = new NamespaceCatalog(makeConfig(memoryDir));
531
+
532
+ await assert.doesNotReject(async () => {
533
+ assert.deepEqual(await catalog.listNamespaces(), []);
534
+ });
535
+ } finally {
536
+ await rm(memoryDir, { recursive: true, force: true });
537
+ }
538
+ });
539
+
408
540
  test("catalog quarantines malformed JSONL record fields at the parse boundary", async () => {
409
541
  const memoryDir = await mkMemoryDir();
410
542
  try {