@remnic/core 9.3.654 → 9.3.656

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 (281) hide show
  1. package/dist/access-cli.js +29 -29
  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 +10 -10
  7. package/dist/{access-service-C8A5hoXJ.d.ts → access-service-D_nbpexW.d.ts} +33 -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-JMQSYGXS.js → chunk-2BD7DG37.js} +2 -2
  26. package/dist/{chunk-FVRBLJP6.js → chunk-2MXEVL75.js} +2 -2
  27. package/dist/{chunk-LJCEWTG3.js → chunk-4UL7VPTD.js} +277 -58
  28. package/dist/chunk-4UL7VPTD.js.map +1 -0
  29. package/dist/{chunk-JYN7QNTA.js → chunk-54XF2FY7.js} +17 -17
  30. package/dist/{chunk-7WEB3FLJ.js → chunk-5PLUC5OB.js} +2 -2
  31. package/dist/{chunk-JX2RINDR.js → chunk-6G5JEN55.js} +2 -2
  32. package/dist/{chunk-ZCORQM74.js → chunk-AGJKWOKV.js} +2 -2
  33. package/dist/{chunk-NE2JBMLN.js → chunk-AZBV4RRY.js} +1 -1
  34. package/dist/chunk-AZBV4RRY.js.map +1 -0
  35. package/dist/{chunk-YLZLPVKK.js → chunk-CTAV55JM.js} +344 -1
  36. package/dist/chunk-CTAV55JM.js.map +1 -0
  37. package/dist/{chunk-2DSTAWNZ.js → chunk-DIBWFCLA.js} +3 -3
  38. package/dist/{chunk-NAZWHTYV.js → chunk-DR67OK4E.js} +5 -5
  39. package/dist/{chunk-XBIACVCO.js → chunk-EC2AYKRX.js} +2 -2
  40. package/dist/{chunk-JVRPJ7D4.js → chunk-EKQMQQ3U.js} +48 -12
  41. package/dist/chunk-EKQMQQ3U.js.map +1 -0
  42. package/dist/{chunk-RGPUQ66K.js → chunk-GCYFUTUC.js} +2 -2
  43. package/dist/{chunk-JBHXMCYN.js → chunk-GRYAECRV.js} +2 -2
  44. package/dist/{chunk-BJA6DQOC.js → chunk-GSHW5VVD.js} +5 -5
  45. package/dist/chunk-GYSYLGNE.js +650 -0
  46. package/dist/chunk-GYSYLGNE.js.map +1 -0
  47. package/dist/{chunk-NCGWXCSW.js → chunk-IOZ5WBWD.js} +2 -2
  48. package/dist/{chunk-QKK64Z6M.js → chunk-JSVFEHLL.js} +7 -5
  49. package/dist/chunk-JSVFEHLL.js.map +1 -0
  50. package/dist/{chunk-7LWRCOP7.js → chunk-LZTFCAKE.js} +2 -2
  51. package/dist/{chunk-2DGQLOOM.js → chunk-M3VYPE2H.js} +1 -1
  52. package/dist/{chunk-2DGQLOOM.js.map → chunk-M3VYPE2H.js.map} +1 -1
  53. package/dist/{chunk-6CVI6BP6.js → chunk-NXCK7DO7.js} +2 -2
  54. package/dist/{chunk-Z5MQI7K2.js → chunk-PEPHBH2W.js} +2 -2
  55. package/dist/{chunk-PYWNNF2I.js → chunk-QRSKPI62.js} +99 -66
  56. package/dist/chunk-QRSKPI62.js.map +1 -0
  57. package/dist/{chunk-XWQ6ERUG.js → chunk-QZRKNA5F.js} +2 -2
  58. package/dist/{chunk-PS3SYNHP.js → chunk-R5DB26G6.js} +2 -2
  59. package/dist/{chunk-OL2364SB.js → chunk-RDW5G6DO.js} +1502 -335
  60. package/dist/chunk-RDW5G6DO.js.map +1 -0
  61. package/dist/{chunk-YM3LR4LS.js → chunk-SSSXWIBP.js} +5 -5
  62. package/dist/{chunk-T2C6QJG2.js → chunk-SWDHVH2P.js} +2 -2
  63. package/dist/{chunk-DBM2BD22.js → chunk-SXYCVRLK.js} +3 -3
  64. package/dist/{chunk-K6X553JB.js → chunk-TFFZUFEP.js} +7 -5
  65. package/dist/chunk-TFFZUFEP.js.map +1 -0
  66. package/dist/{chunk-ENV6RDTD.js → chunk-TIJYQXDI.js} +2 -2
  67. package/dist/{chunk-BP2EV6W5.js → chunk-VAEAGTEQ.js} +4 -4
  68. package/dist/{chunk-3RACUBII.js → chunk-WIKMCJUR.js} +2 -2
  69. package/dist/{chunk-QW6JZO5P.js → chunk-WWMHAMAY.js} +2 -2
  70. package/dist/{chunk-GPW2E4LN.js → chunk-YEZHZCUO.js} +4 -4
  71. package/dist/{chunk-5FOCXX5E.js → chunk-YVVQUAOO.js} +3 -3
  72. package/dist/{chunk-5FOCXX5E.js.map → chunk-YVVQUAOO.js.map} +1 -1
  73. package/dist/{chunk-3XGWCZ63.js → chunk-YXLT4EMM.js} +2 -2
  74. package/dist/{chunk-Y2RIIF6H.js → chunk-Z6UDTNY6.js} +2 -2
  75. package/dist/{cli-uQgvDFNE.d.ts → cli-aYxSuPvP.d.ts} +3 -3
  76. package/dist/cli.d.ts +5 -5
  77. package/dist/cli.js +29 -29
  78. package/dist/compounding/engine.d.ts +1 -1
  79. package/dist/compounding/engine.js +3 -3
  80. package/dist/compounding/preference-consolidator.d.ts +1 -1
  81. package/dist/compression-optimizer.d.ts +1 -1
  82. package/dist/config.d.ts +1 -1
  83. package/dist/config.js +1 -1
  84. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  85. package/dist/connectors/codex-materialize-runner.js +3 -3
  86. package/dist/connectors/codex-materialize.d.ts +1 -1
  87. package/dist/connectors/index.d.ts +1 -1
  88. package/dist/connectors/index.js +3 -3
  89. package/dist/consolidation-provenance-check.d.ts +1 -1
  90. package/dist/consolidation-undo.d.ts +1 -1
  91. package/dist/contradiction/index.d.ts +1 -1
  92. package/dist/conversation-index/backend.d.ts +1 -1
  93. package/dist/conversation-index/chunker.d.ts +1 -1
  94. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  95. package/dist/conversation-index/indexer.d.ts +1 -1
  96. package/dist/conversation-index/search.d.ts +1 -1
  97. package/dist/day-summary.d.ts +1 -1
  98. package/dist/delinearize.d.ts +1 -1
  99. package/dist/direct-answer-wiring.d.ts +1 -1
  100. package/dist/direct-answer.d.ts +1 -1
  101. package/dist/embedding-fallback.d.ts +1 -1
  102. package/dist/enrichment/index.d.ts +1 -1
  103. package/dist/entity-retrieval.d.ts +1 -1
  104. package/dist/entity-retrieval.js +3 -3
  105. package/dist/entity-schema.d.ts +1 -1
  106. package/dist/explicit-capture.d.ts +3 -3
  107. package/dist/explicit-cue-recall.js +2 -2
  108. package/dist/extraction-judge-telemetry.d.ts +1 -1
  109. package/dist/extraction-judge-training.d.ts +1 -1
  110. package/dist/extraction-judge.d.ts +1 -1
  111. package/dist/extraction.d.ts +1 -1
  112. package/dist/fallback-llm.d.ts +1 -1
  113. package/dist/focused-list-recall.js +2 -2
  114. package/dist/identity-continuity.d.ts +1 -1
  115. package/dist/importance.d.ts +1 -1
  116. package/dist/index.d.ts +121 -121
  117. package/dist/index.js +39 -39
  118. package/dist/intent.d.ts +1 -1
  119. package/dist/lcm/engine.d.ts +1 -1
  120. package/dist/lcm/index.d.ts +1 -1
  121. package/dist/lcm/tools.d.ts +1 -1
  122. package/dist/lcm-fallback-read.js +1 -1
  123. package/dist/lifecycle.d.ts +1 -1
  124. package/dist/live-connectors-runner.d.ts +1 -1
  125. package/dist/local-llm.d.ts +1 -1
  126. package/dist/maintenance/memory-governance.d.ts +1 -1
  127. package/dist/maintenance/memory-governance.js +3 -3
  128. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  129. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  130. package/dist/mcp-memory-inspector-app.d.ts +4 -4
  131. package/dist/memory-action-policy.d.ts +1 -1
  132. package/dist/memory-cache.d.ts +1 -1
  133. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  134. package/dist/memory-projection-store.d.ts +1 -1
  135. package/dist/memory-provenance.d.ts +1 -1
  136. package/dist/memory-worth-outcomes.d.ts +1 -1
  137. package/dist/models-json.d.ts +1 -1
  138. package/dist/namespaces/migrate.d.ts +1 -1
  139. package/dist/namespaces/migrate.js +11 -11
  140. package/dist/namespaces/principal.d.ts +1 -1
  141. package/dist/namespaces/search.d.ts +15 -4
  142. package/dist/namespaces/search.js +7 -7
  143. package/dist/namespaces/storage.d.ts +1 -1
  144. package/dist/namespaces/storage.js +3 -3
  145. package/dist/native-knowledge.d.ts +1 -1
  146. package/dist/operator-toolkit.d.ts +1 -1
  147. package/dist/operator-toolkit.js +14 -14
  148. package/dist/{orchestrator-B4Y4sWQH.d.ts → orchestrator-D1wcmPNj.d.ts} +17 -14
  149. package/dist/orchestrator.d.ts +3 -3
  150. package/dist/orchestrator.js +25 -25
  151. package/dist/patterns-cli.d.ts +1 -1
  152. package/dist/policy-runtime.d.ts +1 -1
  153. package/dist/qmd-recall-cache.d.ts +1 -1
  154. package/dist/qmd.d.ts +5 -1
  155. package/dist/qmd.js +2 -2
  156. package/dist/recall-disclosure-escalation.d.ts +1 -1
  157. package/dist/recall-explain-renderer.d.ts +1 -1
  158. package/dist/recall-explain-renderer.js +3 -3
  159. package/dist/recall-planner-llm.d.ts +1 -1
  160. package/dist/recall-state.d.ts +1 -1
  161. package/dist/recall-tag-filter.d.ts +1 -1
  162. package/dist/recall-xray-cli.d.ts +1 -1
  163. package/dist/recall-xray-cli.js +4 -4
  164. package/dist/recall-xray-renderer.d.ts +1 -1
  165. package/dist/recall-xray-renderer.js +3 -3
  166. package/dist/recall-xray.d.ts +1 -1
  167. package/dist/recall-xray.js +2 -2
  168. package/dist/resolve-auth-token.d.ts +1 -1
  169. package/dist/response-guidance-recall.js +2 -2
  170. package/dist/resume-bundles.js +2 -2
  171. package/dist/retrieval-agents.d.ts +1 -1
  172. package/dist/retrieval-tiers.d.ts +1 -1
  173. package/dist/routing/engine.d.ts +1 -1
  174. package/dist/routing/store.d.ts +1 -1
  175. package/dist/schemas.d.ts +22 -22
  176. package/dist/search/embed-helper.d.ts +1 -1
  177. package/dist/search/factory.d.ts +1 -1
  178. package/dist/search/factory.js +6 -6
  179. package/dist/search/index.d.ts +1 -1
  180. package/dist/search/index.js +6 -6
  181. package/dist/search/lancedb-backend.d.ts +1 -1
  182. package/dist/search/lancedb-backend.js +2 -2
  183. package/dist/search/meilisearch-backend.d.ts +1 -1
  184. package/dist/search/meilisearch-backend.js +2 -2
  185. package/dist/search/noop-backend.d.ts +1 -1
  186. package/dist/search/orama-backend.d.ts +1 -1
  187. package/dist/search/orama-backend.js +2 -2
  188. package/dist/search/port.d.ts +17 -1
  189. package/dist/search/port.js +1 -1
  190. package/dist/search/remote-backend.d.ts +1 -1
  191. package/dist/{semantic-consolidation-BKd0Pype.d.ts → semantic-consolidation-MWOdNtSE.d.ts} +1 -1
  192. package/dist/semantic-consolidation.d.ts +2 -2
  193. package/dist/semantic-consolidation.js +4 -4
  194. package/dist/semantic-rule-promotion.js +3 -3
  195. package/dist/semantic-rule-verifier.d.ts +3 -2
  196. package/dist/semantic-rule-verifier.js +5 -3
  197. package/dist/session-observer-bands.d.ts +1 -1
  198. package/dist/session-observer-state.d.ts +1 -1
  199. package/dist/shared-context/manager.d.ts +1 -1
  200. package/dist/signal.d.ts +1 -1
  201. package/dist/storage.d.ts +1 -1
  202. package/dist/storage.js +2 -2
  203. package/dist/summarizer.d.ts +1 -1
  204. package/dist/summary-snapshot.d.ts +1 -1
  205. package/dist/targeted-fact-recall.js +2 -2
  206. package/dist/temporal-supersession.d.ts +1 -1
  207. package/dist/temporal-validity.d.ts +1 -1
  208. package/dist/threading.d.ts +1 -1
  209. package/dist/tier-migration.d.ts +1 -1
  210. package/dist/tier-routing.d.ts +1 -1
  211. package/dist/topics.d.ts +1 -1
  212. package/dist/transcript.d.ts +1 -1
  213. package/dist/transfer/types.d.ts +12 -12
  214. package/dist/{types-BgChEr0M.d.ts → types-CgcCpUrf.d.ts} +51 -1
  215. package/dist/types.d.ts +1 -1
  216. package/dist/types.js +1 -1
  217. package/dist/utility-runtime.d.ts +1 -1
  218. package/dist/verified-recall.d.ts +2 -1
  219. package/dist/verified-recall.js +5 -3
  220. package/package.json +1 -1
  221. package/src/access-service-observe-lcm-parity.test.ts +86 -1
  222. package/src/access-service-observe-scope.test.ts +283 -1
  223. package/src/access-service-raw-excerpt-read-gate.test.ts +53 -0
  224. package/src/access-service.ts +391 -93
  225. package/src/coding/coding-namespace.ts +0 -3
  226. package/src/config.test.ts +69 -0
  227. package/src/config.ts +417 -0
  228. package/src/lcm-fallback-read.ts +2 -6
  229. package/src/maintenance/namespace-planner.test.ts +1120 -0
  230. package/src/maintenance/namespace-planner.ts +893 -0
  231. package/src/namespaces/scope-profiles.test.ts +1074 -0
  232. package/src/namespaces/scope-profiles.ts +456 -0
  233. package/src/namespaces/search.test.ts +130 -2
  234. package/src/namespaces/search.ts +71 -10
  235. package/src/orchestrator-flush.test.ts +606 -44
  236. package/src/orchestrator-source-attribution.test.ts +73 -0
  237. package/src/orchestrator.ts +932 -229
  238. package/src/qmd-client.test.ts +59 -0
  239. package/src/qmd.ts +124 -84
  240. package/src/search/port.ts +16 -0
  241. package/src/semantic-rule-verifier.ts +13 -6
  242. package/src/types.ts +64 -0
  243. package/src/verified-recall.ts +10 -6
  244. package/dist/chunk-JVRPJ7D4.js.map +0 -1
  245. package/dist/chunk-K6X553JB.js.map +0 -1
  246. package/dist/chunk-LJCEWTG3.js.map +0 -1
  247. package/dist/chunk-MMJANTJX.js +0 -339
  248. package/dist/chunk-MMJANTJX.js.map +0 -1
  249. package/dist/chunk-NE2JBMLN.js.map +0 -1
  250. package/dist/chunk-OL2364SB.js.map +0 -1
  251. package/dist/chunk-PYWNNF2I.js.map +0 -1
  252. package/dist/chunk-QKK64Z6M.js.map +0 -1
  253. package/dist/chunk-YLZLPVKK.js.map +0 -1
  254. /package/dist/{chunk-JMQSYGXS.js.map → chunk-2BD7DG37.js.map} +0 -0
  255. /package/dist/{chunk-FVRBLJP6.js.map → chunk-2MXEVL75.js.map} +0 -0
  256. /package/dist/{chunk-JYN7QNTA.js.map → chunk-54XF2FY7.js.map} +0 -0
  257. /package/dist/{chunk-7WEB3FLJ.js.map → chunk-5PLUC5OB.js.map} +0 -0
  258. /package/dist/{chunk-JX2RINDR.js.map → chunk-6G5JEN55.js.map} +0 -0
  259. /package/dist/{chunk-ZCORQM74.js.map → chunk-AGJKWOKV.js.map} +0 -0
  260. /package/dist/{chunk-2DSTAWNZ.js.map → chunk-DIBWFCLA.js.map} +0 -0
  261. /package/dist/{chunk-NAZWHTYV.js.map → chunk-DR67OK4E.js.map} +0 -0
  262. /package/dist/{chunk-XBIACVCO.js.map → chunk-EC2AYKRX.js.map} +0 -0
  263. /package/dist/{chunk-RGPUQ66K.js.map → chunk-GCYFUTUC.js.map} +0 -0
  264. /package/dist/{chunk-JBHXMCYN.js.map → chunk-GRYAECRV.js.map} +0 -0
  265. /package/dist/{chunk-BJA6DQOC.js.map → chunk-GSHW5VVD.js.map} +0 -0
  266. /package/dist/{chunk-NCGWXCSW.js.map → chunk-IOZ5WBWD.js.map} +0 -0
  267. /package/dist/{chunk-7LWRCOP7.js.map → chunk-LZTFCAKE.js.map} +0 -0
  268. /package/dist/{chunk-6CVI6BP6.js.map → chunk-NXCK7DO7.js.map} +0 -0
  269. /package/dist/{chunk-Z5MQI7K2.js.map → chunk-PEPHBH2W.js.map} +0 -0
  270. /package/dist/{chunk-XWQ6ERUG.js.map → chunk-QZRKNA5F.js.map} +0 -0
  271. /package/dist/{chunk-PS3SYNHP.js.map → chunk-R5DB26G6.js.map} +0 -0
  272. /package/dist/{chunk-YM3LR4LS.js.map → chunk-SSSXWIBP.js.map} +0 -0
  273. /package/dist/{chunk-T2C6QJG2.js.map → chunk-SWDHVH2P.js.map} +0 -0
  274. /package/dist/{chunk-DBM2BD22.js.map → chunk-SXYCVRLK.js.map} +0 -0
  275. /package/dist/{chunk-ENV6RDTD.js.map → chunk-TIJYQXDI.js.map} +0 -0
  276. /package/dist/{chunk-BP2EV6W5.js.map → chunk-VAEAGTEQ.js.map} +0 -0
  277. /package/dist/{chunk-3RACUBII.js.map → chunk-WIKMCJUR.js.map} +0 -0
  278. /package/dist/{chunk-QW6JZO5P.js.map → chunk-WWMHAMAY.js.map} +0 -0
  279. /package/dist/{chunk-GPW2E4LN.js.map → chunk-YEZHZCUO.js.map} +0 -0
  280. /package/dist/{chunk-3XGWCZ63.js.map → chunk-YXLT4EMM.js.map} +0 -0
  281. /package/dist/{chunk-Y2RIIF6H.js.map → chunk-Z6UDTNY6.js.map} +0 -0
