@remnic/core 9.3.653 → 9.3.654

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 (240) hide show
  1. package/dist/access-cli.js +17 -17
  2. package/dist/access-http.d.ts +4 -4
  3. package/dist/access-http.js +10 -10
  4. package/dist/access-mcp.d.ts +4 -4
  5. package/dist/access-mcp.js +9 -9
  6. package/dist/access-schema.d.ts +12 -12
  7. package/dist/{access-service-CdJFd3_b.d.ts → access-service-C8A5hoXJ.d.ts} +11 -2
  8. package/dist/access-service.d.ts +4 -4
  9. package/dist/access-service.js +8 -8
  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-KJDKZVF3.js → chunk-2DSTAWNZ.js} +3 -3
  26. package/dist/chunk-3RACUBII.js +212 -0
  27. package/dist/chunk-3RACUBII.js.map +1 -0
  28. package/dist/{chunk-Y7NWBBHV.js → chunk-6CVI6BP6.js} +2 -2
  29. package/dist/{chunk-R3PQUPQ4.js → chunk-6IMKOIZ6.js} +85 -3
  30. package/dist/chunk-6IMKOIZ6.js.map +1 -0
  31. package/dist/{chunk-WTI35CVJ.js → chunk-BJA6DQOC.js} +5 -5
  32. package/dist/{chunk-GI45G4BK.js → chunk-BP2EV6W5.js} +3 -3
  33. package/dist/{chunk-WLGE6KEO.js → chunk-DBM2BD22.js} +3 -3
  34. package/dist/{chunk-IENGGY2C.js → chunk-ENV6RDTD.js} +2 -2
  35. package/dist/{chunk-BEMWL2FZ.js → chunk-FVRBLJP6.js} +2 -2
  36. package/dist/{chunk-H3PHZLMF.js → chunk-GKKAXVAJ.js} +20 -11
  37. package/dist/chunk-GKKAXVAJ.js.map +1 -0
  38. package/dist/{chunk-NOBL7OUP.js → chunk-GPW2E4LN.js} +12 -5
  39. package/dist/{chunk-NOBL7OUP.js.map → chunk-GPW2E4LN.js.map} +1 -1
  40. package/dist/{chunk-KWM33SPU.js → chunk-JMQSYGXS.js} +2 -2
  41. package/dist/{chunk-QQHIQ7JD.js → chunk-JYN7QNTA.js} +87 -18
  42. package/dist/chunk-JYN7QNTA.js.map +1 -0
  43. package/dist/{chunk-AJE7FJVE.js → chunk-K6X553JB.js} +2 -2
  44. package/dist/{chunk-E3J6O6N7.js → chunk-LJCEWTG3.js} +19 -8
  45. package/dist/{chunk-E3J6O6N7.js.map → chunk-LJCEWTG3.js.map} +1 -1
  46. package/dist/{chunk-EW52H5EM.js → chunk-NAZWHTYV.js} +12 -5
  47. package/dist/chunk-NAZWHTYV.js.map +1 -0
  48. package/dist/{chunk-XMN6MMTU.js → chunk-NCGWXCSW.js} +2 -2
  49. package/dist/{chunk-C43KEWEV.js → chunk-NE2JBMLN.js} +1 -1
  50. package/dist/chunk-NE2JBMLN.js.map +1 -0
  51. package/dist/{chunk-SPMZZUEJ.js → chunk-OL2364SB.js} +2020 -368
  52. package/dist/chunk-OL2364SB.js.map +1 -0
  53. package/dist/{chunk-JF7SFXTG.js → chunk-QKK64Z6M.js} +2 -2
  54. package/dist/{chunk-IVYSVAC6.js → chunk-QW6JZO5P.js} +2 -2
  55. package/dist/{chunk-EHQLDFSH.js → chunk-RGPUQ66K.js} +2 -2
  56. package/dist/{chunk-CFOCZPIQ.js → chunk-T2C6QJG2.js} +2 -2
  57. package/dist/{chunk-V4UDXYGG.js → chunk-XWQ6ERUG.js} +2 -2
  58. package/dist/{chunk-BNFRL6QW.js → chunk-Y2RIIF6H.js} +2 -2
  59. package/dist/{chunk-C63WC454.js → chunk-YLZLPVKK.js} +22 -1
  60. package/dist/chunk-YLZLPVKK.js.map +1 -0
  61. package/dist/{chunk-RZOBQ23O.js → chunk-Z5MQI7K2.js} +2 -2
  62. package/dist/{chunk-PRQXUSQV.js → chunk-ZCORQM74.js} +2 -2
  63. package/dist/{cli-DDo7Qgs-.d.ts → cli-uQgvDFNE.d.ts} +3 -3
  64. package/dist/cli.d.ts +5 -5
  65. package/dist/cli.js +22 -22
  66. package/dist/compounding/engine.d.ts +1 -1
  67. package/dist/compounding/engine.js +3 -3
  68. package/dist/compounding/preference-consolidator.d.ts +1 -1
  69. package/dist/compression-optimizer.d.ts +1 -1
  70. package/dist/config.d.ts +1 -1
  71. package/dist/config.js +1 -1
  72. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  73. package/dist/connectors/codex-materialize-runner.js +3 -3
  74. package/dist/connectors/codex-materialize.d.ts +1 -1
  75. package/dist/connectors/index.d.ts +1 -1
  76. package/dist/connectors/index.js +3 -3
  77. package/dist/consolidation-provenance-check.d.ts +1 -1
  78. package/dist/consolidation-undo.d.ts +1 -1
  79. package/dist/contradiction/index.d.ts +19 -1
  80. package/dist/contradiction/index.js +1 -1
  81. package/dist/conversation-index/backend.d.ts +1 -1
  82. package/dist/conversation-index/chunker.d.ts +1 -1
  83. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  84. package/dist/conversation-index/indexer.d.ts +1 -1
  85. package/dist/conversation-index/search.d.ts +1 -1
  86. package/dist/day-summary.d.ts +1 -1
  87. package/dist/delinearize.d.ts +1 -1
  88. package/dist/direct-answer-wiring.d.ts +1 -1
  89. package/dist/direct-answer.d.ts +1 -1
  90. package/dist/embedding-fallback.d.ts +1 -1
  91. package/dist/enrichment/index.d.ts +1 -1
  92. package/dist/entity-retrieval.d.ts +1 -1
  93. package/dist/entity-retrieval.js +3 -3
  94. package/dist/entity-schema.d.ts +1 -1
  95. package/dist/explicit-capture.d.ts +3 -3
  96. package/dist/explicit-capture.js +1 -1
  97. package/dist/extraction-judge-telemetry.d.ts +1 -1
  98. package/dist/extraction-judge-training.d.ts +1 -1
  99. package/dist/extraction-judge.d.ts +1 -1
  100. package/dist/extraction.d.ts +1 -1
  101. package/dist/fallback-llm.d.ts +1 -1
  102. package/dist/identity-continuity.d.ts +1 -1
  103. package/dist/importance.d.ts +1 -1
  104. package/dist/index.d.ts +8 -8
  105. package/dist/index.js +30 -28
  106. package/dist/index.js.map +1 -1
  107. package/dist/intent.d.ts +1 -1
  108. package/dist/lcm/engine.d.ts +1 -1
  109. package/dist/lcm/index.d.ts +1 -1
  110. package/dist/lcm/tools.d.ts +1 -1
  111. package/dist/lifecycle.d.ts +1 -1
  112. package/dist/live-connectors-runner.d.ts +1 -1
  113. package/dist/local-llm.d.ts +1 -1
  114. package/dist/maintenance/memory-governance.d.ts +1 -1
  115. package/dist/maintenance/memory-governance.js +3 -3
  116. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  117. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  118. package/dist/mcp-memory-inspector-app.d.ts +4 -4
  119. package/dist/memory-action-policy.d.ts +1 -1
  120. package/dist/memory-cache.d.ts +1 -1
  121. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  122. package/dist/memory-projection-store.d.ts +1 -1
  123. package/dist/memory-provenance.d.ts +1 -1
  124. package/dist/memory-worth-outcomes.d.ts +1 -1
  125. package/dist/models-json.d.ts +1 -1
  126. package/dist/namespaces/migrate.d.ts +1 -1
  127. package/dist/namespaces/migrate.js +4 -4
  128. package/dist/namespaces/principal.d.ts +1 -1
  129. package/dist/namespaces/search.d.ts +1 -1
  130. package/dist/namespaces/storage.d.ts +52 -3
  131. package/dist/namespaces/storage.js +9 -5
  132. package/dist/native-knowledge.d.ts +1 -1
  133. package/dist/operator-toolkit.d.ts +1 -1
  134. package/dist/operator-toolkit.js +7 -7
  135. package/dist/{orchestrator-8fTZsa0y.d.ts → orchestrator-B4Y4sWQH.d.ts} +503 -3
  136. package/dist/orchestrator.d.ts +3 -3
  137. package/dist/orchestrator.js +13 -13
  138. package/dist/patterns-cli.d.ts +1 -1
  139. package/dist/policy-runtime.d.ts +1 -1
  140. package/dist/qmd-recall-cache.d.ts +1 -1
  141. package/dist/qmd.d.ts +1 -1
  142. package/dist/recall-disclosure-escalation.d.ts +1 -1
  143. package/dist/recall-explain-renderer.d.ts +1 -1
  144. package/dist/recall-explain-renderer.js +3 -3
  145. package/dist/recall-planner-llm.d.ts +1 -1
  146. package/dist/recall-state.d.ts +1 -1
  147. package/dist/recall-tag-filter.d.ts +1 -1
  148. package/dist/recall-xray-cli.d.ts +1 -1
  149. package/dist/recall-xray-cli.js +4 -4
  150. package/dist/recall-xray-renderer.d.ts +1 -1
  151. package/dist/recall-xray-renderer.js +3 -3
  152. package/dist/recall-xray.d.ts +1 -1
  153. package/dist/recall-xray.js +2 -2
  154. package/dist/{resolution-3SAP4SH2.js → resolution-IDTEBJFS.js} +2 -2
  155. package/dist/resolve-auth-token.d.ts +1 -1
  156. package/dist/resume-bundles.js +2 -2
  157. package/dist/retrieval-agents.d.ts +1 -1
  158. package/dist/retrieval-tiers.d.ts +1 -1
  159. package/dist/routing/engine.d.ts +1 -1
  160. package/dist/routing/store.d.ts +1 -1
  161. package/dist/search/embed-helper.d.ts +1 -1
  162. package/dist/search/factory.d.ts +1 -1
  163. package/dist/search/index.d.ts +1 -1
  164. package/dist/search/lancedb-backend.d.ts +1 -1
  165. package/dist/search/meilisearch-backend.d.ts +1 -1
  166. package/dist/search/noop-backend.d.ts +1 -1
  167. package/dist/search/orama-backend.d.ts +1 -1
  168. package/dist/search/port.d.ts +1 -1
  169. package/dist/search/remote-backend.d.ts +1 -1
  170. package/dist/{semantic-consolidation-DKdYzQOg.d.ts → semantic-consolidation-BKd0Pype.d.ts} +1 -1
  171. package/dist/semantic-consolidation.d.ts +2 -2
  172. package/dist/semantic-consolidation.js +4 -4
  173. package/dist/semantic-rule-promotion.js +3 -3
  174. package/dist/semantic-rule-verifier.d.ts +1 -1
  175. package/dist/semantic-rule-verifier.js +3 -3
  176. package/dist/session-observer-bands.d.ts +1 -1
  177. package/dist/session-observer-state.d.ts +1 -1
  178. package/dist/shared-context/manager.d.ts +1 -1
  179. package/dist/signal.d.ts +1 -1
  180. package/dist/storage.d.ts +1 -1
  181. package/dist/storage.js +2 -2
  182. package/dist/summarizer.d.ts +1 -1
  183. package/dist/summary-snapshot.d.ts +1 -1
  184. package/dist/temporal-supersession.d.ts +1 -1
  185. package/dist/temporal-validity.d.ts +1 -1
  186. package/dist/threading.d.ts +1 -1
  187. package/dist/tier-migration.d.ts +1 -1
  188. package/dist/tier-routing.d.ts +1 -1
  189. package/dist/topics.d.ts +1 -1
  190. package/dist/transcript.d.ts +1 -1
  191. package/dist/{types-D8yUmSik.d.ts → types-BgChEr0M.d.ts} +11 -0
  192. package/dist/types.d.ts +1 -1
  193. package/dist/types.js +1 -1
  194. package/dist/utility-runtime.d.ts +1 -1
  195. package/dist/verified-recall.js +3 -3
  196. package/package.json +1 -1
  197. package/src/access-http.ts +7 -0
  198. package/src/access-mcp.ts +7 -0
  199. package/src/access-service.ts +12 -0
  200. package/src/cli.ts +104 -0
  201. package/src/config.test.ts +40 -0
  202. package/src/config.ts +29 -0
  203. package/src/contradiction/contradiction.test.ts +284 -0
  204. package/src/contradiction/resolution.ts +151 -4
  205. package/src/explicit-capture.ts +31 -10
  206. package/src/index.ts +10 -0
  207. package/src/namespaces/catalog.test.ts +3356 -0
  208. package/src/namespaces/catalog.ts +2123 -0
  209. package/src/namespaces/storage.ts +210 -30
  210. package/src/orchestrator-flush.test.ts +300 -0
  211. package/src/orchestrator.ts +851 -240
  212. package/src/types.ts +11 -0
  213. package/dist/chunk-C43KEWEV.js.map +0 -1
  214. package/dist/chunk-C63WC454.js.map +0 -1
  215. package/dist/chunk-EW52H5EM.js.map +0 -1
  216. package/dist/chunk-H3PHZLMF.js.map +0 -1
  217. package/dist/chunk-ORGWWNJG.js +0 -131
  218. package/dist/chunk-ORGWWNJG.js.map +0 -1
  219. package/dist/chunk-QQHIQ7JD.js.map +0 -1
  220. package/dist/chunk-R3PQUPQ4.js.map +0 -1
  221. package/dist/chunk-SPMZZUEJ.js.map +0 -1
  222. /package/dist/{chunk-KJDKZVF3.js.map → chunk-2DSTAWNZ.js.map} +0 -0
  223. /package/dist/{chunk-Y7NWBBHV.js.map → chunk-6CVI6BP6.js.map} +0 -0
  224. /package/dist/{chunk-WTI35CVJ.js.map → chunk-BJA6DQOC.js.map} +0 -0
  225. /package/dist/{chunk-GI45G4BK.js.map → chunk-BP2EV6W5.js.map} +0 -0
  226. /package/dist/{chunk-WLGE6KEO.js.map → chunk-DBM2BD22.js.map} +0 -0
  227. /package/dist/{chunk-IENGGY2C.js.map → chunk-ENV6RDTD.js.map} +0 -0
  228. /package/dist/{chunk-BEMWL2FZ.js.map → chunk-FVRBLJP6.js.map} +0 -0
  229. /package/dist/{chunk-KWM33SPU.js.map → chunk-JMQSYGXS.js.map} +0 -0
  230. /package/dist/{chunk-AJE7FJVE.js.map → chunk-K6X553JB.js.map} +0 -0
  231. /package/dist/{chunk-XMN6MMTU.js.map → chunk-NCGWXCSW.js.map} +0 -0
  232. /package/dist/{chunk-JF7SFXTG.js.map → chunk-QKK64Z6M.js.map} +0 -0
  233. /package/dist/{chunk-IVYSVAC6.js.map → chunk-QW6JZO5P.js.map} +0 -0
  234. /package/dist/{chunk-EHQLDFSH.js.map → chunk-RGPUQ66K.js.map} +0 -0
  235. /package/dist/{chunk-CFOCZPIQ.js.map → chunk-T2C6QJG2.js.map} +0 -0
  236. /package/dist/{chunk-V4UDXYGG.js.map → chunk-XWQ6ERUG.js.map} +0 -0
  237. /package/dist/{chunk-BNFRL6QW.js.map → chunk-Y2RIIF6H.js.map} +0 -0
  238. /package/dist/{chunk-RZOBQ23O.js.map → chunk-Z5MQI7K2.js.map} +0 -0
  239. /package/dist/{chunk-PRQXUSQV.js.map → chunk-ZCORQM74.js.map} +0 -0
  240. /package/dist/{resolution-3SAP4SH2.js.map → resolution-IDTEBJFS.js.map} +0 -0
