@remnic/core 9.3.727 → 9.3.729

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 (95) hide show
  1. package/dist/access-boundary.d.ts +2 -2
  2. package/dist/access-boundary.js +6 -6
  3. package/dist/access-cli.js +11 -11
  4. package/dist/access-http.d.ts +2 -2
  5. package/dist/access-http.js +10 -10
  6. package/dist/access-mcp.d.ts +2 -2
  7. package/dist/access-mcp.js +9 -9
  8. package/dist/access-operations-batch.js +7 -7
  9. package/dist/access-operations.d.ts +8 -8
  10. package/dist/access-operations.js +8 -8
  11. package/dist/access-schema.d.ts +46 -46
  12. package/dist/{access-service-Bgpjxo4f.d.ts → access-service-BXMJqkTA.d.ts} +1 -1
  13. package/dist/access-service.d.ts +2 -2
  14. package/dist/access-service.js +5 -5
  15. package/dist/access-surface-catalog.d.ts +2 -2
  16. package/dist/bootstrap.d.ts +1 -1
  17. package/dist/capabilities.d.ts +43 -1
  18. package/dist/capabilities.js +5 -3
  19. package/dist/{chunk-DVRHLN2O.js → chunk-3RCBIOOJ.js} +18 -2
  20. package/dist/{chunk-DVRHLN2O.js.map → chunk-3RCBIOOJ.js.map} +1 -1
  21. package/dist/{chunk-R7IYU5DZ.js → chunk-4ESAR77N.js} +2 -2
  22. package/dist/{chunk-SNKPUJHR.js → chunk-76NHR6ZL.js} +3 -3
  23. package/dist/{chunk-6PY5YJAV.js → chunk-BQCHEV7O.js} +8 -3
  24. package/dist/{chunk-6PY5YJAV.js.map → chunk-BQCHEV7O.js.map} +1 -1
  25. package/dist/{chunk-ZA2S2VLL.js → chunk-C2PG7IEH.js} +5 -2
  26. package/dist/chunk-C2PG7IEH.js.map +1 -0
  27. package/dist/{chunk-MJIHJ7OG.js → chunk-H5BWGCRZ.js} +11 -10
  28. package/dist/chunk-H5BWGCRZ.js.map +1 -0
  29. package/dist/{chunk-XPHVAOBT.js → chunk-HEDZUSCS.js} +180 -101
  30. package/dist/chunk-HEDZUSCS.js.map +1 -0
  31. package/dist/{chunk-N3VNUM4Z.js → chunk-HGNOCPZX.js} +4 -4
  32. package/dist/{chunk-CSRXQ6NB.js → chunk-I5REUWLH.js} +10 -4
  33. package/dist/chunk-I5REUWLH.js.map +1 -0
  34. package/dist/{chunk-NSA5ZRUL.js → chunk-M4AYVEWS.js} +3 -3
  35. package/dist/{chunk-OEEW4KJA.js → chunk-O6YH5F47.js} +3 -3
  36. package/dist/{chunk-27QQC7ZX.js → chunk-UM5AWJNQ.js} +6 -5
  37. package/dist/chunk-UM5AWJNQ.js.map +1 -0
  38. package/dist/{chunk-2KAYTPPT.js → chunk-UTHQPSRC.js} +2 -2
  39. package/dist/{chunk-CTOQEZSN.js → chunk-V4GNSDFH.js} +2 -2
  40. package/dist/{chunk-EHISUJFN.js → chunk-WAS3NV46.js} +6 -3
  41. package/dist/chunk-WAS3NV46.js.map +1 -0
  42. package/dist/{chunk-DKJCRGWM.js → chunk-YGIQORJ3.js} +2 -2
  43. package/dist/{cli-BLME420h.d.ts → cli-DmAiDd8A.d.ts} +2 -2
  44. package/dist/cli.d.ts +3 -3
  45. package/dist/cli.js +13 -13
  46. package/dist/embedding-fallback.js +2 -1
  47. package/dist/explicit-capture.d.ts +1 -1
  48. package/dist/extraction.js +2 -1
  49. package/dist/{first-start-migration-PG5HBC3K.js → first-start-migration-22LPFHD5.js} +6 -2
  50. package/dist/first-start-migration-22LPFHD5.js.map +1 -0
  51. package/dist/index.d.ts +4 -4
  52. package/dist/index.js +16 -16
  53. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  54. package/dist/namespaces/migrate.js +5 -4
  55. package/dist/namespaces/search.js +4 -3
  56. package/dist/operator-toolkit.js +6 -6
  57. package/dist/{orchestrator-DqPAz8ot.d.ts → orchestrator-BR0sGsME.d.ts} +104 -26
  58. package/dist/orchestrator.d.ts +1 -1
  59. package/dist/orchestrator.js +7 -7
  60. package/dist/schemas.d.ts +74 -74
  61. package/dist/search/embed-helper.js +2 -1
  62. package/dist/search/factory.js +3 -2
  63. package/dist/search/index.js +3 -2
  64. package/dist/shared-context/manager.d.ts +2 -2
  65. package/dist/transfer/types.d.ts +12 -12
  66. package/package.json +2 -2
  67. package/src/access-service.ts +2 -1
  68. package/src/capabilities.test.ts +155 -0
  69. package/src/capabilities.ts +91 -0
  70. package/src/chat/chat-engine.test.ts +39 -1
  71. package/src/chat/chat-session.ts +25 -2
  72. package/src/cli.ts +3 -2
  73. package/src/embedding-fallback.ts +2 -1
  74. package/src/extraction.ts +7 -3
  75. package/src/maintenance/first-start-migration.ts +3 -1
  76. package/src/orchestration/extraction-queue-coordinator.ts +175 -0
  77. package/src/orchestrator-extraction-queue.test.ts +26 -38
  78. package/src/orchestrator-flush.test.ts +10 -15
  79. package/src/orchestrator.ts +52 -104
  80. package/src/search/embed-helper.ts +3 -2
  81. package/dist/chunk-27QQC7ZX.js.map +0 -1
  82. package/dist/chunk-CSRXQ6NB.js.map +0 -1
  83. package/dist/chunk-EHISUJFN.js.map +0 -1
  84. package/dist/chunk-MJIHJ7OG.js.map +0 -1
  85. package/dist/chunk-XPHVAOBT.js.map +0 -1
  86. package/dist/chunk-ZA2S2VLL.js.map +0 -1
  87. package/dist/first-start-migration-PG5HBC3K.js.map +0 -1
  88. /package/dist/{chunk-R7IYU5DZ.js.map → chunk-4ESAR77N.js.map} +0 -0
  89. /package/dist/{chunk-SNKPUJHR.js.map → chunk-76NHR6ZL.js.map} +0 -0
  90. /package/dist/{chunk-N3VNUM4Z.js.map → chunk-HGNOCPZX.js.map} +0 -0
  91. /package/dist/{chunk-NSA5ZRUL.js.map → chunk-M4AYVEWS.js.map} +0 -0
  92. /package/dist/{chunk-OEEW4KJA.js.map → chunk-O6YH5F47.js.map} +0 -0
  93. /package/dist/{chunk-2KAYTPPT.js.map → chunk-UTHQPSRC.js.map} +0 -0
  94. /package/dist/{chunk-CTOQEZSN.js.map → chunk-V4GNSDFH.js.map} +0 -0
  95. /package/dist/{chunk-DKJCRGWM.js.map → chunk-YGIQORJ3.js.map} +0 -0
