@remnic/core 9.3.737 → 9.3.739

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 (144) hide show
  1. package/dist/access-boundary.d.ts +6 -6
  2. package/dist/access-boundary.js +10 -10
  3. package/dist/access-cli.js +26 -26
  4. package/dist/access-http.d.ts +6 -6
  5. package/dist/access-http.js +15 -15
  6. package/dist/access-mcp.d.ts +6 -6
  7. package/dist/access-mcp.js +14 -14
  8. package/dist/access-operations-batch.js +12 -12
  9. package/dist/access-operations.d.ts +6 -6
  10. package/dist/access-operations.js +13 -13
  11. package/dist/{access-service-DwQ6BIQw.d.ts → access-service-Cbk-Kv_A.d.ts} +1 -1
  12. package/dist/access-service.d.ts +6 -6
  13. package/dist/access-service.js +9 -9
  14. package/dist/access-surface-catalog.d.ts +6 -6
  15. package/dist/active-memory-bridge.js +3 -3
  16. package/dist/bootstrap.d.ts +5 -5
  17. package/dist/buffer.js +2 -1
  18. package/dist/capabilities.d.ts +98 -1
  19. package/dist/capabilities.js +7 -1
  20. package/dist/{chunk-G54IDCD3.js → chunk-23PLOBWT.js} +30 -27
  21. package/dist/chunk-23PLOBWT.js.map +1 -0
  22. package/dist/{chunk-K3Y2SJTN.js → chunk-56LP72AD.js} +6 -6
  23. package/dist/{chunk-ZBZ4XULO.js → chunk-66RZ4EIT.js} +3 -3
  24. package/dist/{chunk-IPI45Q4S.js → chunk-75PDC7VT.js} +9 -8
  25. package/dist/chunk-75PDC7VT.js.map +1 -0
  26. package/dist/{chunk-GB2FZOWB.js → chunk-7B7IPPN5.js} +5 -5
  27. package/dist/{chunk-QNTLWH3T.js → chunk-A7INTIC5.js} +3 -3
  28. package/dist/{chunk-FLPKEMSO.js → chunk-AKKFHN3I.js} +2 -2
  29. package/dist/{chunk-AAIHDIMD.js → chunk-BHJAMSVC.js} +7 -7
  30. package/dist/{chunk-BXPG5AVX.js → chunk-BHQQVUEQ.js} +6 -6
  31. package/dist/{chunk-HFXZIF3V.js → chunk-BVG2IZOB.js} +3 -3
  32. package/dist/{chunk-NDKW67ER.js → chunk-C53JDEH3.js} +4 -4
  33. package/dist/{chunk-AESC5OQJ.js → chunk-CKHPUOAP.js} +2 -2
  34. package/dist/{chunk-OYHYBHHI.js → chunk-DS3FZ7GS.js} +4 -4
  35. package/dist/{chunk-UO6MPN77.js → chunk-EU543UDC.js} +10 -9
  36. package/dist/chunk-EU543UDC.js.map +1 -0
  37. package/dist/{chunk-PB5YON2G.js → chunk-IEWUPZ75.js} +659 -584
  38. package/dist/chunk-IEWUPZ75.js.map +1 -0
  39. package/dist/{chunk-E4LYJ7PY.js → chunk-IFCM6OLK.js} +6 -6
  40. package/dist/{chunk-5BOQNB2K.js → chunk-JQSP7YBJ.js} +3 -3
  41. package/dist/{chunk-2T2N7ISV.js → chunk-L3TYGUTH.js} +3 -3
  42. package/dist/{chunk-AB2E5DCN.js → chunk-MAU7DFCJ.js} +2 -2
  43. package/dist/{chunk-P6VLG7KE.js → chunk-N2O23WYA.js} +2 -2
  44. package/dist/{chunk-HYNHLBKA.js → chunk-OQQRWPFZ.js} +5 -2
  45. package/dist/chunk-OQQRWPFZ.js.map +1 -0
  46. package/dist/{chunk-IXGXALRV.js → chunk-QTJOORGN.js} +2 -2
  47. package/dist/{chunk-JWDE7OVJ.js → chunk-RJBW4EOU.js} +46 -2
  48. package/dist/{chunk-JWDE7OVJ.js.map → chunk-RJBW4EOU.js.map} +1 -1
  49. package/dist/{chunk-AE7NTZ75.js → chunk-TNLXCSXV.js} +3 -3
  50. package/dist/{chunk-NNJKEAP2.js → chunk-VRBBZH4U.js} +4 -4
  51. package/dist/{chunk-VOEVVIP3.js → chunk-W2FBVBQ4.js} +4 -4
  52. package/dist/{chunk-WNGXQMRI.js → chunk-WFNH4UDF.js} +5 -2
  53. package/dist/chunk-WFNH4UDF.js.map +1 -0
  54. package/dist/{chunk-CHJIWG2H.js → chunk-WVAEIKUY.js} +2 -2
  55. package/dist/{chunk-FF36YNKY.js → chunk-XIQAOGAA.js} +11 -10
  56. package/dist/chunk-XIQAOGAA.js.map +1 -0
  57. package/dist/{chunk-HLPYL6LC.js → chunk-Y5OOHER4.js} +14 -12
  58. package/dist/chunk-Y5OOHER4.js.map +1 -0
  59. package/dist/{chunk-GYICIICQ.js → chunk-YF2MMBKA.js} +9 -7
  60. package/dist/chunk-YF2MMBKA.js.map +1 -0
  61. package/dist/{chunk-IX4L7E5G.js → chunk-YXOXAQ5Y.js} +2 -2
  62. package/dist/{cli-BlEIWDQw.d.ts → cli-BQW2xKdb.d.ts} +2 -2
  63. package/dist/cli.d.ts +7 -7
  64. package/dist/cli.js +21 -21
  65. package/dist/compounding/engine.js +2 -2
  66. package/dist/contradiction/index.js +2 -2
  67. package/dist/{contradiction-scan-2B4AE4TI.js → contradiction-scan-CWO3LIWV.js} +3 -3
  68. package/dist/embedding-fallback.js +2 -2
  69. package/dist/entity-retrieval.js +2 -2
  70. package/dist/explicit-capture.d.ts +5 -5
  71. package/dist/explicit-capture.js +2 -2
  72. package/dist/extraction.js +3 -3
  73. package/dist/{first-start-migration-2RQYDIZ7.js → first-start-migration-5DXD3TF2.js} +2 -2
  74. package/dist/index.d.ts +8 -8
  75. package/dist/index.js +32 -32
  76. package/dist/local-llm.js +2 -2
  77. package/dist/mcp-memory-inspector-app.d.ts +6 -6
  78. package/dist/namespaces/migrate.js +6 -6
  79. package/dist/namespaces/principal.js +2 -2
  80. package/dist/namespaces/search.js +4 -4
  81. package/dist/namespaces/storage.js +2 -2
  82. package/dist/operator-toolkit.js +11 -11
  83. package/dist/orchestration/compression-guideline-coordinator.js +2 -2
  84. package/dist/orchestration/maintenance.js +6 -6
  85. package/dist/{orchestrator-CJZ21ZiB.d.ts → orchestrator-BOAFN_Yr.d.ts} +186 -56
  86. package/dist/orchestrator.d.ts +4 -4
  87. package/dist/orchestrator.js +21 -21
  88. package/dist/search/embed-helper.js +2 -2
  89. package/dist/search/factory.js +3 -3
  90. package/dist/search/index.js +3 -3
  91. package/dist/summarizer.js +3 -3
  92. package/dist/{tier-stats-KPDIISG7.js → tier-stats-QID73BBB.js} +2 -2
  93. package/package.json +2 -2
  94. package/src/access-service.ts +7 -8
  95. package/src/buffer.ts +2 -1
  96. package/src/capabilities.test.ts +137 -0
  97. package/src/capabilities.ts +209 -0
  98. package/src/cli/creation-ledger-commands.ts +3 -3
  99. package/src/cli/research-status-commands.ts +4 -4
  100. package/src/cli.ts +9 -10
  101. package/src/compounding/engine.ts +7 -7
  102. package/src/correction/correction-access-wiring.ts +3 -2
  103. package/src/extraction.ts +8 -10
  104. package/src/operator-toolkit.ts +6 -7
  105. package/src/orchestration/conversation-index-coordinator.ts +344 -0
  106. package/src/orchestration/entity-synthesis-coordinator.ts +2 -1
  107. package/src/orchestration/maintenance.ts +5 -7
  108. package/src/orchestration/recall-rerank-coordinator.ts +425 -0
  109. package/src/orchestration/semantic-consolidation-coordinator.ts +2 -2
  110. package/src/orchestrator.ts +88 -602
  111. package/dist/chunk-FF36YNKY.js.map +0 -1
  112. package/dist/chunk-G54IDCD3.js.map +0 -1
  113. package/dist/chunk-GYICIICQ.js.map +0 -1
  114. package/dist/chunk-HLPYL6LC.js.map +0 -1
  115. package/dist/chunk-HYNHLBKA.js.map +0 -1
  116. package/dist/chunk-IPI45Q4S.js.map +0 -1
  117. package/dist/chunk-PB5YON2G.js.map +0 -1
  118. package/dist/chunk-UO6MPN77.js.map +0 -1
  119. package/dist/chunk-WNGXQMRI.js.map +0 -1
  120. /package/dist/{chunk-K3Y2SJTN.js.map → chunk-56LP72AD.js.map} +0 -0
  121. /package/dist/{chunk-ZBZ4XULO.js.map → chunk-66RZ4EIT.js.map} +0 -0
  122. /package/dist/{chunk-GB2FZOWB.js.map → chunk-7B7IPPN5.js.map} +0 -0
  123. /package/dist/{chunk-QNTLWH3T.js.map → chunk-A7INTIC5.js.map} +0 -0
  124. /package/dist/{chunk-FLPKEMSO.js.map → chunk-AKKFHN3I.js.map} +0 -0
  125. /package/dist/{chunk-AAIHDIMD.js.map → chunk-BHJAMSVC.js.map} +0 -0
  126. /package/dist/{chunk-BXPG5AVX.js.map → chunk-BHQQVUEQ.js.map} +0 -0
  127. /package/dist/{chunk-HFXZIF3V.js.map → chunk-BVG2IZOB.js.map} +0 -0
  128. /package/dist/{chunk-NDKW67ER.js.map → chunk-C53JDEH3.js.map} +0 -0
  129. /package/dist/{chunk-AESC5OQJ.js.map → chunk-CKHPUOAP.js.map} +0 -0
  130. /package/dist/{chunk-OYHYBHHI.js.map → chunk-DS3FZ7GS.js.map} +0 -0
  131. /package/dist/{chunk-E4LYJ7PY.js.map → chunk-IFCM6OLK.js.map} +0 -0
  132. /package/dist/{chunk-5BOQNB2K.js.map → chunk-JQSP7YBJ.js.map} +0 -0
  133. /package/dist/{chunk-2T2N7ISV.js.map → chunk-L3TYGUTH.js.map} +0 -0
  134. /package/dist/{chunk-AB2E5DCN.js.map → chunk-MAU7DFCJ.js.map} +0 -0
  135. /package/dist/{chunk-P6VLG7KE.js.map → chunk-N2O23WYA.js.map} +0 -0
  136. /package/dist/{chunk-IXGXALRV.js.map → chunk-QTJOORGN.js.map} +0 -0
  137. /package/dist/{chunk-AE7NTZ75.js.map → chunk-TNLXCSXV.js.map} +0 -0
  138. /package/dist/{chunk-NNJKEAP2.js.map → chunk-VRBBZH4U.js.map} +0 -0
  139. /package/dist/{chunk-VOEVVIP3.js.map → chunk-W2FBVBQ4.js.map} +0 -0
  140. /package/dist/{chunk-CHJIWG2H.js.map → chunk-WVAEIKUY.js.map} +0 -0
  141. /package/dist/{chunk-IX4L7E5G.js.map → chunk-YXOXAQ5Y.js.map} +0 -0
  142. /package/dist/{contradiction-scan-2B4AE4TI.js.map → contradiction-scan-CWO3LIWV.js.map} +0 -0
  143. /package/dist/{first-start-migration-2RQYDIZ7.js.map → first-start-migration-5DXD3TF2.js.map} +0 -0
  144. /package/dist/{tier-stats-KPDIISG7.js.map → tier-stats-QID73BBB.js.map} +0 -0
