@remnic/core 9.3.654 → 9.3.655

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 (246) hide show
  1. package/dist/access-cli.js +23 -23
  2. package/dist/access-http.d.ts +4 -4
  3. package/dist/access-http.js +16 -16
  4. package/dist/access-mcp.d.ts +4 -4
  5. package/dist/access-mcp.js +15 -15
  6. package/dist/{access-service-C8A5hoXJ.d.ts → access-service-BEJvriUt.d.ts} +2 -2
  7. package/dist/access-service.d.ts +4 -4
  8. package/dist/access-service.js +14 -14
  9. package/dist/action-confidence.d.ts +1 -1
  10. package/dist/active-memory-bridge.d.ts +1 -1
  11. package/dist/active-recall.d.ts +1 -1
  12. package/dist/active-recall.js +1 -1
  13. package/dist/behavior-learner.d.ts +1 -1
  14. package/dist/behavior-signals.d.ts +1 -1
  15. package/dist/bootstrap.d.ts +3 -3
  16. package/dist/briefing.d.ts +1 -1
  17. package/dist/briefing.js +3 -3
  18. package/dist/buffer-surprise-report.d.ts +1 -1
  19. package/dist/buffer.d.ts +1 -1
  20. package/dist/calibration.d.ts +1 -1
  21. package/dist/causal-behavior.d.ts +1 -1
  22. package/dist/causal-consolidation.d.ts +1 -1
  23. package/dist/causal-consolidation.js +4 -4
  24. package/dist/{chunk-BP2EV6W5.js → chunk-2RCGZ67B.js} +4 -4
  25. package/dist/{chunk-FVRBLJP6.js → chunk-54LOUIBE.js} +2 -2
  26. package/dist/{chunk-LJCEWTG3.js → chunk-55ZMNKMQ.js} +8 -8
  27. package/dist/{chunk-7WEB3FLJ.js → chunk-5PLUC5OB.js} +2 -2
  28. package/dist/{chunk-OL2364SB.js → chunk-5QD3QD76.js} +854 -223
  29. package/dist/chunk-5QD3QD76.js.map +1 -0
  30. package/dist/{chunk-DBM2BD22.js → chunk-67G4T7KI.js} +3 -3
  31. package/dist/{chunk-JX2RINDR.js → chunk-6G5JEN55.js} +2 -2
  32. package/dist/{chunk-2DSTAWNZ.js → chunk-A3Y37UWI.js} +3 -3
  33. package/dist/{chunk-T2C6QJG2.js → chunk-BGKXTVNG.js} +2 -2
  34. package/dist/{chunk-JYN7QNTA.js → chunk-COVZLGMR.js} +17 -17
  35. package/dist/{chunk-JVRPJ7D4.js → chunk-EKQMQQ3U.js} +48 -12
  36. package/dist/chunk-EKQMQQ3U.js.map +1 -0
  37. package/dist/{chunk-JBHXMCYN.js → chunk-GRYAECRV.js} +2 -2
  38. package/dist/{chunk-RGPUQ66K.js → chunk-IQ53ZSXV.js} +2 -2
  39. package/dist/{chunk-YLZLPVKK.js → chunk-KOI765XP.js} +104 -1
  40. package/dist/chunk-KOI765XP.js.map +1 -0
  41. package/dist/{chunk-QW6JZO5P.js → chunk-KZZ4YAEC.js} +2 -2
  42. package/dist/{chunk-2DGQLOOM.js → chunk-M3VYPE2H.js} +1 -1
  43. package/dist/{chunk-2DGQLOOM.js.map → chunk-M3VYPE2H.js.map} +1 -1
  44. package/dist/{chunk-QKK64Z6M.js → chunk-NCSJKK23.js} +2 -2
  45. package/dist/{chunk-NCGWXCSW.js → chunk-NRBGRZW4.js} +2 -2
  46. package/dist/{chunk-GPW2E4LN.js → chunk-OKW6F5S5.js} +3 -3
  47. package/dist/{chunk-Y2RIIF6H.js → chunk-PTMJ2FH2.js} +2 -2
  48. package/dist/{chunk-JMQSYGXS.js → chunk-PVE7KSQP.js} +2 -2
  49. package/dist/{chunk-NAZWHTYV.js → chunk-QDVQ4AN2.js} +4 -4
  50. package/dist/{chunk-PYWNNF2I.js → chunk-QRSKPI62.js} +99 -66
  51. package/dist/chunk-QRSKPI62.js.map +1 -0
  52. package/dist/{chunk-YM3LR4LS.js → chunk-SSSXWIBP.js} +5 -5
  53. package/dist/{chunk-NE2JBMLN.js → chunk-TDZSSJV4.js} +1 -1
  54. package/dist/chunk-TDZSSJV4.js.map +1 -0
  55. package/dist/{chunk-6CVI6BP6.js → chunk-TEO46GMM.js} +2 -2
  56. package/dist/{chunk-K6X553JB.js → chunk-UCEABZZN.js} +2 -2
  57. package/dist/{chunk-ENV6RDTD.js → chunk-UCEDY5M7.js} +2 -2
  58. package/dist/{chunk-ZCORQM74.js → chunk-UYNFWZWG.js} +2 -2
  59. package/dist/{chunk-XWQ6ERUG.js → chunk-WDTUYOLS.js} +2 -2
  60. package/dist/{chunk-Z5MQI7K2.js → chunk-XOFXKASO.js} +2 -2
  61. package/dist/{chunk-3RACUBII.js → chunk-XRKQOQLY.js} +2 -2
  62. package/dist/{chunk-BJA6DQOC.js → chunk-YYN3LIYA.js} +5 -5
  63. package/dist/{cli-uQgvDFNE.d.ts → cli-BGahB_d3.d.ts} +3 -3
  64. package/dist/cli.d.ts +5 -5
  65. package/dist/cli.js +28 -28
  66. package/dist/compounding/engine.d.ts +1 -1
  67. package/dist/compounding/engine.js +3 -3
  68. package/dist/compounding/preference-consolidator.d.ts +1 -1
  69. package/dist/compression-optimizer.d.ts +1 -1
  70. package/dist/config.d.ts +1 -1
  71. package/dist/config.js +1 -1
  72. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  73. package/dist/connectors/codex-materialize-runner.js +3 -3
  74. package/dist/connectors/codex-materialize.d.ts +1 -1
  75. package/dist/connectors/index.d.ts +1 -1
  76. package/dist/connectors/index.js +3 -3
  77. package/dist/consolidation-provenance-check.d.ts +1 -1
  78. package/dist/consolidation-undo.d.ts +1 -1
  79. package/dist/contradiction/index.d.ts +1 -1
  80. package/dist/conversation-index/backend.d.ts +1 -1
  81. package/dist/conversation-index/chunker.d.ts +1 -1
  82. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  83. package/dist/conversation-index/indexer.d.ts +1 -1
  84. package/dist/conversation-index/search.d.ts +1 -1
  85. package/dist/day-summary.d.ts +1 -1
  86. package/dist/delinearize.d.ts +1 -1
  87. package/dist/direct-answer-wiring.d.ts +1 -1
  88. package/dist/direct-answer.d.ts +1 -1
  89. package/dist/embedding-fallback.d.ts +1 -1
  90. package/dist/enrichment/index.d.ts +1 -1
  91. package/dist/entity-retrieval.d.ts +1 -1
  92. package/dist/entity-retrieval.js +3 -3
  93. package/dist/entity-schema.d.ts +1 -1
  94. package/dist/explicit-capture.d.ts +3 -3
  95. package/dist/extraction-judge-telemetry.d.ts +1 -1
  96. package/dist/extraction-judge-training.d.ts +1 -1
  97. package/dist/extraction-judge.d.ts +1 -1
  98. package/dist/extraction.d.ts +1 -1
  99. package/dist/fallback-llm.d.ts +1 -1
  100. package/dist/identity-continuity.d.ts +1 -1
  101. package/dist/importance.d.ts +1 -1
  102. package/dist/index.d.ts +8 -8
  103. package/dist/index.js +33 -33
  104. package/dist/intent.d.ts +1 -1
  105. package/dist/lcm/engine.d.ts +1 -1
  106. package/dist/lcm/index.d.ts +1 -1
  107. package/dist/lcm/tools.d.ts +1 -1
  108. package/dist/lifecycle.d.ts +1 -1
  109. package/dist/live-connectors-runner.d.ts +1 -1
  110. package/dist/local-llm.d.ts +1 -1
  111. package/dist/maintenance/memory-governance.d.ts +1 -1
  112. package/dist/maintenance/memory-governance.js +3 -3
  113. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  114. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  115. package/dist/mcp-memory-inspector-app.d.ts +4 -4
  116. package/dist/memory-action-policy.d.ts +1 -1
  117. package/dist/memory-cache.d.ts +1 -1
  118. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  119. package/dist/memory-projection-store.d.ts +1 -1
  120. package/dist/memory-provenance.d.ts +1 -1
  121. package/dist/memory-worth-outcomes.d.ts +1 -1
  122. package/dist/models-json.d.ts +1 -1
  123. package/dist/namespaces/migrate.d.ts +1 -1
  124. package/dist/namespaces/migrate.js +11 -11
  125. package/dist/namespaces/principal.d.ts +1 -1
  126. package/dist/namespaces/search.d.ts +15 -4
  127. package/dist/namespaces/search.js +7 -7
  128. package/dist/namespaces/storage.d.ts +1 -1
  129. package/dist/namespaces/storage.js +3 -3
  130. package/dist/native-knowledge.d.ts +1 -1
  131. package/dist/operator-toolkit.d.ts +1 -1
  132. package/dist/operator-toolkit.js +14 -14
  133. package/dist/{orchestrator-B4Y4sWQH.d.ts → orchestrator-BgzZlWxH.d.ts} +11 -14
  134. package/dist/orchestrator.d.ts +3 -3
  135. package/dist/orchestrator.js +19 -19
  136. package/dist/patterns-cli.d.ts +1 -1
  137. package/dist/policy-runtime.d.ts +1 -1
  138. package/dist/qmd-recall-cache.d.ts +1 -1
  139. package/dist/qmd.d.ts +5 -1
  140. package/dist/qmd.js +2 -2
  141. package/dist/recall-disclosure-escalation.d.ts +1 -1
  142. package/dist/recall-explain-renderer.d.ts +1 -1
  143. package/dist/recall-explain-renderer.js +3 -3
  144. package/dist/recall-planner-llm.d.ts +1 -1
  145. package/dist/recall-state.d.ts +1 -1
  146. package/dist/recall-tag-filter.d.ts +1 -1
  147. package/dist/recall-xray-cli.d.ts +1 -1
  148. package/dist/recall-xray-cli.js +4 -4
  149. package/dist/recall-xray-renderer.d.ts +1 -1
  150. package/dist/recall-xray-renderer.js +3 -3
  151. package/dist/recall-xray.d.ts +1 -1
  152. package/dist/recall-xray.js +2 -2
  153. package/dist/resolve-auth-token.d.ts +1 -1
  154. package/dist/resume-bundles.js +2 -2
  155. package/dist/retrieval-agents.d.ts +1 -1
  156. package/dist/retrieval-tiers.d.ts +1 -1
  157. package/dist/routing/engine.d.ts +1 -1
  158. package/dist/routing/store.d.ts +1 -1
  159. package/dist/schemas.d.ts +22 -22
  160. package/dist/search/embed-helper.d.ts +1 -1
  161. package/dist/search/factory.d.ts +1 -1
  162. package/dist/search/factory.js +6 -6
  163. package/dist/search/index.d.ts +1 -1
  164. package/dist/search/index.js +6 -6
  165. package/dist/search/lancedb-backend.d.ts +1 -1
  166. package/dist/search/lancedb-backend.js +2 -2
  167. package/dist/search/meilisearch-backend.d.ts +1 -1
  168. package/dist/search/meilisearch-backend.js +2 -2
  169. package/dist/search/noop-backend.d.ts +1 -1
  170. package/dist/search/orama-backend.d.ts +1 -1
  171. package/dist/search/orama-backend.js +2 -2
  172. package/dist/search/port.d.ts +17 -1
  173. package/dist/search/port.js +1 -1
  174. package/dist/search/remote-backend.d.ts +1 -1
  175. package/dist/{semantic-consolidation-BKd0Pype.d.ts → semantic-consolidation-Z8d_uMq8.d.ts} +1 -1
  176. package/dist/semantic-consolidation.d.ts +2 -2
  177. package/dist/semantic-consolidation.js +4 -4
  178. package/dist/semantic-rule-promotion.js +3 -3
  179. package/dist/semantic-rule-verifier.d.ts +1 -1
  180. package/dist/semantic-rule-verifier.js +3 -3
  181. package/dist/session-observer-bands.d.ts +1 -1
  182. package/dist/session-observer-state.d.ts +1 -1
  183. package/dist/shared-context/manager.d.ts +1 -1
  184. package/dist/signal.d.ts +1 -1
  185. package/dist/storage.d.ts +1 -1
  186. package/dist/storage.js +2 -2
  187. package/dist/summarizer.d.ts +1 -1
  188. package/dist/summary-snapshot.d.ts +1 -1
  189. package/dist/temporal-supersession.d.ts +1 -1
  190. package/dist/temporal-validity.d.ts +1 -1
  191. package/dist/threading.d.ts +1 -1
  192. package/dist/tier-migration.d.ts +1 -1
  193. package/dist/tier-routing.d.ts +1 -1
  194. package/dist/topics.d.ts +1 -1
  195. package/dist/transcript.d.ts +1 -1
  196. package/dist/transfer/types.d.ts +12 -12
  197. package/dist/{types-BgChEr0M.d.ts → types-2OPlQWJG.d.ts} +12 -0
  198. package/dist/types.d.ts +1 -1
  199. package/dist/types.js +1 -1
  200. package/dist/utility-runtime.d.ts +1 -1
  201. package/dist/verified-recall.js +3 -3
  202. package/package.json +1 -1
  203. package/src/config.test.ts +69 -0
  204. package/src/config.ts +135 -0
  205. package/src/maintenance/namespace-planner.test.ts +1120 -0
  206. package/src/maintenance/namespace-planner.ts +893 -0
  207. package/src/namespaces/search.test.ts +130 -2
  208. package/src/namespaces/search.ts +71 -10
  209. package/src/orchestrator-flush.test.ts +464 -44
  210. package/src/orchestrator.ts +97 -66
  211. package/src/qmd-client.test.ts +59 -0
  212. package/src/qmd.ts +124 -84
  213. package/src/search/port.ts +16 -0
  214. package/src/types.ts +12 -0
  215. package/dist/chunk-JVRPJ7D4.js.map +0 -1
  216. package/dist/chunk-NE2JBMLN.js.map +0 -1
  217. package/dist/chunk-OL2364SB.js.map +0 -1
  218. package/dist/chunk-PYWNNF2I.js.map +0 -1
  219. package/dist/chunk-YLZLPVKK.js.map +0 -1
  220. /package/dist/{chunk-BP2EV6W5.js.map → chunk-2RCGZ67B.js.map} +0 -0
  221. /package/dist/{chunk-FVRBLJP6.js.map → chunk-54LOUIBE.js.map} +0 -0
  222. /package/dist/{chunk-LJCEWTG3.js.map → chunk-55ZMNKMQ.js.map} +0 -0
  223. /package/dist/{chunk-7WEB3FLJ.js.map → chunk-5PLUC5OB.js.map} +0 -0
  224. /package/dist/{chunk-DBM2BD22.js.map → chunk-67G4T7KI.js.map} +0 -0
  225. /package/dist/{chunk-JX2RINDR.js.map → chunk-6G5JEN55.js.map} +0 -0
  226. /package/dist/{chunk-2DSTAWNZ.js.map → chunk-A3Y37UWI.js.map} +0 -0
  227. /package/dist/{chunk-T2C6QJG2.js.map → chunk-BGKXTVNG.js.map} +0 -0
  228. /package/dist/{chunk-JYN7QNTA.js.map → chunk-COVZLGMR.js.map} +0 -0
  229. /package/dist/{chunk-JBHXMCYN.js.map → chunk-GRYAECRV.js.map} +0 -0
  230. /package/dist/{chunk-RGPUQ66K.js.map → chunk-IQ53ZSXV.js.map} +0 -0
  231. /package/dist/{chunk-QW6JZO5P.js.map → chunk-KZZ4YAEC.js.map} +0 -0
  232. /package/dist/{chunk-QKK64Z6M.js.map → chunk-NCSJKK23.js.map} +0 -0
  233. /package/dist/{chunk-NCGWXCSW.js.map → chunk-NRBGRZW4.js.map} +0 -0
  234. /package/dist/{chunk-GPW2E4LN.js.map → chunk-OKW6F5S5.js.map} +0 -0
  235. /package/dist/{chunk-Y2RIIF6H.js.map → chunk-PTMJ2FH2.js.map} +0 -0
  236. /package/dist/{chunk-JMQSYGXS.js.map → chunk-PVE7KSQP.js.map} +0 -0
  237. /package/dist/{chunk-NAZWHTYV.js.map → chunk-QDVQ4AN2.js.map} +0 -0
  238. /package/dist/{chunk-YM3LR4LS.js.map → chunk-SSSXWIBP.js.map} +0 -0
  239. /package/dist/{chunk-6CVI6BP6.js.map → chunk-TEO46GMM.js.map} +0 -0
  240. /package/dist/{chunk-K6X553JB.js.map → chunk-UCEABZZN.js.map} +0 -0
  241. /package/dist/{chunk-ENV6RDTD.js.map → chunk-UCEDY5M7.js.map} +0 -0
  242. /package/dist/{chunk-ZCORQM74.js.map → chunk-UYNFWZWG.js.map} +0 -0
  243. /package/dist/{chunk-XWQ6ERUG.js.map → chunk-WDTUYOLS.js.map} +0 -0
  244. /package/dist/{chunk-Z5MQI7K2.js.map → chunk-XOFXKASO.js.map} +0 -0
  245. /package/dist/{chunk-3RACUBII.js.map → chunk-XRKQOQLY.js.map} +0 -0
  246. /package/dist/{chunk-BJA6DQOC.js.map → chunk-YYN3LIYA.js.map} +0 -0
