@fgv/ts-agent-memory 5.1.0-39 → 5.1.0-41

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 (210) hide show
  1. package/.rush/temp/{b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log → cbbdbe09515171b4eba8f2592be2dace1e4e8142.tar.log} +14 -2
  2. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +2 -2
  3. package/.rush/temp/operation/build/all.log +2 -2
  4. package/.rush/temp/operation/build/log-chunks.jsonl +2 -2
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/dist/packlets/converters/envelopeConverter.js +17 -3
  7. package/dist/packlets/converters/envelopeConverter.js.map +1 -1
  8. package/dist/packlets/index/memoryIndex.js +58 -10
  9. package/dist/packlets/index/memoryIndex.js.map +1 -1
  10. package/dist/packlets/ingest/cycleGuard.js +13 -6
  11. package/dist/packlets/ingest/cycleGuard.js.map +1 -1
  12. package/dist/packlets/ingest/hostStages.js.map +1 -1
  13. package/dist/packlets/ingest/model.js.map +1 -1
  14. package/dist/packlets/ingest/orchestrator.js +94 -49
  15. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  16. package/dist/packlets/retrieve/hybridRetriever.js +13 -2
  17. package/dist/packlets/retrieve/hybridRetriever.js.map +1 -1
  18. package/dist/packlets/retrieve/linkTraversalRetriever.js +46 -57
  19. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  20. package/dist/packlets/retrieve/recencyRetriever.js +3 -3
  21. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  22. package/dist/packlets/retrieve/retriever.js +50 -7
  23. package/dist/packlets/retrieve/retriever.js.map +1 -1
  24. package/dist/packlets/retrieve/semanticRetriever.js +9 -3
  25. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -1
  26. package/dist/packlets/retrieve/structuredFilterRetriever.js +3 -3
  27. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  28. package/dist/packlets/retrieve/tagRetriever.js +3 -3
  29. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  30. package/dist/packlets/retrieve/temporalRetrievers.js +3 -3
  31. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  32. package/dist/packlets/store/fileTreeMemoryStore.js +72 -16
  33. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  34. package/dist/packlets/tools/memoryTools.js +104 -21
  35. package/dist/packlets/tools/memoryTools.js.map +1 -1
  36. package/dist/packlets/types/envelope.js +13 -1
  37. package/dist/packlets/types/envelope.js.map +1 -1
  38. package/dist/packlets/vector/inMemoryCosineIndex.js +22 -17
  39. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  40. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  41. package/dist/test/unit/converters/antagonistRoundTrip.test.js +3 -3
  42. package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
  43. package/dist/test/unit/converters/envelopeConverter.test.js +125 -8
  44. package/dist/test/unit/converters/envelopeConverter.test.js.map +1 -1
  45. package/dist/test/unit/index/memoryIndex.test.js +87 -25
  46. package/dist/test/unit/index/memoryIndex.test.js.map +1 -1
  47. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
  48. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
  49. package/dist/test/unit/ingest/cycleGuard.test.js +28 -1
  50. package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -1
  51. package/dist/test/unit/ingest/orchestrator.test.js +187 -45
  52. package/dist/test/unit/ingest/orchestrator.test.js.map +1 -1
  53. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
  54. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
  55. package/dist/test/unit/retrieve/retrievers.test.js +299 -37
  56. package/dist/test/unit/retrieve/retrievers.test.js.map +1 -1
  57. package/dist/test/unit/store/embedOnWrite.test.js +69 -12
  58. package/dist/test/unit/store/embedOnWrite.test.js.map +1 -1
  59. package/dist/test/unit/store/listScoped.test.js +109 -0
  60. package/dist/test/unit/store/listScoped.test.js.map +1 -0
  61. package/dist/test/unit/store/rankAxis.test.js +254 -0
  62. package/dist/test/unit/store/rankAxis.test.js.map +1 -0
  63. package/dist/test/unit/tools/memoryTools.test.js +280 -11
  64. package/dist/test/unit/tools/memoryTools.test.js.map +1 -1
  65. package/dist/test/unit/types/writePolicy.test.js +9 -2
  66. package/dist/test/unit/types/writePolicy.test.js.map +1 -1
  67. package/dist/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
  68. package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
  69. package/dist/test/unit/vector/vectorIndex.test.js +24 -15
  70. package/dist/test/unit/vector/vectorIndex.test.js.map +1 -1
  71. package/dist/ts-agent-memory.d.ts +384 -97
  72. package/etc/ts-agent-memory.api.md +64 -22
  73. package/lib/packlets/converters/envelopeConverter.d.ts +8 -1
  74. package/lib/packlets/converters/envelopeConverter.d.ts.map +1 -1
  75. package/lib/packlets/converters/envelopeConverter.js +18 -4
  76. package/lib/packlets/converters/envelopeConverter.js.map +1 -1
  77. package/lib/packlets/index/memoryIndex.d.ts +42 -10
  78. package/lib/packlets/index/memoryIndex.d.ts.map +1 -1
  79. package/lib/packlets/index/memoryIndex.js +58 -10
  80. package/lib/packlets/index/memoryIndex.js.map +1 -1
  81. package/lib/packlets/ingest/cycleGuard.d.ts +5 -5
  82. package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -1
  83. package/lib/packlets/ingest/cycleGuard.js +13 -6
  84. package/lib/packlets/ingest/cycleGuard.js.map +1 -1
  85. package/lib/packlets/ingest/hostStages.d.ts +3 -3
  86. package/lib/packlets/ingest/hostStages.d.ts.map +1 -1
  87. package/lib/packlets/ingest/hostStages.js.map +1 -1
  88. package/lib/packlets/ingest/model.d.ts +25 -14
  89. package/lib/packlets/ingest/model.d.ts.map +1 -1
  90. package/lib/packlets/ingest/model.js.map +1 -1
  91. package/lib/packlets/ingest/orchestrator.d.ts +18 -4
  92. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  93. package/lib/packlets/ingest/orchestrator.js +93 -48
  94. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  95. package/lib/packlets/retrieve/hybridRetriever.d.ts.map +1 -1
  96. package/lib/packlets/retrieve/hybridRetriever.js +12 -1
  97. package/lib/packlets/retrieve/hybridRetriever.js.map +1 -1
  98. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +18 -23
  99. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -1
  100. package/lib/packlets/retrieve/linkTraversalRetriever.js +45 -56
  101. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  102. package/lib/packlets/retrieve/recencyRetriever.js +2 -2
  103. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  104. package/lib/packlets/retrieve/retriever.d.ts +70 -11
  105. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  106. package/lib/packlets/retrieve/retriever.js +52 -7
  107. package/lib/packlets/retrieve/retriever.js.map +1 -1
  108. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -1
  109. package/lib/packlets/retrieve/semanticRetriever.js +9 -3
  110. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -1
  111. package/lib/packlets/retrieve/structuredFilterRetriever.js +2 -2
  112. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  113. package/lib/packlets/retrieve/tagRetriever.js +2 -2
  114. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  115. package/lib/packlets/retrieve/temporalRetrievers.js +3 -3
  116. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  117. package/lib/packlets/store/fileTreeMemoryStore.d.ts +57 -3
  118. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  119. package/lib/packlets/store/fileTreeMemoryStore.js +72 -16
  120. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  121. package/lib/packlets/tools/memoryTools.d.ts +24 -0
  122. package/lib/packlets/tools/memoryTools.d.ts.map +1 -1
  123. package/lib/packlets/tools/memoryTools.js +104 -21
  124. package/lib/packlets/tools/memoryTools.js.map +1 -1
  125. package/lib/packlets/types/envelope.d.ts +61 -8
  126. package/lib/packlets/types/envelope.d.ts.map +1 -1
  127. package/lib/packlets/types/envelope.js +14 -0
  128. package/lib/packlets/types/envelope.js.map +1 -1
  129. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +9 -5
  130. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  131. package/lib/packlets/vector/inMemoryCosineIndex.js +22 -17
  132. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  133. package/lib/packlets/vector/vectorIndex.d.ts +46 -20
  134. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  135. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  136. package/lib/test/unit/converters/antagonistRoundTrip.test.js +3 -3
  137. package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -1
  138. package/lib/test/unit/converters/envelopeConverter.test.js +124 -7
  139. package/lib/test/unit/converters/envelopeConverter.test.js.map +1 -1
  140. package/lib/test/unit/index/memoryIndex.test.js +86 -24
  141. package/lib/test/unit/index/memoryIndex.test.js.map +1 -1
  142. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +20 -16
  143. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -1
  144. package/lib/test/unit/ingest/cycleGuard.test.js +28 -1
  145. package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -1
  146. package/lib/test/unit/ingest/orchestrator.test.js +186 -44
  147. package/lib/test/unit/ingest/orchestrator.test.js.map +1 -1
  148. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +106 -31
  149. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -1
  150. package/lib/test/unit/retrieve/retrievers.test.js +298 -36
  151. package/lib/test/unit/retrieve/retrievers.test.js.map +1 -1
  152. package/lib/test/unit/store/embedOnWrite.test.js +68 -11
  153. package/lib/test/unit/store/embedOnWrite.test.js.map +1 -1
  154. package/lib/test/unit/store/listScoped.test.d.ts +2 -0
  155. package/lib/test/unit/store/listScoped.test.d.ts.map +1 -0
  156. package/lib/test/unit/store/listScoped.test.js +111 -0
  157. package/lib/test/unit/store/listScoped.test.js.map +1 -0
  158. package/lib/test/unit/store/rankAxis.test.d.ts +2 -0
  159. package/lib/test/unit/store/rankAxis.test.d.ts.map +1 -0
  160. package/lib/test/unit/store/rankAxis.test.js +256 -0
  161. package/lib/test/unit/store/rankAxis.test.js.map +1 -0
  162. package/lib/test/unit/tools/memoryTools.test.js +280 -11
  163. package/lib/test/unit/tools/memoryTools.test.js.map +1 -1
  164. package/lib/test/unit/types/writePolicy.test.js +9 -2
  165. package/lib/test/unit/types/writePolicy.test.js.map +1 -1
  166. package/lib/test/unit/vector/inMemoryCosineIndex.test.js +95 -35
  167. package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -1
  168. package/lib/test/unit/vector/vectorIndex.test.js +24 -15
  169. package/lib/test/unit/vector/vectorIndex.test.js.map +1 -1
  170. package/package.json +7 -7
  171. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  172. package/rush-logs/ts-agent-memory.build.log +2 -2
  173. package/src/packlets/converters/envelopeConverter.ts +27 -4
  174. package/src/packlets/index/memoryIndex.ts +86 -22
  175. package/src/packlets/ingest/cycleGuard.ts +22 -11
  176. package/src/packlets/ingest/hostStages.ts +3 -3
  177. package/src/packlets/ingest/model.ts +25 -14
  178. package/src/packlets/ingest/orchestrator.ts +143 -67
  179. package/src/packlets/retrieve/hybridRetriever.ts +14 -1
  180. package/src/packlets/retrieve/linkTraversalRetriever.ts +51 -62
  181. package/src/packlets/retrieve/recencyRetriever.ts +3 -3
  182. package/src/packlets/retrieve/retriever.ts +97 -13
  183. package/src/packlets/retrieve/semanticRetriever.ts +10 -5
  184. package/src/packlets/retrieve/structuredFilterRetriever.ts +3 -3
  185. package/src/packlets/retrieve/tagRetriever.ts +3 -3
  186. package/src/packlets/retrieve/temporalRetrievers.ts +3 -3
  187. package/src/packlets/store/fileTreeMemoryStore.ts +117 -12
  188. package/src/packlets/tools/memoryTools.ts +152 -25
  189. package/src/packlets/types/envelope.ts +66 -8
  190. package/src/packlets/vector/inMemoryCosineIndex.ts +45 -22
  191. package/src/packlets/vector/vectorIndex.ts +47 -20
  192. package/src/test/unit/converters/antagonistRoundTrip.test.ts +3 -3
  193. package/src/test/unit/converters/envelopeConverter.test.ts +168 -11
  194. package/src/test/unit/index/memoryIndex.test.ts +99 -14
  195. package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +23 -18
  196. package/src/test/unit/ingest/cycleGuard.test.ts +44 -2
  197. package/src/test/unit/ingest/orchestrator.test.ts +234 -41
  198. package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +134 -35
  199. package/src/test/unit/retrieve/retrievers.test.ts +381 -25
  200. package/src/test/unit/store/embedOnWrite.test.ts +83 -11
  201. package/src/test/unit/store/listScoped.test.ts +138 -0
  202. package/src/test/unit/store/rankAxis.test.ts +349 -0
  203. package/src/test/unit/tools/memoryTools.test.ts +362 -13
  204. package/src/test/unit/types/writePolicy.test.ts +11 -2
  205. package/src/test/unit/vector/inMemoryCosineIndex.test.ts +115 -39
  206. package/src/test/unit/vector/vectorIndex.test.ts +33 -17
  207. package/temp/build/lint/_eslint-5eVG3S6w.json +41 -33
  208. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  209. package/temp/ts-agent-memory.api.json +1080 -136
  210. package/temp/ts-agent-memory.api.md +64 -22