@@ -102,6 +102,8 @@ import { SemanticConsolidationCoordinator } from "./orchestration/semantic-conso
102
102
  import { LifecyclePolicyCoordinator } from "./orchestration/lifecycle-policy-coordinator.js";
103
103
  import { EntitySynthesisCoordinator } from "./orchestration/entity-synthesis-coordinator.js";
104
104
  import { RecallResultFormatter } from "./orchestration/recall-result-formatter.js";
105
+ import { ConversationIndexCoordinator } from "./orchestration/conversation-index-coordinator.js";
106
+ import { RecallRerankCoordinator } from "./orchestration/recall-rerank-coordinator.js";
105
107
  export { hasIdentityRecoveryIntent, resolveEffectiveIdentityInjectionMode } from "./orchestration/recall-result-formatter.js";
106
108
  import {
107
109
  runLiveConnectorsOnce,
@@ -121,16 +123,8 @@ import {
121
123
  type ParallelSearchResult,
122
124
  } from "./retrieval-agents.js";
123
125
  import { RerankCache, rerankLocalOrNoop } from "./rerank.js";
124
- import {
125
- applyMemoryWorthFilter,
126
- buildMemoryWorthCounterMap,
127
- type MemoryWorthCounters,
128
- } from "./memory-worth-filter.js";
129
- import { applyTrustScoreStage, buildTrustSignalsForRerank, projectTrustForXray } from "./trust-score-stage.js";
126
+ import { projectTrustForXray } from "./trust-score-stage.js";
130
127
  import type { TrustStageResultItem } from "./trust-score-stage.js";
131
- import { renderEpistemicHedge, type TrustSignals } from "./trust-score.js";
132
- import { reorderRecallResultsWithMmr } from "./recall-mmr.js";
133
- import { applyReasoningTraceBoost } from "./reasoning-trace-recall.js";
134
128
  import { buildRetrievedMemoryProvenance } from "./memory-provenance.js";
135
129
  import {
136
130
  applyTemporalSupersession,
@@ -267,8 +261,7 @@ import {
267
261
  type TrustZoneSearchResult,
268
262
  } from "./trust-zones.js";
269
263
  import { tryDirectAnswer, type DirectAnswerSources } from "./direct-answer-wiring.js";
270
- import {
271
- resolveNamespaceCapabilities,
264
+ import { resolveNamespaceCapabilities,
272
265
  resolveCapabilities,
273
266
  resolveGraphConstructionCapabilities,
274
267
  resolveMemoryLifecycleCapabilities,
@@ -282,7 +275,7 @@ import {
282
275
  resolveQmdCapabilities,
283
276
  resolveIdentityContinuityCapabilities,
284
277
  resolveLocalLlmCapabilities,
285
- resolveSecurityCapabilities, resolveEvalCapabilities, resolveUtilityLearningCapabilities, resolveObjectiveStateCapabilities, resolveCompressionCapabilities} from "./capabilities.js";
278
+ resolveSecurityCapabilities, resolveEvalCapabilities, resolveUtilityLearningCapabilities, resolveObjectiveStateCapabilities, resolveCompressionCapabilities, resolvePresentationCapabilities, resolveConsolidationCapabilities, resolveRecallAuxiliaryCapabilities } from "./capabilities.js";
286
279
  import { DEFAULT_TAXONOMY } from "./taxonomy/index.js";
287
280
  import {
288
281
  searchHarmonicRetrieval,
@@ -331,9 +324,6 @@ import {
331
324
  type SemanticConsolidationLlmOperator,
332
325
  type SemanticConsolidationResult,
333
326
  } from "./semantic-consolidation.js";
334
- import { chunkTranscriptEntries } from "./conversation-index/chunker.js";
335
- import { writeConversationChunks } from "./conversation-index/indexer.js";
336
- import { cleanupConversationChunks } from "./conversation-index/cleanup.js";
337
327
  import {
338
328
  type ConversationIndexBackend,
339
329
  type ConversationIndexBackendInspection,
@@ -1818,31 +1808,6 @@ export class Orchestrator {
1818
1808
  readonly lcmEngine: LcmEngine | null = null;
1819
1809
  private readonly rerankCache = new RerankCache();
1820
1810
 
1821
- /**
1822
- * Short-TTL cache for Memory Worth counter lookups so interactive recall
1823
- * doesn't trigger a full `readAllMemories` scan per query. Keyed by
1824
- * namespace; the filter unions across namespaces at query time. The TTL
1825
- * is intentionally short (seconds, not minutes) because counters are
1826
- * mutated by `recordMemoryOutcome` asynchronously and we'd rather serve
1827
- * a 30-second-stale worth score than a stable-but-wrong one.
1828
- */
1829
- private readonly memoryWorthCounterCache = new Map<
1830
- string,
1831
- { at: number; counters: ReadonlyMap<string, MemoryWorthCounters> }
1832
- >();
1833
- private static readonly MEMORY_WORTH_CACHE_TTL_MS = 30_000;
1834
- /**
1835
- * Issue #1577 — per-namespace TrustScore signal map cache. Same TTL/shape
1836
- * discipline as {@link memoryWorthCounterCache}: seconds-scale, evicted on
1837
- * every call, keyed by namespace. Holds the frontmatter-derived signals
1838
- * (worth, provenance, faithfulness, corroboration, recency) so the trust
1839
- * stage doesn't trigger a fresh `readAllMemories` scan per query.
1840
- */
1841
- private readonly trustSignalCache = new Map<
1842
- string,
1843
- { at: number; signals: ReadonlyMap<string, TrustSignals> }
1844
- >();
1845
- private static readonly TRUST_SIGNAL_CACHE_TTL_MS = 30_000;
1846
1811
  /**
1847
1812
  * Per-session workspace selections keyed by sessionKey.
1848
1813
  * Set by the before_agent_start hook so recall() uses the correct
@@ -1910,6 +1875,12 @@ export class Orchestrator {
1910
1875
  * to RecallResultFormatter.
1911
1876
  */
1912
1877
  readonly recallResultFormatter: RecallResultFormatter;
1878
+ /**
1879
+ * Issue #1526: conversation-index subsystem moved to
1880
+ * ConversationIndexCoordinator.
1881
+ */
1882
+ readonly conversationIndexCoordinator: ConversationIndexCoordinator;
1883
+ readonly recallRerankCoordinator: RecallRerankCoordinator;
1913
1884
  private heartbeatObserverChains = new Map<string, Promise<void>>();
1914
1885
  private recentExtractionFingerprints = new Map<string, number>();
1915
1886
  private readonly consolidationObservers = new Set<
@@ -1918,7 +1889,6 @@ export class Orchestrator {
1918
1889
  private wearablesServiceInstance: WearablesService | null = null;
1919
1890
  private wearablesAutoSyncHandle: { stop(): Promise<void> } | null = null;
1920
1891
  private lastQmdReprobeAtMs = 0;
1921
- private readonly conversationIndexLastUpdateAtMs = new Map<string, number>();
1922
1892
  private lastFileHygieneRunAtMs = 0;
1923
1893
  // Pattern-reinforcement cadence gate (issue #687 PR 2/4). Tracks the
1924
1894
  // last successful run so `runPatternReinforcement` can short-circuit
@@ -2565,7 +2535,7 @@ export class Orchestrator {
2565
2535
  private async contentHashIndexForStorage(
2566
2536
  targetStorage: StorageManager,
2567
2537
  ): Promise<ContentHashIndex | null> {
2568
- if (!this.config.factDeduplicationEnabled) return null;
2538
+ if (!resolveRecallAuxiliaryCapabilities(this.config).factDeduplication) return null;
2569
2539
 
2570
2540
  if (targetStorage.dir === this.storage.dir) {
2571
2541
  if (!this.contentHashIndex) {
@@ -2828,7 +2798,7 @@ export class Orchestrator {
2828
2798
  if (config.defaultNamespace) this.storageRouter.bindCatalogWriteHook(this.storage, config.defaultNamespace);
2829
2799
  // Wire page-level versioning (issue #371)
2830
2800
  this.storage.setVersioningConfig({
2831
- enabled: config.versioningEnabled,
2801
+ enabled: resolveRecallAuxiliaryCapabilities(config).versioning,
2832
2802
  maxVersionsPerPage: config.versioningMaxPerPage,
2833
2803
  sidecarDir: config.versioningSidecarDir,
2834
2804
  });
@@ -2851,7 +2821,7 @@ export class Orchestrator {
2851
2821
  // Wire at-rest encryption (issue #690 PR 3/4).
2852
2822
  // If secureStoreEnabled, check whether the keyring already holds a key
2853
2823
  // for this memory dir (e.g. operator unlocked before daemon restart).
2854
- if (config.secureStoreEnabled) {
2824
+ if (resolveRecallAuxiliaryCapabilities(config).secureStore) {
2855
2825
  // Mark the store as required so writes throw SecureStoreLockedError
2856
2826
  // instead of silently falling back to plaintext when locked (P1 finding
2857
2827
  // from Cursor review of PR #767).
@@ -2889,7 +2859,7 @@ export class Orchestrator {
2889
2859
  this.sharedContext = config.sharedContextEnabled
2890
2860
  ? new SharedContextManager(config)
2891
2861
  : undefined;
2892
- this.compounding = config.compoundingEnabled
2862
+ this.compounding = resolveConsolidationCapabilities(config).compounding
2893
2863
  ? new CompoundingEngine(config, this.storage)
2894
2864
  : undefined;
2895
2865
  this.buffer = new SmartBuffer(config, this.storage);
@@ -2899,6 +2869,18 @@ export class Orchestrator {
2899
2869
  "conversation-index",
2900
2870
  "chunks",
2901
2871
  );
2872
+ this.conversationIndexCoordinator = new ConversationIndexCoordinator({
2873
+ config,
2874
+ getTranscript: () => this.transcript,
2875
+ getBackend: () => this.conversationIndexBackend,
2876
+ indexDir: this.conversationIndexDir,
2877
+ });
2878
+ this.recallRerankCoordinator = new RecallRerankCoordinator({
2879
+ getConfig: () => this.config,
2880
+ getStorage: (namespace) => this.getStorage(namespace),
2881
+ readQmdResultMemory: (resultPath, fallbackStorage, recallNamespaces) =>
2882
+ this.readQmdResultMemory(resultPath, fallbackStorage, recallNamespaces),
2883
+ });
2902
2884
  this.modelRegistry = new ModelRegistry(config.memoryDir);
2903
2885
  this.relevance = new RelevanceStore(config.memoryDir);
2904
2886
  this.negatives = new NegativeExampleStore(config.memoryDir);
@@ -3081,7 +3063,7 @@ export class Orchestrator {
3081
3063
  this.boxBuilders.set(
3082
3064
  dir,
3083
3065
  new BoxBuilder(dir, {
3084
- memoryBoxesEnabled: this.config.memoryBoxesEnabled,
3066
+ memoryBoxesEnabled: resolvePresentationCapabilities(this.config).memoryBoxes,
3085
3067
  traceWeaverEnabled: this.config.traceWeaverEnabled,
3086
3068
  boxTopicShiftThreshold: this.config.boxTopicShiftThreshold,
3087
3069
  boxTimeGapMs: this.config.boxTimeGapMs,
@@ -3427,7 +3409,7 @@ export class Orchestrator {
3427
3409
  });
3428
3410
 
3429
3411
  // Initialize content-hash dedup index
3430
- if (this.config.factDeduplicationEnabled) {
3412
+ if (resolveRecallAuxiliaryCapabilities(this.config).factDeduplication) {
3431
3413
  this.contentHashIndex = this.storage.createContentHashIndex();
3432
3414
  await this.contentHashIndex.load();
3433
3415
  log.info(
@@ -3453,7 +3435,7 @@ export class Orchestrator {
3453
3435
  );
3454
3436
  this.buffer.resetToEmpty();
3455
3437
  }
3456
- if (this.config.compactionResetEnabled) {
3438
+ if (resolveRecallAuxiliaryCapabilities(this.config).compactionReset) {
3457
3439
  try {
3458
3440
  const wsDir = this.config.workspaceDir || defaultWorkspaceDir();
3459
3441
  const files = await readdir(wsDir).catch(() => [] as string[]);
@@ -3677,7 +3659,7 @@ export class Orchestrator {
3677
3659
  // Awaited so callers of `deferredReady` can rely on warmups being complete
3678
3660
  // and shutdown sequencing does not race with in-flight cache builds.
3679
3661
  const cacheWarmups: Promise<void>[] = [];
3680
- if (this.config.knowledgeIndexEnabled) {
3662
+ if (resolveRecallAuxiliaryCapabilities(this.config).knowledgeIndex) {
3681
3663
  cacheWarmups.push(
3682
3664
  (async () => {
3683
3665
  try {
@@ -3996,7 +3978,7 @@ export class Orchestrator {
3996
3978
  const cadenceKey = options.namespace ?? "";
3997
3979
  // Master switch: a disabled feature is never bypassed, even with
3998
3980
  // force=true. `force` only relaxes the cadence floor below.
3999
- if (!this.config.patternReinforcementEnabled) {
3981
+ if (!resolveConsolidationCapabilities(this.config).patternReinforcement) {
4000
3982
  return { ran: false, skippedReason: "disabled", namespace: cadenceKey };
4001
3983
  }
4002
3984
  const cadence = this.config.patternReinforcementCadenceMs;
@@ -4055,7 +4037,7 @@ export class Orchestrator {
4055
4037
  ? { itemCount: result.result.clustersFound }
4056
4038
  : { itemCount: 0 };
4057
4039
  },
4058
- { enabled: this.config.patternReinforcementEnabled },
4040
+ { enabled: resolveConsolidationCapabilities(this.config).patternReinforcement },
4059
4041
  );
4060
4042
  }
4061
4043
 
@@ -4099,7 +4081,7 @@ export class Orchestrator {
4099
4081
  });
4100
4082
  return { itemCount: result.clustersFound };
4101
4083
  },
4102
- { enabled: this.config.semanticConsolidationEnabled },
4084
+ { enabled: resolveConsolidationCapabilities(this.config).semanticConsolidation },
4103
4085
  );
4104
4086
  }
4105
4087
 
@@ -5007,63 +4989,21 @@ export class Orchestrator {
5007
4989
  retrievalQuery: string,
5008
4990
  topK: number,
5009
4991
  ): Promise<Array<{ path: string; snippet: string; score: number }>> {
5010
- if (this.conversationIndexBackend) {
5011
- return this.conversationIndexBackend.search(retrievalQuery, topK);
5012
- }
5013
- return [];
4992
+ return this.conversationIndexCoordinator.search(retrievalQuery, topK);
5014
4993
  }
5015
4994
 
5016
4995
  private formatConversationRecallSection(
5017
4996
  results: Array<{ path: string; snippet: string; score: number }>,
5018
4997
  maxChars: number,
5019
4998
  ): string | null {
5020
- if (!Array.isArray(results) || results.length === 0) return null;
5021
- const lines: string[] = ["## Semantic Recall (Past Conversations)", ""];
5022
- let used = 0;
5023
- for (const r of results) {
5024
- if (!r?.snippet) continue;
5025
- const chunk =
5026
- `### ${r.path}\n` +
5027
- `Score: ${r.score.toFixed(3)}\n\n` +
5028
- `${r.snippet.trim()}\n`;
5029
- if (used + chunk.length > maxChars) break;
5030
- lines.push(chunk);
5031
- used += chunk.length;
5032
- }
5033
- return used > 0 ? lines.join("\n") : null;
5034
- }
5035
-
5036
- private async countConversationChunkDocs(dir: string): Promise<number> {
5037
- try {
5038
- const entries = await readdir(dir, { withFileTypes: true });
5039
- let total = 0;
5040
- for (const entry of entries) {
5041
- const fullPath = path.join(dir, entry.name);
5042
- if (entry.isDirectory()) {
5043
- total += await this.countConversationChunkDocs(fullPath);
5044
- continue;
5045
- }
5046
- if (entry.isFile() && entry.name.endsWith(".md")) {
5047
- total += 1;
5048
- }
5049
- }
5050
- return total;
5051
- } catch {
5052
- return 0;
5053
- }
4999
+ return this.conversationIndexCoordinator.formatRecallSection(
5000
+ results,
5001
+ maxChars,
5002
+ );
5054
5003
  }
5055
5004
 
5056
- private async buildConversationIndexChunks(
5057
- sessionKey?: string,
5058
- hours: number = 24,
5059
- ): Promise<ReturnType<typeof chunkTranscriptEntries>> {
5060
- const entries = await this.transcript.readRecent(hours, sessionKey);
5061
- const effectiveSessionKey = sessionKey ?? "all-sessions";
5062
- return chunkTranscriptEntries(effectiveSessionKey, entries, {
5063
- maxChars: this.config.conversationRecallMaxChars * 2,
5064
- maxTurns: Math.max(10, this.config.hourlySummariesMaxTurnsPerRun),
5065
- });
5066
- }
5005
+ // Issue #1526: countConversationChunkDocs / buildConversationIndexChunks moved
5006
+ // to ConversationIndexCoordinator (internal helpers, no orchestrator callers).
5067
5007
 
5068
5008
  async getConversationIndexHealth(): Promise<{
5069
5009
  enabled: boolean;
@@ -5088,37 +5028,7 @@ export class Orchestrator {
5088
5028
  };
5089
5029
  };
5090
5030
  }> {
5091
- const chunkDocCount = await this.countConversationChunkDocs(
5092
- this.conversationIndexDir,
5093
- );
5094
- const lastUpdateAtMs = Math.max(
5095
- 0,
5096
- ...this.conversationIndexLastUpdateAtMs.values(),
5097
- );
5098
- const lastUpdateAt =
5099
- lastUpdateAtMs > 0 ? new Date(lastUpdateAtMs).toISOString() : null;
5100
-
5101
- if (!resolveIndexingCapabilities(this.config).conversationIndex) {
5102
- return {
5103
- enabled: false,
5104
- backend: this.config.conversationIndexBackend,
5105
- status: "disabled",
5106
- chunkDocCount,
5107
- lastUpdateAt,
5108
- };
5109
- }
5110
- const backendHealth = this.conversationIndexBackend
5111
- ? await this.conversationIndexBackend.health()
5112
- : {
5113
- backend: this.config.conversationIndexBackend,
5114
- status: "degraded" as const,
5115
- };
5116
- return {
5117
- enabled: true,
5118
- chunkDocCount,
5119
- lastUpdateAt,
5120
- ...backendHealth,
5121
- };
5031
+ return this.conversationIndexCoordinator.getHealth();
5122
5032
  }
5123
5033
 
5124
5034
  async inspectConversationIndex(): Promise<
@@ -5128,53 +5038,7 @@ export class Orchestrator {
5128
5038
  lastUpdateAt: string | null;
5129
5039
  }
5130
5040
  > {
5131
- const chunkDocCount = await this.countConversationChunkDocs(
5132
- this.conversationIndexDir,
5133
- );
5134
- const lastUpdateAtMs = Math.max(
5135
- 0,
5136
- ...this.conversationIndexLastUpdateAtMs.values(),
5137
- );
5138
- const lastUpdateAt =
5139
- lastUpdateAtMs > 0 ? new Date(lastUpdateAtMs).toISOString() : null;
5140
-
5141
- if (!resolveIndexingCapabilities(this.config).conversationIndex) {
5142
- return {
5143
- enabled: false,
5144
- backend: this.config.conversationIndexBackend,
5145
- status: "disabled",
5146
- available: false,
5147
- indexPath: this.conversationIndexDir,
5148
- supportsIncrementalUpdate: true,
5149
- message: "Conversation index disabled by config",
5150
- metadata: {
5151
- chunkCount: chunkDocCount,
5152
- },
5153
- chunkDocCount,
5154
- lastUpdateAt,
5155
- };
5156
- }
5157
-
5158
- const inspection = this.conversationIndexBackend
5159
- ? await this.conversationIndexBackend.inspect()
5160
- : {
5161
- backend: this.config.conversationIndexBackend,
5162
- status: "degraded" as const,
5163
- available: false,
5164
- indexPath: this.conversationIndexDir,
5165
- supportsIncrementalUpdate: true,
5166
- message: "Conversation index backend unavailable",
5167
- metadata: {
5168
- chunkCount: chunkDocCount,
5169
- },
5170
- };
5171
-
5172
- return {
5173
- enabled: true,
5174
- chunkDocCount,
5175
- lastUpdateAt,
5176
- ...inspection,
5177
- };
5041
+ return this.conversationIndexCoordinator.inspect();
5178
5042
  }
5179
5043
 
5180
5044
  async getRecoverySummary(sessionKey?: string): Promise<{
@@ -5200,54 +5064,7 @@ export class Orchestrator {
5200
5064
  retryAfterMs?: number;
5201
5065
  embedded?: boolean;
5202
5066
  }> {
5203
- if (!resolveIndexingCapabilities(this.config).conversationIndex) {
5204
- return { chunks: 0, skipped: true, reason: "disabled", embedded: false };
5205
- }
5206
- const enforceMinInterval = opts?.enforceMinInterval !== false;
5207
- if (enforceMinInterval) {
5208
- const minIntervalMs = Math.max(
5209
- 0,
5210
- this.config.conversationIndexMinUpdateIntervalMs,
5211
- );
5212
- const now = Date.now();
5213
- const last = this.conversationIndexLastUpdateAtMs.get(sessionKey) ?? 0;
5214
- const elapsed = now - last;
5215
- if (minIntervalMs > 0 && elapsed < minIntervalMs) {
5216
- return {
5217
- chunks: 0,
5218
- skipped: true,
5219
- reason: "min_interval",
5220
- retryAfterMs: minIntervalMs - elapsed,
5221
- embedded: false,
5222
- };
5223
- }
5224
- }
5225
- const chunks = await this.buildConversationIndexChunks(sessionKey, hours);
5226
- await writeConversationChunks(this.conversationIndexDir, chunks);
5227
- const retentionCutoffMs =
5228
- Number.isFinite(this.config.conversationIndexRetentionDays) &&
5229
- this.config.conversationIndexRetentionDays > 0
5230
- ? Date.now() -
5231
- this.config.conversationIndexRetentionDays * 24 * 60 * 60 * 1000
5232
- : undefined;
5233
- await cleanupConversationChunks(
5234
- this.conversationIndexDir,
5235
- this.config.conversationIndexRetentionDays,
5236
- );
5237
- const shouldEmbed =
5238
- opts?.embed ?? this.config.conversationIndexEmbedOnUpdate;
5239
- let embedded = false;
5240
-
5241
- if (this.conversationIndexBackend) {
5242
- const result = await this.conversationIndexBackend.update(chunks, {
5243
- embed: shouldEmbed,
5244
- ...(retentionCutoffMs !== undefined ? { retentionCutoffMs } : {}),
5245
- });
5246
- embedded = result.embedded;
5247
- }
5248
-
5249
- this.conversationIndexLastUpdateAtMs.set(sessionKey, Date.now());
5250
- return { chunks: chunks.length, skipped: false, embedded };
5067
+ return this.conversationIndexCoordinator.update(sessionKey, hours, opts);
5251
5068
  }
5252
5069
 
5253
5070
  async rebuildConversationIndex(
@@ -5261,42 +5078,7 @@ export class Orchestrator {
5261
5078
  embedded?: boolean;
5262
5079
  rebuilt?: boolean;
5263
5080
  }> {
5264
- if (!resolveIndexingCapabilities(this.config).conversationIndex) {
5265
- return {
5266
- chunks: 0,
5267
- skipped: true,
5268
- reason: "disabled",
5269
- embedded: false,
5270
- rebuilt: false,
5271
- };
5272
- }
5273
-
5274
- const chunks = await this.buildConversationIndexChunks(sessionKey, hours);
5275
- await writeConversationChunks(this.conversationIndexDir, chunks);
5276
- await cleanupConversationChunks(
5277
- this.conversationIndexDir,
5278
- this.config.conversationIndexRetentionDays,
5279
- );
5280
-
5281
- const shouldEmbed =
5282
- opts?.embed ?? this.config.conversationIndexEmbedOnUpdate;
5283
- let embedded = false;
5284
- let rebuilt = false;
5285
- if (this.conversationIndexBackend) {
5286
- const result = await this.conversationIndexBackend.rebuild(chunks, {
5287
- embed: shouldEmbed,
5288
- });
5289
- embedded = result.embedded;
5290
- rebuilt = result.rebuilt;
5291
- }
5292
-
5293
- const stamp = Date.now();
5294
- if (sessionKey) {
5295
- this.conversationIndexLastUpdateAtMs.set(sessionKey, stamp);
5296
- } else {
5297
- this.conversationIndexLastUpdateAtMs.set("__rebuild__", stamp);
5298
- }
5299
- return { chunks: chunks.length, skipped: false, embedded, rebuilt };
5081
+ return this.conversationIndexCoordinator.rebuild(sessionKey, hours, opts);
5300
5082
  }
5301
5083
 
5302
5084
  /**
@@ -5430,7 +5212,7 @@ export class Orchestrator {
5430
5212
  // memory directory, reject recall with a clear human-readable error
5431
5213
  // rather than surfacing a cryptic SecureStoreLockedError from deep
5432
5214
  // inside the storage layer.
5433
- if (this.config.secureStoreEnabled && !this.storage.isSecureStoreUnlocked()) {
5215
+ if (resolveRecallAuxiliaryCapabilities(this.config).secureStore && !this.storage.isSecureStoreUnlocked()) {
5434
5216
  const lockedMsg =
5435
5217
  "[secure-store locked] Memory store is encrypted and locked. " +
5436
5218
  "Unlock the secure-store inside this daemon process, or restart the daemon through a secure-store aware launcher that installs the key.";
@@ -5568,7 +5350,7 @@ export class Orchestrator {
5568
5350
  });
5569
5351
  const observationNamespaces = observationScopePlan.readNamespaces;
5570
5352
  const observationQueryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
5571
- cronRecallPolicyEnabled: this.config.cronRecallPolicyEnabled,
5353
+ cronRecallPolicyEnabled: resolveRecallAuxiliaryCapabilities(this.config).cronRecallPolicy,
5572
5354
  cronRecallNormalizedQueryMaxChars:
5573
5355
  this.config.cronRecallNormalizedQueryMaxChars,
5574
5356
  cronRecallInstructionHeavyTokenCap:
@@ -6994,7 +6776,7 @@ export class Orchestrator {
6994
6776
  // recall searches, incl. coding `readFallbacks`). See the
6995
6777
  // `lcmReadSessionIds` derivation after `recallNamespaces` is built.
6996
6778
  const queryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
6997
- cronRecallPolicyEnabled: this.config.cronRecallPolicyEnabled,
6779
+ cronRecallPolicyEnabled: resolveRecallAuxiliaryCapabilities(this.config).cronRecallPolicy,
6998
6780
  cronRecallNormalizedQueryMaxChars:
6999
6781
  this.config.cronRecallNormalizedQueryMaxChars,
7000
6782
  cronRecallInstructionHeavyTokenCap:
@@ -7157,7 +6939,7 @@ export class Orchestrator {
7157
6939
  return limit;
7158
6940
  })()
7159
6941
  : 0;
7160
- const recallHeadroom = this.config.verbatimArtifactsEnabled
6942
+ const recallHeadroom = resolvePresentationCapabilities(this.config).verbatimArtifacts
7161
6943
  ? Math.max(12, this.config.verbatimArtifactsMaxRecall * 4)
7162
6944
  : 12;
7163
6945
  const computedFetchLimit =
@@ -7170,7 +6952,7 @@ export class Orchestrator {
7170
6952
  const qmdFetchLimit = computedFetchLimit;
7171
6953
  const qmdHybridFetchLimit = computeQmdHybridFetchLimit(
7172
6954
  qmdFetchLimit,
7173
- this.config.verbatimArtifactsEnabled,
6955
+ resolvePresentationCapabilities(this.config).verbatimArtifacts,
7174
6956
  this.config.verbatimArtifactsMaxRecall,
7175
6957
  );
7176
6958
  const embeddingFetchLimit = computedFetchLimit;
@@ -7862,11 +7644,11 @@ export class Orchestrator {
7862
7644
  if (
7863
7645
  !this.isRecallSectionEnabled(
7864
7646
  "entity-retrieval",
7865
- this.config.entityRetrievalEnabled,
7647
+ resolveRecallAuxiliaryCapabilities(this.config).entityRetrieval,
7866
7648
  )
7867
7649
  )
7868
7650
  return null;
7869
- if (!this.config.entityRetrievalEnabled) return null;
7651
+ if (!resolveRecallAuxiliaryCapabilities(this.config).entityRetrieval) return null;
7870
7652
  const maxChars =
7871
7653
  this.getRecallSectionMaxChars("entity-retrieval") ??
7872
7654
  this.config.entityRetrievalMaxChars;
@@ -7939,11 +7721,11 @@ export class Orchestrator {
7939
7721
  if (
7940
7722
  !this.isRecallSectionEnabled(
7941
7723
  "knowledge-index",
7942
- this.config.knowledgeIndexEnabled,
7724
+ resolveRecallAuxiliaryCapabilities(this.config).knowledgeIndex,
7943
7725
  )
7944
7726
  )
7945
7727
  return null;
7946
- if (!this.config.knowledgeIndexEnabled) return null;
7728
+ if (!resolveRecallAuxiliaryCapabilities(this.config).knowledgeIndex) return null;
7947
7729
  const t0 = Date.now();
7948
7730
  try {
7949
7731
  const knowledgeIndexMaxChars =
@@ -8034,11 +7816,11 @@ export class Orchestrator {
8034
7816
  if (
8035
7817
  !this.isRecallSectionEnabled(
8036
7818
  "verbatim-artifacts",
8037
- this.config.verbatimArtifactsEnabled === true,
7819
+ resolvePresentationCapabilities(this.config).verbatimArtifacts === true,
8038
7820
  )
8039
7821
  )
8040
7822
  return [];
8041
- if (!this.config.verbatimArtifactsEnabled) return [];
7823
+ if (!resolvePresentationCapabilities(this.config).verbatimArtifacts) return [];
8042
7824
  const t0 = Date.now();
8043
7825
  const targetCount = computeArtifactRecallLimit(
8044
7826
  recallMode,
@@ -8269,10 +8051,10 @@ export class Orchestrator {
8269
8051
  const calibrationPromise = (async (): Promise<string | null> => {
8270
8052
  const t0 = Date.now();
8271
8053
  if (
8272
- !this.config.calibrationEnabled ||
8054
+ !resolveConsolidationCapabilities(this.config).calibration ||
8273
8055
  !this.isRecallSectionEnabled(
8274
8056
  "calibration-rules",
8275
- this.config.calibrationEnabled === true,
8057
+ resolveConsolidationCapabilities(this.config).calibration === true,
8276
8058
  )
8277
8059
  ) {
8278
8060
  recordRecallSectionMetric({
@@ -8434,7 +8216,7 @@ export class Orchestrator {
8434
8216
  query: retrievalQuery,
8435
8217
  maxResults,
8436
8218
  sessionKey,
8437
- anchorsEnabled: this.config.abstractionAnchorsEnabled,
8219
+ anchorsEnabled: resolveConsolidationCapabilities(this.config).abstractionAnchors,
8438
8220
  abortSignal: harmonicRetrievalAbort.signal,
8439
8221
  }),
8440
8222
  ),
@@ -8481,10 +8263,10 @@ export class Orchestrator {
8481
8263
  const verifiedRecallPromise = (async (): Promise<string | null> => {
8482
8264
  const t0 = Date.now();
8483
8265
  if (
8484
- !this.config.verifiedRecallEnabled ||
8266
+ !resolveRecallAuxiliaryCapabilities(this.config).verifiedRecall ||
8485
8267
  !this.isRecallSectionEnabled(
8486
8268
  "verified-episodes",
8487
- this.config.verifiedRecallEnabled === true,
8269
+ resolveRecallAuxiliaryCapabilities(this.config).verifiedRecall === true,
8488
8270
  )
8489
8271
  ) {
8490
8272
  recordRecallSectionMetric({
@@ -8570,10 +8352,10 @@ export class Orchestrator {
8570
8352
  const verifiedRulesPromise = (async (): Promise<string | null> => {
8571
8353
  const t0 = Date.now();
8572
8354
  if (
8573
- !this.config.semanticRuleVerificationEnabled ||
8355
+ !resolveRecallAuxiliaryCapabilities(this.config).semanticRuleVerification ||
8574
8356
  !this.isRecallSectionEnabled(
8575
8357
  "verified-rules",
8576
- this.config.semanticRuleVerificationEnabled === true,
8358
+ resolveRecallAuxiliaryCapabilities(this.config).semanticRuleVerification === true,
8577
8359
  )
8578
8360
  ) {
8579
8361
  recordRecallSectionMetric({
@@ -8659,10 +8441,10 @@ export class Orchestrator {
8659
8441
  const t0 = Date.now();
8660
8442
  if (
8661
8443
  !resolveCreationMemoryCapabilities(this.config).creationMemory ||
8662
- !this.config.workProductRecallEnabled ||
8444
+ !resolveRecallAuxiliaryCapabilities(this.config).workProductRecall ||
8663
8445
  !this.isRecallSectionEnabled(
8664
8446
  "work-products",
8665
- this.config.workProductRecallEnabled === true,
8447
+ resolveRecallAuxiliaryCapabilities(this.config).workProductRecall === true,
8666
8448
  )
8667
8449
  ) {
8668
8450
  recordRecallSectionMetric({
@@ -9170,7 +8952,7 @@ export class Orchestrator {
9170
8952
  const transcriptPromise = (async (): Promise<string | null> => {
9171
8953
  const t0 = Date.now();
9172
8954
  if (
9173
- !this.config.transcriptEnabled ||
8955
+ !resolvePresentationCapabilities(this.config).transcript ||
9174
8956
  !this.isRecallSectionEnabled("transcript", true)
9175
8957
  ) {
9176
8958
  recordRecallSectionMetric({
@@ -9276,7 +9058,7 @@ export class Orchestrator {
9276
9058
  this._recallWorkspaceOverrides.get(effectiveSessionKey);
9277
9059
  this._recallWorkspaceOverrides.delete(effectiveSessionKey);
9278
9060
 
9279
- if (!this.config.compactionResetEnabled) return null;
9061
+ if (!resolveRecallAuxiliaryCapabilities(this.config).compactionReset) return null;
9280
9062
 
9281
9063
  const workspaceDir =
9282
9064
  compactionWorkspaceDir ||
@@ -9629,9 +9411,9 @@ export class Orchestrator {
9629
9411
  const recentBoxesPromise = observeEnrichmentPromise(
9630
9412
  this.isRecallSectionEnabled(
9631
9413
  "memory-boxes",
9632
- this.config.memoryBoxesEnabled === true,
9414
+ resolvePresentationCapabilities(this.config).memoryBoxes === true,
9633
9415
  ) &&
9634
- this.config.memoryBoxesEnabled &&
9416
+ resolvePresentationCapabilities(this.config).memoryBoxes &&
9635
9417
  this.config.boxRecallDays > 0
9636
9418
  ? Promise.all(
9637
9419
  profileStorages.map((storage) =>
@@ -12734,7 +12516,7 @@ export class Orchestrator {
12734
12516
  ): Promise<void> {
12735
12517
  const mode = this.config.correctionCaptureMode;
12736
12518
  if (mode === "off") return;
12737
- if (!this.config.correctionEnabled) return;
12519
+ if (!resolveRecallAuxiliaryCapabilities(this.config).correction) return;
12738
12520
 
12739
12521
  try {
12740
12522
  const corrections = detectPassiveCorrections(
@@ -12754,7 +12536,7 @@ export class Orchestrator {
12754
12536
  const result = await capturePassiveCorrections(
12755
12537
  corrections,
12756
12538
  {
12757
- correctionEnabled: this.config.correctionEnabled,
12539
+ correctionEnabled: resolveRecallAuxiliaryCapabilities(this.config).correction,
12758
12540
  isLiveSession: opts.isLiveSession,
12759
12541
  bufferKey: opts.bufferKey,
12760
12542
  sessionKey: opts.sessionKey,
@@ -13188,7 +12970,7 @@ export class Orchestrator {
13188
12970
  }
13189
12971
 
13190
12972
  let threadIdForExtraction: string | null = null;
13191
- if (this.config.threadingEnabled && turns.length > 0) {
12973
+ if (resolvePresentationCapabilities(this.config).threading && turns.length > 0) {
13192
12974
  const lastTurn = turns[turns.length - 1];
13193
12975
  try {
13194
12976
  threadIdForExtraction = await this.threading.processTurn(lastTurn, []);
@@ -13318,7 +13100,7 @@ export class Orchestrator {
13318
13100
  // Topics are derived from the current extraction's facts and entities only —
13319
13101
  // not from readAllMemories() — so box topics accurately reflect the current
13320
13102
  // session window and the call is free of expensive full-corpus I/O.
13321
- if (this.config.memoryBoxesEnabled && persistedIds.length > 0) {
13103
+ if (resolvePresentationCapabilities(this.config).memoryBoxes && persistedIds.length > 0) {
13322
13104
  const extractionTopics = deriveTopicsFromExtraction(result);
13323
13105
  // Derive episodic metadata from buffer turns (REMem-inspired)
13324
13106
  const firstUserTurn = turns.find((t) => t.role === "user");
@@ -13339,7 +13121,7 @@ export class Orchestrator {
13339
13121
  // Batch-append persisted IDs so non-fact memories (entities/questions) are
13340
13122
  // always attached to the thread. The helper excludes pending_review ids (#1635).
13341
13123
  if (
13342
- this.config.threadingEnabled &&
13124
+ resolvePresentationCapabilities(this.config).threading &&
13343
13125
  threadIdForExtraction &&
13344
13126
  persistedIds.length > 0
13345
13127
  ) {
@@ -13350,7 +13132,7 @@ export class Orchestrator {
13350
13132
  }
13351
13133
 
13352
13134
  // Thread title update for the already-established thread context.
13353
- if (this.config.threadingEnabled && threadIdForExtraction) {
13135
+ if (resolvePresentationCapabilities(this.config).threading && threadIdForExtraction) {
13354
13136
  const conversationContent = turns.map((t) => t.content).join(" ");
13355
13137
  try {
13356
13138
  await this.threading.updateThreadTitle(
@@ -15251,7 +15033,7 @@ export class Orchestrator {
15251
15033
 
15252
15034
  if (chunkResult.chunked && chunkResult.chunks.length > 1) {
15253
15035
  // Classify memory kind (v8.0 Phase 2B: HiMem episode/note dual store)
15254
- const memoryKind = this.config.episodeNoteModeEnabled
15036
+ const memoryKind = resolvePresentationCapabilities(this.config).episodeNoteMode
15255
15037
  ? classifyMemoryKind(fact.content, fact.tags ?? [], writeCategory)
15256
15038
  : undefined;
15257
15039
 
@@ -15508,7 +15290,7 @@ export class Orchestrator {
15508
15290
  }
15509
15291
  try {
15510
15292
  if (
15511
- this.config.verbatimArtifactsEnabled &&
15293
+ resolvePresentationCapabilities(this.config).verbatimArtifacts &&
15512
15294
  this.config.verbatimArtifactCategories.includes(writeCategory) &&
15513
15295
  fact.confidence >= this.config.verbatimArtifactsMinConfidence &&
15514
15296
  !postWriteGuard
@@ -15605,7 +15387,7 @@ export class Orchestrator {
15605
15387
  const memoryKind =
15606
15388
  writeCategory === "procedure"
15607
15389
  ? undefined
15608
- : this.config.episodeNoteModeEnabled
15390
+ : resolvePresentationCapabilities(this.config).episodeNoteMode
15609
15391
  ? classifyMemoryKind(fact.content, fact.tags ?? [], writeCategory)
15610
15392
  : undefined;
15611
15393
 
@@ -15805,7 +15587,7 @@ export class Orchestrator {
15805
15587
  }
15806
15588
  }
15807
15589
  if (
15808
- this.config.verbatimArtifactsEnabled &&
15590
+ resolvePresentationCapabilities(this.config).verbatimArtifacts &&
15809
15591
  this.config.verbatimArtifactCategories.includes(writeCategory) &&
15810
15592
  fact.confidence >= this.config.verbatimArtifactsMinConfidence &&
15811
15593
  !postWriteGuard
@@ -16421,7 +16203,7 @@ export class Orchestrator {
16421
16203
  log.info(`merged ${entitiesMerged} fragmented entity files`);
16422
16204
  }
16423
16205
 
16424
- if (this.config.entitySummaryEnabled) {
16206
+ if (resolvePresentationCapabilities(this.config).entitySummary) {
16425
16207
  try {
16426
16208
  const synthesized = await this.processEntitySynthesisQueue(
16427
16209
  this.config.defaultNamespace,
@@ -16565,7 +16347,7 @@ export class Orchestrator {
16565
16347
  }
16566
16348
 
16567
16349
  // Semantic consolidation pass — find similar memories, synthesize canonical versions
16568
- if (this.config.semanticConsolidationEnabled) {
16350
+ if (resolveConsolidationCapabilities(this.config).semanticConsolidation) {
16569
16351
  try {
16570
16352
  const stateFilePath = path.join(
16571
16353
  this.config.memoryDir,
@@ -17366,156 +17148,15 @@ export class Orchestrator {
17366
17148
  return matches[0] ?? null;
17367
17149
  }
17368
17150
 
17151
+ // Issue #1526: recall-rerank methods moved to RecallRerankCoordinator.
17152
+ // Thin delegation keeps the private API stable for callers + tests.
17369
17153
  private async applyMemoryWorthRerank(
17370
17154
  results: QmdSearchResult[],
17371
17155
  namespaces: string[],
17372
17156
  ): Promise<QmdSearchResult[]> {
17373
- // Build the counter lookup. We union frontmatter counters across every
17374
- // namespace the recall spans — the recall path itself already
17375
- // aggregates candidates from multiple namespaces, so we must do the
17376
- // same when looking up counters. Per-namespace results are cached with
17377
- // a short TTL so interactive recall doesn't trigger a full
17378
- // `readAllMemories` scan per query (addresses codex P2 on PR 4).
17379
- const counters = new Map<string, MemoryWorthCounters>();
17380
- const seenNamespaces = new Set<string>();
17381
- const nowMs = Date.now();
17382
-
17383
- // Evict all expired entries on every call so long-running processes
17384
- // touching a high-cardinality namespace set (coding/project overlays,
17385
- // per-branch) don't grow the cache unboundedly. Without this, an entry
17386
- // for a namespace that's never looked up again would pin its full
17387
- // counter map forever.
17388
- for (const [key, entry] of this.memoryWorthCounterCache) {
17389
- if (nowMs - entry.at >= Orchestrator.MEMORY_WORTH_CACHE_TTL_MS) {
17390
- this.memoryWorthCounterCache.delete(key);
17391
- }
17392
- }
17393
-
17394
- for (const ns of namespaces) {
17395
- if (seenNamespaces.has(ns)) continue;
17396
- seenNamespaces.add(ns);
17397
- try {
17398
- const cached = this.memoryWorthCounterCache.get(ns);
17399
- let nsMap: ReadonlyMap<string, MemoryWorthCounters> | undefined;
17400
- if (
17401
- cached &&
17402
- nowMs - cached.at < Orchestrator.MEMORY_WORTH_CACHE_TTL_MS
17403
- ) {
17404
- nsMap = cached.counters;
17405
- } else {
17406
- const storage = await this.getStorage(ns);
17407
- const memories = await storage.readAllMemories();
17408
- nsMap = buildMemoryWorthCounterMap(memories);
17409
- this.memoryWorthCounterCache.set(ns, { at: nowMs, counters: nsMap });
17410
- }
17411
- for (const [path, c] of nsMap) counters.set(path, c);
17412
- } catch (err) {
17413
- log.debug("memory-worth: failed to read namespace, skipping", {
17414
- namespace: ns,
17415
- error: (err as Error).message,
17416
- });
17417
- }
17418
- }
17419
-
17420
- // For candidates whose path didn't show up in any hot-tier namespace
17421
- // scan (typical of cold-tier / archive fallback), try a direct
17422
- // per-path read. Without this, cold-tier candidates always stay at
17423
- // multiplier 1.0 even when they have outcome history. Errors are
17424
- // swallowed so a single unreadable archive entry can't break the
17425
- // whole recall.
17426
- const missing = results.filter((r) => !counters.has(r.path));
17427
- if (missing.length > 0) {
17428
- // Use the first-seen namespace's storage as the reader — all
17429
- // StorageManagers share the same on-disk format, and
17430
- // `readMemoryByPath` takes an absolute path so the baseDir doesn't
17431
- // have to match.
17432
- let reader: StorageManager | null = null;
17433
- for (const ns of namespaces) {
17434
- try {
17435
- reader = await this.getStorage(ns);
17436
- break;
17437
- } catch {
17438
- // try next namespace
17439
- }
17440
- }
17441
- if (reader) {
17442
- for (const r of missing) {
17443
- try {
17444
- const memory = await this.readQmdResultMemory(r.path, reader, namespaces);
17445
- if (!memory) continue;
17446
- const fm = memory.frontmatter;
17447
- if (fm.mw_success === undefined && fm.mw_fail === undefined) continue;
17448
- counters.set(r.path, {
17449
- mw_success: fm.mw_success,
17450
- mw_fail: fm.mw_fail,
17451
- lastAccessed: fm.lastAccessed,
17452
- });
17453
- } catch (err) {
17454
- log.debug("memory-worth: direct path lookup failed", {
17455
- path: r.path,
17456
- error: (err as Error).message,
17457
- });
17458
- }
17459
- }
17460
- }
17461
- }
17462
-
17463
- // If no memory in the candidate set has any counter data, the filter
17464
- // would be a no-op — skip the reorder to avoid spurious log spam.
17465
- if (counters.size === 0) return results;
17466
-
17467
- // Preserve upstream ordering (reranker, specialized tiers, etc.) for
17468
- // neutral candidates. The upstream stages set `memoryResults` in their
17469
- // intended order but often leave `r.score` as the raw QMD score. If we
17470
- // sorted by `r.score * multiplier` directly, neutral candidates
17471
- // (multiplier 1.0) would snap back to raw-QMD order and silently undo
17472
- // the reranker. Feed the filter a synthetic monotone-decreasing rank
17473
- // score so it uses input position as the baseline, then applies the
17474
- // multiplier on top. Ties fall back to the stable secondary key in
17475
- // `applyMemoryWorthFilter`.
17476
- const rankedInputs = results.map((r, i) => ({
17477
- path: r.path,
17478
- // Large positive rank score so multiplier math stays well-scaled and
17479
- // we never hit zero; descending so earlier items rank higher.
17480
- score: results.length - i,
17481
- }));
17482
- const filtered = applyMemoryWorthFilter(rankedInputs, {
17483
- counters,
17484
- now: new Date(),
17485
- halfLifeMs:
17486
- this.config.recallMemoryWorthHalfLifeMs > 0
17487
- ? this.config.recallMemoryWorthHalfLifeMs
17488
- : undefined,
17489
- });
17490
-
17491
- // Reconstruct the QmdSearchResult list in the new order. `.score` is
17492
- // preserved from the upstream pipeline (rerank, tier scoring, etc.) —
17493
- // we only reorder. Writing the synthetic rank-weighted score back
17494
- // would contaminate downstream logic (telemetry, confidence gates)
17495
- // that expects the original QMD/rerank score semantics.
17496
- const byPath = new Map(results.map((r) => [r.path, r]));
17497
- const reordered: QmdSearchResult[] = [];
17498
- for (const item of filtered) {
17499
- const original = byPath.get(item.path);
17500
- if (original) reordered.push(original);
17501
- }
17502
- return reordered;
17157
+ return this.recallRerankCoordinator.applyMemoryWorthRerank(results, namespaces);
17503
17158
  }
17504
17159
 
17505
- /**
17506
- * Issue #1577 — unified TrustScore recall stage. Thin wiring over the pure
17507
- * {@link applyTrustScoreStage} scorer + the {@link buildTrustSignalsForRerank}
17508
- * signal builder. The stage subsumes the Memory Worth multiplier — the
17509
- * orchestrator runs exactly one of the two (mutual exclusion, rule 39; the
17510
- * double-multiplier test in trust-score-stage.test.ts pins it structurally).
17511
- *
17512
- * Returns the admitted results AND the per-path trust map (including
17513
- * quarantined items) so the caller can: (a) render epistemic hedges, (b)
17514
- * surface quarantined items in X-ray with a reason (rule 34), and (c) filter
17515
- * quarantined paths from fallback recall branches. The trust map is a
17516
- * per-recall local — never instance state — so concurrent recalls cannot
17517
- * race on it (review: shared-trust-map concurrency).
17518
- */
17519
17160
  private async applyTrustScoreRerank(
17520
17161
  results: QmdSearchResult[],
17521
17162
  namespaces: string[],
@@ -17523,74 +17164,9 @@ export class Orchestrator {
17523
17164
  results: QmdSearchResult[];
17524
17165
  trustByPath: Map<string, TrustStageResultItem> | null;
17525
17166
  }> {
17526
- if (results.length === 0) return { results, trustByPath: null };
17527
- const now = new Date();
17528
- const halfLifeDays =
17529
- this.config.recallMemoryWorthHalfLifeMs > 0
17530
- ? this.config.recallMemoryWorthHalfLifeMs / (24 * 60 * 60 * 1000)
17531
- : undefined;
17532
- // Cold-tier direct-fallback reader: resolve once, reuse for every missing
17533
- // candidate (mirrors the memory-worth filter's reader selection).
17534
- let fallbackReader: StorageManager | null = null;
17535
- const signals = await buildTrustSignalsForRerank(
17536
- results.map((r) => r.path),
17537
- namespaces,
17538
- {
17539
- readNamespaceMemories: async (ns) => (await this.getStorage(ns)).readAllMemories(),
17540
- readMemoryFrontmatter: async (path) => {
17541
- if (!fallbackReader) {
17542
- for (const ns of namespaces) {
17543
- try {
17544
- fallbackReader = await this.getStorage(ns);
17545
- break;
17546
- } catch {
17547
- // try next namespace
17548
- }
17549
- }
17550
- }
17551
- if (!fallbackReader) return null;
17552
- const memory = await this.readQmdResultMemory(path, fallbackReader, namespaces);
17553
- return memory ? memory.frontmatter : null;
17554
- },
17555
- },
17556
- { cache: this.trustSignalCache, ttlMs: Orchestrator.TRUST_SIGNAL_CACHE_TTL_MS },
17557
- now,
17558
- {
17559
- recencyHalfLifeDays: halfLifeDays,
17560
- logDebug: (message, context) => log.debug(message, context),
17561
- },
17562
- );
17563
- if (signals.size === 0) {
17564
- return { results, trustByPath: null };
17565
- }
17566
- // Synthetic monotone-decreasing rank so the multiplier rebias is applied
17567
- // on top of upstream ordering, not raw QMD scores (see applyMemoryWorthRerank).
17568
- const rankedInputs = results.map((r, i) => ({ path: r.path, score: results.length - i }));
17569
- const stage = applyTrustScoreStage(rankedInputs, {
17570
- signals,
17571
- weights: this.config.trustScoreWeights,
17572
- minMultiplier: this.config.trustScoreMinMultiplier,
17573
- maxMultiplier: this.config.trustScoreMaxMultiplier,
17574
- quarantine: this.config.trustScoreQuarantine,
17575
- });
17576
- const trustByPath = new Map(stage.all.map((item) => [item.path, item]));
17577
- const byPath = new Map(results.map((r) => [r.path, r]));
17578
- const admitted = stage.admitted
17579
- .map((item) => byPath.get(item.path))
17580
- .filter((r): r is QmdSearchResult => r !== undefined);
17581
- return { results: admitted, trustByPath };
17167
+ return this.recallRerankCoordinator.applyTrustScoreRerank(results, namespaces);
17582
17168
  }
17583
17169
 
17584
- /**
17585
- * Issue #1577 — apply the TrustScore stage (or, when trust is off, the Memory
17586
- * Worth multiplier fallback) to ONE recall branch's results, returning the
17587
- * scored results + the per-path trust map. Thin wiring over
17588
- * {@link applyTrustScoreRerank} so every recall path — hot QMD, embedding
17589
- * fallback, recent scan — applies the SAME multiplier gate (rule 41: a
17590
- * feature gate must apply across ALL parallel recall paths). TrustScore
17591
- * subsumes Memory Worth; exactly one runs (rule 39). Fail-open on lookup
17592
- * errors so a storage hiccup never breaks a fallback path.
17593
- */
17594
17170
  private async applyTrustScoreToBranch(
17595
17171
  results: QmdSearchResult[],
17596
17172
  namespaces: string[],
@@ -17600,25 +17176,7 @@ export class Orchestrator {
17600
17176
  results: QmdSearchResult[];
17601
17177
  trustByPath: Map<string, TrustStageResultItem> | null;
17602
17178
  }> {
17603
- if (caps.recallTrustScore && results.length > 0) {
17604
- try {
17605
- return await this.applyTrustScoreRerank(results, namespaces);
17606
- } catch (err) {
17607
- log.debug(`trust-score stage (${label}) failed open`, {
17608
- error: (err as Error).message,
17609
- });
17610
- }
17611
- } else if (caps.recallMemoryWorthFilter && results.length > 0) {
17612
- try {
17613
- const filtered = await this.applyMemoryWorthRerank(results, namespaces);
17614
- return { results: filtered, trustByPath: null };
17615
- } catch (err) {
17616
- log.debug(`memory-worth filter (${label}) failed open`, {
17617
- error: (err as Error).message,
17618
- });
17619
- }
17620
- }
17621
- return { results, trustByPath: null };
17179
+ return this.recallRerankCoordinator.applyTrustScoreToBranch(results, namespaces, caps, label);
17622
17180
  }
17623
17181
 
17624
17182
  private diversifyAndLimitRecallResults(
@@ -17626,89 +17184,17 @@ export class Orchestrator {
17626
17184
  results: QmdSearchResult[],
17627
17185
  limit: number,
17628
17186
  retrievalQuery?: string,
17629
- // `caps` is additive AND last (issue #1523) so the positional call shape
17630
- // stays backward-compatible: the recall pipeline threads a resolved set,
17631
- // but callers that omit it (e.g. direct unit-test invocations) get an
17632
- // equivalent set derived from the same config — behavior-preserving.
17633
17187
  caps: CapabilitySet = resolveCapabilities(this.config),
17634
17188
  ): QmdSearchResult[] {
17635
- const safeLimit =
17636
- typeof limit === "number" && Number.isFinite(limit)
17637
- ? Math.max(0, Math.floor(limit))
17638
- : 0;
17639
- if (!Array.isArray(results) || results.length === 0) return [];
17640
- // `recallResultLimit === 0` is a true zero limit (e.g. when
17641
- // `memoriesSectionEnabled` is false) and must return an empty array so
17642
- // the memories section is genuinely skipped. This mirrors the
17643
- // `slice(0, 0)` semantics of every call site this helper replaced.
17644
- if (safeLimit === 0) return [];
17645
- // Issue #564 PR 3: when the feature flag is on, boost reasoning_trace
17646
- // memories for problem-solving asks so they bubble up ahead of ordinary
17647
- // facts/decisions before MMR picks the final section. No-op when the
17648
- // flag is off or the query is not a problem-solving ask.
17649
- const boosted =
17650
- caps.recallReasoningTraceBoost && typeof retrievalQuery === "string"
17651
- ? applyReasoningTraceBoost(results, {
17652
- enabled: true,
17653
- query: retrievalQuery,
17654
- })
17655
- : results;
17656
- const diversified = this.applyMmrToQmdResults(sectionId, boosted, caps);
17657
- return diversified.slice(0, safeLimit);
17189
+ return this.recallRerankCoordinator.diversifyAndLimitRecallResults(sectionId, results, limit, retrievalQuery, caps);
17658
17190
  }
17659
17191
 
17660
- /**
17661
- * Apply Maximal Marginal Relevance to a section's ordered candidate list.
17662
- *
17663
- * Operates per-section so one redundant cluster cannot dominate a section,
17664
- * and so one section's MMR pass cannot starve other sections. Returns the
17665
- * input unchanged when disabled, when there are fewer than 2 candidates, or
17666
- * when no budget information is available.
17667
- */
17668
17192
  private applyMmrToQmdResults(
17669
17193
  sectionId: string,
17670
17194
  results: QmdSearchResult[],
17671
- // Additive `caps` (issue #1523); defaults to a config-derived set so direct
17672
- // callers that omit it behave identically to the threaded recall path.
17673
17195
  caps: CapabilitySet = resolveCapabilities(this.config),
17674
17196
  ): QmdSearchResult[] {
17675
- if (!caps.recallMmr) return results;
17676
- if (!Array.isArray(results) || results.length < 2) return results;
17677
-
17678
- // Config is runtime API (see AGENTS.md §4): preserve `0` as a true zero
17679
- // limit rather than coercing it to a non-zero value. A configured topN of
17680
- // 0 means "apply MMR over an empty window" — i.e. skip the reorder and
17681
- // return the upstream candidates unchanged. This keeps read-time
17682
- // behavior symmetric with the write-time semantics parseConfig exposes.
17683
- const configuredTopN = this.config.recallMmrTopN;
17684
- const topN =
17685
- typeof configuredTopN === "number" && Number.isFinite(configuredTopN)
17686
- ? Math.max(0, Math.floor(configuredTopN))
17687
- : 40;
17688
- if (topN === 0) return results;
17689
- const lambda = this.config.recallMmrLambda ?? 0.7;
17690
-
17691
- // Delegate to the pure helper so candidate keying (path-first, index
17692
- // suffixed for uniqueness) and the head-of-list diversity metric are
17693
- // exercised by the same code path that the unit tests cover.
17694
- const { reordered, diversity } = reorderRecallResultsWithMmr(results, {
17695
- lambda,
17696
- topN,
17697
- });
17698
-
17699
- try {
17700
- log.info(
17701
- `recall_mmr: section=${sectionId} kept=${diversity.kept}/${diversity.considered} ` +
17702
- `headReorderCount=${diversity.headReorderCount} ` +
17703
- `avgSimBefore=${diversity.avgPairwiseSimBefore.toFixed(3)} ` +
17704
- `avgSimAfter=${diversity.avgPairwiseSimAfter.toFixed(3)} ` +
17705
- `lambda=${lambda.toFixed(2)}`,
17706
- );
17707
- } catch {
17708
- // Metrics must never break recall.
17709
- }
17710
-
17711
- return reordered;
17197
+ return this.recallRerankCoordinator.applyMmrToQmdResults(sectionId, results, caps);
17712
17198
  }
17713
17199
 
17714
17200
  private buildLastRecallBudgetSummary(options: {