@@ -299,13 +299,16 @@ import {
299
299
  } from "./conversation-index/backend.js";
300
300
  import {
301
301
  NamespaceStorageRouter,
302
- resolveNamespaceStorageRoot,
303
302
  } from "./namespaces/storage.js";
304
303
  import {
305
304
  NamespaceCatalog,
306
- hasMemoryData,
307
- type NamespaceRecord,
308
305
  } from "./namespaces/catalog.js";
306
+ import {
307
+ planNamespaceMaintenance,
308
+ runNamespaceMaintenanceBatchPlan,
309
+ type NamespaceMaintenancePlan,
310
+ type NamespaceMaintenanceSkipReason,
311
+ } from "./maintenance/namespace-planner.js";
309
312
  import {
310
313
  namespaceIdentityFromToken,
311
314
  namespaceIdentityToken,
@@ -1767,6 +1770,13 @@ export function resolvePersistedMemoryRelativePath(options: {
1767
1770
  return path.join(subtree, `${options.memoryId}.md`);
1768
1771
  }
1769
1772
 
1773
+ function qmdMaintenanceSkipReasonForError(error: unknown): NamespaceMaintenanceSkipReason | null {
1774
+ const message = error instanceof Error ? error.message : String(error);
1775
+ return /^QMD (?:update|embed) skipped by .*min-interval gate$/.test(message)
1776
+ ? "throttled"
1777
+ : null;
1778
+ }
1779
+
1770
1780
  export class Orchestrator {
1771
1781
  readonly storage: StorageManager;
1772
1782
  private readonly storageRouter: NamespaceStorageRouter;
@@ -1914,6 +1924,7 @@ export class Orchestrator {
1914
1924
  private qmdMaintenancePending = false;
1915
1925
  private qmdMaintenanceInFlight = false;
1916
1926
  private lastQmdEmbedAtMs = 0;
1927
+ private lastQmdEmbedAtMsByNamespace = new Map<string, number>();
1917
1928
  private lastQmdReprobeAtMs = 0;
1918
1929
  private tierMigrationInFlight = false;
1919
1930
  private lastTierMigrationRunAtMs = 0;
@@ -2417,63 +2428,31 @@ export class Orchestrator {
2417
2428
  }
2418
2429
 
2419
2430
  /**
2420
- * Namespaces that QMD maintenance (update/embed) must cover: the CONFIGURED set
2421
- * PLUS every dynamic namespace recorded in the catalog (NGnei, codex P2). An
2422
- * extraction that writes to a coding-scoped/dynamic namespace (not in
2423
- * defaultNamespace/sharedNamespace/namespacePolicies) is only made discoverable
2424
- * via the catalog; if maintenance embeds only `configuredNamespaces()`, that
2425
- * namespace's QMD collection is never updated/embedded after writes and
2426
- * recall/search stays stale or empty until it is manually configured. We union in
2427
- * the catalog's namespaces so maintenance keeps dynamic namespaces fresh.
2428
- * `updateNamespaces`/`embedNamespaces` already trim, dedup, and skip
2429
- * unavailable/missing collections, so extra names are filtered safely. A catalog
2430
- * read failure must never break maintenance — fall back to the configured set.
2431
+ * Shared namespace maintenance planner (issue #1500). This extends the
2432
+ * #1499 catalog-union QMD helper into a reusable contract: configured
2433
+ * namespaces are always considered, dynamic catalog namespaces are admitted
2434
+ * only when their live router root still matches real memory data, and branch
2435
+ * namespaces are opt-in. Recurring jobs use the per-cycle budget; startup and
2436
+ * recovery discovery paths use the same safety filters without that cycle
2437
+ * budget so every live namespace is ensured/synced.
2431
2438
  */
2432
- private async maintenanceNamespaces(): Promise<string[]> {
2433
- const configured = this.configuredNamespaces();
2434
- if (!this.namespaceCatalog.enabled) return configured;
2435
- const configuredSet = new Set(configured);
2436
- let cataloged: string[] = [];
2437
- try {
2438
- const records = await this.namespaceCatalog.listNamespaces();
2439
- const safeRecords = await Promise.all(
2440
- records.map(async (record) => {
2441
- const namespace = record.namespace.trim();
2442
- if (!namespace || configuredSet.has(namespace)) return null;
2443
- return (await this.isCatalogedMaintenanceRootLive(record))
2444
- ? namespace
2445
- : null;
2446
- }),
2447
- );
2448
- cataloged = safeRecords.filter(
2449
- (namespace): namespace is string => namespace !== null,
2450
- );
2451
- } catch {
2452
- // Best-effort: a catalog read failure must not break QMD maintenance.
2453
- cataloged = [];
2454
- }
2455
- return Array.from(
2456
- new Set(
2457
- [...configured, ...cataloged].map((value) => value.trim()).filter(Boolean),
2458
- ),
2459
- );
2439
+ private async namespaceMaintenancePlan(jobName: string): Promise<NamespaceMaintenancePlan> {
2440
+ return planNamespaceMaintenance(this.config, {
2441
+ jobName,
2442
+ catalog: this.namespaceCatalog,
2443
+ });
2460
2444
  }
2461
2445
 
2462
- private async isCatalogedMaintenanceRootLive(
2463
- record: NamespaceRecord,
2464
- ): Promise<boolean> {
2465
- if (typeof record.storageDir !== "string" || record.storageDir.length === 0) {
2466
- return false;
2467
- }
2468
- try {
2469
- const liveRoot = await resolveNamespaceStorageRoot(this.config, record.namespace);
2470
- if (path.resolve(liveRoot) !== path.resolve(record.storageDir)) {
2471
- return false;
2472
- }
2473
- return hasMemoryData(liveRoot);
2474
- } catch {
2475
- return false;
2476
- }
2446
+ private async maintenanceNamespaces(
2447
+ jobName = "qmd",
2448
+ budgetMode: "cycle" | "unbounded" = "unbounded",
2449
+ ): Promise<string[]> {
2450
+ const plan = await planNamespaceMaintenance(this.config, {
2451
+ jobName,
2452
+ catalog: this.namespaceCatalog,
2453
+ budgetMode,
2454
+ });
2455
+ return plan.namespaces.map((candidate) => candidate.namespace);
2477
2456
  }
2478
2457
 
2479
2458
  private buildConfiguredQmdSearchOptions(
@@ -13337,17 +13316,69 @@ export class Orchestrator {
13337
13316
  try {
13338
13317
  if (this.config.namespacesEnabled) {
13339
13318
  // Include cataloged dynamic namespaces, not just the configured set
13340
- // (NGnei) resolve once and reuse for both update and embed.
13341
- const maintenanceNamespaces = await this.maintenanceNamespaces();
13342
- await this.namespaceSearchRouter.updateNamespaces(maintenanceNamespaces);
13319
+ // (NGnei), but run through the namespace-aware maintenance planner so
13320
+ // each namespace is budgeted, lock-protected, and status-recorded
13321
+ // independently (issue #1500).
13322
+ const plan = await this.namespaceMaintenancePlan("qmd");
13343
13323
  const now = Date.now();
13344
- if (
13345
- this.config.qmdAutoEmbedEnabled &&
13346
- now - this.lastQmdEmbedAtMs >= this.config.qmdEmbedMinIntervalMs
13347
- ) {
13348
- await this.namespaceSearchRouter.embedNamespaces(maintenanceNamespaces);
13324
+ const lastEmbedAtByNamespace =
13325
+ this.lastQmdEmbedAtMsByNamespace ?? (this.lastQmdEmbedAtMsByNamespace = new Map());
13326
+ const dueEmbedNamespaces = (namespaces: string[]): string[] => {
13327
+ if (!this.config.qmdAutoEmbedEnabled) return [];
13328
+ return namespaces.filter(
13329
+ (namespace) =>
13330
+ now - (lastEmbedAtByNamespace.get(namespace) ?? 0) >= this.config.qmdEmbedMinIntervalMs,
13331
+ );
13332
+ };
13333
+ const markEmbedded = (namespaces: string[]): void => {
13334
+ if (namespaces.length === 0) return;
13335
+ for (const namespace of namespaces) {
13336
+ lastEmbedAtByNamespace.set(namespace, now);
13337
+ }
13349
13338
  this.lastQmdEmbedAtMs = now;
13350
- }
13339
+ };
13340
+ await runNamespaceMaintenanceBatchPlan(
13341
+ this.config,
13342
+ plan,
13343
+ async (candidates) => {
13344
+ const namespaces = candidates.map((candidate) => candidate.namespace);
13345
+ const embedNamespaces = dueEmbedNamespaces(namespaces);
13346
+ let result: Awaited<ReturnType<NamespaceSearchRouter["updateNamespacesDetailed"]>>;
13347
+ try {
13348
+ result = await this.namespaceSearchRouter.updateNamespacesDetailed(
13349
+ namespaces,
13350
+ undefined,
13351
+ { strict: true },
13352
+ );
13353
+ } catch (error) {
13354
+ if (
13355
+ embedNamespaces.length > 0 &&
13356
+ qmdMaintenanceSkipReasonForError(error) === "throttled"
13357
+ ) {
13358
+ await this.namespaceSearchRouter.embedNamespaces(embedNamespaces, { strict: true });
13359
+ markEmbedded(embedNamespaces);
13360
+ }
13361
+ throw error;
13362
+ }
13363
+ if (result.backendCount <= 0) {
13364
+ throw new Error("no eligible QMD backend for selected namespaces");
13365
+ }
13366
+ if (result.eligibleNamespaces.length !== namespaces.length) {
13367
+ const eligible = new Set(result.eligibleNamespaces);
13368
+ const missing = namespaces.filter((namespace) => !eligible.has(namespace));
13369
+ throw new Error(`QMD backend ineligible for selected namespaces (${missing.length})`);
13370
+ }
13371
+ if (embedNamespaces.length > 0) {
13372
+ await this.namespaceSearchRouter.embedNamespaces(embedNamespaces, { strict: true });
13373
+ markEmbedded(embedNamespaces);
13374
+ }
13375
+ return { itemCount: result.backendCount };
13376
+ },
13377
+ this.namespaceCatalog,
13378
+ {
13379
+ skipReasonForError: qmdMaintenanceSkipReasonForError,
13380
+ },
13381
+ );
13351
13382
  } else {
13352
13383
  await this.qmd.update();
13353
13384
  const now = Date.now();
@@ -70,6 +70,65 @@ function captureSubprocessArgs(client: QmdClient): string[][] {
70
70
  return calls;
71
71
  }
72
72
 
73
+ test("updateStrict respects QMD update min-interval throttles", async () => {
74
+ const client = new QmdClient("memories", 3, { updateMinIntervalMs: 60_000 });
75
+ client.resetUpdateThrottles();
76
+ const calls = captureSubprocessArgs(client);
77
+
78
+ try {
79
+ await client.updateStrict();
80
+ await assert.rejects(
81
+ () => client.updateStrict(),
82
+ /QMD update skipped by min-interval gate|QMD update skipped by global min-interval gate/,
83
+ );
84
+ } finally {
85
+ client.resetUpdateThrottles();
86
+ }
87
+
88
+ assert.equal(calls.length, 1);
89
+ });
90
+
91
+ test("embedCollectionStrict rejects QMD embed subprocess failures", async () => {
92
+ const client = new QmdClient("memories", 3, { updateMinIntervalMs: 60_000 });
93
+ client.resetUpdateThrottles();
94
+ const internals = client as unknown as SubprocessInternals;
95
+ const calls: string[][] = [];
96
+ internals.available = true;
97
+ internals.runQmdCommand = async (args: string[]) => {
98
+ calls.push(args);
99
+ throw new Error("embed subprocess failed");
100
+ };
101
+
102
+ try {
103
+ await assert.rejects(
104
+ () => client.embedCollectionStrict("memories--project"),
105
+ /embed subprocess failed/,
106
+ );
107
+ } finally {
108
+ client.resetUpdateThrottles();
109
+ }
110
+
111
+ assert.deepEqual(calls, [["embed", "-c", "memories--project"]]);
112
+ });
113
+
114
+ test("embedCollectionStrict respects QMD embed min-interval throttles", async () => {
115
+ const client = new QmdClient("memories", 3, { updateMinIntervalMs: 60_000 });
116
+ client.resetUpdateThrottles();
117
+ const calls = captureSubprocessArgs(client);
118
+
119
+ try {
120
+ await client.embedCollectionStrict("memories--project");
121
+ await assert.rejects(
122
+ () => client.embedCollectionStrict("memories--project"),
123
+ /QMD embed skipped by per-collection min-interval gate/,
124
+ );
125
+ } finally {
126
+ client.resetUpdateThrottles();
127
+ }
128
+
129
+ assert.equal(calls.length, 1);
130
+ });
131
+
73
132
  test("ensureCollection treats cancelled auto-create as unknown", async () => {
74
133
  const client = new QmdClient("memories", 3, {});
75
134
  const internals = client as unknown as SubprocessInternals & {
package/src/qmd.ts CHANGED
@@ -1232,9 +1232,14 @@ export class QmdClient implements SearchBackend {
1232
1232
  resetUpdateThrottles(): void {
1233
1233
  this._lastUpdateFailAtMs = null;
1234
1234
  this.lastUpdateRunAtMs = null;
1235
+ this.lastEmbedFailAtMs = null;
1235
1236
  const gs = getGlobalQmdState();
1236
1237
  gs.lastGlobalUpdateRunAtMs = null;
1237
1238
  gs.lastGlobalUpdateFailAtMs = null;
1239
+ gs.lastGlobalEmbedRunAtMs = null;
1240
+ gs.lastGlobalEmbedFailAtMs = null;
1241
+ gs.lastEmbedByCollectionMs = {};
1242
+ gs.lastEmbedFailByCollectionMs = {};
1238
1243
  }
1239
1244
 
1240
1245
  private readonly updateTimeoutMs: number;
@@ -2493,6 +2498,14 @@ export class QmdClient implements SearchBackend {
2493
2498
  );
2494
2499
  }
2495
2500
 
2501
+ async updateStrict(execution?: SearchExecutionOptions): Promise<void> {
2502
+ await this.runUpdateForCollection(
2503
+ this.collection,
2504
+ { perCollectionThrottle: false, strict: true },
2505
+ execution?.signal,
2506
+ );
2507
+ }
2508
+
2496
2509
  async updateCollection(
2497
2510
  collection: string,
2498
2511
  execution?: SearchExecutionOptions,
@@ -2510,7 +2523,7 @@ export class QmdClient implements SearchBackend {
2510
2523
  ): Promise<void> {
2511
2524
  await this.runUpdateForCollection(
2512
2525
  collection,
2513
- { perCollectionThrottle: true, strict: true },
2526
+ { perCollectionThrottle: true, strict: true, force: true },
2514
2527
  execution?.signal,
2515
2528
  );
2516
2529
  }
@@ -2521,7 +2534,7 @@ export class QmdClient implements SearchBackend {
2521
2534
 
2522
2535
  private async runUpdateForCollection(
2523
2536
  collection: string,
2524
- options: { perCollectionThrottle: boolean; strict?: boolean },
2537
+ options: { perCollectionThrottle: boolean; strict?: boolean; force?: boolean },
2525
2538
  signal?: AbortSignal,
2526
2539
  ): Promise<void> {
2527
2540
  if (this.available === false) {
@@ -2539,12 +2552,13 @@ export class QmdClient implements SearchBackend {
2539
2552
  }
2540
2553
  const globalState = getGlobalQmdState();
2541
2554
  const now = Date.now();
2542
- if (!options.strict && options.perCollectionThrottle) {
2555
+ if (!options.force && options.perCollectionThrottle) {
2543
2556
  if (
2544
2557
  globalState.lastGlobalUpdateFailAtMs &&
2545
2558
  now - globalState.lastGlobalUpdateFailAtMs < QMD_UPDATE_BACKOFF_MS
2546
2559
  ) {
2547
2560
  log.debug("QMD update: suppressed by global failure backoff");
2561
+ if (options.strict) throw new Error("QMD update skipped by global failure backoff");
2548
2562
  return;
2549
2563
  }
2550
2564
  const lastCollectionRun = globalState.lastUpdateByCollectionMs[name];
@@ -2553,6 +2567,7 @@ export class QmdClient implements SearchBackend {
2553
2567
  now - lastCollectionRun < this.updateMinIntervalMs
2554
2568
  ) {
2555
2569
  log.debug(`QMD update: suppressed by per-collection min-interval gate (${name})`);
2570
+ if (options.strict) throw new Error("QMD update skipped by per-collection min-interval gate");
2556
2571
  return;
2557
2572
  }
2558
2573
  const lastCollectionFail = globalState.lastUpdateFailByCollectionMs[name];
@@ -2561,14 +2576,16 @@ export class QmdClient implements SearchBackend {
2561
2576
  now - lastCollectionFail < QMD_UPDATE_BACKOFF_MS
2562
2577
  ) {
2563
2578
  log.debug(`QMD update: suppressed by per-collection failure backoff (${name})`);
2579
+ if (options.strict) throw new Error("QMD update skipped by per-collection failure backoff");
2564
2580
  return;
2565
2581
  }
2566
- } else if (!options.strict) {
2582
+ } else if (!options.force) {
2567
2583
  if (
2568
2584
  this.lastUpdateRunAtMs &&
2569
2585
  now - this.lastUpdateRunAtMs < this.updateMinIntervalMs
2570
2586
  ) {
2571
2587
  log.debug("QMD update: suppressed due to min-interval gate");
2588
+ if (options.strict) throw new Error("QMD update skipped by min-interval gate");
2572
2589
  return;
2573
2590
  }
2574
2591
  if (
@@ -2576,6 +2593,7 @@ export class QmdClient implements SearchBackend {
2576
2593
  now - this._lastUpdateFailAtMs < QMD_UPDATE_BACKOFF_MS
2577
2594
  ) {
2578
2595
  log.debug("QMD update: suppressed due to recent failures (backoff)");
2596
+ if (options.strict) throw new Error("QMD update skipped by recent failure backoff");
2579
2597
  return;
2580
2598
  }
2581
2599
  if (
@@ -2583,6 +2601,7 @@ export class QmdClient implements SearchBackend {
2583
2601
  now - globalState.lastGlobalUpdateRunAtMs < this.updateMinIntervalMs
2584
2602
  ) {
2585
2603
  log.debug("QMD update: suppressed by global min-interval gate");
2604
+ if (options.strict) throw new Error("QMD update skipped by global min-interval gate");
2586
2605
  return;
2587
2606
  }
2588
2607
  if (
@@ -2590,6 +2609,7 @@ export class QmdClient implements SearchBackend {
2590
2609
  now - globalState.lastGlobalUpdateFailAtMs < QMD_UPDATE_BACKOFF_MS
2591
2610
  ) {
2592
2611
  log.debug("QMD update: suppressed by global failure backoff");
2612
+ if (options.strict) throw new Error("QMD update skipped by global failure backoff");
2593
2613
  return;
2594
2614
  }
2595
2615
  }
@@ -2633,117 +2653,137 @@ export class QmdClient implements SearchBackend {
2633
2653
  }
2634
2654
 
2635
2655
  async embed(): Promise<void> {
2636
- if (this.available === false) return;
2637
- const globalState = getGlobalQmdState();
2638
- if (
2639
- this.lastEmbedFailAtMs &&
2640
- Date.now() - this.lastEmbedFailAtMs < QMD_EMBED_BACKOFF_MS
2641
- ) {
2642
- log.debug("QMD embed: suppressed due to recent failures (backoff)");
2656
+ await this.runEmbedForCollection(this.collection, { perCollectionThrottle: false });
2657
+ }
2658
+
2659
+ async embedStrict(): Promise<void> {
2660
+ await this.runEmbedForCollection(this.collection, { perCollectionThrottle: false, strict: true });
2661
+ }
2662
+
2663
+ async embedCollection(collection: string): Promise<void> {
2664
+ await this.runEmbedForCollection(collection, { perCollectionThrottle: true });
2665
+ }
2666
+
2667
+ async embedCollectionStrict(collection: string): Promise<void> {
2668
+ await this.runEmbedForCollection(collection, { perCollectionThrottle: true, strict: true });
2669
+ }
2670
+
2671
+ private async runEmbedForCollection(
2672
+ collection: string,
2673
+ options: { perCollectionThrottle: boolean; strict?: boolean },
2674
+ ): Promise<void> {
2675
+ if (this.available === false) {
2676
+ if (options.strict) throw new Error("QMD unavailable");
2643
2677
  return;
2644
2678
  }
2645
- if (
2646
- globalState.lastGlobalEmbedRunAtMs &&
2647
- Date.now() - globalState.lastGlobalEmbedRunAtMs < this.updateMinIntervalMs
2648
- ) {
2649
- log.debug("QMD embed: suppressed by global min-interval gate");
2679
+ const name = collection.trim();
2680
+ if (!name) {
2681
+ if (options.strict) throw new Error("QMD collection name is required");
2650
2682
  return;
2651
2683
  }
2652
- if (
2653
- globalState.lastGlobalEmbedFailAtMs &&
2654
- Date.now() - globalState.lastGlobalEmbedFailAtMs < QMD_EMBED_BACKOFF_MS
2655
- ) {
2656
- log.debug("QMD embed: suppressed by global failure backoff");
2657
- return;
2684
+ const globalState = getGlobalQmdState();
2685
+ const now = Date.now();
2686
+ if (options.perCollectionThrottle) {
2687
+ if (
2688
+ globalState.lastGlobalEmbedFailAtMs &&
2689
+ now - globalState.lastGlobalEmbedFailAtMs < QMD_EMBED_BACKOFF_MS
2690
+ ) {
2691
+ log.debug(`QMD embed: suppressed by global failure backoff (${name})`);
2692
+ if (options.strict) throw new Error("QMD embed skipped by global failure backoff");
2693
+ return;
2694
+ }
2695
+ const lastCollectionRun = globalState.lastEmbedByCollectionMs[name];
2696
+ if (
2697
+ Number.isFinite(lastCollectionRun) &&
2698
+ now - lastCollectionRun < this.updateMinIntervalMs
2699
+ ) {
2700
+ log.debug(`QMD embed: suppressed by per-collection min-interval gate (${name})`);
2701
+ if (options.strict) throw new Error("QMD embed skipped by per-collection min-interval gate");
2702
+ return;
2703
+ }
2704
+ const lastCollectionFail = globalState.lastEmbedFailByCollectionMs[name];
2705
+ if (
2706
+ Number.isFinite(lastCollectionFail) &&
2707
+ now - lastCollectionFail < QMD_EMBED_BACKOFF_MS
2708
+ ) {
2709
+ log.debug(`QMD embed: suppressed by per-collection failure backoff (${name})`);
2710
+ if (options.strict) throw new Error("QMD embed skipped by per-collection failure backoff");
2711
+ return;
2712
+ }
2713
+ } else {
2714
+ if (
2715
+ this.lastEmbedFailAtMs &&
2716
+ now - this.lastEmbedFailAtMs < QMD_EMBED_BACKOFF_MS
2717
+ ) {
2718
+ log.debug("QMD embed: suppressed due to recent failures (backoff)");
2719
+ if (options.strict) throw new Error("QMD embed skipped by recent failure backoff");
2720
+ return;
2721
+ }
2722
+ if (
2723
+ globalState.lastGlobalEmbedRunAtMs &&
2724
+ now - globalState.lastGlobalEmbedRunAtMs < this.updateMinIntervalMs
2725
+ ) {
2726
+ log.debug("QMD embed: suppressed by global min-interval gate");
2727
+ if (options.strict) throw new Error("QMD embed skipped by global min-interval gate");
2728
+ return;
2729
+ }
2730
+ if (
2731
+ globalState.lastGlobalEmbedFailAtMs &&
2732
+ now - globalState.lastGlobalEmbedFailAtMs < QMD_EMBED_BACKOFF_MS
2733
+ ) {
2734
+ log.debug("QMD embed: suppressed by global failure backoff");
2735
+ if (options.strict) throw new Error("QMD embed skipped by global failure backoff");
2736
+ return;
2737
+ }
2658
2738
  }
2659
2739
  try {
2660
2740
  const startedAtMs = Date.now();
2661
- await this.runQmdCommand(this.buildEmbedArgs(this.collection), 300_000);
2741
+ await this.runQmdCommand(this.buildEmbedArgs(name), 300_000);
2662
2742
  const durationMs = Date.now() - startedAtMs;
2663
2743
  if (this.slowLog?.enabled && durationMs >= this.slowLog.thresholdMs) {
2664
2744
  log.warn(`SLOW QMD embed: durationMs=${durationMs}`);
2665
2745
  }
2666
- globalState.lastGlobalEmbedRunAtMs = Date.now();
2667
- log.debug("QMD embed completed");
2668
- } catch (err) {
2669
- if (isVectorDimensionMismatchError(err)) {
2670
- try {
2671
- log.warn("QMD embed hit a vector dimension mismatch; retrying with force re-embed");
2672
- await this.runQmdCommand(this.buildEmbedArgs(this.collection, true), 300_000);
2673
- globalState.lastGlobalEmbedRunAtMs = Date.now();
2674
- this.lastEmbedFailAtMs = null;
2675
- globalState.lastGlobalEmbedFailAtMs = null;
2676
- log.warn("QMD embed recovered by forcing a full vector rebuild");
2677
- return;
2678
- } catch (retryErr) {
2679
- const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
2680
- log.warn(`QMD force re-embed failed after dimension mismatch: ${retryMsg}`);
2681
- }
2682
- }
2683
- const now = Date.now();
2684
- this.lastEmbedFailAtMs = now;
2685
- globalState.lastGlobalEmbedFailAtMs = now;
2686
- const msg = err instanceof Error ? err.message : String(err);
2687
- log.warn(`QMD embed failed: ${msg}`);
2688
- }
2689
- }
2690
-
2691
- async embedCollection(collection: string): Promise<void> {
2692
- if (this.available === false) return;
2693
- const name = collection.trim();
2694
- if (!name) return;
2695
- const globalState = getGlobalQmdState();
2696
- const now = Date.now();
2697
- if (
2698
- globalState.lastGlobalEmbedFailAtMs &&
2699
- now - globalState.lastGlobalEmbedFailAtMs < QMD_EMBED_BACKOFF_MS
2700
- ) {
2701
- log.debug(`QMD embed: suppressed by global failure backoff (${name})`);
2702
- return;
2703
- }
2704
- const lastCollectionRun = globalState.lastEmbedByCollectionMs[name];
2705
- if (
2706
- Number.isFinite(lastCollectionRun) &&
2707
- now - lastCollectionRun < this.updateMinIntervalMs
2708
- ) {
2709
- log.debug(`QMD embed: suppressed by per-collection min-interval gate (${name})`);
2710
- return;
2711
- }
2712
- const lastCollectionFail = globalState.lastEmbedFailByCollectionMs[name];
2713
- if (
2714
- Number.isFinite(lastCollectionFail) &&
2715
- now - lastCollectionFail < QMD_EMBED_BACKOFF_MS
2716
- ) {
2717
- log.debug(`QMD embed: suppressed by per-collection failure backoff (${name})`);
2718
- return;
2719
- }
2720
- try {
2721
- await this.runQmdCommand(this.buildEmbedArgs(name), 300_000);
2722
2746
  const at = Date.now();
2723
- globalState.lastEmbedByCollectionMs[name] = at;
2747
+ if (options.perCollectionThrottle) {
2748
+ globalState.lastEmbedByCollectionMs[name] = at;
2749
+ }
2724
2750
  globalState.lastGlobalEmbedRunAtMs = at;
2751
+ log.debug(`QMD embed completed for collection=${name}`);
2725
2752
  } catch (err) {
2753
+ let failure: unknown = err;
2726
2754
  if (isVectorDimensionMismatchError(err)) {
2727
2755
  try {
2728
2756
  log.warn(`QMD embed for collection ${name} hit a vector dimension mismatch; retrying with force re-embed`);
2729
2757
  await this.runQmdCommand(this.buildEmbedArgs(name, true), 300_000);
2730
2758
  const recoveredAt = Date.now();
2731
- globalState.lastEmbedByCollectionMs[name] = recoveredAt;
2759
+ if (options.perCollectionThrottle) {
2760
+ globalState.lastEmbedByCollectionMs[name] = recoveredAt;
2761
+ delete globalState.lastEmbedFailByCollectionMs[name];
2762
+ } else {
2763
+ this.lastEmbedFailAtMs = null;
2764
+ }
2732
2765
  globalState.lastGlobalEmbedRunAtMs = recoveredAt;
2733
- delete globalState.lastEmbedFailByCollectionMs[name];
2734
2766
  globalState.lastGlobalEmbedFailAtMs = null;
2735
2767
  log.warn(`QMD embed for collection ${name} recovered by forcing a full vector rebuild`);
2736
2768
  return;
2737
2769
  } catch (retryErr) {
2770
+ failure = retryErr;
2738
2771
  const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
2739
2772
  log.warn(`QMD force re-embed failed for collection ${name}: ${retryMsg}`);
2740
2773
  }
2741
2774
  }
2742
2775
  const at = Date.now();
2743
- globalState.lastEmbedFailByCollectionMs[name] = at;
2776
+ if (options.perCollectionThrottle) {
2777
+ globalState.lastEmbedFailByCollectionMs[name] = at;
2778
+ } else {
2779
+ this.lastEmbedFailAtMs = at;
2780
+ }
2744
2781
  globalState.lastGlobalEmbedFailAtMs = at;
2745
- const msg = err instanceof Error ? err.message : String(err);
2782
+ const msg = failure instanceof Error ? failure.message : String(failure);
2746
2783
  log.warn(`QMD embed failed for collection ${name}: ${msg}`);
2784
+ if (options.strict) {
2785
+ throw failure;
2786
+ }
2747
2787
  }
2748
2788
  }
2749
2789
 
@@ -81,6 +81,12 @@ export interface SearchBackend {
81
81
 
82
82
  // ── Maintenance ──
83
83
  update(execution?: SearchExecutionOptions): Promise<void>;
84
+ /**
85
+ * Optional strict refresh used by callers that must know whether the backend
86
+ * was actually refreshed before writing success markers. Ordinary update
87
+ * calls remain fail-open for migration/maintenance resilience.
88
+ */
89
+ updateStrict?(execution?: SearchExecutionOptions): Promise<void>;
84
90
  updateCollection(collection: string, execution?: SearchExecutionOptions): Promise<void>;
85
91
  updateCollectionFromDir?(collection: string, memoryDir: string, execution?: SearchExecutionOptions): Promise<void>;
86
92
  /**
@@ -96,7 +102,17 @@ export interface SearchBackend {
96
102
  */
97
103
  updateCollectionStrict?(collection: string, execution?: SearchExecutionOptions): Promise<void>;
98
104
  embed(): Promise<void>;
105
+ /**
106
+ * Optional strict embed used by callers that must know vectors were actually
107
+ * refreshed before writing success markers.
108
+ */
109
+ embedStrict?(): Promise<void>;
99
110
  embedCollection(collection: string): Promise<void>;
111
+ /**
112
+ * Optional strict collection embed used by callers that must know vectors were
113
+ * actually refreshed before writing success markers.
114
+ */
115
+ embedCollectionStrict?(collection: string): Promise<void>;
100
116
 
101
117
  // ── Collection management ──
102
118
  /**
package/src/types.ts CHANGED
@@ -1216,6 +1216,18 @@ export interface PluginConfig {
1216
1216
  // QMD maintenance (debounced singleflight)
1217
1217
  qmdMaintenanceEnabled: boolean;
1218
1218
  qmdMaintenanceDebounceMs: number;
1219
+ /**
1220
+ * Namespace-aware maintenance fanout (issue #1500). When namespaces are
1221
+ * enabled, background maintenance jobs use the rebuildable namespace catalog
1222
+ * to discover dynamic project/team namespaces rather than only processing the
1223
+ * configured default/shared/policy set.
1224
+ */
1225
+ maintenanceNamespaceFanoutEnabled: boolean;
1226
+ maintenanceMaxNamespacesPerCycle: number;
1227
+ maintenanceIncludeProjectNamespaces: boolean;
1228
+ maintenanceIncludeBranchNamespaces: boolean;
1229
+ maintenanceIncludeTeamProjectNamespaces: boolean;
1230
+ maintenanceNamespaceLockStaleMs: number;
1219
1231
  qmdAutoEmbedEnabled: boolean;
1220
1232
  qmdEmbedMinIntervalMs: number;
1221
1233
  qmdUpdateTimeoutMs: number;