@remnic/core 9.3.653 → 9.3.655

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 (273) hide show
  1. package/dist/access-cli.js +24 -24
  2. package/dist/access-http.d.ts +4 -4
  3. package/dist/access-http.js +17 -17
  4. package/dist/access-mcp.d.ts +4 -4
  5. package/dist/access-mcp.js +16 -16
  6. package/dist/access-schema.d.ts +12 -12
  7. package/dist/{access-service-CdJFd3_b.d.ts → access-service-BEJvriUt.d.ts} +11 -2
  8. package/dist/access-service.d.ts +4 -4
  9. package/dist/access-service.js +15 -15
  10. package/dist/action-confidence.d.ts +1 -1
  11. package/dist/active-memory-bridge.d.ts +1 -1
  12. package/dist/active-recall.d.ts +1 -1
  13. package/dist/active-recall.js +1 -1
  14. package/dist/behavior-learner.d.ts +1 -1
  15. package/dist/behavior-signals.d.ts +1 -1
  16. package/dist/bootstrap.d.ts +3 -3
  17. package/dist/briefing.d.ts +1 -1
  18. package/dist/briefing.js +3 -3
  19. package/dist/buffer-surprise-report.d.ts +1 -1
  20. package/dist/buffer.d.ts +1 -1
  21. package/dist/calibration.d.ts +1 -1
  22. package/dist/causal-behavior.d.ts +1 -1
  23. package/dist/causal-consolidation.d.ts +1 -1
  24. package/dist/causal-consolidation.js +4 -4
  25. package/dist/{chunk-GI45G4BK.js → chunk-2RCGZ67B.js} +4 -4
  26. package/dist/{chunk-BEMWL2FZ.js → chunk-54LOUIBE.js} +2 -2
  27. package/dist/{chunk-E3J6O6N7.js → chunk-55ZMNKMQ.js} +20 -9
  28. package/dist/{chunk-E3J6O6N7.js.map → chunk-55ZMNKMQ.js.map} +1 -1
  29. package/dist/{chunk-7WEB3FLJ.js → chunk-5PLUC5OB.js} +2 -2
  30. package/dist/{chunk-SPMZZUEJ.js → chunk-5QD3QD76.js} +2684 -401
  31. package/dist/chunk-5QD3QD76.js.map +1 -0
  32. package/dist/{chunk-WLGE6KEO.js → chunk-67G4T7KI.js} +3 -3
  33. package/dist/{chunk-JX2RINDR.js → chunk-6G5JEN55.js} +2 -2
  34. package/dist/{chunk-R3PQUPQ4.js → chunk-6IMKOIZ6.js} +85 -3
  35. package/dist/chunk-6IMKOIZ6.js.map +1 -0
  36. package/dist/{chunk-KJDKZVF3.js → chunk-A3Y37UWI.js} +3 -3
  37. package/dist/{chunk-CFOCZPIQ.js → chunk-BGKXTVNG.js} +2 -2
  38. package/dist/{chunk-QQHIQ7JD.js → chunk-COVZLGMR.js} +87 -18
  39. package/dist/chunk-COVZLGMR.js.map +1 -0
  40. package/dist/{chunk-JVRPJ7D4.js → chunk-EKQMQQ3U.js} +48 -12
  41. package/dist/chunk-EKQMQQ3U.js.map +1 -0
  42. package/dist/{chunk-H3PHZLMF.js → chunk-GKKAXVAJ.js} +20 -11
  43. package/dist/chunk-GKKAXVAJ.js.map +1 -0
  44. package/dist/{chunk-JBHXMCYN.js → chunk-GRYAECRV.js} +2 -2
  45. package/dist/{chunk-EHQLDFSH.js → chunk-IQ53ZSXV.js} +2 -2
  46. package/dist/{chunk-C63WC454.js → chunk-KOI765XP.js} +125 -1
  47. package/dist/chunk-KOI765XP.js.map +1 -0
  48. package/dist/{chunk-IVYSVAC6.js → chunk-KZZ4YAEC.js} +2 -2
  49. package/dist/{chunk-2DGQLOOM.js → chunk-M3VYPE2H.js} +1 -1
  50. package/dist/{chunk-2DGQLOOM.js.map → chunk-M3VYPE2H.js.map} +1 -1
  51. package/dist/{chunk-JF7SFXTG.js → chunk-NCSJKK23.js} +2 -2
  52. package/dist/{chunk-XMN6MMTU.js → chunk-NRBGRZW4.js} +2 -2
  53. package/dist/{chunk-NOBL7OUP.js → chunk-OKW6F5S5.js} +12 -5
  54. package/dist/{chunk-NOBL7OUP.js.map → chunk-OKW6F5S5.js.map} +1 -1
  55. package/dist/{chunk-BNFRL6QW.js → chunk-PTMJ2FH2.js} +2 -2
  56. package/dist/{chunk-KWM33SPU.js → chunk-PVE7KSQP.js} +2 -2
  57. package/dist/{chunk-EW52H5EM.js → chunk-QDVQ4AN2.js} +12 -5
  58. package/dist/chunk-QDVQ4AN2.js.map +1 -0
  59. package/dist/{chunk-PYWNNF2I.js → chunk-QRSKPI62.js} +99 -66
  60. package/dist/chunk-QRSKPI62.js.map +1 -0
  61. package/dist/{chunk-YM3LR4LS.js → chunk-SSSXWIBP.js} +5 -5
  62. package/dist/{chunk-C43KEWEV.js → chunk-TDZSSJV4.js} +1 -1
  63. package/dist/chunk-TDZSSJV4.js.map +1 -0
  64. package/dist/{chunk-Y7NWBBHV.js → chunk-TEO46GMM.js} +2 -2
  65. package/dist/{chunk-AJE7FJVE.js → chunk-UCEABZZN.js} +2 -2
  66. package/dist/{chunk-IENGGY2C.js → chunk-UCEDY5M7.js} +2 -2
  67. package/dist/{chunk-PRQXUSQV.js → chunk-UYNFWZWG.js} +2 -2
  68. package/dist/{chunk-V4UDXYGG.js → chunk-WDTUYOLS.js} +2 -2
  69. package/dist/{chunk-RZOBQ23O.js → chunk-XOFXKASO.js} +2 -2
  70. package/dist/chunk-XRKQOQLY.js +212 -0
  71. package/dist/chunk-XRKQOQLY.js.map +1 -0
  72. package/dist/{chunk-WTI35CVJ.js → chunk-YYN3LIYA.js} +5 -5
  73. package/dist/{cli-DDo7Qgs-.d.ts → cli-BGahB_d3.d.ts} +3 -3
  74. package/dist/cli.d.ts +5 -5
  75. package/dist/cli.js +29 -29
  76. package/dist/compounding/engine.d.ts +1 -1
  77. package/dist/compounding/engine.js +3 -3
  78. package/dist/compounding/preference-consolidator.d.ts +1 -1
  79. package/dist/compression-optimizer.d.ts +1 -1
  80. package/dist/config.d.ts +1 -1
  81. package/dist/config.js +1 -1
  82. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  83. package/dist/connectors/codex-materialize-runner.js +3 -3
  84. package/dist/connectors/codex-materialize.d.ts +1 -1
  85. package/dist/connectors/index.d.ts +1 -1
  86. package/dist/connectors/index.js +3 -3
  87. package/dist/consolidation-provenance-check.d.ts +1 -1
  88. package/dist/consolidation-undo.d.ts +1 -1
  89. package/dist/contradiction/index.d.ts +19 -1
  90. package/dist/contradiction/index.js +1 -1
  91. package/dist/conversation-index/backend.d.ts +1 -1
  92. package/dist/conversation-index/chunker.d.ts +1 -1
  93. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  94. package/dist/conversation-index/indexer.d.ts +1 -1
  95. package/dist/conversation-index/search.d.ts +1 -1
  96. package/dist/day-summary.d.ts +1 -1
  97. package/dist/delinearize.d.ts +1 -1
  98. package/dist/direct-answer-wiring.d.ts +1 -1
  99. package/dist/direct-answer.d.ts +1 -1
  100. package/dist/embedding-fallback.d.ts +1 -1
  101. package/dist/enrichment/index.d.ts +1 -1
  102. package/dist/entity-retrieval.d.ts +1 -1
  103. package/dist/entity-retrieval.js +3 -3
  104. package/dist/entity-schema.d.ts +1 -1
  105. package/dist/explicit-capture.d.ts +3 -3
  106. package/dist/explicit-capture.js +1 -1
  107. package/dist/extraction-judge-telemetry.d.ts +1 -1
  108. package/dist/extraction-judge-training.d.ts +1 -1
  109. package/dist/extraction-judge.d.ts +1 -1
  110. package/dist/extraction.d.ts +1 -1
  111. package/dist/fallback-llm.d.ts +1 -1
  112. package/dist/identity-continuity.d.ts +1 -1
  113. package/dist/importance.d.ts +1 -1
  114. package/dist/index.d.ts +8 -8
  115. package/dist/index.js +37 -35
  116. package/dist/index.js.map +1 -1
  117. package/dist/intent.d.ts +1 -1
  118. package/dist/lcm/engine.d.ts +1 -1
  119. package/dist/lcm/index.d.ts +1 -1
  120. package/dist/lcm/tools.d.ts +1 -1
  121. package/dist/lifecycle.d.ts +1 -1
  122. package/dist/live-connectors-runner.d.ts +1 -1
  123. package/dist/local-llm.d.ts +1 -1
  124. package/dist/maintenance/memory-governance.d.ts +1 -1
  125. package/dist/maintenance/memory-governance.js +3 -3
  126. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  127. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  128. package/dist/mcp-memory-inspector-app.d.ts +4 -4
  129. package/dist/memory-action-policy.d.ts +1 -1
  130. package/dist/memory-cache.d.ts +1 -1
  131. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  132. package/dist/memory-projection-store.d.ts +1 -1
  133. package/dist/memory-provenance.d.ts +1 -1
  134. package/dist/memory-worth-outcomes.d.ts +1 -1
  135. package/dist/models-json.d.ts +1 -1
  136. package/dist/namespaces/migrate.d.ts +1 -1
  137. package/dist/namespaces/migrate.js +11 -11
  138. package/dist/namespaces/principal.d.ts +1 -1
  139. package/dist/namespaces/search.d.ts +15 -4
  140. package/dist/namespaces/search.js +7 -7
  141. package/dist/namespaces/storage.d.ts +52 -3
  142. package/dist/namespaces/storage.js +9 -5
  143. package/dist/native-knowledge.d.ts +1 -1
  144. package/dist/operator-toolkit.d.ts +1 -1
  145. package/dist/operator-toolkit.js +14 -14
  146. package/dist/{orchestrator-8fTZsa0y.d.ts → orchestrator-BgzZlWxH.d.ts} +500 -3
  147. package/dist/orchestrator.d.ts +3 -3
  148. package/dist/orchestrator.js +20 -20
  149. package/dist/patterns-cli.d.ts +1 -1
  150. package/dist/policy-runtime.d.ts +1 -1
  151. package/dist/qmd-recall-cache.d.ts +1 -1
  152. package/dist/qmd.d.ts +5 -1
  153. package/dist/qmd.js +2 -2
  154. package/dist/recall-disclosure-escalation.d.ts +1 -1
  155. package/dist/recall-explain-renderer.d.ts +1 -1
  156. package/dist/recall-explain-renderer.js +3 -3
  157. package/dist/recall-planner-llm.d.ts +1 -1
  158. package/dist/recall-state.d.ts +1 -1
  159. package/dist/recall-tag-filter.d.ts +1 -1
  160. package/dist/recall-xray-cli.d.ts +1 -1
  161. package/dist/recall-xray-cli.js +4 -4
  162. package/dist/recall-xray-renderer.d.ts +1 -1
  163. package/dist/recall-xray-renderer.js +3 -3
  164. package/dist/recall-xray.d.ts +1 -1
  165. package/dist/recall-xray.js +2 -2
  166. package/dist/{resolution-3SAP4SH2.js → resolution-IDTEBJFS.js} +2 -2
  167. package/dist/resolve-auth-token.d.ts +1 -1
  168. package/dist/resume-bundles.js +2 -2
  169. package/dist/retrieval-agents.d.ts +1 -1
  170. package/dist/retrieval-tiers.d.ts +1 -1
  171. package/dist/routing/engine.d.ts +1 -1
  172. package/dist/routing/store.d.ts +1 -1
  173. package/dist/schemas.d.ts +22 -22
  174. package/dist/search/embed-helper.d.ts +1 -1
  175. package/dist/search/factory.d.ts +1 -1
  176. package/dist/search/factory.js +6 -6
  177. package/dist/search/index.d.ts +1 -1
  178. package/dist/search/index.js +6 -6
  179. package/dist/search/lancedb-backend.d.ts +1 -1
  180. package/dist/search/lancedb-backend.js +2 -2
  181. package/dist/search/meilisearch-backend.d.ts +1 -1
  182. package/dist/search/meilisearch-backend.js +2 -2
  183. package/dist/search/noop-backend.d.ts +1 -1
  184. package/dist/search/orama-backend.d.ts +1 -1
  185. package/dist/search/orama-backend.js +2 -2
  186. package/dist/search/port.d.ts +17 -1
  187. package/dist/search/port.js +1 -1
  188. package/dist/search/remote-backend.d.ts +1 -1
  189. package/dist/{semantic-consolidation-DKdYzQOg.d.ts → semantic-consolidation-Z8d_uMq8.d.ts} +1 -1
  190. package/dist/semantic-consolidation.d.ts +2 -2
  191. package/dist/semantic-consolidation.js +4 -4
  192. package/dist/semantic-rule-promotion.js +3 -3
  193. package/dist/semantic-rule-verifier.d.ts +1 -1
  194. package/dist/semantic-rule-verifier.js +3 -3
  195. package/dist/session-observer-bands.d.ts +1 -1
  196. package/dist/session-observer-state.d.ts +1 -1
  197. package/dist/shared-context/manager.d.ts +1 -1
  198. package/dist/signal.d.ts +1 -1
  199. package/dist/storage.d.ts +1 -1
  200. package/dist/storage.js +2 -2
  201. package/dist/summarizer.d.ts +1 -1
  202. package/dist/summary-snapshot.d.ts +1 -1
  203. package/dist/temporal-supersession.d.ts +1 -1
  204. package/dist/temporal-validity.d.ts +1 -1
  205. package/dist/threading.d.ts +1 -1
  206. package/dist/tier-migration.d.ts +1 -1
  207. package/dist/tier-routing.d.ts +1 -1
  208. package/dist/topics.d.ts +1 -1
  209. package/dist/transcript.d.ts +1 -1
  210. package/dist/transfer/types.d.ts +12 -12
  211. package/dist/{types-D8yUmSik.d.ts → types-2OPlQWJG.d.ts} +23 -0
  212. package/dist/types.d.ts +1 -1
  213. package/dist/types.js +1 -1
  214. package/dist/utility-runtime.d.ts +1 -1
  215. package/dist/verified-recall.js +3 -3
  216. package/package.json +1 -1
  217. package/src/access-http.ts +7 -0
  218. package/src/access-mcp.ts +7 -0
  219. package/src/access-service.ts +12 -0
  220. package/src/cli.ts +104 -0
  221. package/src/config.test.ts +109 -0
  222. package/src/config.ts +164 -0
  223. package/src/contradiction/contradiction.test.ts +284 -0
  224. package/src/contradiction/resolution.ts +151 -4
  225. package/src/explicit-capture.ts +31 -10
  226. package/src/index.ts +10 -0
  227. package/src/maintenance/namespace-planner.test.ts +1120 -0
  228. package/src/maintenance/namespace-planner.ts +893 -0
  229. package/src/namespaces/catalog.test.ts +3356 -0
  230. package/src/namespaces/catalog.ts +2123 -0
  231. package/src/namespaces/search.test.ts +130 -2
  232. package/src/namespaces/search.ts +71 -10
  233. package/src/namespaces/storage.ts +210 -30
  234. package/src/orchestrator-flush.test.ts +720 -0
  235. package/src/orchestrator.ts +881 -239
  236. package/src/qmd-client.test.ts +59 -0
  237. package/src/qmd.ts +124 -84
  238. package/src/search/port.ts +16 -0
  239. package/src/types.ts +23 -0
  240. package/dist/chunk-C43KEWEV.js.map +0 -1
  241. package/dist/chunk-C63WC454.js.map +0 -1
  242. package/dist/chunk-EW52H5EM.js.map +0 -1
  243. package/dist/chunk-H3PHZLMF.js.map +0 -1
  244. package/dist/chunk-JVRPJ7D4.js.map +0 -1
  245. package/dist/chunk-ORGWWNJG.js +0 -131
  246. package/dist/chunk-ORGWWNJG.js.map +0 -1
  247. package/dist/chunk-PYWNNF2I.js.map +0 -1
  248. package/dist/chunk-QQHIQ7JD.js.map +0 -1
  249. package/dist/chunk-R3PQUPQ4.js.map +0 -1
  250. package/dist/chunk-SPMZZUEJ.js.map +0 -1
  251. /package/dist/{chunk-GI45G4BK.js.map → chunk-2RCGZ67B.js.map} +0 -0
  252. /package/dist/{chunk-BEMWL2FZ.js.map → chunk-54LOUIBE.js.map} +0 -0
  253. /package/dist/{chunk-7WEB3FLJ.js.map → chunk-5PLUC5OB.js.map} +0 -0
  254. /package/dist/{chunk-WLGE6KEO.js.map → chunk-67G4T7KI.js.map} +0 -0
  255. /package/dist/{chunk-JX2RINDR.js.map → chunk-6G5JEN55.js.map} +0 -0
  256. /package/dist/{chunk-KJDKZVF3.js.map → chunk-A3Y37UWI.js.map} +0 -0
  257. /package/dist/{chunk-CFOCZPIQ.js.map → chunk-BGKXTVNG.js.map} +0 -0
  258. /package/dist/{chunk-JBHXMCYN.js.map → chunk-GRYAECRV.js.map} +0 -0
  259. /package/dist/{chunk-EHQLDFSH.js.map → chunk-IQ53ZSXV.js.map} +0 -0
  260. /package/dist/{chunk-IVYSVAC6.js.map → chunk-KZZ4YAEC.js.map} +0 -0
  261. /package/dist/{chunk-JF7SFXTG.js.map → chunk-NCSJKK23.js.map} +0 -0
  262. /package/dist/{chunk-XMN6MMTU.js.map → chunk-NRBGRZW4.js.map} +0 -0
  263. /package/dist/{chunk-BNFRL6QW.js.map → chunk-PTMJ2FH2.js.map} +0 -0
  264. /package/dist/{chunk-KWM33SPU.js.map → chunk-PVE7KSQP.js.map} +0 -0
  265. /package/dist/{chunk-YM3LR4LS.js.map → chunk-SSSXWIBP.js.map} +0 -0
  266. /package/dist/{chunk-Y7NWBBHV.js.map → chunk-TEO46GMM.js.map} +0 -0
  267. /package/dist/{chunk-AJE7FJVE.js.map → chunk-UCEABZZN.js.map} +0 -0
  268. /package/dist/{chunk-IENGGY2C.js.map → chunk-UCEDY5M7.js.map} +0 -0
  269. /package/dist/{chunk-PRQXUSQV.js.map → chunk-UYNFWZWG.js.map} +0 -0
  270. /package/dist/{chunk-V4UDXYGG.js.map → chunk-WDTUYOLS.js.map} +0 -0
  271. /package/dist/{chunk-RZOBQ23O.js.map → chunk-XOFXKASO.js.map} +0 -0
  272. /package/dist/{chunk-WTI35CVJ.js.map → chunk-YYN3LIYA.js.map} +0 -0
  273. /package/dist/{resolution-3SAP4SH2.js.map → resolution-IDTEBJFS.js.map} +0 -0