@@ -249,10 +249,12 @@ import {
249
249
  type HarmonicRetrievalResult,
250
250
  } from "./harmonic-retrieval.js";
251
251
  import {
252
+ compareVerifiedEpisodeResults,
252
253
  searchVerifiedEpisodes,
253
254
  type VerifiedEpisodeResult,
254
255
  } from "./verified-recall.js";
255
256
  import {
257
+ compareVerifiedSemanticRuleResults,
256
258
  searchVerifiedSemanticRules,
257
259
  type VerifiedSemanticRuleResult,
258
260
  } from "./semantic-rule-verifier.js";
@@ -299,13 +301,16 @@ import {
299
301
  } from "./conversation-index/backend.js";
300
302
  import {
301
303
  NamespaceStorageRouter,
302
- resolveNamespaceStorageRoot,
303
304
  } from "./namespaces/storage.js";
304
305
  import {
305
306
  NamespaceCatalog,
306
- hasMemoryData,
307
- type NamespaceRecord,
308
307
  } from "./namespaces/catalog.js";
308
+ import {
309
+ planNamespaceMaintenance,
310
+ runNamespaceMaintenanceBatchPlan,
311
+ type NamespaceMaintenancePlan,
312
+ type NamespaceMaintenanceSkipReason,
313
+ } from "./maintenance/namespace-planner.js";
309
314
  import {
310
315
  namespaceIdentityFromToken,
311
316
  namespaceIdentityToken,
@@ -317,6 +322,11 @@ import {
317
322
  recallNamespacesForPrincipal,
318
323
  resolvePrincipal,
319
324
  } from "./namespaces/principal.js";
325
+ import {
326
+ expandScopeProfileReadNamespaces,
327
+ resolveScopeProfilePlan,
328
+ type ResolvedScopeProfilePlan,
329
+ } from "./namespaces/scope-profiles.js";
320
330
  import {
321
331
  combineNamespaces,
322
332
  lcmReadSessionIdsForNamespaces,
@@ -1767,6 +1777,13 @@ export function resolvePersistedMemoryRelativePath(options: {
1767
1777
  return path.join(subtree, `${options.memoryId}.md`);
1768
1778
  }
1769
1779
 
1780
+ function qmdMaintenanceSkipReasonForError(error: unknown): NamespaceMaintenanceSkipReason | null {
1781
+ const message = error instanceof Error ? error.message : String(error);
1782
+ return /^QMD (?:update|embed) skipped by .*min-interval gate$/.test(message)
1783
+ ? "throttled"
1784
+ : null;
1785
+ }
1786
+
1770
1787
  export class Orchestrator {
1771
1788
  readonly storage: StorageManager;
1772
1789
  private readonly storageRouter: NamespaceStorageRouter;
@@ -1879,6 +1896,7 @@ export class Orchestrator {
1879
1896
  private readonly _peerIdBySession = new Map<string, string>();
1880
1897
  private routingRulesStore: RoutingRulesStore | null = null;
1881
1898
  private contentHashIndex: ContentHashIndex | null = null;
1899
+ private readonly contentHashIndexesByStorageDir = new Map<string, ContentHashIndex>();
1882
1900
  private readonly artifactSourceStatusCache = new WeakMap<
1883
1901
  StorageManager,
1884
1902
  {
@@ -1914,6 +1932,7 @@ export class Orchestrator {
1914
1932
  private qmdMaintenancePending = false;
1915
1933
  private qmdMaintenanceInFlight = false;
1916
1934
  private lastQmdEmbedAtMs = 0;
1935
+ private lastQmdEmbedAtMsByNamespace = new Map<string, number>();
1917
1936
  private lastQmdReprobeAtMs = 0;
1918
1937
  private tierMigrationInFlight = false;
1919
1938
  private lastTierMigrationRunAtMs = 0;
@@ -2417,63 +2436,31 @@ export class Orchestrator {
2417
2436
  }
2418
2437
 
2419
2438
  /**
2420
- * Namespaces that QMD maintenance (update/embed) must cover: the CONFIGURED set
2421
- * PLUS every dynamic namespace recorded in the catalog (NGnei, codex P2). An
2422
- * extraction that writes to a coding-scoped/dynamic namespace (not in
2423
- * defaultNamespace/sharedNamespace/namespacePolicies) is only made discoverable
2424
- * via the catalog; if maintenance embeds only `configuredNamespaces()`, that
2425
- * namespace's QMD collection is never updated/embedded after writes and
2426
- * recall/search stays stale or empty until it is manually configured. We union in
2427
- * the catalog's namespaces so maintenance keeps dynamic namespaces fresh.
2428
- * `updateNamespaces`/`embedNamespaces` already trim, dedup, and skip
2429
- * unavailable/missing collections, so extra names are filtered safely. A catalog
2430
- * read failure must never break maintenance — fall back to the configured set.
2439
+ * Shared namespace maintenance planner (issue #1500). This extends the
2440
+ * #1499 catalog-union QMD helper into a reusable contract: configured
2441
+ * namespaces are always considered, dynamic catalog namespaces are admitted
2442
+ * only when their live router root still matches real memory data, and branch
2443
+ * namespaces are opt-in. Recurring jobs use the per-cycle budget; startup and
2444
+ * recovery discovery paths use the same safety filters without that cycle
2445
+ * budget so every live namespace is ensured/synced.
2431
2446
  */
2432
- private async maintenanceNamespaces(): Promise<string[]> {
2433
- const configured = this.configuredNamespaces();
2434
- if (!this.namespaceCatalog.enabled) return configured;
2435
- const configuredSet = new Set(configured);
2436
- let cataloged: string[] = [];
2437
- try {
2438
- const records = await this.namespaceCatalog.listNamespaces();
2439
- const safeRecords = await Promise.all(
2440
- records.map(async (record) => {
2441
- const namespace = record.namespace.trim();
2442
- if (!namespace || configuredSet.has(namespace)) return null;
2443
- return (await this.isCatalogedMaintenanceRootLive(record))
2444
- ? namespace
2445
- : null;
2446
- }),
2447
- );
2448
- cataloged = safeRecords.filter(
2449
- (namespace): namespace is string => namespace !== null,
2450
- );
2451
- } catch {
2452
- // Best-effort: a catalog read failure must not break QMD maintenance.
2453
- cataloged = [];
2454
- }
2455
- return Array.from(
2456
- new Set(
2457
- [...configured, ...cataloged].map((value) => value.trim()).filter(Boolean),
2458
- ),
2459
- );
2447
+ private async namespaceMaintenancePlan(jobName: string): Promise<NamespaceMaintenancePlan> {
2448
+ return planNamespaceMaintenance(this.config, {
2449
+ jobName,
2450
+ catalog: this.namespaceCatalog,
2451
+ });
2460
2452
  }
2461
2453
 
2462
- private async isCatalogedMaintenanceRootLive(
2463
- record: NamespaceRecord,
2464
- ): Promise<boolean> {
2465
- if (typeof record.storageDir !== "string" || record.storageDir.length === 0) {
2466
- return false;
2467
- }
2468
- try {
2469
- const liveRoot = await resolveNamespaceStorageRoot(this.config, record.namespace);
2470
- if (path.resolve(liveRoot) !== path.resolve(record.storageDir)) {
2471
- return false;
2472
- }
2473
- return hasMemoryData(liveRoot);
2474
- } catch {
2475
- return false;
2476
- }
2454
+ private async maintenanceNamespaces(
2455
+ jobName = "qmd",
2456
+ budgetMode: "cycle" | "unbounded" = "unbounded",
2457
+ ): Promise<string[]> {
2458
+ const plan = await planNamespaceMaintenance(this.config, {
2459
+ jobName,
2460
+ catalog: this.namespaceCatalog,
2461
+ budgetMode,
2462
+ });
2463
+ return plan.namespaces.map((candidate) => candidate.namespace);
2477
2464
  }
2478
2465
 
2479
2466
  private buildConfiguredQmdSearchOptions(
@@ -2501,6 +2488,13 @@ export class Orchestrator {
2501
2488
  searchOptions?: SearchQueryOptions;
2502
2489
  execution?: SearchExecutionOptions;
2503
2490
  }): Promise<QmdSearchResult[]> {
2491
+ if (
2492
+ this.config.namespacesEnabled &&
2493
+ options.namespaces !== undefined &&
2494
+ options.namespaces.length === 0
2495
+ ) {
2496
+ return [];
2497
+ }
2504
2498
  const namespaces = this.config.namespacesEnabled
2505
2499
  ? Array.from(
2506
2500
  new Set(
@@ -2572,6 +2566,79 @@ export class Orchestrator {
2572
2566
 
2573
2567
  invalidateLiveContentHashIndex(): void {
2574
2568
  this.contentHashIndex = null;
2569
+ this.contentHashIndexesByStorageDir.clear();
2570
+ }
2571
+
2572
+ private async contentHashIndexForStorage(
2573
+ targetStorage: StorageManager,
2574
+ ): Promise<ContentHashIndex | null> {
2575
+ if (!this.config.factDeduplicationEnabled) return null;
2576
+
2577
+ if (targetStorage.dir === this.storage.dir) {
2578
+ if (!this.contentHashIndex) {
2579
+ this.contentHashIndex = this.storage.createContentHashIndex();
2580
+ await this.contentHashIndex.load();
2581
+ }
2582
+ return this.contentHashIndex;
2583
+ }
2584
+
2585
+ const cached = this.contentHashIndexesByStorageDir.get(targetStorage.dir);
2586
+ if (cached) return cached;
2587
+
2588
+ const index = targetStorage.createContentHashIndex();
2589
+ await index.load();
2590
+ this.contentHashIndexesByStorageDir.set(targetStorage.dir, index);
2591
+ log.info(
2592
+ `content-hash dedup: loaded ${index.size} hashes for storage ${targetStorage.dir}`,
2593
+ );
2594
+ return index;
2595
+ }
2596
+
2597
+ private async hasContentHashDedup(
2598
+ targetStorage: StorageManager,
2599
+ content: string,
2600
+ ): Promise<boolean> {
2601
+ const index = await this.contentHashIndexForStorage(targetStorage);
2602
+ return index ? index.has(content) : false;
2603
+ }
2604
+
2605
+ private async addContentHashDedup(
2606
+ targetStorage: StorageManager,
2607
+ content: string,
2608
+ ): Promise<void> {
2609
+ const index = await this.contentHashIndexForStorage(targetStorage);
2610
+ if (!index) return;
2611
+ index.add(content);
2612
+ }
2613
+
2614
+ private async removeContentHashForMemory(
2615
+ targetStorage: StorageManager,
2616
+ memory: MemoryFile,
2617
+ context: string,
2618
+ ): Promise<void> {
2619
+ const index = await this.contentHashIndexForStorage(targetStorage);
2620
+ if (!index) return;
2621
+
2622
+ if (memory.frontmatter.contentHash) {
2623
+ index.removeByHash(memory.frontmatter.contentHash);
2624
+ return;
2625
+ }
2626
+
2627
+ log.warn(
2628
+ `[${context}] removing hash for legacy memory ${memory.frontmatter.id ?? "(unknown)"} via content fallback - no contentHash in frontmatter`,
2629
+ );
2630
+ index.remove(memory.content);
2631
+ }
2632
+
2633
+ private async saveContentHashIndexes(): Promise<void> {
2634
+ const indexes = new Set<ContentHashIndex>();
2635
+ if (this.contentHashIndex) indexes.add(this.contentHashIndex);
2636
+ for (const index of this.contentHashIndexesByStorageDir.values()) {
2637
+ indexes.add(index);
2638
+ }
2639
+ for (const index of indexes) {
2640
+ await index.save();
2641
+ }
2575
2642
  }
2576
2643
 
2577
2644
  constructor(config: PluginConfig) {
@@ -4341,28 +4408,13 @@ export class Orchestrator {
4341
4408
  relatedMemoryIds: [canonicalId],
4342
4409
  });
4343
4410
  if (archiveResult) {
4344
- // Remove from content-hash index.
4345
- // Use the raw-content hash stored on the frontmatter at write
4346
- // time (contentHash) — it is format-agnostic and survives any
4347
- // citation template. Legacy memories without contentHash are
4348
- // skipped (see Finding 2 — Urgw).
4349
- if (this.contentHashIndex) {
4350
- if (m.frontmatter.contentHash) {
4351
- // Modern memory: frontmatter.contentHash is already a SHA-256
4352
- // hex string — use removeByHash to avoid double-hashing.
4353
- this.contentHashIndex.removeByHash(m.frontmatter.contentHash);
4354
- } else {
4355
- // Legacy memory written before contentHash was stored on the
4356
- // frontmatter. Pre-#369 facts were stored without inline
4357
- // citations, so m.content is the raw fact text and we can
4358
- // remove the hash directly from the content. This clears
4359
- // stale dedup entries so the fact can be re-extracted.
4360
- log.warn(
4361
- `[semantic-consolidation] removing hash for legacy memory ${m.frontmatter.id ?? "(unknown)"} via content fallback — no contentHash in frontmatter`,
4362
- );
4363
- this.contentHashIndex.remove(m.content);
4364
- }
4365
- }
4411
+ // Remove from the same storage-scoped content-hash index that
4412
+ // originally deduped this memory.
4413
+ await this.removeContentHashForMemory(
4414
+ targetStorage,
4415
+ m,
4416
+ "semantic-consolidation",
4417
+ );
4366
4418
  // Best-effort index cleanup: a failure here (e.g. on-disk index save
4367
4419
  // under disk-full) must NOT abort the archival loop and thereby skip
4368
4420
  // the catalog write touch below for an already-durable canonical write
@@ -4416,15 +4468,13 @@ export class Orchestrator {
4416
4468
  }
4417
4469
  }
4418
4470
 
4419
- // Save hash index if we modified it
4420
- if (result.memoriesArchived > 0 && this.contentHashIndex) {
4421
- await this.contentHashIndex
4422
- .save()
4423
- .catch((err) =>
4424
- log.warn(
4425
- `[semantic-consolidation] content-hash index save failed: ${err}`,
4426
- ),
4427
- );
4471
+ // Save hash indexes if we modified them.
4472
+ if (result.memoriesArchived > 0) {
4473
+ await this.saveContentHashIndexes().catch((err) =>
4474
+ log.warn(
4475
+ `[semantic-consolidation] content-hash index save failed: ${err}`,
4476
+ ),
4477
+ );
4428
4478
  }
4429
4479
 
4430
4480
  log.info(
@@ -5767,6 +5817,7 @@ export class Orchestrator {
5767
5817
  prompt,
5768
5818
  sessionKey,
5769
5819
  options.namespace?.trim() || undefined,
5820
+ options.principalOverride,
5770
5821
  );
5771
5822
  } catch (err) {
5772
5823
  log.debug(`direct-answer observation setup failed: ${err}`);
@@ -5834,12 +5885,13 @@ export class Orchestrator {
5834
5885
  prompt: string,
5835
5886
  sessionKey: string,
5836
5887
  namespaceOverride: string | undefined,
5888
+ principalOverride: string | undefined,
5837
5889
  ): void {
5838
5890
  const expectedSnapshot = this.lastRecall.get(sessionKey);
5839
5891
  if (expectedSnapshot === null) return;
5840
5892
  if (expectedSnapshot.plannerMode === "no_recall") return;
5841
5893
 
5842
- const principal = resolvePrincipal(sessionKey, this.config);
5894
+ const principal = principalOverride ?? resolvePrincipal(sessionKey, this.config);
5843
5895
  // Coding-agent overlay (issue #569) is applied when the session has a
5844
5896
  // coding context and there is no explicit namespaceOverride — mirrors
5845
5897
  // the main recall path above.
@@ -5851,9 +5903,29 @@ export class Orchestrator {
5851
5903
  const observationCodingSelf = observationCodingOverlay
5852
5904
  ? combineNamespaces(observationPrincipalSelf, observationCodingOverlay.namespace)
5853
5905
  : null;
5906
+ const observationScopeProfilePlan =
5907
+ namespaceOverride && canReadNamespace(principal, namespaceOverride, this.config)
5908
+ ? null
5909
+ : resolveScopeProfilePlan({
5910
+ config: this.config,
5911
+ principal,
5912
+ codingContext: sessionKey
5913
+ ? this.getCodingContextForSession(sessionKey)
5914
+ : null,
5915
+ codingOverlay: observationCodingOverlay,
5916
+ });
5854
5917
  let observationNamespaces: string[];
5855
5918
  if (namespaceOverride && canReadNamespace(principal, namespaceOverride, this.config)) {
5856
5919
  observationNamespaces = [namespaceOverride];
5920
+ } else if (observationScopeProfilePlan) {
5921
+ observationNamespaces = expandScopeProfileReadNamespaces({
5922
+ profilePlan: observationScopeProfilePlan,
5923
+ principalSelfNamespace: observationScopeProfilePlan.baseNamespace,
5924
+ config: this.config,
5925
+ principal,
5926
+ codingOverlay: observationCodingOverlay,
5927
+ legacyRecallNamespaces: recallNamespacesForPrincipal(principal, this.config),
5928
+ });
5857
5929
  } else if (observationCodingOverlay && observationCodingSelf) {
5858
5930
  // Rule 42 / parity with the main recall path: substitute the self
5859
5931
  // namespace within the principal's recall list rather than
@@ -7470,13 +7542,34 @@ export class Orchestrator {
7470
7542
  const codingSelfNamespace = codingOverlay
7471
7543
  ? combineNamespaces(principalSelfNamespace, codingOverlay.namespace)
7472
7544
  : null;
7545
+ const scopeProfilePlan = namespaceOverride
7546
+ ? null
7547
+ : resolveScopeProfilePlan({
7548
+ config: this.config,
7549
+ principal,
7550
+ codingContext: sessionKey
7551
+ ? this.getCodingContextForSession(sessionKey)
7552
+ : null,
7553
+ codingOverlay,
7554
+ });
7555
+ const profileEffectiveNamespace = scopeProfilePlan?.writeNamespace || scopeProfilePlan?.readNamespaces[0];
7473
7556
  const selfNamespace =
7474
7557
  namespaceOverride ??
7558
+ profileEffectiveNamespace ??
7475
7559
  codingSelfNamespace ??
7476
7560
  principalSelfNamespace;
7477
7561
  let recallNamespaces: string[];
7478
7562
  if (namespaceOverride) {
7479
7563
  recallNamespaces = [namespaceOverride];
7564
+ } else if (scopeProfilePlan) {
7565
+ recallNamespaces = expandScopeProfileReadNamespaces({
7566
+ profilePlan: scopeProfilePlan,
7567
+ principalSelfNamespace: scopeProfilePlan.baseNamespace,
7568
+ config: this.config,
7569
+ principal,
7570
+ codingOverlay,
7571
+ legacyRecallNamespaces: readableRecallNamespaces,
7572
+ });
7480
7573
  } else if (codingOverlay && codingSelfNamespace) {
7481
7574
  // Substitute the principal's self namespace with the coding-scoped
7482
7575
  // one, and append any read fallbacks (branch→project, PR 3) combined
@@ -7536,11 +7629,18 @@ export class Orchestrator {
7536
7629
  // so the prior round's authorization invariant is preserved.
7537
7630
  const codingOverlaySelfReadable =
7538
7631
  codingOverlay !== null &&
7539
- readableRecallNamespaces.includes(principalSelfNamespace);
7632
+ (scopeProfilePlan
7633
+ ? scopeProfilePlan.layers.some((layer) => layer.id === "userProject" && layer.readable)
7634
+ : readableRecallNamespaces.includes(principalSelfNamespace));
7540
7635
  let lcmReadNamespaces: string[];
7541
7636
  if (namespaceOverride) {
7542
7637
  // Explicit namespace already read-authorized above (canReadNamespace gate).
7543
7638
  lcmReadNamespaces = [namespaceOverride];
7639
+ } else if (scopeProfilePlan) {
7640
+ // Scope profiles define a layered read stack; LCM-backed evidence uses the
7641
+ // same namespace set as QMD/file recall so team/global/shared observations
7642
+ // are not silently skipped.
7643
+ lcmReadNamespaces = recallNamespaces;
7544
7644
  } else if (codingOverlay && codingSelfNamespace && codingOverlaySelfReadable) {
7545
7645
  // Self base readable → overlay rows authorized. Read the primary overlay
7546
7646
  // key first, then each coding read fallback (project → root), combined with
@@ -7560,11 +7660,14 @@ export class Orchestrator {
7560
7660
  // session_id set. Single-user / no-overlay recall passes a single-namespace
7561
7661
  // set that collapses to the raw `sessionKey`, so this is `[sessionKey]` —
7562
7662
  // byte-for-byte the pre-#1495 single-key behavior.
7563
- const lcmReadSessionIds = lcmReadSessionIdsForNamespaces(
7564
- lcmReadNamespaces,
7565
- sessionKey,
7566
- this.config.defaultNamespace,
7567
- );
7663
+ const lcmReadSessionIds =
7664
+ scopeProfilePlan && !sessionKey
7665
+ ? []
7666
+ : lcmReadSessionIdsForNamespaces(
7667
+ lcmReadNamespaces,
7668
+ sessionKey,
7669
+ this.config.defaultNamespace,
7670
+ );
7568
7671
  // Query an LCM-backed read across the ordered read key set and return the
7569
7672
  // FIRST non-empty result (#1505 fallback-namespace unification). The primary
7570
7673
  // overlay key is tried first; if a branch-scoped session has no rows under its
@@ -7582,9 +7685,12 @@ export class Orchestrator {
7582
7685
  //
7583
7686
  // When the set is a single key (single-user / no-overlay / explicit-namespace),
7584
7687
  // this is exactly one call — unchanged. `lcmSessionId` is `string | undefined`:
7585
- // a SESSIONLESS recall yields the single `undefined` key so the read runs ONE
7586
- // archive-wide read with no `session_id` filter (pre-#1505 behavior). NEVER the
7587
- // literal "default" session id (codex P2).
7688
+ // a legacy SESSIONLESS recall yields the single `undefined` key so the read
7689
+ // runs ONE archive-wide read with no `session_id` filter (pre-#1505 behavior).
7690
+ // Hosted scope profiles are stricter: without a session key there is no
7691
+ // namespace-scoped LCM key to query, so the key set stays empty and LCM cannot
7692
+ // bypass the profile read stack via an archive-wide read. NEVER the literal
7693
+ // "default" session id (codex P2).
7588
7694
  const firstNonEmptyLcmRead = async <T>(
7589
7695
  read: (lcmSessionId: string | undefined) => Promise<T>,
7590
7696
  isEmpty: (value: T) => boolean,
@@ -7782,7 +7888,158 @@ export class Orchestrator {
7782
7888
  return "";
7783
7889
  }
7784
7890
 
7785
- const profileStorage = await this.storageRouter.storageFor(selfNamespace);
7891
+ const profileStorageNamespaces = scopeProfilePlan ? recallNamespaces : [selfNamespace];
7892
+ const profileStorages = await Promise.all(
7893
+ profileStorageNamespaces.map((namespace) => this.storageRouter.storageFor(namespace)),
7894
+ );
7895
+ const emptyProfileStorage = new Proxy(
7896
+ { dir: path.join(this.config.memoryDir, ".empty-scope-profile") } as any,
7897
+ {
7898
+ get(target, prop: string | symbol) {
7899
+ if (prop in target) return target[prop];
7900
+ if (prop === "readProfile") return async () => "";
7901
+ if (
7902
+ prop === "readQuestions" ||
7903
+ prop === "listEntityNames" ||
7904
+ prop === "readContinuityIncidents"
7905
+ )
7906
+ return async () => [];
7907
+ if (
7908
+ prop === "readIdentityAnchor" ||
7909
+ prop === "readIdentityImprovementLoops"
7910
+ )
7911
+ return async () => "";
7912
+ if (prop === "readEntity" || prop === "readMemoryByPath")
7913
+ return async () => null;
7914
+ return async () => [];
7915
+ },
7916
+ },
7917
+ );
7918
+ const profileStorage =
7919
+ profileStorages.length <= 1
7920
+ ? profileStorages[0] ?? emptyProfileStorage
7921
+ : new Proxy(profileStorages[0] as any, {
7922
+ get(target, prop: string | symbol) {
7923
+ if (prop === "readProfile") {
7924
+ return async () => {
7925
+ for (const storage of profileStorages) {
7926
+ const profile = await storage.readProfile();
7927
+ if (profile.trim().length > 0) return profile;
7928
+ }
7929
+ return "";
7930
+ };
7931
+ }
7932
+ if (prop === "readQuestions") {
7933
+ return async (...args: any[]) => {
7934
+ const merged: any[] = [];
7935
+ const seen = new Set<string>();
7936
+ const priorityOf = (question: any): number => {
7937
+ const priority = Number(question?.priority ?? 0);
7938
+ return Number.isFinite(priority) ? priority : 0;
7939
+ };
7940
+ for (const storage of profileStorages) {
7941
+ const questions = await (storage.readQuestions as any)(...args);
7942
+ for (const question of questions) {
7943
+ const key = typeof question === "string" ? question : JSON.stringify(question);
7944
+ if (seen.has(key)) continue;
7945
+ seen.add(key);
7946
+ merged.push(question);
7947
+ }
7948
+ }
7949
+ return merged.sort(
7950
+ (left, right) =>
7951
+ priorityOf(right) - priorityOf(left) ||
7952
+ String(left?.id ?? "").localeCompare(String(right?.id ?? "")),
7953
+ );
7954
+ };
7955
+ }
7956
+ if (prop === "readIdentityAnchor") {
7957
+ return async () => {
7958
+ for (const storage of profileStorages) {
7959
+ const anchor = (await storage.readIdentityAnchor()) ?? "";
7960
+ if (anchor.trim().length > 0) return anchor;
7961
+ }
7962
+ return "";
7963
+ };
7964
+ }
7965
+ if (prop === "readIdentityImprovementLoops") {
7966
+ return async () => {
7967
+ const sections: string[] = [];
7968
+ const seen = new Set<string>();
7969
+ for (const storage of profileStorages) {
7970
+ const loops = ((await storage.readIdentityImprovementLoops()) ?? "").trim();
7971
+ if (!loops || seen.has(loops)) continue;
7972
+ seen.add(loops);
7973
+ sections.push(loops);
7974
+ }
7975
+ return sections.join("\n\n");
7976
+ };
7977
+ }
7978
+ if (prop === "readContinuityIncidents") {
7979
+ return async (...args: any[]) => {
7980
+ const limit = typeof args[0] === "number" && Number.isFinite(args[0]) ? Math.max(0, args[0]) : undefined;
7981
+ const incidents: any[] = [];
7982
+ const seen = new Set<string>();
7983
+ const incidentTime = (incident: any): number => {
7984
+ const raw = incident?.updatedAt ?? incident?.openedAt ?? incident?.createdAt;
7985
+ const parsed = typeof raw === "string" ? Date.parse(raw) : Number.NaN;
7986
+ return Number.isFinite(parsed) ? parsed : 0;
7987
+ };
7988
+ for (const storage of profileStorages) {
7989
+ for (const incident of await (storage.readContinuityIncidents as any)(...args)) {
7990
+ const key = JSON.stringify(incident);
7991
+ if (seen.has(key)) continue;
7992
+ seen.add(key);
7993
+ incidents.push(incident);
7994
+ }
7995
+ }
7996
+ incidents.sort(
7997
+ (left, right) =>
7998
+ incidentTime(right) - incidentTime(left) ||
7999
+ String(left?.id ?? "").localeCompare(String(right?.id ?? "")),
8000
+ );
8001
+ return limit === undefined ? incidents : incidents.slice(0, limit);
8002
+ };
8003
+ }
8004
+ if (prop === "listEntityNames") {
8005
+ return async (...args: any[]) => {
8006
+ const names = new Set<string>();
8007
+ for (const storage of profileStorages) {
8008
+ for (const name of await (storage.listEntityNames as any)(...args)) names.add(name);
8009
+ }
8010
+ return [...names];
8011
+ };
8012
+ }
8013
+ if (prop === "readEntity" || prop === "readMemoryByPath") {
8014
+ return async (...args: any[]) => {
8015
+ for (const storage of profileStorages) {
8016
+ const value = await (storage as any)[prop](...args);
8017
+ if (value) return value;
8018
+ }
8019
+ return null;
8020
+ };
8021
+ }
8022
+ if (prop === "readAllMemories") {
8023
+ return async (...args: any[]) => {
8024
+ const memories: any[] = [];
8025
+ const seen = new Set<string>();
8026
+ for (const storage of profileStorages) {
8027
+ for (const memory of await (storage.readAllMemories as any)(...args)) {
8028
+ const key = String(memory?.path ?? memory?.frontmatter?.id ?? JSON.stringify(memory));
8029
+ if (seen.has(key)) continue;
8030
+ seen.add(key);
8031
+ memories.push(memory);
8032
+ }
8033
+ }
8034
+ return memories;
8035
+ };
8036
+ }
8037
+ return target[prop];
8038
+ },
8039
+ });
8040
+ const profileStorageDirs = Array.from(
8041
+ new Set(profileStorages.map((storage) => storage.dir).filter((dir): dir is string => typeof dir === "string" && dir.length > 0)),
8042
+ );
7786
8043
 
7787
8044
  // --- Phase 1: Launch ALL independent data fetches in parallel ---
7788
8045
  throwIfRecallAborted(options.abortSignal);
@@ -7812,6 +8069,14 @@ export class Orchestrator {
7812
8069
  )
7813
8070
  return null;
7814
8071
  if (!this.sharedContext) return null;
8072
+ if (
8073
+ scopeProfilePlan &&
8074
+ !(
8075
+ scopeProfilePlan.profile.readOrder.includes("serverShared") &&
8076
+ scopeProfilePlan.readNamespaces.includes(this.config.sharedNamespace)
8077
+ )
8078
+ )
8079
+ return null;
7815
8080
  const t0 = Date.now();
7816
8081
  const [priorities, roundtable, crossSignals] = await Promise.all([
7817
8082
  this.sharedContext.readPriorities(),
@@ -8121,13 +8386,64 @@ export class Orchestrator {
8121
8386
  if (!this.config.knowledgeIndexEnabled) return null;
8122
8387
  const t0 = Date.now();
8123
8388
  try {
8124
- const ki = await this.storage.buildKnowledgeIndex(this.config, {
8125
- maxEntities: this.getRecallSectionNumber(
8126
- "knowledge-index",
8127
- "maxEntities",
8128
- ),
8129
- maxChars: this.getRecallSectionNumber("knowledge-index", "maxChars"),
8130
- });
8389
+ const knowledgeIndexMaxChars =
8390
+ this.getRecallSectionNumber("knowledge-index", "maxChars") ??
8391
+ this.config.knowledgeIndexMaxChars;
8392
+ const knowledgeIndexMaxEntities =
8393
+ this.getRecallSectionNumber("knowledge-index", "maxEntities") ??
8394
+ this.config.knowledgeIndexMaxEntities;
8395
+ const knowledgeIndexOptions = {
8396
+ maxEntities: knowledgeIndexMaxEntities,
8397
+ maxChars: knowledgeIndexMaxChars,
8398
+ };
8399
+ const ki = scopeProfilePlan
8400
+ ? await (async () => {
8401
+ const perLayerOptions = {
8402
+ ...knowledgeIndexOptions,
8403
+ maxEntities: Number.MAX_SAFE_INTEGER,
8404
+ maxChars: Number.MAX_SAFE_INTEGER,
8405
+ };
8406
+ const results = await Promise.all(
8407
+ profileStorages.map((storage) =>
8408
+ storage.buildKnowledgeIndex(this.config, perLayerOptions),
8409
+ ),
8410
+ );
8411
+ const sections = results
8412
+ .map((result) => result.result.trim())
8413
+ .filter((section) => section.length > 0);
8414
+ const maxRows = Math.max(0, Math.floor(knowledgeIndexMaxEntities));
8415
+ const rows: string[] = [];
8416
+ let header: string[] | null = null;
8417
+ for (const section of sections) {
8418
+ const lines = section
8419
+ .split("\n")
8420
+ .map((line) => line.trimEnd())
8421
+ .filter((line) => line.length > 0);
8422
+ const tableHeaderIndex = lines.findIndex((line) =>
8423
+ line.startsWith("| Entity |"),
8424
+ );
8425
+ if (tableHeaderIndex === -1) continue;
8426
+ header ??= lines.slice(0, tableHeaderIndex + 2);
8427
+ for (const row of lines.slice(tableHeaderIndex + 2)) {
8428
+ if (!row.startsWith("|")) continue;
8429
+ if (rows.length >= maxRows) break;
8430
+ rows.push(row);
8431
+ }
8432
+ if (rows.length >= maxRows) break;
8433
+ }
8434
+ const merged =
8435
+ header && rows.length > 0
8436
+ ? `${header.join("\n")}\n${rows.join("\n")}\n`
8437
+ : "";
8438
+ return {
8439
+ result: this.truncateRecallSectionToBudget(
8440
+ merged,
8441
+ knowledgeIndexMaxChars,
8442
+ ),
8443
+ cached: results.every((result) => result.cached),
8444
+ };
8445
+ })()
8446
+ : await this.storage.buildKnowledgeIndex(this.config, knowledgeIndexOptions);
8131
8447
  recordRecallSectionMetric({
8132
8448
  section: "ki",
8133
8449
  priority: "core",
@@ -8549,15 +8865,36 @@ export class Orchestrator {
8549
8865
  return null;
8550
8866
  }
8551
8867
 
8552
- const results = await searchHarmonicRetrieval({
8553
- memoryDir: this.config.memoryDir,
8554
- abstractionNodeStoreDir: this.config.abstractionNodeStoreDir,
8555
- query: retrievalQuery,
8556
- maxResults,
8557
- sessionKey,
8558
- anchorsEnabled: this.config.abstractionAnchorsEnabled,
8559
- abortSignal: harmonicRetrievalAbort.signal,
8560
- });
8868
+ const harmonicSearchDirs = scopeProfilePlan ? profileStorageDirs : [this.config.memoryDir];
8869
+ const harmonicResultsByDir = await Promise.all(
8870
+ harmonicSearchDirs.map((memoryDir) =>
8871
+ searchHarmonicRetrieval({
8872
+ memoryDir,
8873
+ abstractionNodeStoreDir: scopeProfilePlan ? undefined : this.config.abstractionNodeStoreDir,
8874
+ query: retrievalQuery,
8875
+ maxResults,
8876
+ sessionKey,
8877
+ anchorsEnabled: this.config.abstractionAnchorsEnabled,
8878
+ abortSignal: harmonicRetrievalAbort.signal,
8879
+ }),
8880
+ ),
8881
+ );
8882
+ const harmonicByNodeId = new Map<string, HarmonicRetrievalResult>();
8883
+ for (const result of harmonicResultsByDir.flat()) {
8884
+ const existing = harmonicByNodeId.get(result.node.nodeId);
8885
+ if (!existing || result.score > existing.score) {
8886
+ harmonicByNodeId.set(result.node.nodeId, result);
8887
+ }
8888
+ }
8889
+ const results = [...harmonicByNodeId.values()]
8890
+ .sort(
8891
+ (left, right) =>
8892
+ right.score - left.score ||
8893
+ right.anchorScore - left.anchorScore ||
8894
+ right.node.recordedAt.localeCompare(left.node.recordedAt) ||
8895
+ left.node.nodeId.localeCompare(right.node.nodeId),
8896
+ )
8897
+ .slice(0, maxResults);
8561
8898
 
8562
8899
  recordRecallSectionMetric({
8563
8900
  section: "harmonicRetrieval",
@@ -8619,11 +8956,28 @@ export class Orchestrator {
8619
8956
  const VERIFIED_RECALL_TIMEOUT_MS = 15_000;
8620
8957
  let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
8621
8958
  const results = await Promise.race([
8622
- searchVerifiedEpisodes({
8623
- memoryDir: profileStorage.dir,
8624
- query: retrievalQuery,
8625
- maxResults,
8626
- boxRecallDays: this.config.boxRecallDays,
8959
+ Promise.all(
8960
+ profileStorageDirs.map((memoryDir) =>
8961
+ searchVerifiedEpisodes({
8962
+ memoryDir,
8963
+ query: retrievalQuery,
8964
+ maxResults,
8965
+ boxRecallDays: this.config.boxRecallDays,
8966
+ }).catch((err) => {
8967
+ log.debug(`verified recall directory scan failed: ${err}`);
8968
+ return [] as VerifiedEpisodeResult[];
8969
+ }),
8970
+ ),
8971
+ ).then((groups) => {
8972
+ const merged: VerifiedEpisodeResult[] = [];
8973
+ const seen = new Set<string>();
8974
+ for (const result of groups.flat()) {
8975
+ const key = result.box.id || JSON.stringify(result);
8976
+ if (seen.has(key)) continue;
8977
+ seen.add(key);
8978
+ merged.push(result);
8979
+ }
8980
+ return merged.sort(compareVerifiedEpisodeResults).slice(0, maxResults);
8627
8981
  }),
8628
8982
  new Promise<[]>((resolve) => {
8629
8983
  timeoutHandle = setTimeout(
@@ -8691,10 +9045,27 @@ export class Orchestrator {
8691
9045
  const VERIFIED_RULES_TIMEOUT_MS = 15_000;
8692
9046
  let rulesTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
8693
9047
  const results = await Promise.race([
8694
- searchVerifiedSemanticRules({
8695
- memoryDir: this.config.memoryDir,
8696
- query: retrievalQuery,
8697
- maxResults,
9048
+ Promise.all(
9049
+ profileStorageDirs.map((memoryDir) =>
9050
+ searchVerifiedSemanticRules({
9051
+ memoryDir,
9052
+ query: retrievalQuery,
9053
+ maxResults,
9054
+ }).catch((err) => {
9055
+ log.debug(`verified rules directory scan failed: ${err}`);
9056
+ return [] as VerifiedSemanticRuleResult[];
9057
+ }),
9058
+ ),
9059
+ ).then((groups) => {
9060
+ const merged: VerifiedSemanticRuleResult[] = [];
9061
+ const seen = new Set<string>();
9062
+ for (const result of groups.flat()) {
9063
+ const key = result.rule.frontmatter.id || result.rule.path || JSON.stringify(result);
9064
+ if (seen.has(key)) continue;
9065
+ seen.add(key);
9066
+ merged.push(result);
9067
+ }
9068
+ return merged.sort(compareVerifiedSemanticRuleResults).slice(0, maxResults);
8698
9069
  }),
8699
9070
  new Promise<[]>((resolve) => {
8700
9071
  rulesTimeoutHandle = setTimeout(
@@ -8760,13 +9131,33 @@ export class Orchestrator {
8760
9131
  return null;
8761
9132
  }
8762
9133
 
8763
- const results = await searchWorkProductLedgerEntries({
8764
- memoryDir: this.config.memoryDir,
8765
- workProductLedgerDir: this.config.workProductLedgerDir,
8766
- query: retrievalQuery,
8767
- maxResults,
8768
- sessionKey,
8769
- });
9134
+ const workProductSearchDirs = scopeProfilePlan ? profileStorageDirs : [this.config.memoryDir];
9135
+ const workProductResultsByDir = await Promise.all(
9136
+ workProductSearchDirs.map((memoryDir) =>
9137
+ searchWorkProductLedgerEntries({
9138
+ memoryDir,
9139
+ workProductLedgerDir: scopeProfilePlan ? undefined : this.config.workProductLedgerDir,
9140
+ query: retrievalQuery,
9141
+ maxResults,
9142
+ sessionKey,
9143
+ }),
9144
+ ),
9145
+ );
9146
+ const workProductByEntryId = new Map<string, WorkProductLedgerSearchResult>();
9147
+ for (const result of workProductResultsByDir.flat()) {
9148
+ const existing = workProductByEntryId.get(result.entry.entryId);
9149
+ if (!existing || result.score > existing.score) {
9150
+ workProductByEntryId.set(result.entry.entryId, result);
9151
+ }
9152
+ }
9153
+ const results = [...workProductByEntryId.values()]
9154
+ .sort(
9155
+ (left, right) =>
9156
+ right.score - left.score ||
9157
+ right.entry.recordedAt.localeCompare(left.entry.recordedAt) ||
9158
+ left.entry.entryId.localeCompare(right.entry.entryId),
9159
+ )
9160
+ .slice(0, maxResults);
8770
9161
 
8771
9162
  recordRecallSectionMetric({
8772
9163
  section: "workProducts",
@@ -8979,24 +9370,56 @@ export class Orchestrator {
8979
9370
  this.config.parallelRetrievalEnabled && maxPerAgent > 0
8980
9371
  ? Promise.all([
8981
9372
  shouldRunAgent("direct", retrievalQuery, 0)
8982
- ? runDirectAgent(
8983
- retrievalQuery,
8984
- profileStorage.dir,
8985
- maxPerAgent,
8986
- ).catch((err) => {
8987
- log.debug(`DirectAgent pre-start failed: ${err}`);
8988
- return [] as ParallelSearchResult[];
9373
+ ? Promise.all(
9374
+ profileStorageDirs.map((memoryDir) =>
9375
+ runDirectAgent(
9376
+ retrievalQuery,
9377
+ memoryDir,
9378
+ maxPerAgent,
9379
+ ).catch((err) => {
9380
+ log.debug(`DirectAgent pre-start failed: ${err}`);
9381
+ return [] as ParallelSearchResult[];
9382
+ }),
9383
+ ),
9384
+ ).then((groups) => {
9385
+ const merged: ParallelSearchResult[] = [];
9386
+ const seen = new Set<string>();
9387
+ for (const result of groups.flat()) {
9388
+ const key = (result as any).path ?? JSON.stringify(result);
9389
+ if (seen.has(key)) continue;
9390
+ seen.add(key);
9391
+ merged.push(result);
9392
+ }
9393
+ return merged
9394
+ .sort((a, b) => b.score - a.score)
9395
+ .slice(0, maxPerAgent);
8989
9396
  })
8990
9397
  : Promise.resolve([] as ParallelSearchResult[]),
8991
9398
  shouldRunAgent("temporal", retrievalQuery, 0)
8992
- ? runTemporalAgent(
8993
- retrievalQuery,
8994
- this.config.memoryDir,
8995
- maxPerAgent,
8996
- queryAwarePrefilter.candidatePaths,
8997
- ).catch((err) => {
8998
- log.debug(`TemporalAgent pre-start failed: ${err}`);
8999
- return [] as ParallelSearchResult[];
9399
+ ? Promise.all(
9400
+ profileStorageDirs.map((memoryDir) =>
9401
+ runTemporalAgent(
9402
+ retrievalQuery,
9403
+ memoryDir,
9404
+ maxPerAgent,
9405
+ queryAwarePrefilter.candidatePaths,
9406
+ ).catch((err) => {
9407
+ log.debug(`TemporalAgent pre-start failed for ${memoryDir}: ${err}`);
9408
+ return [] as ParallelSearchResult[];
9409
+ }),
9410
+ ),
9411
+ ).then((groups) => {
9412
+ const merged: ParallelSearchResult[] = [];
9413
+ const seen = new Set<string>();
9414
+ for (const result of groups.flat()) {
9415
+ const key = (result as any).path ?? JSON.stringify(result);
9416
+ if (seen.has(key)) continue;
9417
+ seen.add(key);
9418
+ merged.push(result);
9419
+ }
9420
+ return merged
9421
+ .sort((a, b) => b.score - a.score)
9422
+ .slice(0, maxPerAgent);
9000
9423
  })
9001
9424
  : Promise.resolve([] as ParallelSearchResult[]),
9002
9425
  ])
@@ -9589,9 +10012,29 @@ export class Orchestrator {
9589
10012
  ) &&
9590
10013
  this.config.memoryBoxesEnabled &&
9591
10014
  this.config.boxRecallDays > 0
9592
- ? this.boxBuilderFor(profileStorage)
9593
- .readRecentBoxes(this.config.boxRecallDays)
9594
- .catch(() => [] as BoxFrontmatter[])
10015
+ ? Promise.all(
10016
+ profileStorages.map((storage) =>
10017
+ this.boxBuilderFor(storage)
10018
+ .readRecentBoxes(this.config.boxRecallDays)
10019
+ .catch(() => [] as BoxFrontmatter[]),
10020
+ ),
10021
+ ).then((groups) => {
10022
+ const boxes: BoxFrontmatter[] = [];
10023
+ const seen = new Set<string>();
10024
+ for (const box of groups.flat()) {
10025
+ const key = JSON.stringify(box);
10026
+ if (seen.has(key)) continue;
10027
+ seen.add(key);
10028
+ boxes.push(box);
10029
+ }
10030
+ return boxes.sort((a, b) => {
10031
+ const aTime = Date.parse(a.sealedAt ?? "");
10032
+ const bTime = Date.parse(b.sealedAt ?? "");
10033
+ const aRank = Number.isFinite(aTime) ? aTime : 0;
10034
+ const bRank = Number.isFinite(bTime) ? bTime : 0;
10035
+ return bRank - aRank;
10036
+ });
10037
+ })
9595
10038
  : Promise.resolve([] as BoxFrontmatter[]),
9596
10039
  );
9597
10040
 
@@ -12684,18 +13127,57 @@ export class Orchestrator {
12684
13127
  options.principalOverride.length > 0
12685
13128
  ? options.principalOverride
12686
13129
  : resolvePrincipal(sessionKey, this.config);
12687
- // Write path — overlay the coding-agent namespace (issue #569) when the
12688
- // session has a codingContext and `codingMode.projectScope` is true.
12689
- // Explicit `writeNamespaceOverride` from callers still wins, matching
12690
- // pre-#569 semantics.
12691
- const selfNamespace =
13130
+ // Write path — explicit callers still win. Otherwise, an active hosted
13131
+ // scope profile owns the extraction write target so hook-captured turns land
13132
+ // in the same layer that profile recall searches. Without a profile, preserve
13133
+ // the existing coding-agent overlay behavior (issue #569).
13134
+ const explicitWriteNamespace =
12692
13135
  typeof options.writeNamespaceOverride === "string" &&
12693
13136
  options.writeNamespaceOverride.length > 0
12694
13137
  ? options.writeNamespaceOverride
12695
- : this.applyCodingNamespaceOverlay(
12696
- sessionKey,
12697
- defaultNamespaceForPrincipal(principal, this.config),
12698
- );
13138
+ : undefined;
13139
+ const codingContextForWrite = sessionKey
13140
+ ? this.getCodingContextForSession(sessionKey)
13141
+ : null;
13142
+ const codingOverlayForWrite = resolveCodingNamespaceOverlay(
13143
+ codingContextForWrite,
13144
+ this.config.codingMode,
13145
+ this.config.defaultNamespace,
13146
+ );
13147
+ const scopeProfileGatePlan = resolveScopeProfilePlan({
13148
+ config: this.config,
13149
+ principal,
13150
+ codingContext: codingContextForWrite,
13151
+ codingOverlay: codingOverlayForWrite,
13152
+ });
13153
+ const scopeProfileWritePlan = explicitWriteNamespace ? null : scopeProfileGatePlan;
13154
+ if (scopeProfileWritePlan) {
13155
+ const selectedLayer = scopeProfileWritePlan.layers.find(
13156
+ (layer) => layer.id === scopeProfileWritePlan.writeLayer,
13157
+ );
13158
+ const writeNamespaceReadable = scopeProfileWritePlan.readNamespaces.includes(
13159
+ scopeProfileWritePlan.writeNamespace,
13160
+ );
13161
+ if (!selectedLayer?.writable || !writeNamespaceReadable) {
13162
+ log.warn(
13163
+ `runExtraction: skipping scope profile ${scopeProfileWritePlan.profileId} because write layer ${scopeProfileWritePlan.writeLayer} is not writable inside the profile read stack`,
13164
+ );
13165
+ await clearBuffer();
13166
+ return {
13167
+ status: "skipped",
13168
+ reason: "scope_profile_no_writable_layer",
13169
+ persistedCount: 0,
13170
+ durableOutputCount: 0,
13171
+ };
13172
+ }
13173
+ }
13174
+ const selfNamespace =
13175
+ explicitWriteNamespace ??
13176
+ scopeProfileWritePlan?.writeNamespace ??
13177
+ this.applyCodingNamespaceOverlay(
13178
+ sessionKey,
13179
+ defaultNamespaceForPrincipal(principal, this.config),
13180
+ );
12699
13181
  const storage = await this.storageRouter.storageFor(selfNamespace);
12700
13182
  const shouldPersistProcessedFingerprint = targetTurns.some(
12701
13183
  (turn) => turn.persistProcessedFingerprint === true,
@@ -12855,6 +13337,7 @@ export class Orchestrator {
12855
13337
  // Pass the KNOWN base namespace (NHIdx) so the catalog write touch records the
12856
13338
  // real namespace rather than a guess decoded from the storage dir.
12857
13339
  selfNamespace,
13340
+ scopeProfileGatePlan,
12858
13341
  );
12859
13342
  let postPersistMetadataFailed = false;
12860
13343
  meta ??= await storage.loadMeta();
@@ -13337,17 +13820,69 @@ export class Orchestrator {
13337
13820
  try {
13338
13821
  if (this.config.namespacesEnabled) {
13339
13822
  // Include cataloged dynamic namespaces, not just the configured set
13340
- // (NGnei) resolve once and reuse for both update and embed.
13341
- const maintenanceNamespaces = await this.maintenanceNamespaces();
13342
- await this.namespaceSearchRouter.updateNamespaces(maintenanceNamespaces);
13823
+ // (NGnei), but run through the namespace-aware maintenance planner so
13824
+ // each namespace is budgeted, lock-protected, and status-recorded
13825
+ // independently (issue #1500).
13826
+ const plan = await this.namespaceMaintenancePlan("qmd");
13343
13827
  const now = Date.now();
13344
- if (
13345
- this.config.qmdAutoEmbedEnabled &&
13346
- now - this.lastQmdEmbedAtMs >= this.config.qmdEmbedMinIntervalMs
13347
- ) {
13348
- await this.namespaceSearchRouter.embedNamespaces(maintenanceNamespaces);
13828
+ const lastEmbedAtByNamespace =
13829
+ this.lastQmdEmbedAtMsByNamespace ?? (this.lastQmdEmbedAtMsByNamespace = new Map());
13830
+ const dueEmbedNamespaces = (namespaces: string[]): string[] => {
13831
+ if (!this.config.qmdAutoEmbedEnabled) return [];
13832
+ return namespaces.filter(
13833
+ (namespace) =>
13834
+ now - (lastEmbedAtByNamespace.get(namespace) ?? 0) >= this.config.qmdEmbedMinIntervalMs,
13835
+ );
13836
+ };
13837
+ const markEmbedded = (namespaces: string[]): void => {
13838
+ if (namespaces.length === 0) return;
13839
+ for (const namespace of namespaces) {
13840
+ lastEmbedAtByNamespace.set(namespace, now);
13841
+ }
13349
13842
  this.lastQmdEmbedAtMs = now;
13350
- }
13843
+ };
13844
+ await runNamespaceMaintenanceBatchPlan(
13845
+ this.config,
13846
+ plan,
13847
+ async (candidates) => {
13848
+ const namespaces = candidates.map((candidate) => candidate.namespace);
13849
+ const embedNamespaces = dueEmbedNamespaces(namespaces);
13850
+ let result: Awaited<ReturnType<NamespaceSearchRouter["updateNamespacesDetailed"]>>;
13851
+ try {
13852
+ result = await this.namespaceSearchRouter.updateNamespacesDetailed(
13853
+ namespaces,
13854
+ undefined,
13855
+ { strict: true },
13856
+ );
13857
+ } catch (error) {
13858
+ if (
13859
+ embedNamespaces.length > 0 &&
13860
+ qmdMaintenanceSkipReasonForError(error) === "throttled"
13861
+ ) {
13862
+ await this.namespaceSearchRouter.embedNamespaces(embedNamespaces, { strict: true });
13863
+ markEmbedded(embedNamespaces);
13864
+ }
13865
+ throw error;
13866
+ }
13867
+ if (result.backendCount <= 0) {
13868
+ throw new Error("no eligible QMD backend for selected namespaces");
13869
+ }
13870
+ if (result.eligibleNamespaces.length !== namespaces.length) {
13871
+ const eligible = new Set(result.eligibleNamespaces);
13872
+ const missing = namespaces.filter((namespace) => !eligible.has(namespace));
13873
+ throw new Error(`QMD backend ineligible for selected namespaces (${missing.length})`);
13874
+ }
13875
+ if (embedNamespaces.length > 0) {
13876
+ await this.namespaceSearchRouter.embedNamespaces(embedNamespaces, { strict: true });
13877
+ markEmbedded(embedNamespaces);
13878
+ }
13879
+ return { itemCount: result.backendCount };
13880
+ },
13881
+ this.namespaceCatalog,
13882
+ {
13883
+ skipReasonForError: qmdMaintenanceSkipReasonForError,
13884
+ },
13885
+ );
13351
13886
  } else {
13352
13887
  await this.qmd.update();
13353
13888
  const now = Date.now();
@@ -13373,6 +13908,7 @@ export class Orchestrator {
13373
13908
  threadIdForExtraction?: string | null,
13374
13909
  sourceContext?: { sessionKey?: string; principal?: string; validAt?: string },
13375
13910
  baseNamespace?: string,
13911
+ scopeProfileWritePlan?: ResolvedScopeProfilePlan | null,
13376
13912
  ): Promise<string[]> {
13377
13913
  // Inline source attribution (issue #369). When enabled, every extracted
13378
13914
  // fact is rewritten to carry a compact provenance tag inside its body so
@@ -13467,6 +14003,60 @@ export class Orchestrator {
13467
14003
  "inferred",
13468
14004
  "speculative",
13469
14005
  ] as const;
14006
+ const sharedProfileLayer = scopeProfileWritePlan?.layers.find(
14007
+ (layer) =>
14008
+ layer.id === "serverShared" &&
14009
+ layer.namespace === this.config.sharedNamespace,
14010
+ );
14011
+ const sharedPromotionTarget = scopeProfileWritePlan?.promotionTargets.find(
14012
+ (target) =>
14013
+ target.target === "serverShared" &&
14014
+ target.namespace === this.config.sharedNamespace,
14015
+ );
14016
+ const profileAllowsSharedWrites =
14017
+ !scopeProfileWritePlan ||
14018
+ Boolean(
14019
+ scopeProfileWritePlan.profile.readOrder.includes("serverShared") &&
14020
+ scopeProfileWritePlan.readNamespaces.includes(this.config.sharedNamespace) &&
14021
+ sharedProfileLayer?.readable &&
14022
+ sharedProfileLayer.writable &&
14023
+ sharedPromotionTarget?.authorized,
14024
+ );
14025
+ const profileAutoPromotionAllows = (
14026
+ category: string,
14027
+ confidence: number,
14028
+ ): boolean => {
14029
+ if (!scopeProfileWritePlan) return false;
14030
+ const actualTier = confidenceTier(confidence);
14031
+ const actualRank = confidenceTierOrder.indexOf(actualTier);
14032
+ if (actualRank === -1) return false;
14033
+ const autoPromote = scopeProfileWritePlan.profile.autoPromote;
14034
+ if (!autoPromote.enabled) return false;
14035
+ if (!autoPromote.categories.includes(category as any)) return false;
14036
+ const minimumRank = confidenceTierOrder.indexOf(autoPromote.minConfidenceTier);
14037
+ return minimumRank !== -1 && actualRank <= minimumRank;
14038
+ };
14039
+ const sharedAutoPromotionAllows = (
14040
+ category: string,
14041
+ confidence: number,
14042
+ ): boolean => {
14043
+ if (!scopeProfileWritePlan) {
14044
+ const actualTier = confidenceTier(confidence);
14045
+ const actualRank = confidenceTierOrder.indexOf(actualTier);
14046
+ if (actualRank === -1) return false;
14047
+ if (!this.config.autoPromoteToSharedEnabled) return false;
14048
+ if (!this.config.autoPromoteToSharedCategories.includes(category as any))
14049
+ return false;
14050
+ const minimumRank = confidenceTierOrder.indexOf(
14051
+ this.config.autoPromoteMinConfidenceTier,
14052
+ );
14053
+ return minimumRank !== -1 && actualRank <= minimumRank;
14054
+ }
14055
+ return (
14056
+ scopeProfileWritePlan.profile.autoPromote.targets.includes("serverShared") &&
14057
+ profileAutoPromotionAllows(category, confidence)
14058
+ );
14059
+ };
13470
14060
  const shouldPromoteToShared = (
13471
14061
  targetStorage: StorageManager,
13472
14062
  category: string,
@@ -13474,7 +14064,8 @@ export class Orchestrator {
13474
14064
  ): boolean => {
13475
14065
  if (
13476
14066
  !this.config.namespacesEnabled ||
13477
- !this.config.autoPromoteToSharedEnabled
14067
+ !profileAllowsSharedWrites ||
14068
+ !sharedAutoPromotionAllows(category, confidence)
13478
14069
  )
13479
14070
  return false;
13480
14071
  if (
@@ -13482,15 +14073,124 @@ export class Orchestrator {
13482
14073
  this.config.sharedNamespace
13483
14074
  )
13484
14075
  return false;
13485
- if (!this.config.autoPromoteToSharedCategories.includes(category as any))
13486
- return false;
13487
- const actualTier = confidenceTier(confidence);
13488
- const actualRank = confidenceTierOrder.indexOf(actualTier);
13489
- const minimumRank = confidenceTierOrder.indexOf(
13490
- this.config.autoPromoteMinConfidenceTier,
14076
+ return true;
14077
+ };
14078
+ const promoteMemoryToProfileTargets = async (options: {
14079
+ sourceStorage: StorageManager;
14080
+ category: string;
14081
+ content: string;
14082
+ confidence: number;
14083
+ tags: string[];
14084
+ entityRef?: string;
14085
+ structuredAttributes?: Record<string, string>;
14086
+ sourceMemoryId: string;
14087
+ importance?: ReturnType<typeof scoreImportance>;
14088
+ intentGoal?: string;
14089
+ intentActionType?: string;
14090
+ intentEntityTypes?: string[];
14091
+ memoryKind?: MemoryFrontmatter["memoryKind"];
14092
+ validAt?: string;
14093
+ source: string;
14094
+ }): Promise<void> => {
14095
+ if (
14096
+ !scopeProfileWritePlan ||
14097
+ !profileAutoPromotionAllows(options.category, options.confidence)
14098
+ )
14099
+ return;
14100
+ const autoTargets = new Set(scopeProfileWritePlan.profile.autoPromote.targets);
14101
+ const targets = scopeProfileWritePlan.promotionTargets.filter(
14102
+ (target) =>
14103
+ target.target !== "serverShared" &&
14104
+ autoTargets.has(target.target) &&
14105
+ target.authorized &&
14106
+ target.namespace,
13491
14107
  );
13492
- if (actualRank === -1 || minimumRank === -1) return false;
13493
- return actualRank <= minimumRank;
14108
+ if (targets.length === 0) return;
14109
+ const rawContent =
14110
+ citationEnabled && hasCitationForTemplate(options.content, citationTemplate)
14111
+ ? stripCitationForTemplate(options.content, citationTemplate)
14112
+ : options.content;
14113
+ const citedContent = applyInlineCitation(rawContent);
14114
+ const sanitizedBase = sanitizeMemoryContent(rawContent);
14115
+ const dedupContent =
14116
+ options.category === "fact" &&
14117
+ options.structuredAttributes &&
14118
+ Object.keys(options.structuredAttributes).length > 0
14119
+ ? `${sanitizedBase.text}\n[Attributes: ${normalizeAttributePairs(options.structuredAttributes)}]`
14120
+ : sanitizedBase.text;
14121
+ for (const target of targets) {
14122
+ if (!target.namespace) continue;
14123
+ try {
14124
+ const targetStorage = await this.storageRouter.storageFor(target.namespace);
14125
+ if (targetStorage.dir === options.sourceStorage.dir) continue;
14126
+ if (
14127
+ options.category === "fact" &&
14128
+ (await targetStorage.hasFactContentHash(dedupContent))
14129
+ ) {
14130
+ continue;
14131
+ }
14132
+ const promotedId = await targetStorage.writeMemory(
14133
+ options.category as any,
14134
+ citedContent,
14135
+ {
14136
+ confidence: options.confidence,
14137
+ tags: [...options.tags, `${target.target}-promotion`],
14138
+ entityRef: options.entityRef,
14139
+ structuredAttributes: options.structuredAttributes,
14140
+ source: `${options.source}-${target.target}-promotion`,
14141
+ importance: options.importance,
14142
+ lineage: [options.sourceMemoryId],
14143
+ sourceMemoryId: options.sourceMemoryId,
14144
+ intentGoal: options.intentGoal,
14145
+ intentActionType: options.intentActionType,
14146
+ intentEntityTypes: options.intentEntityTypes,
14147
+ memoryKind: options.memoryKind,
14148
+ validAt: options.validAt,
14149
+ contentHashSource: options.category === "fact" ? dedupContent : rawContent,
14150
+ },
14151
+ );
14152
+ if (
14153
+ this.config.temporalSupersessionEnabled &&
14154
+ options.category === "fact" &&
14155
+ options.entityRef &&
14156
+ options.structuredAttributes &&
14157
+ Object.keys(options.structuredAttributes).length > 0
14158
+ ) {
14159
+ try {
14160
+ await applyTemporalSupersession({
14161
+ storage: targetStorage,
14162
+ newMemoryId: promotedId,
14163
+ entityRef: options.entityRef,
14164
+ structuredAttributes: options.structuredAttributes,
14165
+ createdAt: supersessionOrderingAt(options.validAt),
14166
+ enabled: true,
14167
+ });
14168
+ } catch (profileSupersessionErr) {
14169
+ log.warn(
14170
+ `persistExtraction: ${target.target} promotion temporal supersession failed open for promoted ${promotedId}: ${profileSupersessionErr}`,
14171
+ );
14172
+ }
14173
+ }
14174
+ this.markCatalogWrite(target.namespace, targetStorage.dir);
14175
+ trackPersistedId(targetStorage, promotedId, { includeReturnedIds: false });
14176
+ await this.indexPersistedMemory(targetStorage, promotedId);
14177
+ trackBehaviorSignals(
14178
+ targetStorage,
14179
+ buildBehaviorSignalsForMemory({
14180
+ memoryId: promotedId,
14181
+ category: options.category as any,
14182
+ content: options.content,
14183
+ namespace: target.namespace,
14184
+ confidence: options.confidence,
14185
+ source: "extraction",
14186
+ }),
14187
+ );
14188
+ } catch (err) {
14189
+ log.warn(
14190
+ `persistExtraction: ${target.target} promotion failed open for ${options.sourceMemoryId}: ${err}`,
14191
+ );
14192
+ }
14193
+ }
13494
14194
  };
13495
14195
  const promoteMemoryToShared = async (options: {
13496
14196
  sourceStorage: StorageManager;
@@ -13509,6 +14209,7 @@ export class Orchestrator {
13509
14209
  validAt?: string;
13510
14210
  source: string;
13511
14211
  }): Promise<void> => {
14212
+ await promoteMemoryToProfileTargets(options);
13512
14213
  if (
13513
14214
  !shouldPromoteToShared(
13514
14215
  options.sourceStorage,
@@ -13733,11 +14434,10 @@ export class Orchestrator {
13733
14434
  intentEntityTypes: options.intentEntityTypes,
13734
14435
  memoryKind: options.memoryKind,
13735
14436
  validAt: options.validAt,
13736
- // Index the RAW content hash so hasFactContentHash(rawContent)
13737
- // returns true on subsequent extractions. Without this, the index
13738
- // would record the hash of citedContent (which changes every call
13739
- // due to an updated timestamp), causing duplicate promotions.
13740
- contentHashSource: rawContent,
14437
+ // Index the same canonical body used by hasFactContentHash above.
14438
+ // For structured facts this includes the normalized Attributes
14439
+ // suffix, matching StorageManager.writeMemory enrichment.
14440
+ contentHashSource: options.category === "fact" ? dedupContent : rawContent,
13741
14441
  },
13742
14442
  );
13743
14443
  // PR #402 Finding 3 fix: run temporal supersession against the shared
@@ -14191,7 +14891,7 @@ export class Orchestrator {
14191
14891
  !routedNamespaceExplicit
14192
14892
  ) {
14193
14893
  const currentNs = this.namespaceFromStorageDir(targetStorage.dir);
14194
- if (currentNs !== this.config.sharedNamespace) {
14894
+ if (currentNs !== this.config.sharedNamespace && profileAllowsSharedWrites) {
14195
14895
  try {
14196
14896
  targetStorage = await this.storageRouter.storageFor(
14197
14897
  this.config.sharedNamespace,
@@ -14205,6 +14905,10 @@ export class Orchestrator {
14205
14905
  `scope-routing: failed to resolve shared namespace storage; writing to session namespace (fail-open): ${scopeRouteErr}`,
14206
14906
  );
14207
14907
  }
14908
+ } else if (currentNs !== this.config.sharedNamespace) {
14909
+ log.debug(
14910
+ `scope-routing: skipped shared namespace for global fact because active scope profile ${scopeProfileWritePlan?.profileId ?? "none"} does not authorize serverShared writes`,
14911
+ );
14208
14912
  }
14209
14913
  }
14210
14914
 
@@ -14219,9 +14923,20 @@ export class Orchestrator {
14219
14923
  writeCategory === "procedure"
14220
14924
  ? buildProcedurePersistBody(fact.content, fact.procedureSteps)
14221
14925
  : canonicalContentForHash;
14222
- if (this.contentHashIndex && this.contentHashIndex.has(contentHashDedupKey)) {
14926
+ let exactDuplicate = false;
14927
+ try {
14928
+ exactDuplicate = await this.hasContentHashDedup(
14929
+ targetStorage,
14930
+ contentHashDedupKey,
14931
+ );
14932
+ } catch (err) {
14933
+ log.warn(
14934
+ `content-hash dedup lookup failed for storage ${targetStorage.dir}; writing fact fail-open: ${err}`,
14935
+ );
14936
+ }
14937
+ if (exactDuplicate) {
14223
14938
  log.debug(
14224
- `dedup: skipping duplicate fact "${fact.content.slice(0, 60)}…"`,
14939
+ `dedup: skipping duplicate fact "${fact.content.slice(0, 60)}…" in storage ${targetStorage.dir}`,
14225
14940
  );
14226
14941
  dedupedCount++;
14227
14942
  continue;
@@ -14669,17 +15384,20 @@ export class Orchestrator {
14669
15384
  validAt: sourceContext?.validAt,
14670
15385
  source: extractionWriteSource,
14671
15386
  });
14672
- // Register chunked content in hash index too.
15387
+ // Register chunked content in the target storage hash index too.
14673
15388
  // Thread 3 fix: canonicalize by stripping any pre-existing citation
14674
- // so the stored hash matches what the dedup check computes via
14675
- // stripCitationForTemplate before calling contentHashIndex.has().
14676
- if (this.contentHashIndex) {
15389
+ // so the stored hash matches what the dedup check computes.
15390
+ try {
14677
15391
  const canonicalChunkedContent =
14678
15392
  citationEnabled &&
14679
15393
  hasCitationForTemplate(fact.content, citationTemplate)
14680
15394
  ? stripCitationForTemplate(fact.content, citationTemplate)
14681
15395
  : fact.content;
14682
- this.contentHashIndex.add(canonicalChunkedContent);
15396
+ await this.addContentHashDedup(targetStorage, canonicalChunkedContent);
15397
+ } catch (err) {
15398
+ log.warn(
15399
+ `content-hash dedup registration failed for chunked memory ${parentId}: ${err}`,
15400
+ );
14683
15401
  }
14684
15402
 
14685
15403
  for (const chunk of chunkResult.chunks) {
@@ -14957,11 +15675,10 @@ export class Orchestrator {
14957
15675
  intentEntityTypes: inferredIntent?.entityTypes,
14958
15676
  });
14959
15677
  }
14960
- // Register in content-hash index after successful write.
14961
- // Thread 3 fix: canonicalize by stripping any pre-existing citation so
14962
- // the stored hash matches what the dedup check computes via
14963
- // stripCitationForTemplate before calling contentHashIndex.has().
14964
- if (this.contentHashIndex) {
15678
+ // Register in the target storage content-hash index after successful
15679
+ // write. Thread 3 fix: canonicalize by stripping any pre-existing
15680
+ // citation so the stored hash matches what the dedup check computes.
15681
+ try {
14965
15682
  const canonicalFactContent =
14966
15683
  citationEnabled &&
14967
15684
  hasCitationForTemplate(fact.content, citationTemplate)
@@ -14971,7 +15688,11 @@ export class Orchestrator {
14971
15688
  writeCategory === "procedure"
14972
15689
  ? buildProcedurePersistBody(fact.content, fact.procedureSteps)
14973
15690
  : canonicalFactContent;
14974
- this.contentHashIndex.add(hashRegisterKey);
15691
+ await this.addContentHashDedup(targetStorage, hashRegisterKey);
15692
+ } catch (err) {
15693
+ log.warn(
15694
+ `content-hash dedup registration failed for memory ${memoryId}: ${err}`,
15695
+ );
14975
15696
  }
14976
15697
  } finally {
14977
15698
  // Catalog touch (issue #1499): record AFTER every synchronous
@@ -15127,12 +15848,10 @@ export class Orchestrator {
15127
15848
  touchBaseNonFactNamespace();
15128
15849
  }
15129
15850
 
15130
- // Save content-hash index after batch
15131
- if (this.contentHashIndex) {
15132
- await this.contentHashIndex
15133
- .save()
15134
- .catch((err) => log.warn(`content-hash index save failed: ${err}`));
15135
- }
15851
+ // Save any content-hash indexes touched during the batch.
15852
+ await this.saveContentHashIndexes().catch((err) =>
15853
+ log.warn(`content-hash index save failed: ${err}`),
15854
+ );
15136
15855
 
15137
15856
  for (const {
15138
15857
  storage: targetStorage,
@@ -16470,27 +17189,13 @@ export class Orchestrator {
16470
17189
  // All criteria met — archive
16471
17190
  const result = await this.storage.archiveMemory(memory);
16472
17191
  if (result) {
16473
- // Remove from content-hash index since it's no longer in hot search.
16474
- // Prefer the raw-content hash stored on the frontmatter at write
16475
- // time (contentHash) — it is format-agnostic and survives any
16476
- // citation template.
16477
- if (this.contentHashIndex) {
16478
- if (memory.frontmatter.contentHash) {
16479
- // Modern memory: frontmatter.contentHash is already a SHA-256
16480
- // hex string — use removeByHash to avoid double-hashing.
16481
- this.contentHashIndex.removeByHash(memory.frontmatter.contentHash);
16482
- } else {
16483
- // Legacy memory written before contentHash was stored on the
16484
- // frontmatter. Pre-#369 facts were stored without inline
16485
- // citations, so memory.content is the raw fact text and we can
16486
- // remove the hash directly from the content. This clears
16487
- // stale dedup entries so the fact can be re-extracted.
16488
- log.warn(
16489
- `[fact-archival] removing hash for legacy memory ${memory.frontmatter.id ?? "(unknown)"} via content fallback — no contentHash in frontmatter`,
16490
- );
16491
- this.contentHashIndex.remove(memory.content);
16492
- }
16493
- }
17192
+ // Remove from the same storage-scoped content-hash index since it is
17193
+ // no longer in hot search.
17194
+ await this.removeContentHashForMemory(
17195
+ this.storage,
17196
+ memory,
17197
+ "fact-archival",
17198
+ );
16494
17199
  await this.embeddingFallback.removeFromIndex(memory.frontmatter.id);
16495
17200
  if (
16496
17201
  this.config.queryAwareIndexingEnabled &&
@@ -16508,13 +17213,11 @@ export class Orchestrator {
16508
17213
  }
16509
17214
  }
16510
17215
 
16511
- // Save hash index if we removed any entries
16512
- if (archivedCount > 0 && this.contentHashIndex) {
16513
- await this.contentHashIndex
16514
- .save()
16515
- .catch((err) =>
16516
- log.warn(`content-hash index save failed during archival: ${err}`),
16517
- );
17216
+ // Save hash indexes if we removed any entries.
17217
+ if (archivedCount > 0) {
17218
+ await this.saveContentHashIndexes().catch((err) =>
17219
+ log.warn(`content-hash index save failed during archival: ${err}`),
17220
+ );
16518
17221
  }
16519
17222
 
16520
17223
  return archivedCount;