@@ -99,6 +99,7 @@ import {
99
99
  } from "./fallback-llm.js";
100
100
  import { MaintenanceScheduler } from "./orchestration/maintenance.js";
101
101
  import { TierMigrationCoordinator } from "./orchestration/tier-migration-coordinator.js";
102
+ import { ExtractionQueueCoordinator } from "./orchestration/extraction-queue-coordinator.js";
102
103
  import {
103
104
  runLiveConnectorsOnce,
104
105
  type LiveConnectorsRunSummary,
@@ -171,7 +172,6 @@ import {
171
172
  import { SessionObserverState } from "./session-observer-state.js";
172
173
  import {
173
174
  abortError as sharedAbortError,
174
- isAbortError,
175
175
  throwIfAborted as sharedThrowIfAborted,
176
176
  } from "./abort-error.js";
177
177
  import { CODEX_THREAD_KEY_PREFIX } from "./thread-key.js";
@@ -269,8 +269,10 @@ import { tryDirectAnswer, type DirectAnswerSources } from "./direct-answer-wirin
269
269
  import {
270
270
  resolveCapabilities,
271
271
  resolveGraphConstructionCapabilities,
272
+ resolveMemoryLifecycleCapabilities,
272
273
  type CapabilitySet,
273
274
  type GraphConstructionCapabilitySet,
275
+ type MemoryLifecycleCapabilitySet,
274
276
  } from "./capabilities.js";
275
277
  import { DEFAULT_TAXONOMY } from "./taxonomy/index.js";
276
278
  import {
@@ -2019,10 +2021,14 @@ export class Orchestrator {
2019
2021
  suppressedReasonCounts: Record<string, number>;
2020
2022
  } = { detected: 0, queued: 0, autoApplied: 0, suppressedReasonCounts: {} };
2021
2023
 
2022
- // Background serial queue for extractions (agent_end optimization)
2023
- // Queue stores promises that resolve when extraction should run
2024
- private extractionQueue: Array<() => Promise<void>> = [];
2025
- private queueProcessing = false;
2024
+ /**
2025
+ * Background serial extraction queue coordinator (issue #1526 moved
2026
+ * from inline `extractionQueue`/`queueProcessing` fields). Owns queue
2027
+ * state + scheduling + the serial drain + failure classification; the
2028
+ * orchestrator builds each task closure in `queueBufferedExtraction` and
2029
+ * hands it to `enqueue`.
2030
+ */
2031
+ readonly extractionQueueCoordinator: ExtractionQueueCoordinator;
2026
2032
  private heartbeatObserverChains = new Map<string, Promise<void>>();
2027
2033
  private recentExtractionFingerprints = new Map<string, number>();
2028
2034
  private readonly consolidationObservers = new Set<
@@ -2989,6 +2995,8 @@ export class Orchestrator {
2989
2995
  namespaceSearchRouter: this.namespaceSearchRouter,
2990
2996
  namespaceCatalog: this.namespaceCatalog,
2991
2997
  });
2998
+ // Issue #1526: background extraction queue lives on its own coordinator.
2999
+ this.extractionQueueCoordinator = new ExtractionQueueCoordinator();
2992
3000
  const conversationIndexRuntime = createConversationIndexRuntime(config, {
2993
3001
  getQmd: () => this.conversationQmd,
2994
3002
  getFaiss: () => this.conversationFaiss,
@@ -3094,9 +3102,10 @@ export class Orchestrator {
3094
3102
  );
3095
3103
  // BoxBuilders are created per-namespace on first use in runExtraction().
3096
3104
 
3105
+ const lifecycleCaps = resolveMemoryLifecycleCapabilities(config);
3097
3106
  // Temporal Memory Tree (v8.2) — lazy build during consolidation
3098
3107
  this.tmtBuilder = new TmtBuilder(config.memoryDir, {
3099
- temporalMemoryTreeEnabled: config.temporalMemoryTreeEnabled,
3108
+ temporalMemoryTreeEnabled: lifecycleCaps.temporalMemoryTree,
3100
3109
  tmtHourlyMinMemories: config.tmtHourlyMinMemories,
3101
3110
  tmtSummaryMaxTokens: config.tmtSummaryMaxTokens,
3102
3111
  });
@@ -3654,6 +3663,7 @@ export class Orchestrator {
3654
3663
  }
3655
3664
 
3656
3665
  private async deferredInitialize(signal: AbortSignal): Promise<void> {
3666
+ const lifecycleCaps = resolveMemoryLifecycleCapabilities(this.config);
3657
3667
 
3658
3668
  // Sync QMD index with current disk state so recall finds recently-written
3659
3669
  // facts. Without this, the index stays stale from the last extraction-
@@ -3707,7 +3717,7 @@ export class Orchestrator {
3707
3717
  }),
3708
3718
  );
3709
3719
  }
3710
- if (this.config.embeddingFallbackEnabled) {
3720
+ if (resolveMemoryLifecycleCapabilities(this.config).embeddingFallback) {
3711
3721
  warmupPromises.push(
3712
3722
  this.embeddingFallback
3713
3723
  .isAvailable()
@@ -3792,7 +3802,7 @@ export class Orchestrator {
3792
3802
  // sweep (capped at 50 demotions) so the hot tier isn't flooded on the
3793
3803
  // first real cron pass. Non-fatal — a failure here must not break init.
3794
3804
  if (signal.aborted) return;
3795
- if (this.config.lifecyclePolicyEnabled && this.config.qmdTierMigrationEnabled) {
3805
+ if (lifecycleCaps.lifecyclePolicy && this.config.qmdTierMigrationEnabled) {
3796
3806
  try {
3797
3807
  const { runFirstStartMigration } = await import(
3798
3808
  "./maintenance/first-start-migration.js"
@@ -4117,6 +4127,7 @@ export class Orchestrator {
4117
4127
  * default storage.
4118
4128
  */
4119
4129
  async runLifecyclePolicyFanout(): Promise<NamespaceMaintenanceSummary> {
4130
+ const lifecycleCaps = resolveMemoryLifecycleCapabilities(this.config);
4120
4131
  return this.runNamespaceMaintenanceFanoutForJob(
4121
4132
  "lifecycle",
4122
4133
  async (ctx) => {
@@ -4125,7 +4136,7 @@ export class Orchestrator {
4125
4136
  const assessed = await this.runLifecyclePolicyPass(corpus, storage);
4126
4137
  return { itemCount: assessed };
4127
4138
  },
4128
- { enabled: this.config.lifecyclePolicyEnabled },
4139
+ { enabled: lifecycleCaps.lifecyclePolicy },
4129
4140
  );
4130
4141
  }
4131
4142
 
@@ -4715,15 +4726,8 @@ export class Orchestrator {
4715
4726
  }
4716
4727
 
4717
4728
  async waitForExtractionIdle(timeoutMs: number = 60_000): Promise<boolean> {
4718
- const started = Date.now();
4719
- while (this.queueProcessing || this.extractionQueue.length > 0) {
4720
- if (Date.now() - started > timeoutMs) {
4721
- log.warn(`waitForExtractionIdle timed out after ${timeoutMs}ms`);
4722
- return false;
4723
- }
4724
- await new Promise((resolve) => setTimeout(resolve, 50));
4725
- }
4726
- return true;
4729
+ // Issue #1526: queue state + idle wait moved to ExtractionQueueCoordinator.
4730
+ return this.extractionQueueCoordinator.waitForIdle(timeoutMs);
4727
4731
  }
4728
4732
 
4729
4733
  async waitForConsolidationIdle(timeoutMs: number = 60_000): Promise<boolean> {
@@ -7438,6 +7442,7 @@ export class Orchestrator {
7438
7442
  options: RecallInvocationOptions = {},
7439
7443
  caps: CapabilitySet = resolveCapabilities(this.config),
7440
7444
  graphCaps: GraphConstructionCapabilitySet = resolveGraphConstructionCapabilities(this.config),
7445
+ lifecycleCaps: MemoryLifecycleCapabilitySet = resolveMemoryLifecycleCapabilities(this.config),
7441
7446
  ): Promise<string> {
7442
7447
  const recallStart = Date.now();
7443
7448
  // Backend degradations observed by this recall's QMD searches (#1536):
@@ -10845,9 +10850,9 @@ export class Orchestrator {
10845
10850
  if (
10846
10851
  this.isRecallSectionEnabled(
10847
10852
  "temporal-memory-tree",
10848
- this.config.temporalMemoryTreeEnabled === true,
10853
+ lifecycleCaps.temporalMemoryTree,
10849
10854
  ) &&
10850
- this.config.temporalMemoryTreeEnabled &&
10855
+ lifecycleCaps.temporalMemoryTree &&
10851
10856
  recallMode !== "minimal" &&
10852
10857
  (recallMode as RecallPlanMode) !== "no_recall"
10853
10858
  ) {
@@ -11707,7 +11712,7 @@ export class Orchestrator {
11707
11712
  // Using the shared gate fixes both Finding 2 and Finding 3 from
11708
11713
  // PR #402 (round 6).
11709
11714
  const supersessionOptions = {
11710
- enabled: this.config.temporalSupersessionEnabled,
11715
+ enabled: lifecycleCaps.temporalSupersession,
11711
11716
  includeInRecall: this.config.temporalSupersessionIncludeInRecall,
11712
11717
  };
11713
11718
  // Cursor Medium on PR #713: when `as_of` is active, the
@@ -13159,7 +13164,7 @@ export class Orchestrator {
13159
13164
  }, remainingMs);
13160
13165
  }
13161
13166
 
13162
- this.extractionQueue.push(async () => {
13167
+ this.extractionQueueCoordinator.enqueue(async () => {
13163
13168
  if (settled) return;
13164
13169
  if (
13165
13170
  typeof extractionDeadlineMs === "number" &&
@@ -13190,13 +13195,6 @@ export class Orchestrator {
13190
13195
  }
13191
13196
  });
13192
13197
 
13193
- if (!this.queueProcessing) {
13194
- this.queueProcessing = true;
13195
- this.processQueue().catch((err) => {
13196
- this.logExtractionQueueFailure(err, "processor");
13197
- this.queueProcessing = false;
13198
- });
13199
- }
13200
13198
  log.debug(`queued extraction from ${reason}`);
13201
13199
  }
13202
13200
 
@@ -13231,7 +13229,8 @@ export class Orchestrator {
13231
13229
  turns: BufferTurn[],
13232
13230
  options: { commit?: boolean; bufferKey?: string } = {},
13233
13231
  ): boolean {
13234
- if (!this.config.extractionDedupeEnabled) return true;
13232
+ const lifecycleCaps = resolveMemoryLifecycleCapabilities(this.config);
13233
+ if (!lifecycleCaps.extractionDedupe) return true;
13235
13234
  if (!Array.isArray(turns) || turns.length === 0) return false;
13236
13235
 
13237
13236
  const bufferKey = options.bufferKey ?? turns[0]?.sessionKey ?? "default";
@@ -13263,62 +13262,6 @@ export class Orchestrator {
13263
13262
  return true;
13264
13263
  }
13265
13264
 
13266
- /**
13267
- * Background serial queue processor.
13268
- * Processes extractions one at a time to avoid race conditions.
13269
- * Called automatically when items are queued.
13270
- */
13271
- private async processQueue(): Promise<void> {
13272
- while (this.extractionQueue.length > 0) {
13273
- const task = this.extractionQueue.shift();
13274
- if (task) {
13275
- try {
13276
- await task();
13277
- } catch (err) {
13278
- this.logExtractionQueueFailure(err, "task");
13279
- }
13280
- }
13281
- }
13282
-
13283
- this.queueProcessing = false;
13284
- }
13285
-
13286
- /**
13287
- * Classify + log a failure from either the per-task catch inside
13288
- * `processQueue()` or the outer `processQueue().catch(...)` in
13289
- * `queueBufferedExtraction()`. Issue #549: `throwIfRecallAborted`
13290
- * (used throughout `runExtraction`) raises an Error whose `name` is
13291
- * `"AbortError"`. That path fires when `before_reset` aborts a
13292
- * queued task to avoid duplicate extraction — it is intentional
13293
- * cancellation, not a failure. Downgrading the log to debug
13294
- * prevents spurious `error`-level lines that routinely appear
13295
- * right next to a successful `persisted: N facts, M entities` log
13296
- * and that confuse operators into thinking extraction is broken.
13297
- * Genuine extraction failures (network, parse, I/O) still log at
13298
- * `error`.
13299
- *
13300
- * Source differentiates the two call sites so the log message
13301
- * names the right layer (`task` vs `processor`).
13302
- */
13303
- private logExtractionQueueFailure(
13304
- err: unknown,
13305
- source: "task" | "processor",
13306
- ): void {
13307
- const aborted =
13308
- source === "task"
13309
- ? "background extraction task aborted (session transition)"
13310
- : "background extraction queue processor aborted (session transition)";
13311
- const failed =
13312
- source === "task"
13313
- ? "background extraction task failed"
13314
- : "background extraction queue processor failed";
13315
- if (isAbortError(err)) {
13316
- log.debug(aborted);
13317
- } else {
13318
- log.error(failed, err);
13319
- }
13320
- }
13321
-
13322
13265
  /**
13323
13266
  * Passive correction capture (issue #1581) — detects corrections expressed
13324
13267
  * passively in conversation turns and routes them to the Correction Contract
@@ -14090,6 +14033,7 @@ export class Orchestrator {
14090
14033
  /** Verbatim source turn text the facts were extracted from (faithfulness gate #1576). */
14091
14034
  sourceText?: string,
14092
14035
  graphCaps: GraphConstructionCapabilitySet = resolveGraphConstructionCapabilities(this.config),
14036
+ lifecycleCaps: MemoryLifecycleCapabilitySet = resolveMemoryLifecycleCapabilities(this.config),
14093
14037
  ): Promise<string[]> {
14094
14038
  // Inline source attribution (issue #369). When enabled, every extracted
14095
14039
  // fact is rewritten to carry a compact provenance tag inside its body so
@@ -14388,7 +14332,7 @@ export class Orchestrator {
14388
14332
  },
14389
14333
  );
14390
14334
  if (
14391
- this.config.temporalSupersessionEnabled &&
14335
+ lifecycleCaps.temporalSupersession &&
14392
14336
  options.category === "fact" &&
14393
14337
  options.entityRef &&
14394
14338
  options.structuredAttributes &&
@@ -14552,7 +14496,7 @@ export class Orchestrator {
14552
14496
  // would have run post-writeMemory. This is a best-effort / fail-open
14553
14497
  // step — if the lookup fails we skip silently (same as the normal path).
14554
14498
  if (
14555
- this.config.temporalSupersessionEnabled &&
14499
+ lifecycleCaps.temporalSupersession &&
14556
14500
  options.entityRef &&
14557
14501
  options.structuredAttributes &&
14558
14502
  Object.keys(options.structuredAttributes).length > 0
@@ -14714,7 +14658,7 @@ export class Orchestrator {
14714
14658
  // shared recall continues returning the stale state. Reuses the same
14715
14659
  // applyTemporalSupersession helper — no logic duplication.
14716
14660
  if (
14717
- this.config.temporalSupersessionEnabled &&
14661
+ lifecycleCaps.temporalSupersession &&
14718
14662
  options.entityRef &&
14719
14663
  options.structuredAttributes &&
14720
14664
  Object.keys(options.structuredAttributes).length > 0
@@ -15024,7 +14968,7 @@ export class Orchestrator {
15024
14968
  // persistExtraction call so stale state from a prior call cannot leak
15025
14969
  // into the caller's buffer-retention decision.
15026
14970
  this.lastPersistExtractionDeferredCount = 0;
15027
- if (this.config.extractionJudgeEnabled) {
14971
+ if (lifecycleCaps.extractionJudge) {
15028
14972
  // #1669 P1: pre-filter redacted facts from judge candidates so never-store
15029
14973
  // content is not persisted as judge training data.
15030
14974
  let preJudgeRedactionRules: CompiledRedactionRule[] = [];
@@ -15090,7 +15034,7 @@ export class Orchestrator {
15090
15034
  // off; the combined callback itself is undefined when both are
15091
15035
  // disabled so there is zero overhead in the default configuration.
15092
15036
  const judgeTelemetryOpts = {
15093
- enabled: this.config.extractionJudgeTelemetryEnabled === true,
15037
+ enabled: lifecycleCaps.extractionJudgeTelemetry,
15094
15038
  memoryDir: this.config.memoryDir,
15095
15039
  };
15096
15040
  const judgeTrainingOpts = {
@@ -15301,7 +15245,7 @@ export class Orchestrator {
15301
15245
  // do not block scope routing). Rule 30: gated by
15302
15246
  // extractionScopeClassificationEnabled.
15303
15247
  if (
15304
- this.config.extractionScopeClassificationEnabled &&
15248
+ lifecycleCaps.extractionScopeClassification &&
15305
15249
  this.config.namespacesEnabled &&
15306
15250
  fact.scope === "global" &&
15307
15251
  !routedNamespaceExplicit
@@ -15954,7 +15898,7 @@ export class Orchestrator {
15954
15898
  // #1578 r3: an extracted end-only bound (validFrom absent) is
15955
15899
  // historical, not a new authoritative state — never let it
15956
15900
  // supersede a later active fact (codex P1 on :15534).
15957
- enabled: this.config.temporalSupersessionEnabled &&
15901
+ enabled: lifecycleCaps.temporalSupersession &&
15958
15902
  !(biTemporal && !biTemporal.validFrom),
15959
15903
  });
15960
15904
  } catch (err) {
@@ -16190,7 +16134,7 @@ export class Orchestrator {
16190
16134
  entityRef: supersessionEntityRef,
16191
16135
  structuredAttributes: fact.structuredAttributes,
16192
16136
  createdAt: supersessionOrderingAt(biTemporal?.validFrom ?? sourceContext?.validAt),
16193
- enabled: this.config.temporalSupersessionEnabled &&
16137
+ enabled: lifecycleCaps.temporalSupersession &&
16194
16138
  !(biTemporal && !biTemporal.validFrom),
16195
16139
  });
16196
16140
  } catch (err) {
@@ -16557,7 +16501,7 @@ export class Orchestrator {
16557
16501
  storage: StorageManager,
16558
16502
  memoryId: string,
16559
16503
  ): Promise<void> {
16560
- if (!this.config.embeddingFallbackEnabled) return;
16504
+ if (!resolveMemoryLifecycleCapabilities(this.config).embeddingFallback) return;
16561
16505
  if (!(await this.embeddingFallback.isAvailable())) return;
16562
16506
  const memory = await storage.getMemoryById(memoryId);
16563
16507
  if (!memory) return;
@@ -16749,6 +16693,7 @@ export class Orchestrator {
16749
16693
  merged: number;
16750
16694
  invalidated: number;
16751
16695
  }> {
16696
+ const lifecycleCaps = resolveMemoryLifecycleCapabilities(this.config);
16752
16697
  log.info("running consolidation pass");
16753
16698
  let merged = 0;
16754
16699
  let invalidated = 0;
@@ -16985,7 +16930,7 @@ export class Orchestrator {
16985
16930
  }
16986
16931
 
16987
16932
  // v8.3 Lifecycle policy pass — deterministic promotion/decay metadata
16988
- if (this.config.lifecyclePolicyEnabled) {
16933
+ if (lifecycleCaps.lifecyclePolicy) {
16989
16934
  try {
16990
16935
  const lightSleepStartedAt = new Date().toISOString();
16991
16936
  const lifecycleCorpus = await this.storage.readAllMemories();
@@ -17173,7 +17118,7 @@ export class Orchestrator {
17173
17118
  await this.storage.saveMeta(meta);
17174
17119
 
17175
17120
  // Temporal Memory Tree (v8.2) — rebuild nodes from all memories, fail-open
17176
- if (this.config.temporalMemoryTreeEnabled) {
17121
+ if (lifecycleCaps.temporalMemoryTree) {
17177
17122
  try {
17178
17123
  const tmtEntries = allMemories
17179
17124
  .filter(
@@ -17715,6 +17660,7 @@ export class Orchestrator {
17715
17660
  let updatedCount = 0;
17716
17661
  let disputedCount = 0;
17717
17662
  let evaluatedCount = 0;
17663
+ const lifecycleCaps = resolveMemoryLifecycleCapabilities(this.config);
17718
17664
 
17719
17665
  const thresholds = this.effectiveLifecycleThresholds();
17720
17666
  const policy = {
@@ -17780,7 +17726,7 @@ export class Orchestrator {
17780
17726
 
17781
17727
  // Report how many memories had frontmatter rewritten so callers can record a
17782
17728
  // catalog write touch for lifecycle-only passes (codex NR-tS).
17783
- if (!this.config.lifecycleMetricsEnabled) return updatedCount;
17729
+ if (!lifecycleCaps.lifecycleMetrics) return updatedCount;
17784
17730
 
17785
17731
  const total = evaluatedCount;
17786
17732
  const metrics = {
@@ -19226,7 +19172,7 @@ export class Orchestrator {
19226
19172
  // • search() throws → re-throw (network/provider error).
19227
19173
  // • search() returns [] → return [] (empty index, not a
19228
19174
  // backend failure; decideSemanticDedup reports no_candidates).
19229
- if (!this.config.embeddingFallbackEnabled) {
19175
+ if (!resolveMemoryLifecycleCapabilities(this.config).embeddingFallback) {
19230
19176
  throw new Error("semantic dedup: embedding backend not configured");
19231
19177
  }
19232
19178
  if (!(await this.embeddingFallback.isAvailable())) {
@@ -19300,7 +19246,7 @@ export class Orchestrator {
19300
19246
  query: string,
19301
19247
  limit: number,
19302
19248
  ): Promise<QmdSearchResult[]> {
19303
- if (!this.config.embeddingFallbackEnabled) return [];
19249
+ if (!resolveMemoryLifecycleCapabilities(this.config).embeddingFallback) return [];
19304
19250
  if (!(await this.embeddingFallback.isAvailable())) return [];
19305
19251
  const hits = await this.embeddingFallback.search(query, limit);
19306
19252
  if (hits.length === 0) return [];
@@ -20010,6 +19956,7 @@ export class Orchestrator {
20010
19956
  blockedPaths?: Set<string>;
20011
19957
  },
20012
19958
  ): QmdSearchResult[] {
19959
+ const lifecycleCaps = resolveMemoryLifecycleCapabilities(this.config);
20013
19960
  let lifecycleFilteredCount = 0;
20014
19961
  let temporalSupersededFilteredCount = 0;
20015
19962
  let biTemporalExpiredFilteredCount = 0;
@@ -20043,9 +19990,9 @@ export class Orchestrator {
20043
19990
  if (
20044
19991
  options?.allowLifecycleFiltered !== true &&
20045
19992
  shouldFilterLifecycleRecallCandidate(memory.frontmatter, {
20046
- lifecyclePolicyEnabled: this.config.lifecyclePolicyEnabled,
19993
+ lifecyclePolicyEnabled: lifecycleCaps.lifecyclePolicy,
20047
19994
  lifecycleFilterStaleEnabled:
20048
- this.config.lifecycleFilterStaleEnabled,
19995
+ lifecycleCaps.lifecycleFilterStale,
20049
19996
  })
20050
19997
  ) {
20051
19998
  lifecycleFilteredCount += 1;
@@ -20079,7 +20026,7 @@ export class Orchestrator {
20079
20026
  // half-open `[valid_at, invalid_at)` evaluation in isValidAsOf
20080
20027
  // is the authoritative gate for historical recall.
20081
20028
  shouldFilterSupersededFromRecall(memory.frontmatter, {
20082
- enabled: this.config.temporalSupersessionEnabled,
20029
+ enabled: lifecycleCaps.temporalSupersession,
20083
20030
  includeInRecall: this.config.temporalSupersessionIncludeInRecall,
20084
20031
  })
20085
20032
  ) {
@@ -20223,6 +20170,7 @@ export class Orchestrator {
20223
20170
  asOfMs?: number;
20224
20171
  },
20225
20172
  ): Promise<QmdSearchResult[]> {
20173
+ const lifecycleCaps = resolveMemoryLifecycleCapabilities(this.config);
20226
20174
  if (results.length === 0) return results;
20227
20175
 
20228
20176
  const safety = await this.filterSearchResultsForRecall(
@@ -20397,7 +20345,7 @@ export class Orchestrator {
20397
20345
  const lifecycleDelta = lifecycleRecallScoreAdjustment(
20398
20346
  memory.frontmatter,
20399
20347
  {
20400
- lifecyclePolicyEnabled: this.config.lifecyclePolicyEnabled,
20348
+ lifecyclePolicyEnabled: lifecycleCaps.lifecyclePolicy,
20401
20349
  },
20402
20350
  );
20403
20351
  score += applyUtilityRankingRuntimeDelta(
@@ -1,4 +1,5 @@
1
1
  import { log } from "../logger.js";
2
+ import { resolveMemoryLifecycleCapabilities } from "../capabilities.js";
2
3
  import type { PluginConfig } from "../types.js";
3
4
  import { isAbortError } from "../abort-error.js";
4
5
  import { withTimeoutSignal } from "./abort.js";
@@ -209,7 +210,7 @@ export class EmbedHelper {
209
210
  }
210
211
 
211
212
  private resolveProvider(options: { includeHost?: boolean } = {}): ProviderConfig | null {
212
- if (!this.config.embeddingFallbackEnabled) return null;
213
+ if (!resolveMemoryLifecycleCapabilities(this.config).embeddingFallback) return null;
213
214
 
214
215
  if (
215
216
  options.includeHost !== false &&
@@ -244,7 +245,7 @@ export class EmbedHelper {
244
245
  }
245
246
 
246
247
  private resolveFallbackProviderForIdentity(identity: EmbedProviderIdentity): ProviderConfig | null {
247
- if (!this.config.embeddingFallbackEnabled) return null;
248
+ if (!resolveMemoryLifecycleCapabilities(this.config).embeddingFallback) return null;
248
249
  const separator = identity.indexOf(":");
249
250
  if (separator <= 0 || separator === identity.length - 1) return null;
250
251
  const type = identity.slice(0, separator);