@remnic/core 9.3.654 → 9.3.656

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/dist/access-cli.js +29 -29
  2. package/dist/access-http.d.ts +4 -4
  3. package/dist/access-http.js +17 -17
  4. package/dist/access-mcp.d.ts +4 -4
  5. package/dist/access-mcp.js +16 -16
  6. package/dist/access-schema.d.ts +10 -10
  7. package/dist/{access-service-C8A5hoXJ.d.ts → access-service-D_nbpexW.d.ts} +33 -2
  8. package/dist/access-service.d.ts +4 -4
  9. package/dist/access-service.js +15 -15
  10. package/dist/action-confidence.d.ts +1 -1
  11. package/dist/active-memory-bridge.d.ts +1 -1
  12. package/dist/active-recall.d.ts +1 -1
  13. package/dist/active-recall.js +1 -1
  14. package/dist/behavior-learner.d.ts +1 -1
  15. package/dist/behavior-signals.d.ts +1 -1
  16. package/dist/bootstrap.d.ts +3 -3
  17. package/dist/briefing.d.ts +1 -1
  18. package/dist/briefing.js +3 -3
  19. package/dist/buffer-surprise-report.d.ts +1 -1
  20. package/dist/buffer.d.ts +1 -1
  21. package/dist/calibration.d.ts +1 -1
  22. package/dist/causal-behavior.d.ts +1 -1
  23. package/dist/causal-consolidation.d.ts +1 -1
  24. package/dist/causal-consolidation.js +4 -4
  25. package/dist/{chunk-JMQSYGXS.js → chunk-2BD7DG37.js} +2 -2
  26. package/dist/{chunk-FVRBLJP6.js → chunk-2MXEVL75.js} +2 -2
  27. package/dist/{chunk-LJCEWTG3.js → chunk-4UL7VPTD.js} +277 -58
  28. package/dist/chunk-4UL7VPTD.js.map +1 -0
  29. package/dist/{chunk-JYN7QNTA.js → chunk-54XF2FY7.js} +17 -17
  30. package/dist/{chunk-7WEB3FLJ.js → chunk-5PLUC5OB.js} +2 -2
  31. package/dist/{chunk-JX2RINDR.js → chunk-6G5JEN55.js} +2 -2
  32. package/dist/{chunk-ZCORQM74.js → chunk-AGJKWOKV.js} +2 -2
  33. package/dist/{chunk-NE2JBMLN.js → chunk-AZBV4RRY.js} +1 -1
  34. package/dist/chunk-AZBV4RRY.js.map +1 -0
  35. package/dist/{chunk-YLZLPVKK.js → chunk-CTAV55JM.js} +344 -1
  36. package/dist/chunk-CTAV55JM.js.map +1 -0
  37. package/dist/{chunk-2DSTAWNZ.js → chunk-DIBWFCLA.js} +3 -3
  38. package/dist/{chunk-NAZWHTYV.js → chunk-DR67OK4E.js} +5 -5
  39. package/dist/{chunk-XBIACVCO.js → chunk-EC2AYKRX.js} +2 -2
  40. package/dist/{chunk-JVRPJ7D4.js → chunk-EKQMQQ3U.js} +48 -12
  41. package/dist/chunk-EKQMQQ3U.js.map +1 -0
  42. package/dist/{chunk-RGPUQ66K.js → chunk-GCYFUTUC.js} +2 -2
  43. package/dist/{chunk-JBHXMCYN.js → chunk-GRYAECRV.js} +2 -2
  44. package/dist/{chunk-BJA6DQOC.js → chunk-GSHW5VVD.js} +5 -5
  45. package/dist/chunk-GYSYLGNE.js +650 -0
  46. package/dist/chunk-GYSYLGNE.js.map +1 -0
  47. package/dist/{chunk-NCGWXCSW.js → chunk-IOZ5WBWD.js} +2 -2
  48. package/dist/{chunk-QKK64Z6M.js → chunk-JSVFEHLL.js} +7 -5
  49. package/dist/chunk-JSVFEHLL.js.map +1 -0
  50. package/dist/{chunk-7LWRCOP7.js → chunk-LZTFCAKE.js} +2 -2
  51. package/dist/{chunk-2DGQLOOM.js → chunk-M3VYPE2H.js} +1 -1
  52. package/dist/{chunk-2DGQLOOM.js.map → chunk-M3VYPE2H.js.map} +1 -1
  53. package/dist/{chunk-6CVI6BP6.js → chunk-NXCK7DO7.js} +2 -2
  54. package/dist/{chunk-Z5MQI7K2.js → chunk-PEPHBH2W.js} +2 -2
  55. package/dist/{chunk-PYWNNF2I.js → chunk-QRSKPI62.js} +99 -66
  56. package/dist/chunk-QRSKPI62.js.map +1 -0
  57. package/dist/{chunk-XWQ6ERUG.js → chunk-QZRKNA5F.js} +2 -2
  58. package/dist/{chunk-PS3SYNHP.js → chunk-R5DB26G6.js} +2 -2
  59. package/dist/{chunk-OL2364SB.js → chunk-RDW5G6DO.js} +1502 -335
  60. package/dist/chunk-RDW5G6DO.js.map +1 -0
  61. package/dist/{chunk-YM3LR4LS.js → chunk-SSSXWIBP.js} +5 -5
  62. package/dist/{chunk-T2C6QJG2.js → chunk-SWDHVH2P.js} +2 -2
  63. package/dist/{chunk-DBM2BD22.js → chunk-SXYCVRLK.js} +3 -3
  64. package/dist/{chunk-K6X553JB.js → chunk-TFFZUFEP.js} +7 -5
  65. package/dist/chunk-TFFZUFEP.js.map +1 -0
  66. package/dist/{chunk-ENV6RDTD.js → chunk-TIJYQXDI.js} +2 -2
  67. package/dist/{chunk-BP2EV6W5.js → chunk-VAEAGTEQ.js} +4 -4
  68. package/dist/{chunk-3RACUBII.js → chunk-WIKMCJUR.js} +2 -2
  69. package/dist/{chunk-QW6JZO5P.js → chunk-WWMHAMAY.js} +2 -2
  70. package/dist/{chunk-GPW2E4LN.js → chunk-YEZHZCUO.js} +4 -4
  71. package/dist/{chunk-5FOCXX5E.js → chunk-YVVQUAOO.js} +3 -3
  72. package/dist/{chunk-5FOCXX5E.js.map → chunk-YVVQUAOO.js.map} +1 -1
  73. package/dist/{chunk-3XGWCZ63.js → chunk-YXLT4EMM.js} +2 -2
  74. package/dist/{chunk-Y2RIIF6H.js → chunk-Z6UDTNY6.js} +2 -2
  75. package/dist/{cli-uQgvDFNE.d.ts → cli-aYxSuPvP.d.ts} +3 -3
  76. package/dist/cli.d.ts +5 -5
  77. package/dist/cli.js +29 -29
  78. package/dist/compounding/engine.d.ts +1 -1
  79. package/dist/compounding/engine.js +3 -3
  80. package/dist/compounding/preference-consolidator.d.ts +1 -1
  81. package/dist/compression-optimizer.d.ts +1 -1
  82. package/dist/config.d.ts +1 -1
  83. package/dist/config.js +1 -1
  84. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  85. package/dist/connectors/codex-materialize-runner.js +3 -3
  86. package/dist/connectors/codex-materialize.d.ts +1 -1
  87. package/dist/connectors/index.d.ts +1 -1
  88. package/dist/connectors/index.js +3 -3
  89. package/dist/consolidation-provenance-check.d.ts +1 -1
  90. package/dist/consolidation-undo.d.ts +1 -1
  91. package/dist/contradiction/index.d.ts +1 -1
  92. package/dist/conversation-index/backend.d.ts +1 -1
  93. package/dist/conversation-index/chunker.d.ts +1 -1
  94. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  95. package/dist/conversation-index/indexer.d.ts +1 -1
  96. package/dist/conversation-index/search.d.ts +1 -1
  97. package/dist/day-summary.d.ts +1 -1
  98. package/dist/delinearize.d.ts +1 -1
  99. package/dist/direct-answer-wiring.d.ts +1 -1
  100. package/dist/direct-answer.d.ts +1 -1
  101. package/dist/embedding-fallback.d.ts +1 -1
  102. package/dist/enrichment/index.d.ts +1 -1
  103. package/dist/entity-retrieval.d.ts +1 -1
  104. package/dist/entity-retrieval.js +3 -3
  105. package/dist/entity-schema.d.ts +1 -1
  106. package/dist/explicit-capture.d.ts +3 -3
  107. package/dist/explicit-cue-recall.js +2 -2
  108. package/dist/extraction-judge-telemetry.d.ts +1 -1
  109. package/dist/extraction-judge-training.d.ts +1 -1
  110. package/dist/extraction-judge.d.ts +1 -1
  111. package/dist/extraction.d.ts +1 -1
  112. package/dist/fallback-llm.d.ts +1 -1
  113. package/dist/focused-list-recall.js +2 -2
  114. package/dist/identity-continuity.d.ts +1 -1
  115. package/dist/importance.d.ts +1 -1
  116. package/dist/index.d.ts +121 -121
  117. package/dist/index.js +39 -39
  118. package/dist/intent.d.ts +1 -1
  119. package/dist/lcm/engine.d.ts +1 -1
  120. package/dist/lcm/index.d.ts +1 -1
  121. package/dist/lcm/tools.d.ts +1 -1
  122. package/dist/lcm-fallback-read.js +1 -1
  123. package/dist/lifecycle.d.ts +1 -1
  124. package/dist/live-connectors-runner.d.ts +1 -1
  125. package/dist/local-llm.d.ts +1 -1
  126. package/dist/maintenance/memory-governance.d.ts +1 -1
  127. package/dist/maintenance/memory-governance.js +3 -3
  128. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  129. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  130. package/dist/mcp-memory-inspector-app.d.ts +4 -4
  131. package/dist/memory-action-policy.d.ts +1 -1
  132. package/dist/memory-cache.d.ts +1 -1
  133. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  134. package/dist/memory-projection-store.d.ts +1 -1
  135. package/dist/memory-provenance.d.ts +1 -1
  136. package/dist/memory-worth-outcomes.d.ts +1 -1
  137. package/dist/models-json.d.ts +1 -1
  138. package/dist/namespaces/migrate.d.ts +1 -1
  139. package/dist/namespaces/migrate.js +11 -11
  140. package/dist/namespaces/principal.d.ts +1 -1
  141. package/dist/namespaces/search.d.ts +15 -4
  142. package/dist/namespaces/search.js +7 -7
  143. package/dist/namespaces/storage.d.ts +1 -1
  144. package/dist/namespaces/storage.js +3 -3
  145. package/dist/native-knowledge.d.ts +1 -1
  146. package/dist/operator-toolkit.d.ts +1 -1
  147. package/dist/operator-toolkit.js +14 -14
  148. package/dist/{orchestrator-B4Y4sWQH.d.ts → orchestrator-D1wcmPNj.d.ts} +17 -14
  149. package/dist/orchestrator.d.ts +3 -3
  150. package/dist/orchestrator.js +25 -25
  151. package/dist/patterns-cli.d.ts +1 -1
  152. package/dist/policy-runtime.d.ts +1 -1
  153. package/dist/qmd-recall-cache.d.ts +1 -1
  154. package/dist/qmd.d.ts +5 -1
  155. package/dist/qmd.js +2 -2
  156. package/dist/recall-disclosure-escalation.d.ts +1 -1
  157. package/dist/recall-explain-renderer.d.ts +1 -1
  158. package/dist/recall-explain-renderer.js +3 -3
  159. package/dist/recall-planner-llm.d.ts +1 -1
  160. package/dist/recall-state.d.ts +1 -1
  161. package/dist/recall-tag-filter.d.ts +1 -1
  162. package/dist/recall-xray-cli.d.ts +1 -1
  163. package/dist/recall-xray-cli.js +4 -4
  164. package/dist/recall-xray-renderer.d.ts +1 -1
  165. package/dist/recall-xray-renderer.js +3 -3
  166. package/dist/recall-xray.d.ts +1 -1
  167. package/dist/recall-xray.js +2 -2
  168. package/dist/resolve-auth-token.d.ts +1 -1
  169. package/dist/response-guidance-recall.js +2 -2
  170. package/dist/resume-bundles.js +2 -2
  171. package/dist/retrieval-agents.d.ts +1 -1
  172. package/dist/retrieval-tiers.d.ts +1 -1
  173. package/dist/routing/engine.d.ts +1 -1
  174. package/dist/routing/store.d.ts +1 -1
  175. package/dist/schemas.d.ts +22 -22
  176. package/dist/search/embed-helper.d.ts +1 -1
  177. package/dist/search/factory.d.ts +1 -1
  178. package/dist/search/factory.js +6 -6
  179. package/dist/search/index.d.ts +1 -1
  180. package/dist/search/index.js +6 -6
  181. package/dist/search/lancedb-backend.d.ts +1 -1
  182. package/dist/search/lancedb-backend.js +2 -2
  183. package/dist/search/meilisearch-backend.d.ts +1 -1
  184. package/dist/search/meilisearch-backend.js +2 -2
  185. package/dist/search/noop-backend.d.ts +1 -1
  186. package/dist/search/orama-backend.d.ts +1 -1
  187. package/dist/search/orama-backend.js +2 -2
  188. package/dist/search/port.d.ts +17 -1
  189. package/dist/search/port.js +1 -1
  190. package/dist/search/remote-backend.d.ts +1 -1
  191. package/dist/{semantic-consolidation-BKd0Pype.d.ts → semantic-consolidation-MWOdNtSE.d.ts} +1 -1
  192. package/dist/semantic-consolidation.d.ts +2 -2
  193. package/dist/semantic-consolidation.js +4 -4
  194. package/dist/semantic-rule-promotion.js +3 -3
  195. package/dist/semantic-rule-verifier.d.ts +3 -2
  196. package/dist/semantic-rule-verifier.js +5 -3
  197. package/dist/session-observer-bands.d.ts +1 -1
  198. package/dist/session-observer-state.d.ts +1 -1
  199. package/dist/shared-context/manager.d.ts +1 -1
  200. package/dist/signal.d.ts +1 -1
  201. package/dist/storage.d.ts +1 -1
  202. package/dist/storage.js +2 -2
  203. package/dist/summarizer.d.ts +1 -1
  204. package/dist/summary-snapshot.d.ts +1 -1
  205. package/dist/targeted-fact-recall.js +2 -2
  206. package/dist/temporal-supersession.d.ts +1 -1
  207. package/dist/temporal-validity.d.ts +1 -1
  208. package/dist/threading.d.ts +1 -1
  209. package/dist/tier-migration.d.ts +1 -1
  210. package/dist/tier-routing.d.ts +1 -1
  211. package/dist/topics.d.ts +1 -1
  212. package/dist/transcript.d.ts +1 -1
  213. package/dist/transfer/types.d.ts +12 -12
  214. package/dist/{types-BgChEr0M.d.ts → types-CgcCpUrf.d.ts} +51 -1
  215. package/dist/types.d.ts +1 -1
  216. package/dist/types.js +1 -1
  217. package/dist/utility-runtime.d.ts +1 -1
  218. package/dist/verified-recall.d.ts +2 -1
  219. package/dist/verified-recall.js +5 -3
  220. package/package.json +1 -1
  221. package/src/access-service-observe-lcm-parity.test.ts +86 -1
  222. package/src/access-service-observe-scope.test.ts +283 -1
  223. package/src/access-service-raw-excerpt-read-gate.test.ts +53 -0
  224. package/src/access-service.ts +391 -93
  225. package/src/coding/coding-namespace.ts +0 -3
  226. package/src/config.test.ts +69 -0
  227. package/src/config.ts +417 -0
  228. package/src/lcm-fallback-read.ts +2 -6
  229. package/src/maintenance/namespace-planner.test.ts +1120 -0
  230. package/src/maintenance/namespace-planner.ts +893 -0
  231. package/src/namespaces/scope-profiles.test.ts +1074 -0
  232. package/src/namespaces/scope-profiles.ts +456 -0
  233. package/src/namespaces/search.test.ts +130 -2
  234. package/src/namespaces/search.ts +71 -10
  235. package/src/orchestrator-flush.test.ts +606 -44
  236. package/src/orchestrator-source-attribution.test.ts +73 -0
  237. package/src/orchestrator.ts +932 -229
  238. package/src/qmd-client.test.ts +59 -0
  239. package/src/qmd.ts +124 -84
  240. package/src/search/port.ts +16 -0
  241. package/src/semantic-rule-verifier.ts +13 -6
  242. package/src/types.ts +64 -0
  243. package/src/verified-recall.ts +10 -6
  244. package/dist/chunk-JVRPJ7D4.js.map +0 -1
  245. package/dist/chunk-K6X553JB.js.map +0 -1
  246. package/dist/chunk-LJCEWTG3.js.map +0 -1
  247. package/dist/chunk-MMJANTJX.js +0 -339
  248. package/dist/chunk-MMJANTJX.js.map +0 -1
  249. package/dist/chunk-NE2JBMLN.js.map +0 -1
  250. package/dist/chunk-OL2364SB.js.map +0 -1
  251. package/dist/chunk-PYWNNF2I.js.map +0 -1
  252. package/dist/chunk-QKK64Z6M.js.map +0 -1
  253. package/dist/chunk-YLZLPVKK.js.map +0 -1
  254. /package/dist/{chunk-JMQSYGXS.js.map → chunk-2BD7DG37.js.map} +0 -0
  255. /package/dist/{chunk-FVRBLJP6.js.map → chunk-2MXEVL75.js.map} +0 -0
  256. /package/dist/{chunk-JYN7QNTA.js.map → chunk-54XF2FY7.js.map} +0 -0
  257. /package/dist/{chunk-7WEB3FLJ.js.map → chunk-5PLUC5OB.js.map} +0 -0
  258. /package/dist/{chunk-JX2RINDR.js.map → chunk-6G5JEN55.js.map} +0 -0
  259. /package/dist/{chunk-ZCORQM74.js.map → chunk-AGJKWOKV.js.map} +0 -0
  260. /package/dist/{chunk-2DSTAWNZ.js.map → chunk-DIBWFCLA.js.map} +0 -0
  261. /package/dist/{chunk-NAZWHTYV.js.map → chunk-DR67OK4E.js.map} +0 -0
  262. /package/dist/{chunk-XBIACVCO.js.map → chunk-EC2AYKRX.js.map} +0 -0
  263. /package/dist/{chunk-RGPUQ66K.js.map → chunk-GCYFUTUC.js.map} +0 -0
  264. /package/dist/{chunk-JBHXMCYN.js.map → chunk-GRYAECRV.js.map} +0 -0
  265. /package/dist/{chunk-BJA6DQOC.js.map → chunk-GSHW5VVD.js.map} +0 -0
  266. /package/dist/{chunk-NCGWXCSW.js.map → chunk-IOZ5WBWD.js.map} +0 -0
  267. /package/dist/{chunk-7LWRCOP7.js.map → chunk-LZTFCAKE.js.map} +0 -0
  268. /package/dist/{chunk-6CVI6BP6.js.map → chunk-NXCK7DO7.js.map} +0 -0
  269. /package/dist/{chunk-Z5MQI7K2.js.map → chunk-PEPHBH2W.js.map} +0 -0
  270. /package/dist/{chunk-XWQ6ERUG.js.map → chunk-QZRKNA5F.js.map} +0 -0
  271. /package/dist/{chunk-PS3SYNHP.js.map → chunk-R5DB26G6.js.map} +0 -0
  272. /package/dist/{chunk-YM3LR4LS.js.map → chunk-SSSXWIBP.js.map} +0 -0
  273. /package/dist/{chunk-T2C6QJG2.js.map → chunk-SWDHVH2P.js.map} +0 -0
  274. /package/dist/{chunk-DBM2BD22.js.map → chunk-SXYCVRLK.js.map} +0 -0
  275. /package/dist/{chunk-ENV6RDTD.js.map → chunk-TIJYQXDI.js.map} +0 -0
  276. /package/dist/{chunk-BP2EV6W5.js.map → chunk-VAEAGTEQ.js.map} +0 -0
  277. /package/dist/{chunk-3RACUBII.js.map → chunk-WIKMCJUR.js.map} +0 -0
  278. /package/dist/{chunk-QW6JZO5P.js.map → chunk-WWMHAMAY.js.map} +0 -0
  279. /package/dist/{chunk-GPW2E4LN.js.map → chunk-YEZHZCUO.js.map} +0 -0
  280. /package/dist/{chunk-3XGWCZ63.js.map → chunk-YXLT4EMM.js.map} +0 -0
  281. /package/dist/{chunk-Y2RIIF6H.js.map → chunk-Z6UDTNY6.js.map} +0 -0