@@ -4,12 +4,29 @@
4
4
  */
5
5
 
6
6
  import '@fgv/ts-utils-jest';
7
- import { ICycleGuardEdge, LinkType, MemoryId, assertNoCycles, buildCycleKey } from '../../../index';
7
+ import {
8
+ ICycleGuardEdge,
9
+ IEdgeTarget,
10
+ LinkType,
11
+ MemoryId,
12
+ MemoryScopeKey,
13
+ assertNoCycles,
14
+ buildCycleKey
15
+ } from '../../../index';
8
16
 
9
17
  const rel: LinkType = 'rel' as LinkType;
18
+ const DEFAULT_SCOPE: string = 'exp';
19
+
20
+ /** A scope-qualified node, `scope#id` or a bare id defaulting to {@link DEFAULT_SCOPE}. */
21
+ function node(spec: string): IEdgeTarget {
22
+ const hash: number = spec.indexOf('#');
23
+ const scope: string = hash >= 0 ? spec.slice(0, hash) : DEFAULT_SCOPE;
24
+ const id: string = hash >= 0 ? spec.slice(hash + 1) : spec;
25
+ return { scope: scope as MemoryScopeKey, id: id as MemoryId };
26
+ }
10
27
 
11
28
  function edge(source: string, target: string, type: LinkType = rel): ICycleGuardEdge {
12
- return { source: source as MemoryId, target: target as MemoryId, type };
29
+ return { source: node(source), target: node(target), type };
13
30
  }