@@ -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
  }
@@ -1,5 +1,8 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { mkdir, mkdtemp, rm, symlink } from "node:fs/promises";
3
6
  import {
4
7
  BulkImportBatchPartialFailureError,
5
8
  Orchestrator,
@@ -7,6 +10,7 @@ import {
7
10
  import { parseConfig } from "./config.js";
8
11
  import type { BufferTurn } from "./types.js";
9
12
  import type { ImportTurn } from "./bulk-import/types.js";
13
+ import { namespaceIdentityToken } from "./namespaces/identity.js";
10
14
 
11
15
  function makeTurn(sessionKey: string, content: string): BufferTurn {
12
16
  return {
@@ -1767,3 +1771,299 @@ test("runExtraction still clears the session buffer after persistence even if re
1767
1771
  "persisted reset flushes must still clear the session buffer even when the reset timeout aborts after persistence",
1768
1772
  );
1769
1773
  });
1774
+
1775
+ // ── NGnei (codex P2): runQmdMaintenance must cover CATALOGED dynamic namespaces,
1776
+ // not only the configured set. An extraction writing to a coding-scoped/dynamic
1777
+ // namespace is made discoverable via the catalog; if maintenance embeds only
1778
+ // configuredNamespaces(), that namespace's QMD collection stays stale. We stub the
1779
+ // orchestrator internals and assert update/embed receive the UNION of configured +
1780
+ // cataloged namespaces.
1781
+ test("runQmdMaintenance updates and embeds cataloged dynamic namespaces (NGnei)", async () => {
1782
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
1783
+ let updateArg: string[] | undefined;
1784
+ let embedArg: string[] | undefined;
1785
+ const memoryDir = path.join(os.tmpdir(), "remnic-qmd-maintenance-ngnei");
1786
+ const dynamicNamespace = "project-origin-dynamic";
1787
+ const dynamicStorageDir = path.join(
1788
+ memoryDir,
1789
+ "namespaces",
1790
+ namespaceIdentityToken(dynamicNamespace),
1791
+ );
1792
+ await mkdir(path.join(dynamicStorageDir, "facts"), { recursive: true });
1793
+
1794
+ orchestrator.config = {
1795
+ memoryDir,
1796
+ namespacesEnabled: true,
1797
+ defaultNamespace: "default",
1798
+ sharedNamespace: "shared",
1799
+ namespacePolicies: [],
1800
+ qmdAutoEmbedEnabled: true,
1801
+ qmdEmbedMinIntervalMs: 0,
1802
+ };
1803
+ orchestrator.qmdMaintenanceInFlight = false;
1804
+ orchestrator.qmdMaintenancePending = true;
1805
+ orchestrator.lastQmdEmbedAtMs = 0;
1806
+ orchestrator.namespaceCatalog = {
1807
+ enabled: true,
1808
+ async listNamespaces() {
1809
+ return [
1810
+ { namespace: "default" },
1811
+ {
1812
+ namespace: dynamicNamespace,
1813
+ identityToken: namespaceIdentityToken(dynamicNamespace),
1814
+ kind: "project",
1815
+ createdAt: "2026-04-12T12:00:00.000Z",
1816
+ storageDir: dynamicStorageDir,
1817
+ discoveredBy: "write",
1818
+ }, // dynamic, NOT configured
1819
+ ];
1820
+ },
1821
+ };
1822
+ orchestrator.namespaceSearchRouter = {
1823
+ async updateNamespaces(ns: string[]) {
1824
+ updateArg = ns;
1825
+ return ns.length;
1826
+ },
1827
+ async embedNamespaces(ns: string[]) {
1828
+ embedArg = ns;
1829
+ },
1830
+ };
1831
+
1832
+ await orchestrator.runQmdMaintenance();
1833
+
1834
+ assert.ok(updateArg, "updateNamespaces must be called");
1835
+ assert.ok(
1836
+ updateArg!.includes(dynamicNamespace),
1837
+ "QMD update must cover the cataloged dynamic namespace, not just configured ones",
1838
+ );
1839
+ assert.ok(
1840
+ updateArg!.includes("default") && updateArg!.includes("shared"),
1841
+ "configured namespaces remain covered",
1842
+ );
1843
+ assert.ok(
1844
+ embedArg && embedArg.includes(dynamicNamespace),
1845
+ "QMD embed must cover the cataloged dynamic namespace",
1846
+ );
1847
+ });
1848
+
1849
+ test("runQmdMaintenance skips cataloged dynamic namespaces whose live root is unsafe", async () => {
1850
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
1851
+ let updateArg: string[] | undefined;
1852
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-unsafe-root-"));
1853
+ const outsideDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-unsafe-target-"));
1854
+ try {
1855
+ const dynamicNamespace = "project-origin-symlinked";
1856
+ const liveLegacyRoot = path.join(memoryDir, "namespaces", dynamicNamespace);
1857
+ const catalogSafeRoot = path.join(
1858
+ memoryDir,
1859
+ "namespaces",
1860
+ namespaceIdentityToken(dynamicNamespace),
1861
+ );
1862
+ await mkdir(path.dirname(liveLegacyRoot), { recursive: true });
1863
+ await symlink(outsideDir, liveLegacyRoot, "dir");
1864
+
1865
+ orchestrator.config = {
1866
+ memoryDir,
1867
+ namespacesEnabled: true,
1868
+ defaultNamespace: "default",
1869
+ sharedNamespace: "shared",
1870
+ namespacePolicies: [],
1871
+ qmdAutoEmbedEnabled: false,
1872
+ qmdEmbedMinIntervalMs: 0,
1873
+ };
1874
+ orchestrator.qmdMaintenanceInFlight = false;
1875
+ orchestrator.qmdMaintenancePending = true;
1876
+ orchestrator.lastQmdEmbedAtMs = 0;
1877
+ orchestrator.namespaceCatalog = {
1878
+ enabled: true,
1879
+ async listNamespaces() {
1880
+ return [
1881
+ {
1882
+ namespace: dynamicNamespace,
1883
+ identityToken: namespaceIdentityToken(dynamicNamespace),
1884
+ kind: "project",
1885
+ createdAt: "2026-04-12T12:00:00.000Z",
1886
+ storageDir: catalogSafeRoot,
1887
+ discoveredBy: "write",
1888
+ },
1889
+ ];
1890
+ },
1891
+ };
1892
+ orchestrator.namespaceSearchRouter = {
1893
+ async updateNamespaces(ns: string[]) {
1894
+ updateArg = ns;
1895
+ return ns.length;
1896
+ },
1897
+ async embedNamespaces() {},
1898
+ };
1899
+
1900
+ await orchestrator.runQmdMaintenance();
1901
+
1902
+ assert.ok(updateArg, "updateNamespaces must be called");
1903
+ assert.deepEqual(
1904
+ [...updateArg!].sort(),
1905
+ ["default", "shared"],
1906
+ "cataloged dynamic namespaces are skipped when the live router root differs from the catalog-sanitized root",
1907
+ );
1908
+ } finally {
1909
+ await rm(memoryDir, { recursive: true, force: true });
1910
+ await rm(outsideDir, { recursive: true, force: true });
1911
+ }
1912
+ });
1913
+
1914
+ // NGnei fallback: when the catalog is disabled, maintenance covers exactly the
1915
+ // configured set (no catalog read), and a catalog read failure degrades to the
1916
+ // configured set rather than breaking maintenance.
1917
+ test("runQmdMaintenance falls back to configured namespaces when the catalog is disabled (NGnei)", async () => {
1918
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
1919
+ let updateArg: string[] | undefined;
1920
+
1921
+ orchestrator.config = {
1922
+ namespacesEnabled: true,
1923
+ defaultNamespace: "default",
1924
+ sharedNamespace: "shared",
1925
+ namespacePolicies: [],
1926
+ qmdAutoEmbedEnabled: false,
1927
+ qmdEmbedMinIntervalMs: 0,
1928
+ };
1929
+ orchestrator.qmdMaintenanceInFlight = false;
1930
+ orchestrator.qmdMaintenancePending = true;
1931
+ orchestrator.lastQmdEmbedAtMs = 0;
1932
+ orchestrator.namespaceCatalog = {
1933
+ enabled: false,
1934
+ async listNamespaces() {
1935
+ throw new Error("catalog disabled — must not be read");
1936
+ },
1937
+ };
1938
+ orchestrator.namespaceSearchRouter = {
1939
+ async updateNamespaces(ns: string[]) {
1940
+ updateArg = ns;
1941
+ return ns.length;
1942
+ },
1943
+ async embedNamespaces() {},
1944
+ };
1945
+
1946
+ await orchestrator.runQmdMaintenance();
1947
+
1948
+ assert.ok(updateArg, "updateNamespaces must be called");
1949
+ assert.deepEqual(
1950
+ [...updateArg!].sort(),
1951
+ ["default", "shared"],
1952
+ "a disabled catalog covers exactly the configured set",
1953
+ );
1954
+ });
1955
+
1956
+ // ── NHZEV (codex P2): the QMD STARTUP sync in deferredInitialize() must cover
1957
+ // cataloged dynamic namespaces too, not only configuredNamespaces(). A dynamic
1958
+ // namespace written before a daemon restart exists ONLY in the persisted catalog;
1959
+ // if the boot-time "sync current disk state" pass embeds only the configured set,
1960
+ // that namespace's QMD collection stays stale after restart. We drive
1961
+ // deferredInitialize() with stubbed internals and abort the signal right after the
1962
+ // sync (the next `if (signal.aborted) return;` bails before warmup), then assert the
1963
+ // startup updateNamespaces() received the UNION of configured + cataloged namespaces.
1964
+ test("deferredInitialize startup sync covers cataloged dynamic namespaces (NHZEV)", async () => {
1965
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
1966
+ let updateArg: string[] | undefined;
1967
+ const abortController = new AbortController();
1968
+ const memoryDir = path.join(os.tmpdir(), "remnic-startup-maintenance-nhzev");
1969
+ const dynamicNamespace = "project-origin-dynamic";
1970
+ const dynamicStorageDir = path.join(
1971
+ memoryDir,
1972
+ "namespaces",
1973
+ namespaceIdentityToken(dynamicNamespace),
1974
+ );
1975
+ await mkdir(path.join(dynamicStorageDir, "facts"), { recursive: true });
1976
+
1977
+ orchestrator.config = {
1978
+ memoryDir,
1979
+ namespacesEnabled: true,
1980
+ defaultNamespace: "default",
1981
+ sharedNamespace: "shared",
1982
+ namespacePolicies: [],
1983
+ qmdMaintenanceEnabled: true,
1984
+ };
1985
+ orchestrator.qmd = {
1986
+ isAvailable: () => true,
1987
+ async update() {},
1988
+ };
1989
+ orchestrator.namespaceCatalog = {
1990
+ enabled: true,
1991
+ async listNamespaces() {
1992
+ return [
1993
+ { namespace: "default" },
1994
+ {
1995
+ namespace: dynamicNamespace,
1996
+ identityToken: namespaceIdentityToken(dynamicNamespace),
1997
+ kind: "project",
1998
+ createdAt: "2026-04-12T12:00:00.000Z",
1999
+ storageDir: dynamicStorageDir,
2000
+ discoveredBy: "write",
2001
+ }, // dynamic, catalog-ONLY, NOT configured
2002
+ ];
2003
+ },
2004
+ };
2005
+ orchestrator.namespaceSearchRouter = {
2006
+ async updateNamespaces(ns: string[]) {
2007
+ updateArg = ns;
2008
+ // Abort AFTER the startup sync records its arg so deferredInitialize bails
2009
+ // at the next `if (signal.aborted) return;` before warmup/caches run.
2010
+ abortController.abort();
2011
+ return ns.length;
2012
+ },
2013
+ };
2014
+
2015
+ await orchestrator.deferredInitialize(abortController.signal);
2016
+
2017
+ assert.ok(updateArg, "startup updateNamespaces must be called");
2018
+ assert.ok(
2019
+ updateArg!.includes(dynamicNamespace),
2020
+ "startup sync must cover the cataloged dynamic namespace (NHZEV), not just configured ones",
2021
+ );
2022
+ assert.ok(
2023
+ updateArg!.includes("default") && updateArg!.includes("shared"),
2024
+ "configured namespaces remain covered at startup",
2025
+ );
2026
+ });
2027
+
2028
+ // NHZEV fallback: a catalog read failure during startup sync must degrade to the
2029
+ // configured set rather than breaking deferredInitialize — same failure-tolerance
2030
+ // contract as runQmdMaintenance (maintenanceNamespaces swallows the read error).
2031
+ test("deferredInitialize startup sync falls back to configured set on catalog read failure (NHZEV)", async () => {
2032
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
2033
+ let updateArg: string[] | undefined;
2034
+ const abortController = new AbortController();
2035
+
2036
+ orchestrator.config = {
2037
+ namespacesEnabled: true,
2038
+ defaultNamespace: "default",
2039
+ sharedNamespace: "shared",
2040
+ namespacePolicies: [],
2041
+ qmdMaintenanceEnabled: true,
2042
+ };
2043
+ orchestrator.qmd = {
2044
+ isAvailable: () => true,
2045
+ async update() {},
2046
+ };
2047
+ orchestrator.namespaceCatalog = {
2048
+ enabled: true,
2049
+ async listNamespaces() {
2050
+ throw new Error("catalog read failed");
2051
+ },
2052
+ };
2053
+ orchestrator.namespaceSearchRouter = {
2054
+ async updateNamespaces(ns: string[]) {
2055
+ updateArg = ns;
2056
+ abortController.abort();
2057
+ return ns.length;
2058
+ },
2059
+ };
2060
+
2061
+ await orchestrator.deferredInitialize(abortController.signal);
2062
+
2063
+ assert.ok(updateArg, "startup updateNamespaces must be called");
2064
+ assert.deepEqual(
2065
+ [...updateArg!].sort(),
2066
+ ["default", "shared"],
2067
+ "a catalog read failure degrades startup sync to the configured set",
2068
+ );
2069
+ });