@@ -13,6 +13,12 @@ type CollectionState = "present" | "missing" | "unknown" | "skipped";
13
13
 
14
14
  class FakeBackend implements SearchBackend {
15
15
  updates = 0;
16
+ strictUpdates = 0;
17
+ strictCollectionUpdates: string[] = [];
18
+ embeds = 0;
19
+ collectionEmbeds: string[] = [];
20
+ strictEmbeds = 0;
21
+ strictCollectionEmbeds: string[] = [];
16
22
  disposed = 0;
17
23
  available = true;
18
24
  calls: Array<{
@@ -130,15 +136,35 @@ class FakeBackend implements SearchBackend {
130
136
  this.updates += 1;
131
137
  }
132
138
 
139
+ async updateStrict(): Promise<void> {
140
+ this.strictUpdates += 1;
141
+ }
142
+
133
143
  async updateCollection(): Promise<void> {}
134
144
 
145
+ async updateCollectionStrict(collection: string): Promise<void> {
146
+ this.strictCollectionUpdates.push(collection);
147
+ }
148
+
135
149
  updatesAllCollections(): boolean {
136
150
  return this.globalUpdate;
137
151
  }
138
152
 
139
- async embed(): Promise<void> {}
153
+ async embed(): Promise<void> {
154
+ this.embeds += 1;
155
+ }
156
+
157
+ async embedStrict(): Promise<void> {
158
+ this.strictEmbeds += 1;
159
+ }
140
160
 
141
- async embedCollection(): Promise<void> {}
161
+ async embedCollection(collection: string): Promise<void> {
162
+ this.collectionEmbeds.push(collection);
163
+ }
164
+
165
+ async embedCollectionStrict(collection: string): Promise<void> {
166
+ this.strictCollectionEmbeds.push(collection);
167
+ }
142
168
 
143
169
  async ensureCollection(
144
170
  _memoryDir?: string,
@@ -253,6 +279,108 @@ test("updateNamespaces still updates every namespace for scoped backends", async
253
279
  assert.equal(created.reduce((sum, backend) => sum + backend.updates, 0), 3);
254
280
  });
255
281
 
282
+ test("updateNamespaces uses strict global update when requested", async () => {
283
+ const created: FakeBackend[] = [];
284
+ const router = new NamespaceSearchRouter(
285
+ config(),
286
+ { storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
287
+ () => {
288
+ const backend = new FakeBackend(true);
289
+ created.push(backend);
290
+ return backend;
291
+ },
292
+ );
293
+
294
+ const updated = await router.updateNamespaces(
295
+ ["main", "shared", "main", "project"],
296
+ undefined,
297
+ { strict: true },
298
+ );
299
+
300
+ assert.equal(updated, 1);
301
+ assert.equal(created.reduce((sum, backend) => sum + backend.strictUpdates, 0), 1);
302
+ assert.equal(created.reduce((sum, backend) => sum + backend.updates, 0), 0);
303
+ });
304
+
305
+ test("updateNamespaces uses strict collection updates for scoped backends when requested", async () => {
306
+ const created: FakeBackend[] = [];
307
+ const router = new NamespaceSearchRouter(
308
+ config(),
309
+ { storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
310
+ () => {
311
+ const backend = new FakeBackend(false);
312
+ created.push(backend);
313
+ return backend;
314
+ },
315
+ );
316
+
317
+ const updated = await router.updateNamespaces(
318
+ ["main", "shared", "main", "project"],
319
+ undefined,
320
+ { strict: true },
321
+ );
322
+
323
+ assert.equal(updated, 3);
324
+ assert.equal(created.reduce((sum, backend) => sum + backend.strictCollectionUpdates.length, 0), 3);
325
+ assert.equal(created.reduce((sum, backend) => sum + backend.updates, 0), 0);
326
+ });
327
+
328
+ test("embedNamespaces uses strict collection embeds when requested", async () => {
329
+ const created: FakeBackend[] = [];
330
+ const router = new NamespaceSearchRouter(
331
+ config(),
332
+ { storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
333
+ () => {
334
+ const backend = new FakeBackend(false);
335
+ created.push(backend);
336
+ return backend;
337
+ },
338
+ );
339
+
340
+ await router.embedNamespaces(["main", "shared", "main", "project"], { strict: true });
341
+
342
+ assert.equal(created.reduce((sum, backend) => sum + backend.strictCollectionEmbeds.length, 0), 3);
343
+ assert.equal(created.reduce((sum, backend) => sum + backend.collectionEmbeds.length, 0), 0);
344
+ assert.equal(created.reduce((sum, backend) => sum + backend.embeds, 0), 0);
345
+ });
346
+
347
+ test("embedNamespaces propagates strict embed failures", async () => {
348
+ const router = new NamespaceSearchRouter(
349
+ config(),
350
+ { storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
351
+ () => {
352
+ const backend = new FakeBackend(false);
353
+ backend.embedCollectionStrict = async () => {
354
+ throw new Error("embed failed");
355
+ };
356
+ return backend;
357
+ },
358
+ );
359
+
360
+ await assert.rejects(
361
+ () => router.embedNamespaces(["main"], { strict: true }),
362
+ /embed failed/,
363
+ );
364
+ });
365
+
366
+ test("updateNamespacesDetailed reports only eligible namespaces", async () => {
367
+ const router = new NamespaceSearchRouter(
368
+ config(),
369
+ { storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
370
+ (scopedConfig) => {
371
+ const backend = new FakeBackend(false, [], {
372
+ ensure: scopedConfig.memoryDir.endsWith("/missing") ? "missing" : "present",
373
+ });
374
+ return backend;
375
+ },
376
+ );
377
+
378
+ const result = await router.updateNamespacesDetailed(["main", "missing", "shared"]);
379
+
380
+ assert.equal(result.backendCount, 2);
381
+ assert.deepEqual(result.eligibleNamespaces.sort(), ["main", "shared"]);
382
+ });
383
+
256
384
  test("searchAcrossNamespaces preserves same path results from distinct namespaces", async () => {
257
385
  const router = new NamespaceSearchRouter(
258
386
  config(),
@@ -44,6 +44,11 @@ type NamespaceBackendRecord = {
44
44
  filtersNestedNamespaces: boolean;
45
45
  };
46
46
 
47
+ export interface NamespaceUpdateResult {
48
+ backendCount: number;
49
+ eligibleNamespaces: string[];
50
+ }
51
+
47
52
  export type CollectionState = "present" | "missing" | "unknown" | "skipped";
48
53
 
49
54
  export interface NamespaceSearchHealth {
@@ -161,37 +166,51 @@ export class NamespaceSearchRouter {
161
166
  async updateNamespaces(
162
167
  namespaces: string[],
163
168
  execution?: SearchExecutionOptions,
169
+ options?: { strict?: boolean },
164
170
  ): Promise<number> {
171
+ return (await this.updateNamespacesDetailed(namespaces, execution, options)).backendCount;
172
+ }
173
+
174
+ async updateNamespacesDetailed(
175
+ namespaces: string[],
176
+ execution?: SearchExecutionOptions,
177
+ options?: { strict?: boolean },
178
+ ): Promise<NamespaceUpdateResult> {
165
179
  const unique = Array.from(new Set(namespaces.map((value) => value.trim()).filter(Boolean)));
166
180
  const eligible = (await Promise.all(
167
181
  unique.map(async (namespace) => {
168
182
  const record = await this.backendRecordFor(namespace);
169
183
  return record.available && record.collectionState !== "missing"
170
- ? record
184
+ ? { namespace, record }
171
185
  : null;
172
186
  }),
173
- )).filter((record): record is NamespaceBackendRecord => record !== null);
187
+ )).filter((entry): entry is { namespace: string; record: NamespaceBackendRecord } => entry !== null);
174
188
 
175
- const globalRecord = eligible.find((record) => record.backend.updatesAllCollections?.() === true);
176
- const scopedRecords = globalRecord
177
- ? eligible.filter((record) => record.backend.updatesAllCollections?.() !== true)
189
+ const globalEntry = eligible.find(({ record }) => record.backend.updatesAllCollections?.() === true);
190
+ const scopedEntries = globalEntry
191
+ ? eligible.filter(({ record }) => record.backend.updatesAllCollections?.() !== true)
178
192
  : eligible;
179
193
 
180
194
  await Promise.all([
181
- globalRecord ? globalRecord.backend.update(execution) : Promise.resolve(),
182
- ...scopedRecords.map((record) => record.backend.update(execution)),
195
+ globalEntry
196
+ ? updateBackendRecord(globalEntry.record, execution, options)
197
+ : Promise.resolve(),
198
+ ...scopedEntries.map(({ record }) => updateBackendRecord(record, execution, options)),
183
199
  ]);
184
200
 
185
- return (globalRecord ? 1 : 0) + scopedRecords.length;
201
+ return {
202
+ backendCount: (globalEntry ? 1 : 0) + scopedEntries.length,
203
+ eligibleNamespaces: eligible.map(({ namespace }) => namespace),
204
+ };
186
205
  }
187
206
 
188
- async embedNamespaces(namespaces: string[]): Promise<void> {
207
+ async embedNamespaces(namespaces: string[], options?: { strict?: boolean }): Promise<void> {
189
208
  const unique = Array.from(new Set(namespaces.map((value) => value.trim()).filter(Boolean)));
190
209
  await Promise.all(
191
210
  unique.map(async (namespace) => {
192
211
  const record = await this.backendRecordFor(namespace);
193
212
  if (!record.available || record.collectionState === "missing") return;
194
- await record.backend.embed();
213
+ await embedBackendRecord(record, options);
195
214
  }),
196
215
  );
197
216
  }
@@ -442,6 +461,48 @@ function backendSearchLimit(
442
461
  );
443
462
  }
444
463
 
464
+ async function updateBackendRecord(
465
+ record: NamespaceBackendRecord,
466
+ execution?: SearchExecutionOptions,
467
+ options?: { strict?: boolean },
468
+ ): Promise<void> {
469
+ if (options?.strict === true) {
470
+ if (
471
+ record.backend.updatesAllCollections?.() === true &&
472
+ typeof record.backend.updateStrict === "function"
473
+ ) {
474
+ await record.backend.updateStrict(execution);
475
+ return;
476
+ }
477
+ if (typeof record.backend.updateCollectionStrict === "function") {
478
+ await record.backend.updateCollectionStrict(record.collection, execution);
479
+ return;
480
+ }
481
+ }
482
+ await record.backend.update(execution);
483
+ }
484
+
485
+ async function embedBackendRecord(
486
+ record: NamespaceBackendRecord,
487
+ options?: { strict?: boolean },
488
+ ): Promise<void> {
489
+ if (options?.strict === true) {
490
+ if (typeof record.backend.embedCollectionStrict === "function") {
491
+ await record.backend.embedCollectionStrict(record.collection);
492
+ return;
493
+ }
494
+ if (typeof record.backend.embedStrict === "function") {
495
+ await record.backend.embedStrict();
496
+ return;
497
+ }
498
+ }
499
+ if (typeof record.backend.embedCollection === "function") {
500
+ await record.backend.embedCollection(record.collection);
501
+ return;
502
+ }
503
+ await record.backend.embed();
504
+ }
505
+
445
506
  function daemonModeForBackend(backend: SearchBackend): boolean | null {
446
507
  return "isDaemonMode" in backend && typeof backend.isDaemonMode === "function"
447
508
  ? backend.isDaemonMode() === true
@@ -104,52 +104,156 @@ async function hasAnyNamespaceStorageMarker(
104
104
  * This avoids surprising "lost memories" when an install flips namespaces on without
105
105
  * migrating existing data.
106
106
  */
107
+ /**
108
+ * Optional hooks for the storage router. `onResolve` fires whenever a namespace's
109
+ * storage is resolved/created, so a downstream consumer (e.g. the namespace
110
+ * catalog, issue #1499) can register the namespace. The hook MUST NOT throw into
111
+ * the router; the router invokes it defensively and a hook failure never affects
112
+ * storage resolution.
113
+ *
114
+ * The hook MAY return (or resolve to) a boolean indicating whether the
115
+ * registration actually PERSISTED (round 6, codex P2 — NEFoX). When it resolves
116
+ * to `false` (a dropped/no-op registration), the router does NOT mark the
117
+ * (namespace, storageDir) pair as notified, so the next resolve RETRIES it
118
+ * instead of suppressing it forever. A `void`/`undefined` result is treated as
119
+ * success (legacy hooks).
120
+ */
121
+ export interface NamespaceStorageRouterHooks {
122
+ onResolve?: (
123
+ namespace: string,
124
+ storageDir: string,
125
+ ) => void | boolean | Promise<void | boolean>;
126
+ }
127
+
128
+ /**
129
+ * Resolve the runtime storage root for the configured DEFAULT namespace.
130
+ *
131
+ * Shared between the live router (`NamespaceStorageRouter.defaultNamespaceRoot`)
132
+ * and the rebuildable catalog (`NamespaceCatalog.rebuildFromDisk`) so the two
133
+ * can never diverge (CLAUDE.md rule #22/#42 — read & write paths resolve through
134
+ * the same logic). The contract is: while legacy memory data still lives
135
+ * directly under `memoryDir`, the default root stays `memoryDir`; only once the
136
+ * legacy root is empty and a `namespaces/<default|token>` dir holds data does
137
+ * the default migrate into that tokenized/legacy-named dir.
138
+ */
139
+ export async function resolveDefaultNamespaceRoot(config: PluginConfig): Promise<string> {
140
+ if (!config.namespacesEnabled) {
141
+ return config.memoryDir;
142
+ }
143
+
144
+ // Build the legacy default root from the NORMALIZED (trimmed) name so a
145
+ // whitespace-padded `defaultNamespace` still finds the live `namespaces/default`
146
+ // root (NIabe). `storageFor()` classifies the trimmed value as the default, and
147
+ // the on-disk legacy dir is created under the trimmed name; using the raw spaced
148
+ // name here would look for `namespaces/<spaced>` and miss the real root, falling
149
+ // back to memoryDir/tokenized. `namespaceIdentityToken` already normalizes
150
+ // internally, so the tokenized path is unaffected.
151
+ const defaultIdentity = normalizeNamespaceIdentity(config.defaultNamespace);
152
+ const legacyNsDir = resolveNamespaceDir(config.memoryDir, defaultIdentity);
153
+ const tokenizedNsDir = resolveNamespaceDir(
154
+ config.memoryDir,
155
+ namespaceIdentityToken(config.defaultNamespace),
156
+ );
157
+ const tokenizedHasData =
158
+ (await exists(tokenizedNsDir)) &&
159
+ (await hasAnyNamespaceStorageMarker(tokenizedNsDir, { includeRuntimeState: true }));
160
+ const nsDir = tokenizedHasData
161
+ ? tokenizedNsDir
162
+ : (await exists(legacyNsDir))
163
+ ? legacyNsDir
164
+ : tokenizedNsDir;
165
+ return (await exists(nsDir)) && !(await hasAnyLegacyData(config.memoryDir))
166
+ ? nsDir
167
+ : config.memoryDir;
168
+ }
169
+
170
+ /**
171
+ * Resolve the runtime storage root for ANY namespace exactly as the live router
172
+ * would (`NamespaceStorageRouter.namespaceRoot`). Shared so the rebuildable
173
+ * catalog records the SAME on-disk root the router routes to — a recall/read
174
+ * touch must not guess `namespaces/<token>` when the router actually serves a
175
+ * legacy raw-name dir or a migrated default root (CLAUDE.md rule #22/#42; round
176
+ * 4, cursor Medium). The default namespace delegates to `resolveDefaultNamespaceRoot`;
177
+ * every other namespace prefers the tokenized root when it has a storage marker,
178
+ * else a legacy raw-name dir when present, else the tokenized root.
179
+ */
180
+ export async function resolveNamespaceStorageRoot(
181
+ config: PluginConfig,
182
+ namespace: string,
183
+ ): Promise<string> {
184
+ if (!config.namespacesEnabled) return config.memoryDir;
185
+ // Compare on NORMALIZED identity so a whitespace-padded configured default name
186
+ // still routes to the default root rather than a tokenized non-default dir
187
+ // (NH-FH). The catalog keys records by the same normalized identity.
188
+ if (normalizeNamespaceIdentity(namespace) === normalizeNamespaceIdentity(config.defaultNamespace)) {
189
+ return resolveDefaultNamespaceRoot(config);
190
+ }
191
+ const legacyRoot = resolveNamespaceDir(config.memoryDir, namespace);
192
+ const tokenizedRoot = resolveNamespaceDir(config.memoryDir, namespaceIdentityToken(namespace));
193
+ if (
194
+ (await exists(tokenizedRoot)) &&
195
+ (await hasAnyNamespaceStorageMarker(tokenizedRoot, { includeRuntimeState: true }))
196
+ ) {
197
+ return tokenizedRoot;
198
+ }
199
+ return (await exists(legacyRoot)) ? legacyRoot : tokenizedRoot;
200
+ }
201
+
107
202
  export class NamespaceStorageRouter {
108
203
  private readonly cache = new Map<string, StorageManager>();
109
204
  private defaultNsRootResolved: string | null = null;
205
+ // Dedup the resolve hook (round 6, cursor Medium — NCNL2). Recall/extraction
206
+ // call `storageFor` repeatedly; firing `onResolve` (→ catalog loadCompacted +
207
+ // append) on every cache hit grows `namespaces.jsonl` without bound between
208
+ // rebuilds. We fire the hook only when the (namespace, storageDir) pair is new
209
+ // or its dir changed, so a steady-state cache hit is a no-op for the catalog.
210
+ private readonly notifiedResolved = new Map<string, string>();
211
+ // In-flight resolve-hook dedup (NFJV-, codex P2). The catalog's `onResolve`
212
+ // hook is ASYNC (it returns `registerResolved(...)`), so `notifiedResolved` is
213
+ // only set after the hook's promise SETTLES. Without tracking the in-flight
214
+ // window, a burst of `storageFor()` cache hits for the SAME namespace before
215
+ // the first registration finishes would each pass the `notifiedResolved` guard
216
+ // and fire their OWN `onResolve` — queueing N duplicate catalog touches + lock
217
+ // acquisitions despite the once-per-namespace intent. We therefore record the
218
+ // (namespace → storageDir) being registered BEFORE awaiting the hook so a
219
+ // concurrent call for the same pair skips firing. On SUCCESS the pair is
220
+ // promoted to `notifiedResolved` (future calls skip permanently); on `false`
221
+ // (dropped touch — e.g. rebuild-lock timeout) OR rejection the in-flight marker
222
+ // is CLEARED so a later `storageFor()` can RETRY the dropped registration. The
223
+ // entry is always removed when the promise settles, so the map cannot grow
224
+ // unbounded (one transient entry per concurrently-resolving namespace).
225
+ private readonly inFlightResolved = new Map<string, string>();
110
226
 
111
- constructor(private readonly config: PluginConfig) {}
227
+ // Normalized (trimmed) default namespace identity (NH-FH). `storageFor`
228
+ // normalizes its input, so default-namespace branches must compare against the
229
+ // normalized config default too — otherwise a whitespace-padded configured
230
+ // default name routes the default namespace to a tokenized non-default root.
231
+ private readonly defaultNamespaceIdentity: string;
112
232
 
113
- private async defaultNamespaceRoot(): Promise<string> {
114
- if (!this.config.namespacesEnabled) {
115
- this.defaultNsRootResolved = this.config.memoryDir;
116
- return this.defaultNsRootResolved;
117
- }
233
+ constructor(
234
+ private readonly config: PluginConfig,
235
+ private readonly hooks: NamespaceStorageRouterHooks = {},
236
+ ) {
237
+ this.defaultNamespaceIdentity = normalizeNamespaceIdentity(config.defaultNamespace);
238
+ }
118
239
 
119
- const legacyNsDir = resolveNamespaceDir(this.config.memoryDir, this.config.defaultNamespace);
120
- const tokenizedNsDir = resolveNamespaceDir(
121
- this.config.memoryDir,
122
- namespaceIdentityToken(this.config.defaultNamespace),
123
- );
124
- const tokenizedHasData =
125
- (await exists(tokenizedNsDir)) && (await hasAnyNamespaceStorageMarker(tokenizedNsDir, { includeRuntimeState: true }));
126
- const nsDir = tokenizedHasData
127
- ? tokenizedNsDir
128
- : (await exists(legacyNsDir)) ? legacyNsDir : tokenizedNsDir;
129
- this.defaultNsRootResolved =
130
- (await exists(nsDir)) && !(await hasAnyLegacyData(this.config.memoryDir))
131
- ? nsDir
132
- : this.config.memoryDir;
240
+ private async defaultNamespaceRoot(): Promise<string> {
241
+ this.defaultNsRootResolved = await resolveDefaultNamespaceRoot(this.config);
133
242
  return this.defaultNsRootResolved;
134
243
  }
135
244
 
136
245
  private async namespaceRoot(namespace: string): Promise<string> {
137
246
  // NOTE: only used after defaultNamespaceRoot() resolution.
138
247
  if (!this.config.namespacesEnabled) return this.config.memoryDir;
139
- if (namespace === this.config.defaultNamespace) {
248
+ if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {
140
249
  return this.defaultNsRootResolved ?? this.config.memoryDir;
141
250
  }
142
- const legacyRoot = resolveNamespaceDir(this.config.memoryDir, namespace);
143
- const tokenizedRoot = resolveNamespaceDir(this.config.memoryDir, namespaceIdentityToken(namespace));
144
- if ((await exists(tokenizedRoot)) && (await hasAnyNamespaceStorageMarker(tokenizedRoot, { includeRuntimeState: true }))) {
145
- return tokenizedRoot;
146
- }
147
- return (await exists(legacyRoot)) ? legacyRoot : tokenizedRoot;
251
+ return resolveNamespaceStorageRoot(this.config, namespace);
148
252
  }
149
253
 
150
254
  async storageFor(namespace: string): Promise<StorageManager> {
151
255
  const ns = normalizeNamespaceIdentity(namespace || this.config.defaultNamespace);
152
- if (ns !== this.config.defaultNamespace && !isSafeRouteNamespace(ns)) {
256
+ if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
153
257
  throw new Error(`unsafe namespace: ${ns}`);
154
258
  }
155
259
  // Even when the default namespace is exempt from the check above, every
@@ -158,16 +262,20 @@ export class NamespaceStorageRouter {
158
262
  // <memoryDir>/namespaces (CodeQL js/path-injection).
159
263
 
160
264
  let root: string;
161
- if (ns === this.config.defaultNamespace) {
265
+ if (ns === this.defaultNamespaceIdentity) {
162
266
  root = await this.defaultNamespaceRoot();
163
267
  const cached = this.cache.get(ns);
164
268
  if (cached && cached.dir === root) {
269
+ this.notifyResolved(ns, root);
165
270
  return cached;
166
271
  }
167
272
  } else {
168
273
  const cached = this.cache.get(ns);
169
274
  root = await this.namespaceRoot(ns);
170
- if (cached && cached.dir === root) return cached;
275
+ if (cached && cached.dir === root) {
276
+ this.notifyResolved(ns, root);
277
+ return cached;
278
+ }
171
279
  }
172
280
 
173
281
  const sm = new StorageManager(root, this.config.entitySchemas);
@@ -176,6 +284,78 @@ export class NamespaceStorageRouter {
176
284
  // matching the behaviour of the primary this.storage instance in the orchestrator.
177
285
  sm.citationTemplate = this.config.inlineSourceAttributionFormat;
178
286
  this.cache.set(ns, sm);
287
+ this.notifyResolved(ns, root);
179
288
  return sm;
180
289
  }
290
+
291
+ /**
292
+ * Fire the resolve hook defensively. A hook failure (e.g. a catalog write
293
+ * error) MUST NOT crash storage resolution — see CLAUDE.md gotcha #13.
294
+ */
295
+ private notifyResolved(namespace: string, storageDir: string): void {
296
+ const hook = this.hooks.onResolve;
297
+ if (!hook) return;
298
+ // Skip when we've already SUCCESSFULLY notified this exact (namespace,
299
+ // storageDir) — a steady-state cache hit must not re-append to the catalog
300
+ // log (NCNL2). A changed dir (rare: migration/realignment) still re-fires
301
+ // once. We mark the pair as notified ONLY AFTER the hook succeeds, and CLEAR
302
+ // it on failure, so a dropped registration (e.g. rebuild-lock timeout) is
303
+ // RETRIED on the next cache hit instead of being suppressed forever (round 6,
304
+ // cursor Medium — ND3EJ).
305
+ if (this.notifiedResolved.get(namespace) === storageDir) return;
306
+ // In-flight dedup (NFJV-, codex P2): if a registration for this exact
307
+ // (namespace, storageDir) is already AWAITING its async hook, do not fire a
308
+ // second one. Without this, concurrent cache-hit bursts before the first
309
+ // append settles each pass the `notifiedResolved` guard above and queue
310
+ // duplicate catalog touches/lock acquisitions. A pair with a DIFFERENT
311
+ // in-flight dir (rare mid-migration realignment) still fires once.
312
+ if (this.inFlightResolved.get(namespace) === storageDir) return;
313
+ try {
314
+ // Handle BOTH synchronous throws and asynchronous rejections (round 6,
315
+ // codex P2 — NDo8C). The hook may be `async`; its rejected promise would
316
+ // bypass this try/catch and, where unhandled rejections are fatal, crash
317
+ // storage resolution. Mark the dedup pair as notified ONLY when the hook
318
+ // resolves to a PERSISTED result (round 6, codex P2 — NEFoX): a result of
319
+ // `false` means the registration was dropped/no-op (e.g. rebuild-lock
320
+ // timeout), so we must NOT suppress its retry. `void`/`undefined` is treated
321
+ // as success for legacy hooks. On rejection we leave it un-notified to retry.
322
+ //
323
+ // Record the in-flight marker BEFORE awaiting so concurrent calls for the
324
+ // same pair skip (NFJV-). It is always cleared once the promise settles, so
325
+ // the map holds at most one transient entry per concurrently-resolving
326
+ // namespace and cannot grow unbounded.
327
+ this.inFlightResolved.set(namespace, storageDir);
328
+ Promise.resolve(hook(namespace, storageDir)).then(
329
+ (persisted) => {
330
+ // Clear the in-flight marker ONLY if it is still ours (a newer resolve
331
+ // for a different dir may have replaced it).
332
+ if (this.inFlightResolved.get(namespace) === storageDir) {
333
+ this.inFlightResolved.delete(namespace);
334
+ }
335
+ if (persisted !== false) {
336
+ this.notifiedResolved.set(namespace, storageDir);
337
+ }
338
+ // On `false` (dropped touch) we intentionally do NOT mark notified, so
339
+ // a later `storageFor()` retries the registration. Clearing the
340
+ // in-flight marker above is what re-enables that retry.
341
+ },
342
+ () => {
343
+ // Registration failed — clear in-flight AND do NOT mark as notified, so
344
+ // it is retried on the next cache hit.
345
+ if (this.inFlightResolved.get(namespace) === storageDir) {
346
+ this.inFlightResolved.delete(namespace);
347
+ }
348
+ if (this.notifiedResolved.get(namespace) === storageDir) {
349
+ this.notifiedResolved.delete(namespace);
350
+ }
351
+ },
352
+ );
353
+ } catch {
354
+ // Synchronous throw: clear any in-flight marker we just set and leave the
355
+ // pair un-notified so a later resolve retries.
356
+ if (this.inFlightResolved.get(namespace) === storageDir) {
357
+ this.inFlightResolved.delete(namespace);
358
+ }
359
+ }
360
+ }
181
361
  }