14
31
 
15
32
  describe('buildCycleKey', () => {
@@ -65,4 +82,29 @@ describe('assertNoCycles', () => {
65
82
  // visited-node skip in the traversal.
66
83
  expect(assertNoCycles([edge('a', 'c'), edge('a', 'b'), edge('b', 'c')], [edge('z', 'a')])).toSucceed();
67
84
  });
85
+
86
+ describe('scope-qualified nodes', () => {
87
+ test('does NOT conflate a stem shared across scopes into one node', () => {
88
+ // ca#x -> cb#y and cb#x -> ca#z reuse the stem `x` across scopes ca/cb.
89
+ // If the guard keyed on bare id, the two `x` nodes would merge and a false
90
+ // cycle could appear; with scoped keys they are distinct and the graph is acyclic.
91
+ expect(assertNoCycles([], [edge('ca#x', 'cb#y'), edge('cb#x', 'ca#z')])).toSucceed();
92
+ });
93
+
94
+ test('still rejects a genuine within-scope cycle across the same stems', () => {
95
+ expect(assertNoCycles([], [edge('ca#x', 'ca#y'), edge('ca#y', 'ca#x')])).toFailWith(
96
+ /would create a cycle/
97
+ );
98
+ });
99
+
100
+ test('a self-loop is detected on the full scoped identity', () => {
101
+ expect(assertNoCycles([], [edge('ca#x', 'ca#x')])).toFailWith(/self-loop/);
102
+ // Same stem, different scope is NOT a self-loop.
103
+ expect(assertNoCycles([], [edge('ca#x', 'cb#x')])).toSucceed();
104
+ });
105
+
106
+ test('names the scope in the diagnostic', () => {
107
+ expect(assertNoCycles([], [edge('ca#x', 'ca#x')])).toFailWith(/ca\/x/);
108
+ });
109
+ });
68
110
  });
