@remnic/core 9.3.655 → 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 (247) hide show
  1. package/dist/access-cli.js +22 -22
  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 +10 -10
  7. package/dist/{access-service-BEJvriUt.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 +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-PVE7KSQP.js → chunk-2BD7DG37.js} +2 -2
  26. package/dist/{chunk-54LOUIBE.js → chunk-2MXEVL75.js} +2 -2
  27. package/dist/{chunk-55ZMNKMQ.js → chunk-4UL7VPTD.js} +276 -57
  28. package/dist/chunk-4UL7VPTD.js.map +1 -0
  29. package/dist/{chunk-COVZLGMR.js → chunk-54XF2FY7.js} +17 -17
  30. package/dist/{chunk-UYNFWZWG.js → chunk-AGJKWOKV.js} +2 -2
  31. package/dist/{chunk-TDZSSJV4.js → chunk-AZBV4RRY.js} +1 -1
  32. package/dist/chunk-AZBV4RRY.js.map +1 -0
  33. package/dist/{chunk-KOI765XP.js → chunk-CTAV55JM.js} +241 -1
  34. package/dist/chunk-CTAV55JM.js.map +1 -0
  35. package/dist/{chunk-A3Y37UWI.js → chunk-DIBWFCLA.js} +3 -3
  36. package/dist/{chunk-QDVQ4AN2.js → chunk-DR67OK4E.js} +5 -5
  37. package/dist/{chunk-XBIACVCO.js → chunk-EC2AYKRX.js} +2 -2
  38. package/dist/{chunk-IQ53ZSXV.js → chunk-GCYFUTUC.js} +2 -2
  39. package/dist/{chunk-YYN3LIYA.js → chunk-GSHW5VVD.js} +5 -5
  40. package/dist/chunk-GYSYLGNE.js +650 -0
  41. package/dist/chunk-GYSYLGNE.js.map +1 -0
  42. package/dist/{chunk-NRBGRZW4.js → chunk-IOZ5WBWD.js} +2 -2
  43. package/dist/{chunk-NCSJKK23.js → chunk-JSVFEHLL.js} +7 -5
  44. package/dist/chunk-JSVFEHLL.js.map +1 -0
  45. package/dist/{chunk-7LWRCOP7.js → chunk-LZTFCAKE.js} +2 -2
  46. package/dist/{chunk-TEO46GMM.js → chunk-NXCK7DO7.js} +2 -2
  47. package/dist/{chunk-XOFXKASO.js → chunk-PEPHBH2W.js} +2 -2
  48. package/dist/{chunk-WDTUYOLS.js → chunk-QZRKNA5F.js} +2 -2
  49. package/dist/{chunk-PS3SYNHP.js → chunk-R5DB26G6.js} +2 -2
  50. package/dist/{chunk-5QD3QD76.js → chunk-RDW5G6DO.js} +659 -123
  51. package/dist/chunk-RDW5G6DO.js.map +1 -0
  52. package/dist/{chunk-BGKXTVNG.js → chunk-SWDHVH2P.js} +2 -2
  53. package/dist/{chunk-67G4T7KI.js → chunk-SXYCVRLK.js} +3 -3
  54. package/dist/{chunk-UCEABZZN.js → chunk-TFFZUFEP.js} +7 -5
  55. package/dist/chunk-TFFZUFEP.js.map +1 -0
  56. package/dist/{chunk-UCEDY5M7.js → chunk-TIJYQXDI.js} +2 -2
  57. package/dist/{chunk-2RCGZ67B.js → chunk-VAEAGTEQ.js} +3 -3
  58. package/dist/{chunk-XRKQOQLY.js → chunk-WIKMCJUR.js} +2 -2
  59. package/dist/{chunk-KZZ4YAEC.js → chunk-WWMHAMAY.js} +2 -2
  60. package/dist/{chunk-OKW6F5S5.js → chunk-YEZHZCUO.js} +4 -4
  61. package/dist/{chunk-5FOCXX5E.js → chunk-YVVQUAOO.js} +3 -3
  62. package/dist/{chunk-5FOCXX5E.js.map → chunk-YVVQUAOO.js.map} +1 -1
  63. package/dist/{chunk-3XGWCZ63.js → chunk-YXLT4EMM.js} +2 -2
  64. package/dist/{chunk-PTMJ2FH2.js → chunk-Z6UDTNY6.js} +2 -2
  65. package/dist/{cli-BGahB_d3.d.ts → cli-aYxSuPvP.d.ts} +3 -3
  66. package/dist/cli.d.ts +5 -5
  67. package/dist/cli.js +22 -22
  68. package/dist/compounding/engine.d.ts +1 -1
  69. package/dist/compounding/engine.js +3 -3
  70. package/dist/compounding/preference-consolidator.d.ts +1 -1
  71. package/dist/compression-optimizer.d.ts +1 -1
  72. package/dist/config.d.ts +1 -1
  73. package/dist/config.js +1 -1
  74. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  75. package/dist/connectors/codex-materialize-runner.js +3 -3
  76. package/dist/connectors/codex-materialize.d.ts +1 -1
  77. package/dist/connectors/index.d.ts +1 -1
  78. package/dist/connectors/index.js +3 -3
  79. package/dist/consolidation-provenance-check.d.ts +1 -1
  80. package/dist/consolidation-undo.d.ts +1 -1
  81. package/dist/contradiction/index.d.ts +1 -1
  82. package/dist/conversation-index/backend.d.ts +1 -1
  83. package/dist/conversation-index/chunker.d.ts +1 -1
  84. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  85. package/dist/conversation-index/indexer.d.ts +1 -1
  86. package/dist/conversation-index/search.d.ts +1 -1
  87. package/dist/day-summary.d.ts +1 -1
  88. package/dist/delinearize.d.ts +1 -1
  89. package/dist/direct-answer-wiring.d.ts +1 -1
  90. package/dist/direct-answer.d.ts +1 -1
  91. package/dist/embedding-fallback.d.ts +1 -1
  92. package/dist/enrichment/index.d.ts +1 -1
  93. package/dist/entity-retrieval.d.ts +1 -1
  94. package/dist/entity-retrieval.js +3 -3
  95. package/dist/entity-schema.d.ts +1 -1
  96. package/dist/explicit-capture.d.ts +3 -3
  97. package/dist/explicit-cue-recall.js +2 -2
  98. package/dist/extraction-judge-telemetry.d.ts +1 -1
  99. package/dist/extraction-judge-training.d.ts +1 -1
  100. package/dist/extraction-judge.d.ts +1 -1
  101. package/dist/extraction.d.ts +1 -1
  102. package/dist/fallback-llm.d.ts +1 -1
  103. package/dist/focused-list-recall.js +2 -2
  104. package/dist/identity-continuity.d.ts +1 -1
  105. package/dist/importance.d.ts +1 -1
  106. package/dist/index.d.ts +121 -121
  107. package/dist/index.js +32 -32
  108. package/dist/intent.d.ts +1 -1
  109. package/dist/lcm/engine.d.ts +1 -1
  110. package/dist/lcm/index.d.ts +1 -1
  111. package/dist/lcm/tools.d.ts +1 -1
  112. package/dist/lcm-fallback-read.js +1 -1
  113. package/dist/lifecycle.d.ts +1 -1
  114. package/dist/live-connectors-runner.d.ts +1 -1
  115. package/dist/local-llm.d.ts +1 -1
  116. package/dist/maintenance/memory-governance.d.ts +1 -1
  117. package/dist/maintenance/memory-governance.js +3 -3
  118. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  119. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  120. package/dist/mcp-memory-inspector-app.d.ts +4 -4
  121. package/dist/memory-action-policy.d.ts +1 -1
  122. package/dist/memory-cache.d.ts +1 -1
  123. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  124. package/dist/memory-projection-store.d.ts +1 -1
  125. package/dist/memory-provenance.d.ts +1 -1
  126. package/dist/memory-worth-outcomes.d.ts +1 -1
  127. package/dist/models-json.d.ts +1 -1
  128. package/dist/namespaces/migrate.d.ts +1 -1
  129. package/dist/namespaces/migrate.js +4 -4
  130. package/dist/namespaces/principal.d.ts +1 -1
  131. package/dist/namespaces/search.d.ts +1 -1
  132. package/dist/namespaces/storage.d.ts +1 -1
  133. package/dist/namespaces/storage.js +3 -3
  134. package/dist/native-knowledge.d.ts +1 -1
  135. package/dist/operator-toolkit.d.ts +1 -1
  136. package/dist/operator-toolkit.js +7 -7
  137. package/dist/{orchestrator-BgzZlWxH.d.ts → orchestrator-D1wcmPNj.d.ts} +8 -2
  138. package/dist/orchestrator.d.ts +3 -3
  139. package/dist/orchestrator.js +18 -18
  140. package/dist/patterns-cli.d.ts +1 -1
  141. package/dist/policy-runtime.d.ts +1 -1
  142. package/dist/qmd-recall-cache.d.ts +1 -1
  143. package/dist/qmd.d.ts +1 -1
  144. package/dist/recall-disclosure-escalation.d.ts +1 -1
  145. package/dist/recall-explain-renderer.d.ts +1 -1
  146. package/dist/recall-explain-renderer.js +3 -3
  147. package/dist/recall-planner-llm.d.ts +1 -1
  148. package/dist/recall-state.d.ts +1 -1
  149. package/dist/recall-tag-filter.d.ts +1 -1
  150. package/dist/recall-xray-cli.d.ts +1 -1
  151. package/dist/recall-xray-cli.js +4 -4
  152. package/dist/recall-xray-renderer.d.ts +1 -1
  153. package/dist/recall-xray-renderer.js +3 -3
  154. package/dist/recall-xray.d.ts +1 -1
  155. package/dist/recall-xray.js +2 -2
  156. package/dist/resolve-auth-token.d.ts +1 -1
  157. package/dist/response-guidance-recall.js +2 -2
  158. package/dist/resume-bundles.js +2 -2
  159. package/dist/retrieval-agents.d.ts +1 -1
  160. package/dist/retrieval-tiers.d.ts +1 -1
  161. package/dist/routing/engine.d.ts +1 -1
  162. package/dist/routing/store.d.ts +1 -1
  163. package/dist/search/embed-helper.d.ts +1 -1
  164. package/dist/search/factory.d.ts +1 -1
  165. package/dist/search/index.d.ts +1 -1
  166. package/dist/search/lancedb-backend.d.ts +1 -1
  167. package/dist/search/meilisearch-backend.d.ts +1 -1
  168. package/dist/search/noop-backend.d.ts +1 -1
  169. package/dist/search/orama-backend.d.ts +1 -1
  170. package/dist/search/port.d.ts +1 -1
  171. package/dist/search/remote-backend.d.ts +1 -1
  172. package/dist/{semantic-consolidation-Z8d_uMq8.d.ts → semantic-consolidation-MWOdNtSE.d.ts} +1 -1
  173. package/dist/semantic-consolidation.d.ts +2 -2
  174. package/dist/semantic-consolidation.js +4 -4
  175. package/dist/semantic-rule-promotion.js +3 -3
  176. package/dist/semantic-rule-verifier.d.ts +3 -2
  177. package/dist/semantic-rule-verifier.js +5 -3
  178. package/dist/session-observer-bands.d.ts +1 -1
  179. package/dist/session-observer-state.d.ts +1 -1
  180. package/dist/shared-context/manager.d.ts +1 -1
  181. package/dist/signal.d.ts +1 -1
  182. package/dist/storage.d.ts +1 -1
  183. package/dist/storage.js +2 -2
  184. package/dist/summarizer.d.ts +1 -1
  185. package/dist/summary-snapshot.d.ts +1 -1
  186. package/dist/targeted-fact-recall.js +2 -2
  187. package/dist/temporal-supersession.d.ts +1 -1
  188. package/dist/temporal-validity.d.ts +1 -1
  189. package/dist/threading.d.ts +1 -1
  190. package/dist/tier-migration.d.ts +1 -1
  191. package/dist/tier-routing.d.ts +1 -1
  192. package/dist/topics.d.ts +1 -1
  193. package/dist/transcript.d.ts +1 -1
  194. package/dist/{types-2OPlQWJG.d.ts → types-CgcCpUrf.d.ts} +39 -1
  195. package/dist/types.d.ts +1 -1
  196. package/dist/types.js +1 -1
  197. package/dist/utility-runtime.d.ts +1 -1
  198. package/dist/verified-recall.d.ts +2 -1
  199. package/dist/verified-recall.js +5 -3
  200. package/package.json +1 -1
  201. package/src/access-service-observe-lcm-parity.test.ts +86 -1
  202. package/src/access-service-observe-scope.test.ts +283 -1
  203. package/src/access-service-raw-excerpt-read-gate.test.ts +53 -0
  204. package/src/access-service.ts +391 -93
  205. package/src/coding/coding-namespace.ts +0 -3
  206. package/src/config.ts +282 -0
  207. package/src/lcm-fallback-read.ts +2 -6
  208. package/src/namespaces/scope-profiles.test.ts +1074 -0
  209. package/src/namespaces/scope-profiles.ts +456 -0
  210. package/src/orchestrator-flush.test.ts +142 -0
  211. package/src/orchestrator-source-attribution.test.ts +73 -0
  212. package/src/orchestrator.ts +835 -163
  213. package/src/semantic-rule-verifier.ts +13 -6
  214. package/src/types.ts +52 -0
  215. package/src/verified-recall.ts +10 -6
  216. package/dist/chunk-55ZMNKMQ.js.map +0 -1
  217. package/dist/chunk-5QD3QD76.js.map +0 -1
  218. package/dist/chunk-KOI765XP.js.map +0 -1
  219. package/dist/chunk-MMJANTJX.js +0 -339
  220. package/dist/chunk-MMJANTJX.js.map +0 -1
  221. package/dist/chunk-NCSJKK23.js.map +0 -1
  222. package/dist/chunk-TDZSSJV4.js.map +0 -1
  223. package/dist/chunk-UCEABZZN.js.map +0 -1
  224. /package/dist/{chunk-PVE7KSQP.js.map → chunk-2BD7DG37.js.map} +0 -0
  225. /package/dist/{chunk-54LOUIBE.js.map → chunk-2MXEVL75.js.map} +0 -0
  226. /package/dist/{chunk-COVZLGMR.js.map → chunk-54XF2FY7.js.map} +0 -0
  227. /package/dist/{chunk-UYNFWZWG.js.map → chunk-AGJKWOKV.js.map} +0 -0
  228. /package/dist/{chunk-A3Y37UWI.js.map → chunk-DIBWFCLA.js.map} +0 -0
  229. /package/dist/{chunk-QDVQ4AN2.js.map → chunk-DR67OK4E.js.map} +0 -0
  230. /package/dist/{chunk-XBIACVCO.js.map → chunk-EC2AYKRX.js.map} +0 -0
  231. /package/dist/{chunk-IQ53ZSXV.js.map → chunk-GCYFUTUC.js.map} +0 -0
  232. /package/dist/{chunk-YYN3LIYA.js.map → chunk-GSHW5VVD.js.map} +0 -0
  233. /package/dist/{chunk-NRBGRZW4.js.map → chunk-IOZ5WBWD.js.map} +0 -0
  234. /package/dist/{chunk-7LWRCOP7.js.map → chunk-LZTFCAKE.js.map} +0 -0
  235. /package/dist/{chunk-TEO46GMM.js.map → chunk-NXCK7DO7.js.map} +0 -0
  236. /package/dist/{chunk-XOFXKASO.js.map → chunk-PEPHBH2W.js.map} +0 -0
  237. /package/dist/{chunk-WDTUYOLS.js.map → chunk-QZRKNA5F.js.map} +0 -0
  238. /package/dist/{chunk-PS3SYNHP.js.map → chunk-R5DB26G6.js.map} +0 -0
  239. /package/dist/{chunk-BGKXTVNG.js.map → chunk-SWDHVH2P.js.map} +0 -0
  240. /package/dist/{chunk-67G4T7KI.js.map → chunk-SXYCVRLK.js.map} +0 -0
  241. /package/dist/{chunk-UCEDY5M7.js.map → chunk-TIJYQXDI.js.map} +0 -0
  242. /package/dist/{chunk-2RCGZ67B.js.map → chunk-VAEAGTEQ.js.map} +0 -0
  243. /package/dist/{chunk-XRKQOQLY.js.map → chunk-WIKMCJUR.js.map} +0 -0
  244. /package/dist/{chunk-KZZ4YAEC.js.map → chunk-WWMHAMAY.js.map} +0 -0
  245. /package/dist/{chunk-OKW6F5S5.js.map → chunk-YEZHZCUO.js.map} +0 -0
  246. /package/dist/{chunk-3XGWCZ63.js.map → chunk-YXLT4EMM.js.map} +0 -0
  247. /package/dist/{chunk-PTMJ2FH2.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";
@@ -320,6 +322,11 @@ import {
320
322
  recallNamespacesForPrincipal,
321
323
  resolvePrincipal,
322
324
  } from "./namespaces/principal.js";
325
+ import {
326
+ expandScopeProfileReadNamespaces,
327
+ resolveScopeProfilePlan,
328
+ type ResolvedScopeProfilePlan,
329
+ } from "./namespaces/scope-profiles.js";
323
330
  import {
324
331
  combineNamespaces,
325
332
  lcmReadSessionIdsForNamespaces,
@@ -1889,6 +1896,7 @@ export class Orchestrator {
1889
1896
  private readonly _peerIdBySession = new Map<string, string>();
1890
1897
  private routingRulesStore: RoutingRulesStore | null = null;
1891
1898
  private contentHashIndex: ContentHashIndex | null = null;
1899
+ private readonly contentHashIndexesByStorageDir = new Map<string, ContentHashIndex>();
1892
1900
  private readonly artifactSourceStatusCache = new WeakMap<
1893
1901
  StorageManager,
1894
1902
  {
@@ -2480,6 +2488,13 @@ export class Orchestrator {
2480
2488
  searchOptions?: SearchQueryOptions;
2481
2489
  execution?: SearchExecutionOptions;
2482
2490
  }): Promise<QmdSearchResult[]> {
2491
+ if (
2492
+ this.config.namespacesEnabled &&
2493
+ options.namespaces !== undefined &&
2494
+ options.namespaces.length === 0
2495
+ ) {
2496
+ return [];
2497
+ }
2483
2498
  const namespaces = this.config.namespacesEnabled
2484
2499
  ? Array.from(
2485
2500
  new Set(
@@ -2551,6 +2566,79 @@ export class Orchestrator {
2551
2566
 
2552
2567
  invalidateLiveContentHashIndex(): void {
2553
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
+ }
2554
2642
  }
2555
2643
 
2556
2644
  constructor(config: PluginConfig) {
@@ -4320,28 +4408,13 @@ export class Orchestrator {
4320
4408
  relatedMemoryIds: [canonicalId],
4321
4409
  });
4322
4410
  if (archiveResult) {
4323
- // Remove from content-hash index.
4324
- // Use the raw-content hash stored on the frontmatter at write
4325
- // time (contentHash) — it is format-agnostic and survives any
4326
- // citation template. Legacy memories without contentHash are
4327
- // skipped (see Finding 2 — Urgw).
4328
- if (this.contentHashIndex) {
4329
- if (m.frontmatter.contentHash) {
4330
- // Modern memory: frontmatter.contentHash is already a SHA-256
4331
- // hex string — use removeByHash to avoid double-hashing.
4332
- this.contentHashIndex.removeByHash(m.frontmatter.contentHash);
4333
- } else {
4334
- // Legacy memory written before contentHash was stored on the
4335
- // frontmatter. Pre-#369 facts were stored without inline
4336
- // citations, so m.content is the raw fact text and we can
4337
- // remove the hash directly from the content. This clears
4338
- // stale dedup entries so the fact can be re-extracted.
4339
- log.warn(
4340
- `[semantic-consolidation] removing hash for legacy memory ${m.frontmatter.id ?? "(unknown)"} via content fallback — no contentHash in frontmatter`,
4341
- );
4342
- this.contentHashIndex.remove(m.content);
4343
- }
4344
- }
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
+ );
4345
4418
  // Best-effort index cleanup: a failure here (e.g. on-disk index save
4346
4419
  // under disk-full) must NOT abort the archival loop and thereby skip
4347
4420
  // the catalog write touch below for an already-durable canonical write
@@ -4395,15 +4468,13 @@ export class Orchestrator {
4395
4468
  }
4396
4469
  }
4397
4470
 
4398
- // Save hash index if we modified it
4399
- if (result.memoriesArchived > 0 && this.contentHashIndex) {
4400
- await this.contentHashIndex
4401
- .save()
4402
- .catch((err) =>
4403
- log.warn(
4404
- `[semantic-consolidation] content-hash index save failed: ${err}`,
4405
- ),
4406
- );
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
+ );
4407
4478
  }
4408
4479
 
4409
4480
  log.info(
@@ -5746,6 +5817,7 @@ export class Orchestrator {
5746
5817
  prompt,
5747
5818
  sessionKey,
5748
5819
  options.namespace?.trim() || undefined,
5820
+ options.principalOverride,
5749
5821
  );
5750
5822
  } catch (err) {
5751
5823
  log.debug(`direct-answer observation setup failed: ${err}`);
@@ -5813,12 +5885,13 @@ export class Orchestrator {
5813
5885
  prompt: string,
5814
5886
  sessionKey: string,
5815
5887
  namespaceOverride: string | undefined,
5888
+ principalOverride: string | undefined,
5816
5889
  ): void {
5817
5890
  const expectedSnapshot = this.lastRecall.get(sessionKey);
5818
5891
  if (expectedSnapshot === null) return;
5819
5892
  if (expectedSnapshot.plannerMode === "no_recall") return;
5820
5893
 
5821
- const principal = resolvePrincipal(sessionKey, this.config);
5894
+ const principal = principalOverride ?? resolvePrincipal(sessionKey, this.config);
5822
5895
  // Coding-agent overlay (issue #569) is applied when the session has a
5823
5896
  // coding context and there is no explicit namespaceOverride — mirrors
5824
5897
  // the main recall path above.
@@ -5830,9 +5903,29 @@ export class Orchestrator {
5830
5903
  const observationCodingSelf = observationCodingOverlay
5831
5904
  ? combineNamespaces(observationPrincipalSelf, observationCodingOverlay.namespace)
5832
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
+ });
5833
5917
  let observationNamespaces: string[];
5834
5918
  if (namespaceOverride && canReadNamespace(principal, namespaceOverride, this.config)) {
5835
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
+ });
5836
5929
  } else if (observationCodingOverlay && observationCodingSelf) {
5837
5930
  // Rule 42 / parity with the main recall path: substitute the self
5838
5931
  // namespace within the principal's recall list rather than
@@ -7449,13 +7542,34 @@ export class Orchestrator {
7449
7542
  const codingSelfNamespace = codingOverlay
7450
7543
  ? combineNamespaces(principalSelfNamespace, codingOverlay.namespace)
7451
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];
7452
7556
  const selfNamespace =
7453
7557
  namespaceOverride ??
7558
+ profileEffectiveNamespace ??
7454
7559
  codingSelfNamespace ??
7455
7560
  principalSelfNamespace;
7456
7561
  let recallNamespaces: string[];
7457
7562
  if (namespaceOverride) {
7458
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
+ });
7459
7573
  } else if (codingOverlay && codingSelfNamespace) {
7460
7574
  // Substitute the principal's self namespace with the coding-scoped
7461
7575
  // one, and append any read fallbacks (branch→project, PR 3) combined
@@ -7515,11 +7629,18 @@ export class Orchestrator {
7515
7629
  // so the prior round's authorization invariant is preserved.
7516
7630
  const codingOverlaySelfReadable =
7517
7631
  codingOverlay !== null &&
7518
- readableRecallNamespaces.includes(principalSelfNamespace);
7632
+ (scopeProfilePlan
7633
+ ? scopeProfilePlan.layers.some((layer) => layer.id === "userProject" && layer.readable)
7634
+ : readableRecallNamespaces.includes(principalSelfNamespace));
7519
7635
  let lcmReadNamespaces: string[];
7520
7636
  if (namespaceOverride) {
7521
7637
  // Explicit namespace already read-authorized above (canReadNamespace gate).
7522
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;
7523
7644
  } else if (codingOverlay && codingSelfNamespace && codingOverlaySelfReadable) {
7524
7645
  // Self base readable → overlay rows authorized. Read the primary overlay
7525
7646
  // key first, then each coding read fallback (project → root), combined with
@@ -7539,11 +7660,14 @@ export class Orchestrator {
7539
7660
  // session_id set. Single-user / no-overlay recall passes a single-namespace
7540
7661
  // set that collapses to the raw `sessionKey`, so this is `[sessionKey]` —
7541
7662
  // byte-for-byte the pre-#1495 single-key behavior.
7542
- const lcmReadSessionIds = lcmReadSessionIdsForNamespaces(
7543
- lcmReadNamespaces,
7544
- sessionKey,
7545
- this.config.defaultNamespace,
7546
- );
7663
+ const lcmReadSessionIds =
7664
+ scopeProfilePlan && !sessionKey
7665
+ ? []
7666
+ : lcmReadSessionIdsForNamespaces(
7667
+ lcmReadNamespaces,
7668
+ sessionKey,
7669
+ this.config.defaultNamespace,
7670
+ );
7547
7671
  // Query an LCM-backed read across the ordered read key set and return the
7548
7672
  // FIRST non-empty result (#1505 fallback-namespace unification). The primary
7549
7673
  // overlay key is tried first; if a branch-scoped session has no rows under its
@@ -7561,9 +7685,12 @@ export class Orchestrator {
7561
7685
  //
7562
7686
  // When the set is a single key (single-user / no-overlay / explicit-namespace),
7563
7687
  // this is exactly one call — unchanged. `lcmSessionId` is `string | undefined`:
7564
- // a SESSIONLESS recall yields the single `undefined` key so the read runs ONE
7565
- // archive-wide read with no `session_id` filter (pre-#1505 behavior). NEVER the
7566
- // 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).
7567
7694
  const firstNonEmptyLcmRead = async <T>(
7568
7695
  read: (lcmSessionId: string | undefined) => Promise<T>,
7569
7696
  isEmpty: (value: T) => boolean,
@@ -7761,7 +7888,158 @@ export class Orchestrator {
7761
7888
  return "";
7762
7889
  }
7763
7890
 
7764
- 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
+ );
7765
8043
 
7766
8044
  // --- Phase 1: Launch ALL independent data fetches in parallel ---
7767
8045
  throwIfRecallAborted(options.abortSignal);
@@ -7791,6 +8069,14 @@ export class Orchestrator {
7791
8069
  )
7792
8070
  return null;
7793
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;
7794
8080
  const t0 = Date.now();
7795
8081
  const [priorities, roundtable, crossSignals] = await Promise.all([
7796
8082
  this.sharedContext.readPriorities(),
@@ -8100,13 +8386,64 @@ export class Orchestrator {
8100
8386
  if (!this.config.knowledgeIndexEnabled) return null;
8101
8387
  const t0 = Date.now();
8102
8388
  try {
8103
- const ki = await this.storage.buildKnowledgeIndex(this.config, {
8104
- maxEntities: this.getRecallSectionNumber(
8105
- "knowledge-index",
8106
- "maxEntities",
8107
- ),
8108
- maxChars: this.getRecallSectionNumber("knowledge-index", "maxChars"),
8109
- });
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);
8110
8447
  recordRecallSectionMetric({
8111
8448
  section: "ki",
8112
8449
  priority: "core",
@@ -8528,15 +8865,36 @@ export class Orchestrator {
8528
8865
  return null;
8529
8866
  }
8530
8867
 
8531
- const results = await searchHarmonicRetrieval({
8532
- memoryDir: this.config.memoryDir,
8533
- abstractionNodeStoreDir: this.config.abstractionNodeStoreDir,
8534
- query: retrievalQuery,
8535
- maxResults,
8536
- sessionKey,
8537
- anchorsEnabled: this.config.abstractionAnchorsEnabled,
8538
- abortSignal: harmonicRetrievalAbort.signal,
8539
- });
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);
8540
8898
 
8541
8899
  recordRecallSectionMetric({
8542
8900
  section: "harmonicRetrieval",
@@ -8598,11 +8956,28 @@ export class Orchestrator {
8598
8956
  const VERIFIED_RECALL_TIMEOUT_MS = 15_000;
8599
8957
  let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
8600
8958
  const results = await Promise.race([
8601
- searchVerifiedEpisodes({
8602
- memoryDir: profileStorage.dir,
8603
- query: retrievalQuery,
8604
- maxResults,
8605
- 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);
8606
8981
  }),
8607
8982
  new Promise<[]>((resolve) => {
8608
8983
  timeoutHandle = setTimeout(
@@ -8670,10 +9045,27 @@ export class Orchestrator {
8670
9045
  const VERIFIED_RULES_TIMEOUT_MS = 15_000;
8671
9046
  let rulesTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
8672
9047
  const results = await Promise.race([
8673
- searchVerifiedSemanticRules({
8674
- memoryDir: this.config.memoryDir,
8675
- query: retrievalQuery,
8676
- 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);
8677
9069
  }),
8678
9070
  new Promise<[]>((resolve) => {
8679
9071
  rulesTimeoutHandle = setTimeout(
@@ -8739,13 +9131,33 @@ export class Orchestrator {
8739
9131
  return null;
8740
9132
  }
8741
9133
 
8742
- const results = await searchWorkProductLedgerEntries({
8743
- memoryDir: this.config.memoryDir,
8744
- workProductLedgerDir: this.config.workProductLedgerDir,
8745
- query: retrievalQuery,
8746
- maxResults,
8747
- sessionKey,
8748
- });
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);
8749
9161
 
8750
9162
  recordRecallSectionMetric({
8751
9163
  section: "workProducts",
@@ -8958,24 +9370,56 @@ export class Orchestrator {
8958
9370
  this.config.parallelRetrievalEnabled && maxPerAgent > 0
8959
9371
  ? Promise.all([
8960
9372
  shouldRunAgent("direct", retrievalQuery, 0)
8961
- ? runDirectAgent(
8962
- retrievalQuery,
8963
- profileStorage.dir,
8964
- maxPerAgent,
8965
- ).catch((err) => {
8966
- log.debug(`DirectAgent pre-start failed: ${err}`);
8967
- 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);
8968
9396
  })
8969
9397
  : Promise.resolve([] as ParallelSearchResult[]),
8970
9398
  shouldRunAgent("temporal", retrievalQuery, 0)
8971
- ? runTemporalAgent(
8972
- retrievalQuery,
8973
- this.config.memoryDir,
8974
- maxPerAgent,
8975
- queryAwarePrefilter.candidatePaths,
8976
- ).catch((err) => {
8977
- log.debug(`TemporalAgent pre-start failed: ${err}`);
8978
- 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);
8979
9423
  })
8980
9424
  : Promise.resolve([] as ParallelSearchResult[]),
8981
9425
  ])
@@ -9568,9 +10012,29 @@ export class Orchestrator {
9568
10012
  ) &&
9569
10013
  this.config.memoryBoxesEnabled &&
9570
10014
  this.config.boxRecallDays > 0
9571
- ? this.boxBuilderFor(profileStorage)
9572
- .readRecentBoxes(this.config.boxRecallDays)
9573
- .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
+ })
9574
10038
  : Promise.resolve([] as BoxFrontmatter[]),
9575
10039
  );
9576
10040
 
@@ -12663,18 +13127,57 @@ export class Orchestrator {
12663
13127
  options.principalOverride.length > 0
12664
13128
  ? options.principalOverride
12665
13129
  : resolvePrincipal(sessionKey, this.config);
12666
- // Write path — overlay the coding-agent namespace (issue #569) when the
12667
- // session has a codingContext and `codingMode.projectScope` is true.
12668
- // Explicit `writeNamespaceOverride` from callers still wins, matching
12669
- // pre-#569 semantics.
12670
- 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 =
12671
13135
  typeof options.writeNamespaceOverride === "string" &&
12672
13136
  options.writeNamespaceOverride.length > 0
12673
13137
  ? options.writeNamespaceOverride
12674
- : this.applyCodingNamespaceOverlay(
12675
- sessionKey,
12676
- defaultNamespaceForPrincipal(principal, this.config),
12677
- );
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
+ );
12678
13181
  const storage = await this.storageRouter.storageFor(selfNamespace);
12679
13182
  const shouldPersistProcessedFingerprint = targetTurns.some(
12680
13183
  (turn) => turn.persistProcessedFingerprint === true,
@@ -12834,6 +13337,7 @@ export class Orchestrator {
12834
13337
  // Pass the KNOWN base namespace (NHIdx) so the catalog write touch records the
12835
13338
  // real namespace rather than a guess decoded from the storage dir.
12836
13339
  selfNamespace,
13340
+ scopeProfileGatePlan,
12837
13341
  );
12838
13342
  let postPersistMetadataFailed = false;
12839
13343
  meta ??= await storage.loadMeta();
@@ -13404,6 +13908,7 @@ export class Orchestrator {
13404
13908
  threadIdForExtraction?: string | null,
13405
13909
  sourceContext?: { sessionKey?: string; principal?: string; validAt?: string },
13406
13910
  baseNamespace?: string,
13911
+ scopeProfileWritePlan?: ResolvedScopeProfilePlan | null,
13407
13912
  ): Promise<string[]> {
13408
13913
  // Inline source attribution (issue #369). When enabled, every extracted
13409
13914
  // fact is rewritten to carry a compact provenance tag inside its body so
@@ -13498,6 +14003,60 @@ export class Orchestrator {
13498
14003
  "inferred",
13499
14004
  "speculative",
13500
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
+ };
13501
14060
  const shouldPromoteToShared = (
13502
14061
  targetStorage: StorageManager,
13503
14062
  category: string,
@@ -13505,7 +14064,8 @@ export class Orchestrator {
13505
14064
  ): boolean => {
13506
14065
  if (
13507
14066
  !this.config.namespacesEnabled ||
13508
- !this.config.autoPromoteToSharedEnabled
14067
+ !profileAllowsSharedWrites ||
14068
+ !sharedAutoPromotionAllows(category, confidence)
13509
14069
  )
13510
14070
  return false;
13511
14071
  if (
@@ -13513,15 +14073,124 @@ export class Orchestrator {
13513
14073
  this.config.sharedNamespace
13514
14074
  )
13515
14075
  return false;
13516
- if (!this.config.autoPromoteToSharedCategories.includes(category as any))
13517
- return false;
13518
- const actualTier = confidenceTier(confidence);
13519
- const actualRank = confidenceTierOrder.indexOf(actualTier);
13520
- const minimumRank = confidenceTierOrder.indexOf(
13521
- 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,
13522
14107
  );
13523
- if (actualRank === -1 || minimumRank === -1) return false;
13524
- 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
+ }
13525
14194
  };
13526
14195
  const promoteMemoryToShared = async (options: {
13527
14196
  sourceStorage: StorageManager;
@@ -13540,6 +14209,7 @@ export class Orchestrator {
13540
14209
  validAt?: string;
13541
14210
  source: string;
13542
14211
  }): Promise<void> => {
14212
+ await promoteMemoryToProfileTargets(options);
13543
14213
  if (
13544
14214
  !shouldPromoteToShared(
13545
14215
  options.sourceStorage,
@@ -13764,11 +14434,10 @@ export class Orchestrator {
13764
14434
  intentEntityTypes: options.intentEntityTypes,
13765
14435
  memoryKind: options.memoryKind,
13766
14436
  validAt: options.validAt,
13767
- // Index the RAW content hash so hasFactContentHash(rawContent)
13768
- // returns true on subsequent extractions. Without this, the index
13769
- // would record the hash of citedContent (which changes every call
13770
- // due to an updated timestamp), causing duplicate promotions.
13771
- 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,
13772
14441
  },
13773
14442
  );
13774
14443
  // PR #402 Finding 3 fix: run temporal supersession against the shared
@@ -14222,7 +14891,7 @@ export class Orchestrator {
14222
14891
  !routedNamespaceExplicit
14223
14892
  ) {
14224
14893
  const currentNs = this.namespaceFromStorageDir(targetStorage.dir);
14225
- if (currentNs !== this.config.sharedNamespace) {
14894
+ if (currentNs !== this.config.sharedNamespace && profileAllowsSharedWrites) {
14226
14895
  try {
14227
14896
  targetStorage = await this.storageRouter.storageFor(
14228
14897
  this.config.sharedNamespace,
@@ -14236,6 +14905,10 @@ export class Orchestrator {
14236
14905
  `scope-routing: failed to resolve shared namespace storage; writing to session namespace (fail-open): ${scopeRouteErr}`,
14237
14906
  );
14238
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
+ );
14239
14912
  }
14240
14913
  }
14241
14914
 
@@ -14250,9 +14923,20 @@ export class Orchestrator {
14250
14923
  writeCategory === "procedure"
14251
14924
  ? buildProcedurePersistBody(fact.content, fact.procedureSteps)
14252
14925
  : canonicalContentForHash;
14253
- 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) {
14254
14938
  log.debug(
14255
- `dedup: skipping duplicate fact "${fact.content.slice(0, 60)}…"`,
14939
+ `dedup: skipping duplicate fact "${fact.content.slice(0, 60)}…" in storage ${targetStorage.dir}`,
14256
14940
  );
14257
14941
  dedupedCount++;
14258
14942
  continue;
@@ -14700,17 +15384,20 @@ export class Orchestrator {
14700
15384
  validAt: sourceContext?.validAt,
14701
15385
  source: extractionWriteSource,
14702
15386
  });
14703
- // Register chunked content in hash index too.
15387
+ // Register chunked content in the target storage hash index too.
14704
15388
  // Thread 3 fix: canonicalize by stripping any pre-existing citation
14705
- // so the stored hash matches what the dedup check computes via
14706
- // stripCitationForTemplate before calling contentHashIndex.has().
14707
- if (this.contentHashIndex) {
15389
+ // so the stored hash matches what the dedup check computes.
15390
+ try {
14708
15391
  const canonicalChunkedContent =
14709
15392
  citationEnabled &&
14710
15393
  hasCitationForTemplate(fact.content, citationTemplate)
14711
15394
  ? stripCitationForTemplate(fact.content, citationTemplate)
14712
15395
  : fact.content;
14713
- 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
+ );
14714
15401
  }
14715
15402
 
14716
15403
  for (const chunk of chunkResult.chunks) {
@@ -14988,11 +15675,10 @@ export class Orchestrator {
14988
15675
  intentEntityTypes: inferredIntent?.entityTypes,
14989
15676
  });
14990
15677
  }
14991
- // Register in content-hash index after successful write.
14992
- // Thread 3 fix: canonicalize by stripping any pre-existing citation so
14993
- // the stored hash matches what the dedup check computes via
14994
- // stripCitationForTemplate before calling contentHashIndex.has().
14995
- 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 {
14996
15682
  const canonicalFactContent =
14997
15683
  citationEnabled &&
14998
15684
  hasCitationForTemplate(fact.content, citationTemplate)
@@ -15002,7 +15688,11 @@ export class Orchestrator {
15002
15688
  writeCategory === "procedure"
15003
15689
  ? buildProcedurePersistBody(fact.content, fact.procedureSteps)
15004
15690
  : canonicalFactContent;
15005
- 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
+ );
15006
15696
  }
15007
15697
  } finally {
15008
15698
  // Catalog touch (issue #1499): record AFTER every synchronous
@@ -15158,12 +15848,10 @@ export class Orchestrator {
15158
15848
  touchBaseNonFactNamespace();
15159
15849
  }
15160
15850
 
15161
- // Save content-hash index after batch
15162
- if (this.contentHashIndex) {
15163
- await this.contentHashIndex
15164
- .save()
15165
- .catch((err) => log.warn(`content-hash index save failed: ${err}`));
15166
- }
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
+ );
15167
15855
 
15168
15856
  for (const {
15169
15857
  storage: targetStorage,
@@ -16501,27 +17189,13 @@ export class Orchestrator {
16501
17189
  // All criteria met — archive
16502
17190
  const result = await this.storage.archiveMemory(memory);
16503
17191
  if (result) {
16504
- // Remove from content-hash index since it's no longer in hot search.
16505
- // Prefer the raw-content hash stored on the frontmatter at write
16506
- // time (contentHash) — it is format-agnostic and survives any
16507
- // citation template.
16508
- if (this.contentHashIndex) {
16509
- if (memory.frontmatter.contentHash) {
16510
- // Modern memory: frontmatter.contentHash is already a SHA-256
16511
- // hex string — use removeByHash to avoid double-hashing.
16512
- this.contentHashIndex.removeByHash(memory.frontmatter.contentHash);
16513
- } else {
16514
- // Legacy memory written before contentHash was stored on the
16515
- // frontmatter. Pre-#369 facts were stored without inline
16516
- // citations, so memory.content is the raw fact text and we can
16517
- // remove the hash directly from the content. This clears
16518
- // stale dedup entries so the fact can be re-extracted.
16519
- log.warn(
16520
- `[fact-archival] removing hash for legacy memory ${memory.frontmatter.id ?? "(unknown)"} via content fallback — no contentHash in frontmatter`,
16521
- );
16522
- this.contentHashIndex.remove(memory.content);
16523
- }
16524
- }
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
+ );
16525
17199
  await this.embeddingFallback.removeFromIndex(memory.frontmatter.id);
16526
17200
  if (
16527
17201
  this.config.queryAwareIndexingEnabled &&
@@ -16539,13 +17213,11 @@ export class Orchestrator {
16539
17213
  }
16540
17214
  }
16541
17215
 
16542
- // Save hash index if we removed any entries
16543
- if (archivedCount > 0 && this.contentHashIndex) {
16544
- await this.contentHashIndex
16545
- .save()
16546
- .catch((err) =>
16547
- log.warn(`content-hash index save failed during archival: ${err}`),
16548
- );
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
+ );
16549
17221
  }
16550
17222
 
16551
17223
  return archivedCount;