@@ -8,9 +8,11 @@ import {
8
8
  Orchestrator,
9
9
  } from "./orchestrator.js";
10
10
  import { parseConfig } from "./config.js";
11
+ import { stableHash } from "./coding/git-context.js";
11
12
  import type { BufferTurn } from "./types.js";
12
13
  import type { ImportTurn } from "./bulk-import/types.js";
13
14
  import { namespaceIdentityToken } from "./namespaces/identity.js";
15
+ import { readNamespaceMaintenanceStatuses } from "./maintenance/namespace-planner.js";
14
16
 
15
17
  function makeTurn(sessionKey: string, content: string): BufferTurn {
16
18
  return {
@@ -634,6 +636,147 @@ test("flushSession drains every discovered buffer for the session", async () =>
634
636
  ]);
635
637
  });
636
638
 
639
+ test("runExtraction skips active scope profile writes when no layer is writable", async () => {
640
+ const config = parseConfig({
641
+ namespacesEnabled: true,
642
+ defaultNamespace: "default",
643
+ sharedNamespace: "shared",
644
+ defaultScopeProfile: "teamCoding",
645
+ codingMode: { projectScope: true },
646
+ principalFromSessionKeyMode: "prefix",
647
+ principalFromSessionKeyRules: [{ match: "pi-observer:", principal: "pi-observer" }],
648
+ namespacePolicies: [
649
+ { name: "pi-observer", readPrincipals: ["pi-observer"], writePrincipals: [] },
650
+ ],
651
+ scopeProfiles: {
652
+ teamCoding: {
653
+ readOrder: ["teamProject"],
654
+ writeDefault: "teamProject",
655
+ promotionTargets: ["teamProject"],
656
+ teamProject: { namespaceTemplate: "team-{teamId}-project-{projectHash}" },
657
+ },
658
+ },
659
+ teams: {
660
+ pi: {
661
+ principals: ["pi-observer"],
662
+ read: ["pi-observer"],
663
+ write: [],
664
+ promote: [],
665
+ },
666
+ },
667
+ });
668
+ config.extractionMinChars = 0;
669
+ config.extractionMinUserTurns = 1;
670
+
671
+ let clearCalls = 0;
672
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
673
+ orchestrator.config = config;
674
+ orchestrator.buffer = {
675
+ clearAfterExtraction: async () => {
676
+ clearCalls += 1;
677
+ },
678
+ };
679
+ orchestrator.getCodingContextForSession = () => ({
680
+ projectId: "tag:remnic",
681
+ branch: null,
682
+ rootPath: "tag:remnic",
683
+ defaultBranch: "main",
684
+ });
685
+ orchestrator.storageRouter = {
686
+ storageFor: async () => {
687
+ throw new Error("unauthorized profile write must not choose fallback storage");
688
+ },
689
+ };
690
+
691
+ const result = await orchestrator.runExtraction(
692
+ [makeTurn("pi-observer:abc123", "remember unauthorized profile target")],
693
+ { bufferKey: "pi-observer:abc123" },
694
+ );
695
+
696
+ assert.equal(result.status, "skipped");
697
+ assert.equal(result.reason, "scope_profile_no_writable_layer");
698
+ assert.equal(clearCalls, 1);
699
+ });
700
+
701
+ test("runExtraction writes buffered turns to active scope profile write layer", async () => {
702
+ const config = parseConfig({
703
+ namespacesEnabled: true,
704
+ defaultNamespace: "default",
705
+ sharedNamespace: "shared",
706
+ defaultScopeProfile: "teamCoding",
707
+ codingMode: { projectScope: true },
708
+ principalFromSessionKeyMode: "prefix",
709
+ principalFromSessionKeyRules: [{ match: "pi-observer:", principal: "pi-observer" }],
710
+ namespacePolicies: [
711
+ { name: "pi-observer", readPrincipals: ["pi-observer"], writePrincipals: ["pi-observer"] },
712
+ ],
713
+ scopeProfiles: {
714
+ teamCoding: {
715
+ readOrder: ["teamProject"],
716
+ writeDefault: "teamProject",
717
+ promotionTargets: ["teamProject"],
718
+ teamProject: { namespaceTemplate: "team-{teamId}-project-{projectHash}" },
719
+ },
720
+ },
721
+ teams: {
722
+ pi: {
723
+ principals: ["pi-observer"],
724
+ read: ["pi-observer"],
725
+ write: ["pi-observer"],
726
+ promote: ["pi-observer"],
727
+ },
728
+ },
729
+ });
730
+ config.extractionMinChars = 0;
731
+ config.extractionMinUserTurns = 1;
732
+
733
+ const turn = {
734
+ ...makeTurn("pi-observer:abc123", "remember the team profile target"),
735
+ persistProcessedFingerprint: true,
736
+ };
737
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
738
+ orchestrator.config = config;
739
+ orchestrator.buffer = { clearAfterExtraction: async () => undefined };
740
+ orchestrator.getCodingContextForSession = () => ({
741
+ projectId: "tag:remnic",
742
+ branch: null,
743
+ rootPath: "tag:remnic",
744
+ defaultBranch: "main",
745
+ });
746
+ let requestedNamespace: string | undefined;
747
+ orchestrator.storageRouter = {
748
+ storageFor: async (namespace: string) => {
749
+ requestedNamespace = namespace;
750
+ return {
751
+ listEntityNames: async () => [],
752
+ loadMeta: async () => ({
753
+ extractionCount: 0,
754
+ lastExtractionAt: null,
755
+ lastConsolidationAt: null,
756
+ totalMemories: 0,
757
+ totalEntities: 0,
758
+ processedExtractionFingerprints: [
759
+ {
760
+ fingerprint: orchestrator.buildExtractionFingerprint([turn], "pi-observer:abc123"),
761
+ observedAt: "2026-04-15T00:00:00.000Z",
762
+ },
763
+ ],
764
+ }),
765
+ saveMeta: async () => undefined,
766
+ };
767
+ },
768
+ };
769
+ orchestrator.extraction = {
770
+ extract: async () => {
771
+ throw new Error("extraction should be skipped by processed fingerprint");
772
+ },
773
+ };
774
+
775
+ await orchestrator.runExtraction([turn], { bufferKey: "pi-observer:abc123" });
776
+
777
+ assert.equal(requestedNamespace, `team-pi-project-${stableHash("tag:remnic")}`);
778
+ });
779
+
637
780
  test("runExtraction skips batches whose persisted fingerprint already exists in storage meta", async () => {
638
781
  const config = parseConfig({});
639
782
  config.extractionMinChars = 0;
@@ -1780,8 +1923,10 @@ test("runExtraction still clears the session buffer after persistence even if re
1780
1923
  // cataloged namespaces.
1781
1924
  test("runQmdMaintenance updates and embeds cataloged dynamic namespaces (NGnei)", async () => {
1782
1925
  const orchestrator = Object.create(Orchestrator.prototype) as any;
1783
- let updateArg: string[] | undefined;
1784
- let embedArg: string[] | undefined;
1926
+ const updateArgs: string[] = [];
1927
+ const updateCalls: Array<{ namespaces: string[]; strict: boolean | undefined }> = [];
1928
+ const embedArgs: string[] = [];
1929
+ const embedCalls: string[][] = [];
1785
1930
  const memoryDir = path.join(os.tmpdir(), "remnic-qmd-maintenance-ngnei");
1786
1931
  const dynamicNamespace = "project-origin-dynamic";
1787
1932
  const dynamicStorageDir = path.join(
@@ -1797,6 +1942,7 @@ test("runQmdMaintenance updates and embeds cataloged dynamic namespaces (NGnei)"
1797
1942
  defaultNamespace: "default",
1798
1943
  sharedNamespace: "shared",
1799
1944
  namespacePolicies: [],
1945
+ maintenanceNamespaceLockStaleMs: 100,
1800
1946
  qmdAutoEmbedEnabled: true,
1801
1947
  qmdEmbedMinIntervalMs: 0,
1802
1948
  };
@@ -1820,35 +1966,98 @@ test("runQmdMaintenance updates and embeds cataloged dynamic namespaces (NGnei)"
1820
1966
  },
1821
1967
  };
1822
1968
  orchestrator.namespaceSearchRouter = {
1823
- async updateNamespaces(ns: string[]) {
1824
- updateArg = ns;
1825
- return ns.length;
1969
+ async updateNamespacesDetailed(ns: string[], _execution?: unknown, options?: { strict?: boolean }) {
1970
+ updateCalls.push({ namespaces: [...ns], strict: options?.strict });
1971
+ updateArgs.push(...ns);
1972
+ return { backendCount: ns.length, eligibleNamespaces: ns };
1826
1973
  },
1827
1974
  async embedNamespaces(ns: string[]) {
1828
- embedArg = ns;
1975
+ embedCalls.push([...ns]);
1976
+ embedArgs.push(...ns);
1829
1977
  },
1830
1978
  };
1831
1979
 
1832
1980
  await orchestrator.runQmdMaintenance();
1833
1981
 
1834
- assert.ok(updateArg, "updateNamespaces must be called");
1982
+ assert.ok(updateArgs.length > 0, "updateNamespaces must be called");
1983
+ assert.equal(updateCalls.length, 1, "global QMD maintenance must batch selected namespaces into one update call");
1984
+ assert.equal(updateCalls[0]?.strict, true, "recurring QMD maintenance must use strict update semantics");
1835
1985
  assert.ok(
1836
- updateArg!.includes(dynamicNamespace),
1986
+ updateArgs.includes(dynamicNamespace),
1837
1987
  "QMD update must cover the cataloged dynamic namespace, not just configured ones",
1838
1988
  );
1839
1989
  assert.ok(
1840
- updateArg!.includes("default") && updateArg!.includes("shared"),
1990
+ updateArgs.includes("default") && updateArgs.includes("shared"),
1841
1991
  "configured namespaces remain covered",
1842
1992
  );
1843
1993
  assert.ok(
1844
- embedArg && embedArg.includes(dynamicNamespace),
1994
+ embedArgs.includes(dynamicNamespace),
1845
1995
  "QMD embed must cover the cataloged dynamic namespace",
1846
1996
  );
1997
+ assert.equal(embedCalls.length, 1, "QMD embed must batch all selected namespaces into one router call");
1998
+ assert.deepEqual(new Set(embedCalls[0]), new Set(["default", "shared", dynamicNamespace]));
1999
+ });
2000
+
2001
+ test("runQmdMaintenance tracks namespace embed cadence across budget rotation", async () => {
2002
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
2003
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-namespace-embed-cadence-"));
2004
+ const updateCalls: string[][] = [];
2005
+ const embedCalls: string[][] = [];
2006
+
2007
+ try {
2008
+ orchestrator.config = {
2009
+ memoryDir,
2010
+ namespacesEnabled: true,
2011
+ defaultNamespace: "default",
2012
+ sharedNamespace: "shared",
2013
+ namespacePolicies: [{ name: "project-a" }, { name: "project-b" }],
2014
+ maintenanceMaxNamespacesPerCycle: 3,
2015
+ maintenanceNamespaceLockStaleMs: 100,
2016
+ qmdAutoEmbedEnabled: true,
2017
+ qmdEmbedMinIntervalMs: 60_000,
2018
+ };
2019
+ orchestrator.qmdMaintenanceInFlight = false;
2020
+ orchestrator.qmdMaintenancePending = true;
2021
+ orchestrator.lastQmdEmbedAtMs = 0;
2022
+ orchestrator.lastQmdEmbedAtMsByNamespace = new Map();
2023
+ orchestrator.namespaceCatalog = {
2024
+ enabled: false,
2025
+ async listNamespaces() {
2026
+ throw new Error("catalog disabled - must not be read");
2027
+ },
2028
+ };
2029
+ orchestrator.namespaceSearchRouter = {
2030
+ async updateNamespacesDetailed(ns: string[]) {
2031
+ updateCalls.push([...ns]);
2032
+ return { backendCount: ns.length, eligibleNamespaces: ns };
2033
+ },
2034
+ async embedNamespaces(ns: string[]) {
2035
+ embedCalls.push([...ns]);
2036
+ },
2037
+ };
2038
+
2039
+ await orchestrator.runQmdMaintenance();
2040
+ orchestrator.qmdMaintenancePending = true;
2041
+ await orchestrator.runQmdMaintenance();
2042
+
2043
+ assert.deepEqual(updateCalls, [
2044
+ ["default", "shared", "project-a"],
2045
+ ["default", "shared", "project-b"],
2046
+ ]);
2047
+ assert.deepEqual(
2048
+ embedCalls,
2049
+ [["default", "shared", "project-a"], ["project-b"]],
2050
+ "a global embed timestamp must not suppress embeddings for newly budgeted namespaces",
2051
+ );
2052
+ } finally {
2053
+ await rm(memoryDir, { recursive: true, force: true });
2054
+ }
1847
2055
  });
1848
2056
 
1849
2057
  test("runQmdMaintenance skips cataloged dynamic namespaces whose live root is unsafe", async () => {
1850
2058
  const orchestrator = Object.create(Orchestrator.prototype) as any;
1851
- let updateArg: string[] | undefined;
2059
+ const updateArgs: string[] = [];
2060
+ const updateCalls: string[][] = [];
1852
2061
  const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-unsafe-root-"));
1853
2062
  const outsideDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-unsafe-target-"));
1854
2063
  try {
@@ -1868,6 +2077,7 @@ test("runQmdMaintenance skips cataloged dynamic namespaces whose live root is un
1868
2077
  defaultNamespace: "default",
1869
2078
  sharedNamespace: "shared",
1870
2079
  namespacePolicies: [],
2080
+ maintenanceNamespaceLockStaleMs: 100,
1871
2081
  qmdAutoEmbedEnabled: false,
1872
2082
  qmdEmbedMinIntervalMs: 0,
1873
2083
  };
@@ -1890,18 +2100,20 @@ test("runQmdMaintenance skips cataloged dynamic namespaces whose live root is un
1890
2100
  },
1891
2101
  };
1892
2102
  orchestrator.namespaceSearchRouter = {
1893
- async updateNamespaces(ns: string[]) {
1894
- updateArg = ns;
1895
- return ns.length;
2103
+ async updateNamespacesDetailed(ns: string[]) {
2104
+ updateCalls.push([...ns]);
2105
+ updateArgs.push(...ns);
2106
+ return { backendCount: ns.length, eligibleNamespaces: ns };
1896
2107
  },
1897
2108
  async embedNamespaces() {},
1898
2109
  };
1899
2110
 
1900
2111
  await orchestrator.runQmdMaintenance();
1901
2112
 
1902
- assert.ok(updateArg, "updateNamespaces must be called");
2113
+ assert.ok(updateArgs.length > 0, "updateNamespaces must be called");
2114
+ assert.equal(updateCalls.length, 1, "global QMD maintenance must update once for the locked namespace set");
1903
2115
  assert.deepEqual(
1904
- [...updateArg!].sort(),
2116
+ [...updateArgs].sort(),
1905
2117
  ["default", "shared"],
1906
2118
  "cataloged dynamic namespaces are skipped when the live router root differs from the catalog-sanitized root",
1907
2119
  );
@@ -1911,46 +2123,395 @@ test("runQmdMaintenance skips cataloged dynamic namespaces whose live root is un
1911
2123
  }
1912
2124
  });
1913
2125
 
2126
+ test("runQmdMaintenance treats zero namespace updates as failed maintenance", async () => {
2127
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
2128
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-zero-update-"));
2129
+ let markMaintenanceCalls = 0;
2130
+ let embedCalls = 0;
2131
+ try {
2132
+ orchestrator.config = {
2133
+ memoryDir,
2134
+ namespacesEnabled: true,
2135
+ defaultNamespace: "default",
2136
+ sharedNamespace: "shared",
2137
+ namespacePolicies: [],
2138
+ maintenanceNamespaceLockStaleMs: 100,
2139
+ qmdAutoEmbedEnabled: false,
2140
+ qmdEmbedMinIntervalMs: 0,
2141
+ };
2142
+ orchestrator.qmdMaintenanceInFlight = false;
2143
+ orchestrator.qmdMaintenancePending = true;
2144
+ orchestrator.lastQmdEmbedAtMs = 0;
2145
+ orchestrator.namespaceCatalog = {
2146
+ enabled: true,
2147
+ async listNamespaces() {
2148
+ return [{ namespace: "default" }];
2149
+ },
2150
+ async markMaintenance() {
2151
+ markMaintenanceCalls += 1;
2152
+ },
2153
+ };
2154
+ orchestrator.namespaceSearchRouter = {
2155
+ async updateNamespacesDetailed() {
2156
+ return { backendCount: 0, eligibleNamespaces: [] };
2157
+ },
2158
+ async embedNamespaces() {
2159
+ embedCalls += 1;
2160
+ },
2161
+ };
2162
+
2163
+ await orchestrator.runQmdMaintenance();
2164
+
2165
+ const statuses = await readNamespaceMaintenanceStatuses(orchestrator.config);
2166
+ assert.ok(
2167
+ statuses.some((status) => status.namespace === "default" && status.state === "failed"),
2168
+ "zero updates should be recorded as failed maintenance, not a successful run",
2169
+ );
2170
+ assert.equal(markMaintenanceCalls, 0);
2171
+ assert.equal(embedCalls, 0);
2172
+ assert.equal(orchestrator.lastQmdEmbedAtMs, 0);
2173
+ } finally {
2174
+ await rm(memoryDir, { recursive: true, force: true });
2175
+ }
2176
+ });
2177
+
2178
+ test("runQmdMaintenance treats partial namespace update eligibility as failed maintenance", async () => {
2179
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
2180
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-partial-update-"));
2181
+ let markMaintenanceCalls = 0;
2182
+ let embedCalls = 0;
2183
+ try {
2184
+ orchestrator.config = {
2185
+ memoryDir,
2186
+ namespacesEnabled: true,
2187
+ defaultNamespace: "default",
2188
+ sharedNamespace: "shared",
2189
+ namespacePolicies: [],
2190
+ maintenanceNamespaceLockStaleMs: 100,
2191
+ qmdAutoEmbedEnabled: false,
2192
+ qmdEmbedMinIntervalMs: 0,
2193
+ };
2194
+ orchestrator.qmdMaintenanceInFlight = false;
2195
+ orchestrator.qmdMaintenancePending = true;
2196
+ orchestrator.lastQmdEmbedAtMs = 0;
2197
+ orchestrator.namespaceCatalog = {
2198
+ enabled: true,
2199
+ async listNamespaces() {
2200
+ return [{ namespace: "default" }];
2201
+ },
2202
+ async markMaintenance() {
2203
+ markMaintenanceCalls += 1;
2204
+ },
2205
+ };
2206
+ orchestrator.namespaceSearchRouter = {
2207
+ async updateNamespacesDetailed(ns: string[]) {
2208
+ assert.ok(ns.includes("default") && ns.includes("shared"));
2209
+ return { backendCount: 1, eligibleNamespaces: ["default"] };
2210
+ },
2211
+ async embedNamespaces() {
2212
+ embedCalls += 1;
2213
+ },
2214
+ };
2215
+
2216
+ await orchestrator.runQmdMaintenance();
2217
+
2218
+ const statuses = await readNamespaceMaintenanceStatuses(orchestrator.config);
2219
+ assert.ok(
2220
+ statuses.some((status) => status.namespace === "default" && status.state === "failed"),
2221
+ "partial update eligibility should not be recorded as successful maintenance",
2222
+ );
2223
+ assert.ok(
2224
+ statuses.some((status) => status.namespace === "shared" && status.state === "failed"),
2225
+ "ineligible selected namespaces should not be rotated as maintained",
2226
+ );
2227
+ assert.equal(markMaintenanceCalls, 0);
2228
+ assert.equal(embedCalls, 0);
2229
+ assert.equal(orchestrator.lastQmdEmbedAtMs, 0);
2230
+ } finally {
2231
+ await rm(memoryDir, { recursive: true, force: true });
2232
+ }
2233
+ });
2234
+
2235
+ test("runQmdMaintenance treats namespace embed errors as failed maintenance", async () => {
2236
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
2237
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-embed-failure-"));
2238
+ let markMaintenanceCalls = 0;
2239
+ try {
2240
+ orchestrator.config = {
2241
+ memoryDir,
2242
+ namespacesEnabled: true,
2243
+ defaultNamespace: "default",
2244
+ sharedNamespace: "shared",
2245
+ namespacePolicies: [],
2246
+ maintenanceNamespaceLockStaleMs: 100,
2247
+ qmdAutoEmbedEnabled: true,
2248
+ qmdEmbedMinIntervalMs: 0,
2249
+ };
2250
+ orchestrator.qmdMaintenanceInFlight = false;
2251
+ orchestrator.qmdMaintenancePending = true;
2252
+ orchestrator.lastQmdEmbedAtMs = 0;
2253
+ orchestrator.namespaceCatalog = {
2254
+ enabled: true,
2255
+ async listNamespaces() {
2256
+ return [{ namespace: "default" }];
2257
+ },
2258
+ async markMaintenance() {
2259
+ markMaintenanceCalls += 1;
2260
+ },
2261
+ };
2262
+ orchestrator.namespaceSearchRouter = {
2263
+ async updateNamespacesDetailed(ns: string[]) {
2264
+ return { backendCount: 1, eligibleNamespaces: ns };
2265
+ },
2266
+ async embedNamespaces() {
2267
+ throw Object.assign(new Error("embed failed"), { code: "EQMD" });
2268
+ },
2269
+ };
2270
+
2271
+ await orchestrator.runQmdMaintenance();
2272
+
2273
+ const statuses = await readNamespaceMaintenanceStatuses(orchestrator.config);
2274
+ assert.ok(
2275
+ statuses.some((status) => status.namespace === "default" && status.state === "failed"),
2276
+ "embed failures should not be recorded as successful namespace maintenance",
2277
+ );
2278
+ assert.equal(markMaintenanceCalls, 0);
2279
+ assert.equal(orchestrator.lastQmdEmbedAtMs, 0);
2280
+ } finally {
2281
+ await rm(memoryDir, { recursive: true, force: true });
2282
+ }
2283
+ });
2284
+
2285
+ test("runQmdMaintenance records QMD min-interval throttles as skipped maintenance", async () => {
2286
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
2287
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-throttled-update-"));
2288
+ let markMaintenanceCalls = 0;
2289
+ let embedCalls = 0;
2290
+ let updateCalls = 0;
2291
+ try {
2292
+ orchestrator.config = {
2293
+ memoryDir,
2294
+ namespacesEnabled: true,
2295
+ defaultNamespace: "default",
2296
+ sharedNamespace: "shared",
2297
+ namespacePolicies: [],
2298
+ maintenanceNamespaceLockStaleMs: 100,
2299
+ qmdAutoEmbedEnabled: false,
2300
+ qmdEmbedMinIntervalMs: 0,
2301
+ };
2302
+ orchestrator.qmdMaintenanceInFlight = false;
2303
+ orchestrator.qmdMaintenancePending = true;
2304
+ orchestrator.lastQmdEmbedAtMs = 0;
2305
+ orchestrator.namespaceCatalog = {
2306
+ enabled: true,
2307
+ async listNamespaces() {
2308
+ return [{ namespace: "default" }];
2309
+ },
2310
+ async markMaintenance() {
2311
+ markMaintenanceCalls += 1;
2312
+ },
2313
+ };
2314
+ orchestrator.namespaceSearchRouter = {
2315
+ async updateNamespacesDetailed(_ns: string[], _execution?: unknown, options?: { strict?: boolean }) {
2316
+ updateCalls += 1;
2317
+ assert.equal(options?.strict, true, "recurring maintenance must use strict QMD updates");
2318
+ throw new Error("QMD update skipped by global min-interval gate");
2319
+ },
2320
+ async embedNamespaces() {
2321
+ embedCalls += 1;
2322
+ },
2323
+ };
2324
+
2325
+ await orchestrator.runQmdMaintenance();
2326
+
2327
+ const statuses = await readNamespaceMaintenanceStatuses(orchestrator.config);
2328
+ assert.equal(updateCalls, 1, "strict global QMD maintenance should be attempted once");
2329
+ assert.ok(
2330
+ statuses.some(
2331
+ (status) =>
2332
+ status.namespace === "default" &&
2333
+ status.state === "skipped" &&
2334
+ status.reason === "throttled",
2335
+ ),
2336
+ "QMD min-interval throttles should be recorded as skipped maintenance",
2337
+ );
2338
+ assert.ok(
2339
+ statuses.some(
2340
+ (status) =>
2341
+ status.namespace === "shared" &&
2342
+ status.state === "skipped" &&
2343
+ status.reason === "throttled",
2344
+ ),
2345
+ "every selected namespace should receive the throttled skip status",
2346
+ );
2347
+ assert.equal(markMaintenanceCalls, 0);
2348
+ assert.equal(embedCalls, 0);
2349
+ assert.equal(orchestrator.lastQmdEmbedAtMs, 0);
2350
+ } finally {
2351
+ await rm(memoryDir, { recursive: true, force: true });
2352
+ }
2353
+ });
2354
+
2355
+ test("runQmdMaintenance still embeds when due update is throttled", async () => {
2356
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
2357
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-throttled-update-embed-"));
2358
+ let markMaintenanceCalls = 0;
2359
+ let updateCalls = 0;
2360
+ const embedCalls: string[][] = [];
2361
+ try {
2362
+ orchestrator.config = {
2363
+ memoryDir,
2364
+ namespacesEnabled: true,
2365
+ defaultNamespace: "default",
2366
+ sharedNamespace: "shared",
2367
+ namespacePolicies: [],
2368
+ maintenanceNamespaceLockStaleMs: 100,
2369
+ qmdAutoEmbedEnabled: true,
2370
+ qmdEmbedMinIntervalMs: 0,
2371
+ };
2372
+ orchestrator.qmdMaintenanceInFlight = false;
2373
+ orchestrator.qmdMaintenancePending = true;
2374
+ orchestrator.lastQmdEmbedAtMs = 0;
2375
+ orchestrator.namespaceCatalog = {
2376
+ enabled: true,
2377
+ async listNamespaces() {
2378
+ return [{ namespace: "default" }];
2379
+ },
2380
+ async markMaintenance() {
2381
+ markMaintenanceCalls += 1;
2382
+ },
2383
+ };
2384
+ orchestrator.namespaceSearchRouter = {
2385
+ async updateNamespacesDetailed(_ns: string[], _execution?: unknown, options?: { strict?: boolean }) {
2386
+ updateCalls += 1;
2387
+ assert.equal(options?.strict, true, "recurring maintenance must use strict QMD updates");
2388
+ throw new Error("QMD update skipped by global min-interval gate");
2389
+ },
2390
+ async embedNamespaces(ns: string[], options?: { strict?: boolean }) {
2391
+ assert.equal(options?.strict, true, "due embed retries must surface embed failures");
2392
+ embedCalls.push([...ns]);
2393
+ },
2394
+ };
2395
+
2396
+ await orchestrator.runQmdMaintenance();
2397
+
2398
+ const statuses = await readNamespaceMaintenanceStatuses(orchestrator.config);
2399
+ assert.equal(updateCalls, 1, "strict global QMD maintenance should be attempted once");
2400
+ assert.deepEqual(embedCalls, [["default", "shared"]]);
2401
+ assert.ok(
2402
+ statuses.every((status) => status.state === "skipped" && status.reason === "throttled"),
2403
+ "a throttled update should still be recorded as skipped after the due embed retry",
2404
+ );
2405
+ assert.equal(markMaintenanceCalls, 0);
2406
+ assert.notEqual(orchestrator.lastQmdEmbedAtMs, 0);
2407
+ } finally {
2408
+ await rm(memoryDir, { recursive: true, force: true });
2409
+ }
2410
+ });
2411
+
2412
+ test("runQmdMaintenance treats strict namespace update errors as failed maintenance", async () => {
2413
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
2414
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-update-failure-"));
2415
+ let markMaintenanceCalls = 0;
2416
+ let embedCalls = 0;
2417
+ let updateCalls = 0;
2418
+ try {
2419
+ orchestrator.config = {
2420
+ memoryDir,
2421
+ namespacesEnabled: true,
2422
+ defaultNamespace: "default",
2423
+ sharedNamespace: "shared",
2424
+ namespacePolicies: [],
2425
+ maintenanceNamespaceLockStaleMs: 100,
2426
+ qmdAutoEmbedEnabled: true,
2427
+ qmdEmbedMinIntervalMs: 0,
2428
+ };
2429
+ orchestrator.qmdMaintenanceInFlight = false;
2430
+ orchestrator.qmdMaintenancePending = true;
2431
+ orchestrator.lastQmdEmbedAtMs = 0;
2432
+ orchestrator.namespaceCatalog = {
2433
+ enabled: true,
2434
+ async listNamespaces() {
2435
+ return [{ namespace: "default" }];
2436
+ },
2437
+ async markMaintenance() {
2438
+ markMaintenanceCalls += 1;
2439
+ },
2440
+ };
2441
+ orchestrator.namespaceSearchRouter = {
2442
+ async updateNamespacesDetailed(_ns: string[], _execution?: unknown, options?: { strict?: boolean }) {
2443
+ updateCalls += 1;
2444
+ assert.equal(options?.strict, true, "recurring maintenance must require strict update failure propagation");
2445
+ throw new Error("qmd exploded");
2446
+ },
2447
+ async embedNamespaces() {
2448
+ embedCalls += 1;
2449
+ },
2450
+ };
2451
+
2452
+ await orchestrator.runQmdMaintenance();
2453
+
2454
+ const statuses = await readNamespaceMaintenanceStatuses(orchestrator.config);
2455
+ assert.equal(updateCalls, 1, "strict global QMD maintenance should be attempted once");
2456
+ assert.ok(
2457
+ statuses.some((status) => status.namespace === "default" && status.state === "failed"),
2458
+ "strict update errors should be recorded as failed maintenance",
2459
+ );
2460
+ assert.equal(markMaintenanceCalls, 0);
2461
+ assert.equal(embedCalls, 0);
2462
+ assert.equal(orchestrator.lastQmdEmbedAtMs, 0);
2463
+ } finally {
2464
+ await rm(memoryDir, { recursive: true, force: true });
2465
+ }
2466
+ });
2467
+
1914
2468
  // NGnei fallback: when the catalog is disabled, maintenance covers exactly the
1915
2469
  // configured set (no catalog read), and a catalog read failure degrades to the
1916
2470
  // configured set rather than breaking maintenance.
1917
2471
  test("runQmdMaintenance falls back to configured namespaces when the catalog is disabled (NGnei)", async () => {
1918
2472
  const orchestrator = Object.create(Orchestrator.prototype) as any;
1919
- let updateArg: string[] | undefined;
2473
+ const updateArgs: string[] = [];
2474
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-disabled-catalog-"));
1920
2475
 
1921
- orchestrator.config = {
1922
- namespacesEnabled: true,
1923
- defaultNamespace: "default",
1924
- sharedNamespace: "shared",
2476
+ try {
2477
+ orchestrator.config = {
2478
+ memoryDir,
2479
+ namespacesEnabled: true,
2480
+ defaultNamespace: "default",
2481
+ sharedNamespace: "shared",
1925
2482
  namespacePolicies: [],
2483
+ maintenanceNamespaceLockStaleMs: 100,
1926
2484
  qmdAutoEmbedEnabled: false,
1927
- qmdEmbedMinIntervalMs: 0,
1928
- };
1929
- orchestrator.qmdMaintenanceInFlight = false;
1930
- orchestrator.qmdMaintenancePending = true;
1931
- orchestrator.lastQmdEmbedAtMs = 0;
1932
- orchestrator.namespaceCatalog = {
1933
- enabled: false,
1934
- async listNamespaces() {
1935
- throw new Error("catalog disabled — must not be read");
1936
- },
2485
+ qmdEmbedMinIntervalMs: 0,
2486
+ };
2487
+ orchestrator.qmdMaintenanceInFlight = false;
2488
+ orchestrator.qmdMaintenancePending = true;
2489
+ orchestrator.lastQmdEmbedAtMs = 0;
2490
+ orchestrator.namespaceCatalog = {
2491
+ enabled: false,
2492
+ async listNamespaces() {
2493
+ throw new Error("catalog disabled — must not be read");
2494
+ },
1937
2495
  };
1938
2496
  orchestrator.namespaceSearchRouter = {
1939
- async updateNamespaces(ns: string[]) {
1940
- updateArg = ns;
1941
- return ns.length;
2497
+ async updateNamespacesDetailed(ns: string[]) {
2498
+ updateArgs.push(...ns);
2499
+ return { backendCount: ns.length, eligibleNamespaces: ns };
1942
2500
  },
1943
- async embedNamespaces() {},
1944
- };
2501
+ async embedNamespaces() {},
2502
+ };
1945
2503
 
1946
- await orchestrator.runQmdMaintenance();
2504
+ await orchestrator.runQmdMaintenance();
1947
2505
 
1948
- assert.ok(updateArg, "updateNamespaces must be called");
1949
- assert.deepEqual(
1950
- [...updateArg!].sort(),
1951
- ["default", "shared"],
1952
- "a disabled catalog covers exactly the configured set",
1953
- );
2506
+ assert.ok(updateArgs.length > 0, "updateNamespaces must be called");
2507
+ assert.deepEqual(
2508
+ [...updateArgs].sort(),
2509
+ ["default", "shared"],
2510
+ "a disabled catalog covers exactly the configured set",
2511
+ );
2512
+ } finally {
2513
+ await rm(memoryDir, { recursive: true, force: true });
2514
+ }
1954
2515
  });
1955
2516
 
1956
2517
  // ── NHZEV (codex P2): the QMD STARTUP sync in deferredInitialize() must cover
@@ -1981,6 +2542,7 @@ test("deferredInitialize startup sync covers cataloged dynamic namespaces (NHZEV
1981
2542
  sharedNamespace: "shared",
1982
2543
  namespacePolicies: [],
1983
2544
  qmdMaintenanceEnabled: true,
2545
+ maintenanceMaxNamespacesPerCycle: 2,
1984
2546
  };
1985
2547
  orchestrator.qmd = {
1986
2548
  isAvailable: () => true,
@@ -2017,7 +2579,7 @@ test("deferredInitialize startup sync covers cataloged dynamic namespaces (NHZEV
2017
2579
  assert.ok(updateArg, "startup updateNamespaces must be called");
2018
2580
  assert.ok(
2019
2581
  updateArg!.includes(dynamicNamespace),
2020
- "startup sync must cover the cataloged dynamic namespace (NHZEV), not just configured ones",
2582
+ "startup sync must cover the cataloged dynamic namespace even when it exceeds the recurring maintenance cycle budget",
2021
2583
  );
2022
2584
  assert.ok(
2023
2585
  updateArg!.includes("default") && updateArg!.includes("shared"),