@@ -17,6 +17,7 @@ import {
17
17
  IBodyConverterRegistry,
18
18
  ICandidateEdge,
19
19
  ICandidateRecord,
20
+ IEdgeTarget,
20
21
  IEntityResolutionCandidate,
21
22
  IEntityResolver,
22
23
  IFactExtractor,
@@ -28,8 +29,10 @@ import {
28
29
  IMemoryClassifier,
29
30
  IMemoryEnvelope,
30
31
  IMemoryRecord,
32
+ IMemoryRecordSource,
31
33
  IMemoryStore,
32
34
  IProvenance,
35
+ IScopedMemoryRecord,
33
36
  IRelationContext,
34
37
  IRelationExtractor,
35
38
  IVectorIndex,
@@ -40,6 +43,7 @@ import {
40
43
  LinkType,
41
44
  LtmIdentityCodec,
42
45
  MemoryEmbedder,
46
+ MtmIdentityCodec,
43
47
  MemoryId,
44
48
  MemoryIngestOrchestrator,
45
49
  MemoryScopeKey,
@@ -168,12 +172,36 @@ function candidate(
168
172
  };
169
173
  }
170
174
 
175
+ /**
176
+ * A scope-qualified target, authored as `scope#id` or a bare id defaulting to the
177
+ * knowledge scope (where the note/num fixtures live). `kt` is the assertion-side
178
+ * spelling of the same helper.
179
+ */
180
+ function scopedTarget(spec: string): IEdgeTarget {
181
+ const hash: number = spec.indexOf('#');
182
+ const scope: string = hash >= 0 ? spec.slice(0, hash) : 'knowledge';
183
+ const id: string = hash >= 0 ? spec.slice(hash + 1) : spec;
184
+ return { scope: scope as MemoryScopeKey, id: id as MemoryId };
185
+ }
186
+ function kt(id: string, scope: string = 'knowledge'): IEdgeTarget {
187
+ return { scope: scope as MemoryScopeKey, id: id as MemoryId };
188
+ }
189
+
190
+ /** The scope-qualified reference of a candidate about to be written, by its entityId. */
191
+ function srcOf(ctx: IRelationContext, entityId: string): IEdgeTarget {
192
+ const found = ctx.candidates.find((rc) => rc.candidate.envelope.entityId === (entityId as EntityId));
193
+ if (found === undefined) {
194
+ throw new Error(`test relater: no candidate with entityId '${entityId}'`);
195
+ }
196
+ return found.id;
197
+ }
198
+
171
199
  function candEdge(source: string, type: string, target: string, confidence?: number): ICandidateEdge {
172
200
  return {
173
- source: source as MemoryId,
201
+ source: scopedTarget(source),
174
202
  edge: {
175
203
  type: type as LinkType,
176
- target: target as MemoryId,
204
+ target: scopedTarget(target),
177
205
  ...(confidence !== undefined ? { confidence } : {})
178
206
  }
179
207
  };
@@ -280,7 +308,10 @@ describe('MemoryIngestOrchestrator', () => {
280
308
  test('classifies, extracts, and writes one record end-to-end with host-ingest provenance + derivedFrom', async () => {
281
309
  const store = buildStore();
282
310
  const orch = buildOrchestrator({ store });
283
- const sourceId: MemoryId = 'turn-3' as MemoryId;
311
+ const sourceId: IEdgeTarget = {
312
+ scope: 'conversations/c1' as MemoryScopeKey,
313
+ id: 'turn-3' as MemoryId
314
+ };
284
315
  const result = await orch.ingestItem({ id: 'doc-1', content: 'the sky is blue', sourceId });
285
316
  expect(result).toSucceedAndSatisfy((r: IIngestItemResult) => {
286
317
  expect(r.item.id).toBe('doc-1');
@@ -290,7 +321,7 @@ describe('MemoryIngestOrchestrator', () => {
290
321
  expect(rec.resolution.verdict).toBe('new');
291
322
  expect(rec.id).toBe('doc-1');
292
323
  expect(rec.record?.envelope.provenance.source).toBe(HOST_INGEST_PROVENANCE_SOURCE);
293
- expect(rec.record?.envelope.provenance.derivedFrom).toBe('turn-3');
324
+ expect(rec.record?.envelope.provenance.derivedFrom).toEqual(sourceId);
294
325
  expect(rec.record?.body).toBe('the sky is blue');
295
326
  expect(rec.interlock).toBeUndefined();
296
327
  });
@@ -381,6 +412,28 @@ describe('MemoryIngestOrchestrator', () => {
381
412
  });
382
413
  expect(await orch.ingestItem({ id: 'o-1', content: 'x' })).toFailWith(/no identity codec/);
383
414
  });
415
+
416
+ test('fails loudly when a pre-existing snapshot record cannot be scoped (no codec for its kind)', async () => {
417
+ // The store can persist a note (it has the note codec), but the orchestrator
418
+ // below is wired WITHOUT a note codec — so when it snapshots the store to run
419
+ // the edge path, it cannot resolve the seeded note's scope and fails loudly
420
+ // (never silently drops it from the cycle graph). This is the branch the
421
+ // scope-qualified edge change added.
422
+ const store = buildStore();
423
+ await putNote(store, 'legacy', 'anchor');
424
+ const orch = MemoryIngestOrchestrator.create({
425
+ store,
426
+ registry: registry(),
427
+ // Deliberately omit the note codec (and any default) that the seeded record needs.
428
+ codecs: new Map<Kind, IIdentityCodec>([[numKind, new KnowledgeIdentityCodec()]]),
429
+ classifier: classifier(() => succeed({ kind: numKind })),
430
+ extractor: extractor(() => succeed([candidate(numKind, 'n-1', 7)])),
431
+ relationExtractor: noEdges
432
+ }).orThrow();
433
+ expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
434
+ /cannot resolve scope for stored record 'legacy'.*no identity codec registered for kind 'note'/i
435
+ );
436
+ });
384
437
  });
385
438
 
386
439
  describe('stage 4 — exact dedup (layer 1)', () => {
@@ -392,7 +445,7 @@ describe('MemoryIngestOrchestrator', () => {
392
445
  });
393
446
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
394
447
  expect(r.records[0].disposition).toBe('deduped');
395
- expect(r.records[0].resolution).toEqual({ verdict: 'duplicate-of', target: 'doc-a' });
448
+ expect(r.records[0].resolution).toEqual({ verdict: 'duplicate-of', target: kt('doc-a') });
396
449
  expect(r.records[0].id).toBe('doc-a');
397
450
  });
398
451
  // doc-b was never written.
@@ -464,6 +517,14 @@ function mockStore(overrides: Partial<IMemoryStore>): IMemoryStore {
464
517
  list:
465
518
  overrides.list ??
466
519
  ((): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> => Promise.resolve(succeed([]))),
520
+ listScoped:
521
+ overrides.listScoped ??
522
+ ((): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>> => Promise.resolve(succeed([]))),
523
+ asRecordSource:
524
+ overrides.asRecordSource ??
525
+ ((): IMemoryRecordSource => ({
526
+ list: (): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>> => Promise.resolve(succeed([]))
527
+ })),
467
528
  put: overrides.put ?? ((r): Promise<Result<IMemoryRecord<unknown>>> => Promise.resolve(succeed(r))),
468
529
  delete: overrides.delete ?? ((): Promise<Result<MemoryId>> => Promise.resolve(fail('n/a')))
469
530
  };
@@ -495,7 +556,9 @@ describe('MemoryIngestOrchestrator — stage 4 similarity (layer 2)', () => {
495
556
  store,
496
557
  vectorIndex,
497
558
  embed,
498
- entityResolver: resolver((__c, similar) => succeed({ verdict: 'duplicate-of', target: similar[0].id })),
559
+ entityResolver: resolver((__c, similar) =>
560
+ succeed({ verdict: 'duplicate-of', target: similar[0].target })
561
+ ),
499
562
  extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
500
563
  });
501
564
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
@@ -513,12 +576,14 @@ describe('MemoryIngestOrchestrator — stage 4 similarity (layer 2)', () => {
513
576
  store,
514
577
  vectorIndex,
515
578
  embed,
516
- entityResolver: resolver((__c, similar) => succeed({ verdict: 'supersede', target: similar[0].id })),
579
+ entityResolver: resolver((__c, similar) =>
580
+ succeed({ verdict: 'supersede', target: similar[0].target })
581
+ ),
517
582
  extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
518
583
  });
519
584
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
520
585
  expect(r.records[0].disposition).toBe('written');
521
- expect(r.records[0].resolution).toEqual({ verdict: 'supersede', target: 'doc-a' });
586
+ expect(r.records[0].resolution).toEqual({ verdict: 'supersede', target: kt('doc-a') });
522
587
  expect(r.records[0].id).toBe('doc-b');
523
588
  });
524
589
  expect(await store.get(noteKind, 'doc-b' as EntityId)).toSucceedAndSatisfy(
@@ -534,7 +599,9 @@ describe('MemoryIngestOrchestrator — stage 4 similarity (layer 2)', () => {
534
599
  store,
535
600
  vectorIndex,
536
601
  embed,
537
- entityResolver: resolver((__c, similar) => succeed({ verdict: 'merge-into', target: similar[0].id })),
602
+ entityResolver: resolver((__c, similar) =>
603
+ succeed({ verdict: 'merge-into', target: similar[0].target })
604
+ ),
538
605
  extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
539
606
  });
540
607
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
@@ -556,11 +623,11 @@ describe('MemoryIngestOrchestrator — stage 4 similarity (layer 2)', () => {
556
623
  store,
557
624
  vectorIndex,
558
625
  embed,
559
- entityResolver: resolver(() => succeed({ verdict: 'merge-into', target: 'ghost' as MemoryId })),
626
+ entityResolver: resolver(() => succeed({ verdict: 'merge-into', target: kt('ghost') })),
560
627
  extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
561
628
  });
562
629
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
563
- /merge-into target 'ghost' does not exist/
630
+ /merge-into target 'knowledge\/ghost' does not exist/
564
631
  );
565
632
  });
566
633
 
@@ -601,10 +668,10 @@ describe('MemoryIngestOrchestrator — stage 4 similarity (layer 2)', () => {
601
668
 
602
669
  test('a surfaced hit with no backing record is skipped', async () => {
603
670
  const ghostIndex: IVectorIndex = {
604
- add: (id) => Promise.resolve(succeed(id as string)),
605
- remove: (id) => Promise.resolve(succeed(id)),
671
+ add: (t) => Promise.resolve(succeed(t.id as string)),
672
+ remove: (t) => Promise.resolve(succeed(t)),
606
673
  query: (): Promise<Result<ReadonlyArray<IVectorQueryHit>>> =>
607
- Promise.resolve(succeed([{ id: 'ghost' as MemoryId, score: 0.99 }]))
674
+ Promise.resolve(succeed([{ target: kt('ghost'), score: 0.99 }]))
608
675
  };
609
676
  const orch = buildOrchestrator({
610
677
  store: buildStore(),
@@ -632,8 +699,8 @@ describe('MemoryIngestOrchestrator — stage 4 similarity (layer 2)', () => {
632
699
 
633
700
  test('a similarity-query failure surfaces loudly', async () => {
634
701
  const failingIndex: IVectorIndex = {
635
- add: (id) => Promise.resolve(succeed(id as string)),
636
- remove: (id) => Promise.resolve(succeed(id)),
702
+ add: (t) => Promise.resolve(succeed(t.id as string)),
703
+ remove: (t) => Promise.resolve(succeed(t)),
637
704
  query: (): Promise<Result<ReadonlyArray<IVectorQueryHit>>> => Promise.resolve(fail('query kaput'))
638
705
  };
639
706
  const orch = buildOrchestrator({
@@ -716,7 +783,7 @@ describe('MemoryIngestOrchestrator — stage 5 relate + cycle guard', () => {
716
783
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
717
784
  expect(r.records[0].edges).toHaveLength(1);
718
785
  expect(r.records[0].record?.envelope.links).toEqual([
719
- { type: 'mentions', target: 'doc-a', confidence: 0.7 }
786
+ { type: 'mentions', target: kt('doc-a'), confidence: 0.7 }
720
787
  ]);
721
788
  });
722
789
  });
@@ -731,7 +798,7 @@ describe('MemoryIngestOrchestrator — stage 5 relate + cycle guard', () => {
731
798
  });
732
799
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
733
800
  expect(r.records).toHaveLength(2);
734
- expect(r.records[0].record?.envelope.links).toEqual([{ type: 'rel', target: 'doc-c' }]);
801
+ expect(r.records[0].record?.envelope.links).toEqual([{ type: 'rel', target: kt('doc-c') }]);
735
802
  expect(r.records[1].edges).toEqual([]);
736
803
  });
737
804
  });
@@ -743,7 +810,7 @@ describe('MemoryIngestOrchestrator — stage 5 relate + cycle guard', () => {
743
810
  relationExtractor: relater(() => succeed([candEdge('ghost', 'rel', 'doc-b')]))
744
811
  });
745
812
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
746
- /edge source 'ghost' is not a candidate being written/
813
+ /edge source 'knowledge\/ghost' is not a candidate being written/
747
814
  );
748
815
  });
749
816
 
@@ -754,7 +821,7 @@ describe('MemoryIngestOrchestrator — stage 5 relate + cycle guard', () => {
754
821
  relationExtractor: relater(() => succeed([candEdge('doc-b', 'rel', 'ghost')]))
755
822
  });
756
823
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
757
- /edge target 'ghost' resolves to neither/
824
+ /edge target 'knowledge\/ghost' resolves to neither/
758
825
  );
759
826
  });
760
827
 
@@ -783,8 +850,8 @@ describe('MemoryIngestOrchestrator — stage 5 relate + cycle guard', () => {
783
850
  )
784
851
  });
785
852
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
786
- expect(r.records[0].record?.envelope.links).toEqual([{ type: 'rel', target: 'doc-c' }]);
787
- expect(r.records[1].record?.envelope.links).toEqual([{ type: 'rel', target: 'doc-b' }]);
853
+ expect(r.records[0].record?.envelope.links).toEqual([{ type: 'rel', target: kt('doc-c') }]);
854
+ expect(r.records[1].record?.envelope.links).toEqual([{ type: 'rel', target: kt('doc-b') }]);
788
855
  });
789
856
  });
790
857
 
@@ -798,7 +865,111 @@ describe('MemoryIngestOrchestrator — stage 5 relate + cycle guard', () => {
798
865
  });
799
866
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
800
867
  expect(r.records[0].interlock).toBeUndefined();
801
- expect(r.records[0].record?.envelope.links).toEqual([{ type: 'contradicts', target: 'doc-a' }]);
868
+ expect(r.records[0].record?.envelope.links).toEqual([{ type: 'contradicts', target: kt('doc-a') }]);
869
+ });
870
+ });
871
+
872
+ describe('_validateEdges cross-scope target disambiguation', () => {
873
+ // Two EXISTING records share the idStem 'turn-3' across two conversations
874
+ // (the MTM codec mints per-scope stems). A stage-5 edge target must resolve
875
+ // to the CORRECT (scope, id) — with bare-id keying both would alias and a
876
+ // wrong-scope target would silently validate. Guards the ingest-validation seam.
877
+ const mtmKind: Kind = 'mtm' as Kind;
878
+
879
+ function mtmRegistry(): IBodyConverterRegistry {
880
+ const reg = registry();
881
+ reg.register(mtmKind, Converters.string);
882
+ return reg;
883
+ }
884
+
885
+ const mtmCodecs: ReadonlyMap<Kind, IIdentityCodec> = new Map<Kind, IIdentityCodec>([
886
+ [mtmKind, new MtmIdentityCodec()]
887
+ ]);
888
+
889
+ async function seedTurn(store: FileTreeMemoryStore, entityId: string, body: string): Promise<void> {
890
+ const addr = new MtmIdentityCodec().encode(entityId as EntityId).orThrow();
891
+ const rec: IMemoryRecord<unknown> = {
892
+ envelope: {
893
+ id: addr.idStem as MemoryId,
894
+ entityId: entityId as EntityId,
895
+ kind: mtmKind,
896
+ tags: [],
897
+ links: [],
898
+ created: 0,
899
+ updated: 0,
900
+ seq: 0,
901
+ contentHash: '',
902
+ provenance: { source: 'agent' }
903
+ },
904
+ body
905
+ };
906
+ (await store.put(rec)).orThrow();
907
+ }
908
+
909
+ function buildMtmOrchestrator(
910
+ store: FileTreeMemoryStore,
911
+ relationExtractor: IRelationExtractor
912
+ ): MemoryIngestOrchestrator {
913
+ return MemoryIngestOrchestrator.create({
914
+ store,
915
+ registry: mtmRegistry(),
916
+ codecs: mtmCodecs,
917
+ classifier: classifier(() => succeed({ kind: mtmKind })),
918
+ // The candidate is a fresh turn-5 under conv-a (distinct stem from the seeds).
919
+ extractor: extractor(() => succeed([candidate(mtmKind, 'conv-a:5', 'a new turn')])),
920
+ relationExtractor
921
+ }).orThrow();
922
+ }
923
+
924
+ async function seededStore(): Promise<FileTreeMemoryStore> {
925
+ const store = FileTreeMemoryStore.create({
926
+ root: mutableRoot(),
927
+ registry: mtmRegistry(),
928
+ codecs: mtmCodecs,
929
+ clock
930
+ }).orThrow();
931
+ await seedTurn(store, 'conv-a:3', 'anchor in conversation a');
932
+ await seedTurn(store, 'conv-b:3', 'anchor in conversation b');
933
+ return store;
934
+ }
935
+
936
+ test('accepts a same-stem edge target that resolves to the correct scope', async () => {
937
+ const store = await seededStore();
938
+ const orch = buildMtmOrchestrator(
939
+ store,
940
+ relater((ctx) =>
941
+ succeed([
942
+ {
943
+ source: srcOf(ctx, 'conv-a:5'),
944
+ edge: { type: 'rel' as LinkType, target: kt('turn-3', 'conversations/conv-a') }
945
+ }
946
+ ])
947
+ )
948
+ );
949
+ expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
950
+ expect(r.records[0].record?.envelope.links).toEqual([
951
+ { type: 'rel', target: kt('turn-3', 'conversations/conv-a') }
952
+ ]);
953
+ });
954
+ });
955
+
956
+ test('rejects a same-stem edge target whose scope has no matching record', async () => {
957
+ const store = await seededStore();
958
+ const orch = buildMtmOrchestrator(
959
+ store,
960
+ relater((ctx) =>
961
+ succeed([
962
+ {
963
+ source: srcOf(ctx, 'conv-a:5'),
964
+ // turn-3 exists under conv-a and conv-b, but NOT conv-z.
965
+ edge: { type: 'rel' as LinkType, target: kt('turn-3', 'conversations/conv-z') }
966
+ }
967
+ ])
968
+ )
969
+ );
970
+ expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
971
+ /edge target 'conversations\/conv-z\/turn-3' resolves to neither a sibling candidate nor an existing record/
972
+ );
802
973
  });
803
974
  });
804
975
  });
@@ -821,13 +992,22 @@ describe('MemoryIngestOrchestrator — contradicts→temporal interlock', () =>
821
992
  store,
822
993
  classifier: classifyFact,
823
994
  extractor: extractor(() => succeed([candidate(factKind, 'fact-1', 'the sky is grey')])),
824
- relationExtractor: relater(() => succeed([candEdge('fact-1', 'contradicts', v1Id)]))
995
+ relationExtractor: relater((ctx) =>
996
+ succeed([
997
+ {
998
+ source: srcOf(ctx, 'fact-1'),
999
+ edge: { type: CONTRADICTS_LINK_TYPE, target: kt(v1Id, 'facts/entities/fact-1') }
1000
+ }
1001
+ ])
1002
+ )
825
1003
  });
826
1004
  expect(await secondOrch.ingestItem({ id: 'fact-1', content: 'the sky is grey' })).toSucceedAndSatisfy(
827
1005
  (r: IIngestItemResult) => {
828
1006
  expect(r.records[0].interlock).toBe('temporal-versioned');
829
1007
  expect(r.records[0].disposition).toBe('written');
830
- expect(r.records[0].record?.envelope.links).toEqual([{ type: CONTRADICTS_LINK_TYPE, target: v1Id }]);
1008
+ expect(r.records[0].record?.envelope.links).toEqual([
1009
+ { type: CONTRADICTS_LINK_TYPE, target: kt(v1Id, 'facts/entities/fact-1') }
1010
+ ]);
831
1011
  }
832
1012
  );
833
1013
 
@@ -854,7 +1034,14 @@ describe('MemoryIngestOrchestrator — contradicts→temporal interlock', () =>
854
1034
  store,
855
1035
  classifier: classifyFact,
856
1036
  extractor: extractor(() => succeed([candidate(factKind, 'fact-1', 'grey')])),
857
- relationExtractor: relater(() => succeed([candEdge('fact-1', 'contradicts', v1Id)]))
1037
+ relationExtractor: relater((ctx) =>
1038
+ succeed([
1039
+ {
1040
+ source: srcOf(ctx, 'fact-1'),
1041
+ edge: { type: CONTRADICTS_LINK_TYPE, target: kt(v1Id, 'facts/entities/fact-1') }
1042
+ }
1043
+ ])
1044
+ )
858
1045
  });
859
1046
  (await o2.ingestItem({ id: 'fact-1', content: 'grey' })).orThrow();
860
1047
 
@@ -1012,7 +1199,7 @@ describe('MemoryIngestOrchestrator — review-punch-list fixes', () => {
1012
1199
  )
1013
1200
  });
1014
1201
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
1015
- expect(r.records[0].record?.envelope.links).toEqual([{ type: 'rel', target: 'doc-a' }]);
1202
+ expect(r.records[0].record?.envelope.links).toEqual([{ type: 'rel', target: kt('doc-a') }]);
1016
1203
  });
1017
1204
  });
1018
1205
 
@@ -1025,7 +1212,9 @@ describe('MemoryIngestOrchestrator — review-punch-list fixes', () => {
1025
1212
  store,
1026
1213
  vectorIndex,
1027
1214
  embed,
1028
- entityResolver: resolver((__c, similar) => succeed({ verdict: 'merge-into', target: similar[0].id })),
1215
+ entityResolver: resolver((__c, similar) =>
1216
+ succeed({ verdict: 'merge-into', target: similar[0].target })
1217
+ ),
1029
1218
  extractor: extractor(() =>
1030
1219
  succeed([candidate(noteKind, 'doc-b', 'apple tart'), candidate(noteKind, 'doc-c', 'apple tart')])
1031
1220
  ),
@@ -1034,7 +1223,7 @@ describe('MemoryIngestOrchestrator — review-punch-list fixes', () => {
1034
1223
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceed();
1035
1224
  expect(await store.get(noteKind, 'doc-a' as EntityId)).toSucceedAndSatisfy(
1036
1225
  (rec: IMemoryRecord<unknown> | undefined) => {
1037
- expect(rec?.envelope.links).toEqual([{ type: 'rel', target: 'doc-x' }]);
1226
+ expect(rec?.envelope.links).toEqual([{ type: 'rel', target: kt('doc-x') }]);
1038
1227
  }
1039
1228
  );
1040
1229
  });
@@ -1072,19 +1261,21 @@ describe('MemoryIngestOrchestrator — merge-into safety (second review)', () =>
1072
1261
  const store = buildStore({ vectorIndex, embed });
1073
1262
  await putFull(store, noteKind, 'doc-a', 'apple pie', {
1074
1263
  tags: ['old-tag'],
1075
- links: [{ type: 'existing' as LinkType, target: 'doc-z' as MemoryId }]
1264
+ links: [{ type: 'existing' as LinkType, target: kt('doc-z') }]
1076
1265
  });
1077
1266
  await putFull(store, noteKind, 'doc-x', 'target of stage-5');
1078
1267
  const orch = buildOrchestrator({
1079
1268
  store,
1080
1269
  vectorIndex,
1081
1270
  embed,
1082
- entityResolver: resolver((__c, similar) => succeed({ verdict: 'merge-into', target: similar[0].id })),
1271
+ entityResolver: resolver((__c, similar) =>
1272
+ succeed({ verdict: 'merge-into', target: similar[0].target })
1273
+ ),
1083
1274
  extractor: extractor(() =>
1084
1275
  succeed([
1085
1276
  candidate(noteKind, 'doc-b', 'apple tart', {
1086
1277
  tags: ['new-tag'],
1087
- links: [{ type: 'candlink' as LinkType, target: 'doc-z' as MemoryId }]
1278
+ links: [{ type: 'candlink' as LinkType, target: kt('doc-z') }]
1088
1279
  })
1089
1280
  ])
1090
1281
  ),
@@ -1099,9 +1290,9 @@ describe('MemoryIngestOrchestrator — merge-into safety (second review)', () =>
1099
1290
  expect([...(rec?.envelope.tags ?? [])].sort()).toEqual(['new-tag', 'old-tag']);
1100
1291
  // Original link + candidate link + stage-5 edge, each exactly once.
1101
1292
  expect(rec?.envelope.links).toEqual([
1102
- { type: 'existing', target: 'doc-z' },
1103
- { type: 'candlink', target: 'doc-z' },
1104
- { type: 'stage5', target: 'doc-x' }
1293
+ { type: 'existing', target: kt('doc-z') },
1294
+ { type: 'candlink', target: kt('doc-z') },
1295
+ { type: 'stage5', target: kt('doc-x') }
1105
1296
  ]);
1106
1297
  }
1107
1298
  );
@@ -1116,11 +1307,13 @@ describe('MemoryIngestOrchestrator — merge-into safety (second review)', () =>
1116
1307
  store,
1117
1308
  vectorIndex,
1118
1309
  embed,
1119
- entityResolver: resolver((__c, similar) => succeed({ verdict: 'merge-into', target: similar[0].id })),
1310
+ entityResolver: resolver((__c, similar) =>
1311
+ succeed({ verdict: 'merge-into', target: similar[0].target })
1312
+ ),
1120
1313
  extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
1121
1314
  });
1122
1315
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
1123
- /merge-into target 'conv-1' is kind 'ltm' but the candidate is kind 'note'/
1316
+ /merge-into target 'conversations\/conv-1' is kind 'ltm' but the candidate is kind 'note'/
1124
1317
  );
1125
1318
  });
1126
1319
 
@@ -1132,11 +1325,11 @@ describe('MemoryIngestOrchestrator — merge-into safety (second review)', () =>
1132
1325
  store,
1133
1326
  vectorIndex,
1134
1327
  embed,
1135
- entityResolver: resolver(() => succeed({ verdict: 'supersede', target: 'ghost' as MemoryId })),
1328
+ entityResolver: resolver(() => succeed({ verdict: 'supersede', target: kt('ghost') })),
1136
1329
  extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
1137
1330
  });
1138
1331
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
1139
- /supersede target 'ghost' does not exist/
1332
+ /supersede target 'knowledge\/ghost' does not exist/
1140
1333
  );
1141
1334
  });
1142
1335
 
@@ -1148,11 +1341,11 @@ describe('MemoryIngestOrchestrator — merge-into safety (second review)', () =>
1148
1341
  store,
1149
1342
  vectorIndex,
1150
1343
  embed,
1151
- entityResolver: resolver(() => succeed({ verdict: 'duplicate-of', target: 'ghost' as MemoryId })),
1344
+ entityResolver: resolver(() => succeed({ verdict: 'duplicate-of', target: kt('ghost') })),
1152
1345
  extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
1153
1346
  });
1154
1347
  expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
1155
- /duplicate-of target 'ghost' does not exist/
1348
+ /duplicate-of target 'knowledge\/ghost' does not exist/
1156
1349
  );
1157
1350
  });
1158
1351
  });