@remnic/core 9.3.726 → 9.3.727

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 (318) hide show
  1. package/dist/access-boundary.d.ts +5 -5
  2. package/dist/access-boundary.js +20 -20
  3. package/dist/access-cli.js +42 -42
  4. package/dist/access-http.d.ts +20 -5
  5. package/dist/access-http.js +25 -25
  6. package/dist/access-mcp.d.ts +5 -5
  7. package/dist/access-mcp.js +24 -24
  8. package/dist/access-operations-batch.js +21 -21
  9. package/dist/access-operations.d.ts +5 -5
  10. package/dist/access-operations.js +23 -23
  11. package/dist/access-schema.js +3 -3
  12. package/dist/{access-service-B2PlPtxh.d.ts → access-service-Bgpjxo4f.d.ts} +36 -4
  13. package/dist/access-service.d.ts +5 -5
  14. package/dist/access-service.js +19 -19
  15. package/dist/access-surface-catalog.d.ts +5 -5
  16. package/dist/action-confidence.d.ts +1 -1
  17. package/dist/active-memory-bridge.d.ts +1 -1
  18. package/dist/active-recall.d.ts +1 -1
  19. package/dist/active-recall.js +3 -3
  20. package/dist/behavior-learner.d.ts +1 -1
  21. package/dist/behavior-signals.d.ts +1 -1
  22. package/dist/bootstrap.d.ts +4 -4
  23. package/dist/briefing.d.ts +1 -1
  24. package/dist/briefing.js +7 -7
  25. package/dist/buffer-surprise-report.d.ts +1 -1
  26. package/dist/buffer.d.ts +1 -1
  27. package/dist/calibration.d.ts +1 -1
  28. package/dist/capabilities.d.ts +1 -1
  29. package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
  30. package/dist/{catalog-CzUb-GyD.d.ts → catalog-bMczlO2G.d.ts} +1 -1
  31. package/dist/causal-behavior.d.ts +1 -1
  32. package/dist/causal-consolidation.d.ts +1 -1
  33. package/dist/causal-consolidation.js +8 -8
  34. package/dist/causal-trajectory-graph.d.ts +1 -1
  35. package/dist/{chunk-Q6D75LVH.js → chunk-27QQC7ZX.js} +75 -14
  36. package/dist/{chunk-Q6D75LVH.js.map → chunk-27QQC7ZX.js.map} +1 -1
  37. package/dist/{chunk-53EEQYE5.js → chunk-2KAYTPPT.js} +7 -7
  38. package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
  39. package/dist/{chunk-BIA7ELIB.js → chunk-36SEDFAU.js} +3 -3
  40. package/dist/{chunk-UQSVUEN2.js → chunk-37436GML.js} +2 -2
  41. package/dist/{chunk-RRHHQYJS.js → chunk-3TSTGI6F.js} +2 -2
  42. package/dist/{chunk-FO3HO52S.js → chunk-4H4F3BFL.js} +2 -2
  43. package/dist/{chunk-BEUCACTV.js → chunk-4NOI577P.js} +2 -2
  44. package/dist/{chunk-BBIE7W52.js → chunk-4NR2PDZ5.js} +2 -2
  45. package/dist/{chunk-TQQH2FTX.js → chunk-6PY5YJAV.js} +101 -15
  46. package/dist/{chunk-TQQH2FTX.js.map → chunk-6PY5YJAV.js.map} +1 -1
  47. package/dist/{chunk-PKMQK2WY.js → chunk-773DPI6T.js} +4 -4
  48. package/dist/{chunk-MQVKHFIU.js → chunk-7RLCCNHU.js} +8 -8
  49. package/dist/{chunk-XGTBNVIU.js → chunk-AAD2ZS6P.js} +11 -1
  50. package/dist/chunk-AAD2ZS6P.js.map +1 -0
  51. package/dist/{chunk-EZNDNMRH.js → chunk-AQLOP5F4.js} +2 -2
  52. package/dist/{chunk-UNNDFYUX.js → chunk-ARV3AUOM.js} +5 -5
  53. package/dist/{chunk-NPL7VDM6.js → chunk-ASKXIAHD.js} +2 -2
  54. package/dist/{chunk-K4ZNARDO.js → chunk-CBWI22JN.js} +2 -2
  55. package/dist/{chunk-ZOHMPFOB.js → chunk-CKFLLAIS.js} +3 -3
  56. package/dist/{chunk-DCKNMLEQ.js → chunk-CSRXQ6NB.js} +2 -2
  57. package/dist/{chunk-6AFVIOVZ.js → chunk-CTOQEZSN.js} +2 -2
  58. package/dist/{chunk-7NS7AWMZ.js → chunk-DKJCRGWM.js} +3 -3
  59. package/dist/{chunk-JPUT5NGH.js → chunk-DMQWV6SQ.js} +2 -2
  60. package/dist/{chunk-F3XZN24Z.js → chunk-DRUDC2UX.js} +2 -2
  61. package/dist/{chunk-KJR66XOM.js → chunk-EY6MDMHK.js} +2 -2
  62. package/dist/{chunk-3TUT4PWB.js → chunk-FVZ2ISIX.js} +2 -2
  63. package/dist/{chunk-C4X4QGIO.js → chunk-G6NKALRN.js} +5 -2
  64. package/dist/chunk-G6NKALRN.js.map +1 -0
  65. package/dist/{chunk-6SQQOJ4U.js → chunk-GKRLPHPU.js} +2 -2
  66. package/dist/{chunk-5NWYQUQC.js → chunk-HOHVIVDR.js} +2 -2
  67. package/dist/{chunk-6WAJQKBS.js → chunk-JYLFZWUX.js} +2 -2
  68. package/dist/{chunk-5W7CTZ2E.js → chunk-KQAFEZQX.js} +4 -4
  69. package/dist/{chunk-YTZ3VGC7.js → chunk-M4V2JVSZ.js} +2 -2
  70. package/dist/{chunk-PLG6JHQS.js → chunk-MJIHJ7OG.js} +38 -38
  71. package/dist/{chunk-TA3Y6GEX.js → chunk-MLKWJOAJ.js} +2 -2
  72. package/dist/{chunk-2NBP25BG.js → chunk-N3VNUM4Z.js} +171 -79
  73. package/dist/chunk-N3VNUM4Z.js.map +1 -0
  74. package/dist/{chunk-UKTDXTWX.js → chunk-NSA5ZRUL.js} +6 -6
  75. package/dist/{chunk-DQXPJ6HD.js → chunk-OEEW4KJA.js} +4 -4
  76. package/dist/{chunk-7AG54AF3.js → chunk-PUR2CKX2.js} +1 -1
  77. package/dist/chunk-PUR2CKX2.js.map +1 -0
  78. package/dist/{chunk-5OOA7SGX.js → chunk-R7IYU5DZ.js} +2 -2
  79. package/dist/{chunk-WGTWGDR5.js → chunk-SNKPUJHR.js} +5 -5
  80. package/dist/{chunk-BB57OZWC.js → chunk-UVPL565C.js} +2 -2
  81. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  82. package/dist/{chunk-OAHOL3ZC.js → chunk-XPHVAOBT.js} +111 -30
  83. package/dist/chunk-XPHVAOBT.js.map +1 -0
  84. package/dist/{chunk-V7CMKMHM.js → chunk-Y4YD5TYW.js} +3 -3
  85. package/dist/{chunk-MDBZTH7N.js → chunk-YKBHOBJK.js} +44 -10
  86. package/dist/chunk-YKBHOBJK.js.map +1 -0
  87. package/dist/{chunk-3MS3QVX6.js → chunk-YOLZS7NY.js} +2 -2
  88. package/dist/{chunk-6OIDDKAA.js → chunk-YTU7WKYN.js} +35 -9
  89. package/dist/chunk-YTU7WKYN.js.map +1 -0
  90. package/dist/{cli-IOlDHSBT.d.ts → cli-BLME420h.d.ts} +3 -3
  91. package/dist/cli.d.ts +6 -6
  92. package/dist/cli.js +47 -47
  93. package/dist/compat/codex-cli-fallback.d.ts +1 -0
  94. package/dist/compat/codex-cli-fallback.js +22 -0
  95. package/dist/compat/codex-cli-fallback.js.map +1 -0
  96. package/dist/compat/codex-thread-key.d.ts +1 -0
  97. package/dist/compat/codex-thread-key.js +18 -0
  98. package/dist/compat/codex-thread-key.js.map +1 -0
  99. package/dist/compounding/engine.d.ts +1 -1
  100. package/dist/compounding/engine.js +7 -7
  101. package/dist/compounding/preference-consolidator.d.ts +1 -1
  102. package/dist/compression-optimizer.d.ts +1 -1
  103. package/dist/config.d.ts +1 -1
  104. package/dist/config.js +3 -3
  105. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  106. package/dist/connectors/codex-materialize-runner.js +7 -7
  107. package/dist/connectors/codex-materialize.d.ts +1 -1
  108. package/dist/connectors/index.d.ts +1 -1
  109. package/dist/connectors/index.js +7 -7
  110. package/dist/consolidation-provenance-check.d.ts +1 -1
  111. package/dist/consolidation-undo.d.ts +1 -1
  112. package/dist/contradiction/index.d.ts +1 -1
  113. package/dist/conversation-index/backend.d.ts +1 -1
  114. package/dist/conversation-index/chunker.d.ts +1 -1
  115. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  116. package/dist/conversation-index/indexer.d.ts +1 -1
  117. package/dist/conversation-index/search.d.ts +1 -1
  118. package/dist/day-summary.d.ts +1 -1
  119. package/dist/delinearize.d.ts +1 -1
  120. package/dist/direct-answer-wiring.d.ts +1 -1
  121. package/dist/direct-answer.d.ts +1 -1
  122. package/dist/embedding-fallback.d.ts +1 -1
  123. package/dist/enrichment/index.d.ts +1 -1
  124. package/dist/entity-retrieval.d.ts +1 -1
  125. package/dist/entity-retrieval.js +7 -7
  126. package/dist/entity-schema.d.ts +1 -1
  127. package/dist/explicit-capture.d.ts +4 -4
  128. package/dist/extraction-faithfulness.d.ts +1 -1
  129. package/dist/extraction-faithfulness.js +2 -2
  130. package/dist/extraction-judge-telemetry.d.ts +1 -1
  131. package/dist/extraction-judge-training.d.ts +1 -1
  132. package/dist/extraction-judge.d.ts +1 -1
  133. package/dist/extraction.d.ts +1 -1
  134. package/dist/extraction.js +2 -2
  135. package/dist/faithfulness-config.d.ts +1 -0
  136. package/dist/faithfulness-config.js +1 -1
  137. package/dist/fallback-llm.d.ts +1 -1
  138. package/dist/graph-dashboard-diff.d.ts +1 -1
  139. package/dist/graph-dashboard-key.d.ts +1 -1
  140. package/dist/graph-dashboard-parser.d.ts +1 -1
  141. package/dist/graph-edge-reinforcement.d.ts +1 -1
  142. package/dist/graph-snapshot.d.ts +1 -1
  143. package/dist/graph.d.ts +1 -1
  144. package/dist/identity-continuity.d.ts +1 -1
  145. package/dist/importance.d.ts +1 -1
  146. package/dist/index.d.ts +9 -9
  147. package/dist/index.js +61 -61
  148. package/dist/intent.d.ts +1 -1
  149. package/dist/lcm/engine.d.ts +1 -1
  150. package/dist/lcm/engine.js +2 -2
  151. package/dist/lcm/index.d.ts +1 -1
  152. package/dist/lcm/index.js +7 -7
  153. package/dist/lcm/tools.d.ts +1 -1
  154. package/dist/lifecycle.d.ts +1 -1
  155. package/dist/live-connectors-runner.d.ts +1 -1
  156. package/dist/local-llm.d.ts +1 -1
  157. package/dist/local-model-endpoint.d.ts +9 -4
  158. package/dist/local-model-endpoint.js +1 -1
  159. package/dist/maintenance/memory-governance.d.ts +1 -1
  160. package/dist/maintenance/memory-governance.js +7 -7
  161. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  162. package/dist/maintenance/rebuild-memory-projection.js +8 -8
  163. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  164. package/dist/memory-action-policy.d.ts +1 -1
  165. package/dist/memory-cache.d.ts +1 -1
  166. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  167. package/dist/memory-projection-store.d.ts +1 -1
  168. package/dist/memory-provenance.d.ts +1 -1
  169. package/dist/memory-worth-outcomes.d.ts +1 -1
  170. package/dist/models-json.d.ts +1 -1
  171. package/dist/namespaces/migrate.d.ts +2 -2
  172. package/dist/namespaces/migrate.js +12 -12
  173. package/dist/namespaces/principal.d.ts +1 -1
  174. package/dist/namespaces/search.d.ts +1 -1
  175. package/dist/namespaces/search.js +4 -4
  176. package/dist/namespaces/storage.d.ts +2 -2
  177. package/dist/namespaces/storage.js +7 -7
  178. package/dist/native-knowledge.d.ts +1 -1
  179. package/dist/operator-toolkit.d.ts +1 -1
  180. package/dist/operator-toolkit.js +18 -18
  181. package/dist/orchestration/maintenance.d.ts +2 -2
  182. package/dist/orchestration/maintenance.js +9 -9
  183. package/dist/{orchestrator-C_OMSIMZ.d.ts → orchestrator-DqPAz8ot.d.ts} +3 -3
  184. package/dist/orchestrator.d.ts +4 -4
  185. package/dist/orchestrator.js +30 -30
  186. package/dist/patterns-cli.d.ts +1 -1
  187. package/dist/policy-runtime.d.ts +1 -1
  188. package/dist/provenance.d.ts +1 -1
  189. package/dist/provenance.js +1 -1
  190. package/dist/qmd-recall-cache.d.ts +1 -1
  191. package/dist/qmd.d.ts +1 -1
  192. package/dist/recall-disclosure-escalation.d.ts +1 -1
  193. package/dist/recall-explain-renderer.d.ts +1 -1
  194. package/dist/recall-explain-renderer.js +3 -3
  195. package/dist/recall-planner-llm.d.ts +1 -1
  196. package/dist/recall-state.d.ts +1 -1
  197. package/dist/recall-tag-filter.d.ts +1 -1
  198. package/dist/recall-xray-cli.d.ts +1 -1
  199. package/dist/recall-xray-cli.js +4 -4
  200. package/dist/recall-xray-renderer.d.ts +1 -1
  201. package/dist/recall-xray-renderer.js +3 -3
  202. package/dist/recall-xray.d.ts +1 -1
  203. package/dist/recall-xray.js +2 -2
  204. package/dist/resolve-auth-token.d.ts +1 -1
  205. package/dist/resume-bundles.js +4 -4
  206. package/dist/retrieval-agents.d.ts +1 -1
  207. package/dist/retrieval-tiers.d.ts +1 -1
  208. package/dist/routing/engine.d.ts +1 -1
  209. package/dist/routing/store.d.ts +1 -1
  210. package/dist/schemas.d.ts +22 -22
  211. package/dist/search/embed-helper.d.ts +1 -1
  212. package/dist/search/factory.d.ts +1 -1
  213. package/dist/search/factory.js +3 -3
  214. package/dist/search/index.d.ts +1 -1
  215. package/dist/search/index.js +7 -7
  216. package/dist/search/lancedb-backend.d.ts +1 -1
  217. package/dist/search/meilisearch-backend.d.ts +1 -1
  218. package/dist/search/noop-backend.d.ts +1 -1
  219. package/dist/search/orama-backend.d.ts +1 -1
  220. package/dist/search/port.d.ts +1 -1
  221. package/dist/search/remote-backend.d.ts +1 -1
  222. package/dist/{semantic-consolidation-B8LiYtvx.d.ts → semantic-consolidation-Cl15ksZy.d.ts} +1 -1
  223. package/dist/semantic-consolidation.d.ts +2 -2
  224. package/dist/semantic-consolidation.js +8 -8
  225. package/dist/semantic-rule-promotion.js +7 -7
  226. package/dist/semantic-rule-verifier.d.ts +1 -1
  227. package/dist/semantic-rule-verifier.js +7 -7
  228. package/dist/session-observer-bands.d.ts +1 -1
  229. package/dist/session-observer-state.d.ts +1 -1
  230. package/dist/shared-context/manager.d.ts +1 -1
  231. package/dist/signal.d.ts +1 -1
  232. package/dist/storage.d.ts +1 -1
  233. package/dist/storage.js +6 -6
  234. package/dist/summarizer.d.ts +1 -1
  235. package/dist/summary-snapshot.d.ts +1 -1
  236. package/dist/temporal-supersession.d.ts +1 -1
  237. package/dist/temporal-validity.d.ts +1 -1
  238. package/dist/threading.d.ts +1 -1
  239. package/dist/tier-migration.d.ts +1 -1
  240. package/dist/tier-routing.d.ts +1 -1
  241. package/dist/topics.d.ts +1 -1
  242. package/dist/transcript.d.ts +1 -1
  243. package/dist/transfer/backup.js +2 -2
  244. package/dist/transfer/capsule-export.js +2 -2
  245. package/dist/transfer/capsule-import.js +2 -2
  246. package/dist/transfer/types.d.ts +12 -12
  247. package/dist/trust-score-stage.d.ts +1 -1
  248. package/dist/trust-score.d.ts +1 -1
  249. package/dist/{types-DmTaqob2.d.ts → types-ChQb9D6s.d.ts} +8 -0
  250. package/dist/types.d.ts +1 -1
  251. package/dist/types.js +1 -1
  252. package/dist/utility-runtime.d.ts +1 -1
  253. package/dist/verified-recall.js +7 -7
  254. package/package.json +26 -2
  255. package/src/access-http.ts +68 -1
  256. package/src/admin/admin-surfaces.test.ts +289 -0
  257. package/src/admin/admin-surfaces.ts +138 -7
  258. package/src/bitemporal-dedup-backfill.test.ts +294 -0
  259. package/src/chat/chat-engine.test.ts +66 -1
  260. package/src/chat/chat-http.test.ts +119 -1
  261. package/src/chat/chat-http.ts +252 -97
  262. package/src/chat/chat-session.ts +143 -7
  263. package/src/compat/codex-cli-fallback.ts +23 -0
  264. package/src/compat/codex-subpath-aliases.test.ts +93 -0
  265. package/src/compat/codex-thread-key.ts +23 -0
  266. package/src/extraction-faithfulness.test.ts +240 -0
  267. package/src/extraction-faithfulness.ts +58 -6
  268. package/src/faithfulness-config.ts +16 -1
  269. package/src/local-model-endpoint.ts +24 -3
  270. package/src/orchestrator.ts +200 -8
  271. package/src/provenance-extraction.test.ts +266 -0
  272. package/src/provenance.ts +66 -16
  273. package/src/types.ts +8 -0
  274. package/dist/chunk-2NBP25BG.js.map +0 -1
  275. package/dist/chunk-6OIDDKAA.js.map +0 -1
  276. package/dist/chunk-7AG54AF3.js.map +0 -1
  277. package/dist/chunk-C4X4QGIO.js.map +0 -1
  278. package/dist/chunk-MDBZTH7N.js.map +0 -1
  279. package/dist/chunk-OAHOL3ZC.js.map +0 -1
  280. package/dist/chunk-XGTBNVIU.js.map +0 -1
  281. /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
  282. /package/dist/{chunk-53EEQYE5.js.map → chunk-2KAYTPPT.js.map} +0 -0
  283. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  284. /package/dist/{chunk-BIA7ELIB.js.map → chunk-36SEDFAU.js.map} +0 -0
  285. /package/dist/{chunk-UQSVUEN2.js.map → chunk-37436GML.js.map} +0 -0
  286. /package/dist/{chunk-RRHHQYJS.js.map → chunk-3TSTGI6F.js.map} +0 -0
  287. /package/dist/{chunk-FO3HO52S.js.map → chunk-4H4F3BFL.js.map} +0 -0
  288. /package/dist/{chunk-BEUCACTV.js.map → chunk-4NOI577P.js.map} +0 -0
  289. /package/dist/{chunk-BBIE7W52.js.map → chunk-4NR2PDZ5.js.map} +0 -0
  290. /package/dist/{chunk-PKMQK2WY.js.map → chunk-773DPI6T.js.map} +0 -0
  291. /package/dist/{chunk-MQVKHFIU.js.map → chunk-7RLCCNHU.js.map} +0 -0
  292. /package/dist/{chunk-EZNDNMRH.js.map → chunk-AQLOP5F4.js.map} +0 -0
  293. /package/dist/{chunk-UNNDFYUX.js.map → chunk-ARV3AUOM.js.map} +0 -0
  294. /package/dist/{chunk-NPL7VDM6.js.map → chunk-ASKXIAHD.js.map} +0 -0
  295. /package/dist/{chunk-K4ZNARDO.js.map → chunk-CBWI22JN.js.map} +0 -0
  296. /package/dist/{chunk-ZOHMPFOB.js.map → chunk-CKFLLAIS.js.map} +0 -0
  297. /package/dist/{chunk-DCKNMLEQ.js.map → chunk-CSRXQ6NB.js.map} +0 -0
  298. /package/dist/{chunk-6AFVIOVZ.js.map → chunk-CTOQEZSN.js.map} +0 -0
  299. /package/dist/{chunk-7NS7AWMZ.js.map → chunk-DKJCRGWM.js.map} +0 -0
  300. /package/dist/{chunk-JPUT5NGH.js.map → chunk-DMQWV6SQ.js.map} +0 -0
  301. /package/dist/{chunk-F3XZN24Z.js.map → chunk-DRUDC2UX.js.map} +0 -0
  302. /package/dist/{chunk-KJR66XOM.js.map → chunk-EY6MDMHK.js.map} +0 -0
  303. /package/dist/{chunk-3TUT4PWB.js.map → chunk-FVZ2ISIX.js.map} +0 -0
  304. /package/dist/{chunk-6SQQOJ4U.js.map → chunk-GKRLPHPU.js.map} +0 -0
  305. /package/dist/{chunk-5NWYQUQC.js.map → chunk-HOHVIVDR.js.map} +0 -0
  306. /package/dist/{chunk-6WAJQKBS.js.map → chunk-JYLFZWUX.js.map} +0 -0
  307. /package/dist/{chunk-5W7CTZ2E.js.map → chunk-KQAFEZQX.js.map} +0 -0
  308. /package/dist/{chunk-YTZ3VGC7.js.map → chunk-M4V2JVSZ.js.map} +0 -0
  309. /package/dist/{chunk-PLG6JHQS.js.map → chunk-MJIHJ7OG.js.map} +0 -0
  310. /package/dist/{chunk-TA3Y6GEX.js.map → chunk-MLKWJOAJ.js.map} +0 -0
  311. /package/dist/{chunk-UKTDXTWX.js.map → chunk-NSA5ZRUL.js.map} +0 -0
  312. /package/dist/{chunk-DQXPJ6HD.js.map → chunk-OEEW4KJA.js.map} +0 -0
  313. /package/dist/{chunk-5OOA7SGX.js.map → chunk-R7IYU5DZ.js.map} +0 -0
  314. /package/dist/{chunk-WGTWGDR5.js.map → chunk-SNKPUJHR.js.map} +0 -0
  315. /package/dist/{chunk-BB57OZWC.js.map → chunk-UVPL565C.js.map} +0 -0
  316. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  317. /package/dist/{chunk-V7CMKMHM.js.map → chunk-Y4YD5TYW.js.map} +0 -0
  318. /package/dist/{chunk-3MS3QVX6.js.map → chunk-YOLZS7NY.js.map} +0 -0
@@ -34,6 +34,9 @@ import { getOperation } from "./access-boundary.js";
34
34
  // dispatch the migrated routes through the registry (issue #1525).
35
35
  import "./access-operations.js";
36
36
  import { handleChatMessage, handleChatEventsSSE } from "./chat/chat-http.js";
37
+ // cleanupExpiredChatSessions wires the chat session TTL sweep into the
38
+ // server lifecycle (issue #1685 item 1 / #1687 Thread 21).
39
+ import { cleanupExpiredChatSessions } from "./chat/chat-session.js";
37
40
 
38
41
  export interface EngramAccessHttpServerOptions {
39
42
  service: EngramAccessService;
@@ -293,6 +296,13 @@ export class EngramAccessHttpServer {
293
296
  */
294
297
  private readonly sseCleanupFns = new Set<() => void>();
295
298
 
299
+ /**
300
+ * Periodic chat-session TTL sweep handle (issue #1685 item 1 /
301
+ * #1687 Thread 21). unref'd so it never blocks process exit
302
+ * (rule 47); cleared in `stop()`.
303
+ */
304
+ private chatTtlTimer: NodeJS.Timeout | null = null;
305
+
296
306
  constructor(options: EngramAccessHttpServerOptions) {
297
307
  this.service = options.service;
298
308
  this.host = options.host?.trim() || "127.0.0.1";
@@ -386,11 +396,25 @@ export class EngramAccessHttpServer {
386
396
  this.server = server;
387
397
  const address = server.address();
388
398
  this.boundPort = typeof address === "object" && address ? address.port : this.requestedPort;
399
+
400
+ // ── Chat session TTL sweep (issue #1685 item 1 / #1687 Thread 21) ──
401
+ // Expire idle chat sessions so the JSONL store cannot grow unbounded.
402
+ // A one-shot sweep runs on startup; a periodic re-sweep follows on an
403
+ // unref'd 1-hour cadence (rule 47 — never blocks process exit). Both
404
+ // are no-ops when chat is disabled (no TTL configured).
405
+ this.scheduleChatSessionTtlSweep();
406
+
389
407
  return this.status();
390
408
  }
391
409
 
392
410
  async stop(): Promise<void> {
393
411
  if (!this.server) return;
412
+ // Release the chat-session TTL sweep timer (issue #1685) alongside
413
+ // the SSE cleanups below.
414
+ if (this.chatTtlTimer) {
415
+ clearInterval(this.chatTtlTimer);
416
+ this.chatTtlTimer = null;
417
+ }
394
418
  const server = this.server;
395
419
  this.server = null;
396
420
  this.boundPort = 0;
@@ -418,6 +442,36 @@ export class EngramAccessHttpServer {
418
442
  });
419
443
  }
420
444
 
445
+ /**
446
+ * Schedule the chat-session TTL sweep: one immediate pass plus a
447
+ * recurring hourly pass (issue #1685 item 1 / #1687 Thread 21). Both
448
+ * are skipped when chat is disabled. The recurring timer is unref'd
449
+ * so it never keeps the event loop alive on its own (rule 47). The
450
+ * sweep itself is best-effort — cleanup failures are swallowed inside
451
+ * `cleanupExpiredChatSessions`.
452
+ */
453
+ private scheduleChatSessionTtlSweep(): void {
454
+ // Clear any prior handle so a second start() without an intervening
455
+ // stop() cannot leak a dangling setInterval (Kilo review thread).
456
+ if (this.chatTtlTimer) {
457
+ clearInterval(this.chatTtlTimer);
458
+ this.chatTtlTimer = null;
459
+ }
460
+ const chat = this.service.configRef?.chat;
461
+ if (!chat?.enabled) return;
462
+ const ttlHours = typeof chat.sessionTtlHours === "number" && chat.sessionTtlHours > 0
463
+ ? chat.sessionTtlHours
464
+ : 72;
465
+ // Startup sweep — fire and forget; failures never block the server.
466
+ void cleanupExpiredChatSessions(this.service.memoryDir, ttlHours).catch(() => { /* best-effort */ });
467
+ // Hourly re-sweep (unref'd — rule 47).
468
+ const SWEEP_INTERVAL_MS = 60 * 60 * 1000;
469
+ this.chatTtlTimer = setInterval(() => {
470
+ void cleanupExpiredChatSessions(this.service.memoryDir, ttlHours).catch(() => { /* best-effort */ });
471
+ }, SWEEP_INTERVAL_MS);
472
+ this.chatTtlTimer.unref?.();
473
+ }
474
+
421
475
  status(): EngramAccessHttpServerStatus {
422
476
  return {
423
477
  running: this.server !== null,
@@ -2083,7 +2137,20 @@ export class EngramAccessHttpServer {
2083
2137
  void getOperation("chat_events"); // boundary dispatch (issue #1525)
2084
2138
  await handleChatEventsSSE(
2085
2139
  req, res, chatEventsMatch[1] ?? "",
2086
- { service: this.service, config: this.service.configRef?.chat, memoryDir: this.service.memoryDir },
2140
+ {
2141
+ service: this.service,
2142
+ config: this.service.configRef?.chat,
2143
+ memoryDir: this.service.memoryDir,
2144
+ // Register the chat-SSE disconnect cleanup with this server's
2145
+ // stop() set so an HTTP shutdown forcibly releases the heartbeat
2146
+ // and transcript subscription even when a lingering client never
2147
+ // emits 'close' (cursor Medium review; mirrors handleGraphEventsSSE
2148
+ // which registers in sseCleanupFns at access-http.ts:2604).
2149
+ registerSseCleanup: (cleanup) => {
2150
+ this.sseCleanupFns.add(cleanup);
2151
+ return () => { this.sseCleanupFns.delete(cleanup); };
2152
+ },
2153
+ },
2087
2154
  this.resolveRequestPrincipal(req),
2088
2155
  );
2089
2156
  return;
@@ -13,6 +13,7 @@ import path from "node:path";
13
13
  import test from "node:test";
14
14
 
15
15
  import { NamespaceCatalog } from "../namespaces/catalog.js";
16
+ import { resolveScopeProfilePlan } from "../namespaces/scope-profiles.js";
16
17
  import { parseConfig } from "../config.js";
17
18
  import {
18
19
  AdminPromotionError,
@@ -456,3 +457,291 @@ test("promoteMemory throws source_not_found when the source memory is absent", a
456
457
  (err: unknown) => err instanceof AdminPromotionError && err.code === "source_not_found",
457
458
  );
458
459
  });
460
+
461
+ // ---------------------------------------------------------------------------
462
+ // gatherMaintenanceHealth — honest QMD state classification (issue #1658 thread 1)
463
+ // ---------------------------------------------------------------------------
464
+
465
+ /** Build a QMD health snapshot fixture for the provider. */
466
+ function qmdFixture(namespace: string, overrides: Partial<Record<string, unknown>> = {}) {
467
+ return {
468
+ namespace,
469
+ collection: `col-${namespace}`,
470
+ available: true,
471
+ collectionState: "present",
472
+ debugStatus: "ok",
473
+ installedVersion: "1.0.0",
474
+ supportedVersion: "1.0.0",
475
+ supported: true,
476
+ upgradeAvailable: false,
477
+ daemonMode: false,
478
+ ...overrides,
479
+ };
480
+ }
481
+
482
+ test("gatherMaintenanceHealth classifies a present QMD collection as healthy (issue #1658 thread 1)", async () => {
483
+ const memoryDir = await makeTempDir();
484
+ try {
485
+ const config = makeConfig({ memoryDir });
486
+ const catalog = new NamespaceCatalog(config);
487
+ await catalog.registerConfiguredNamespaces();
488
+ const report = await gatherMaintenanceHealth({
489
+ catalog,
490
+ qmdHealthProvider: async (ns) => qmdFixture(ns, { collectionState: "present" }),
491
+ });
492
+ assert.equal(report.degradedMode, false);
493
+ const entry = report.perNamespace.find((e) => e.namespace === "default");
494
+ assert.ok(entry, "default namespace entry must exist");
495
+ assert.equal(entry!.qmdState, "healthy");
496
+ assert.equal(entry!.qmdDegraded, false);
497
+ assert.match(entry!.qmdStateReason, /ready/i);
498
+ } finally {
499
+ await rm(memoryDir, { recursive: true, force: true });
500
+ }
501
+ });
502
+
503
+ test("gatherMaintenanceHealth classifies an unknown collection state distinctly from missing (issue #1658 thread 1)", async () => {
504
+ const memoryDir = await makeTempDir();
505
+ try {
506
+ const config = makeConfig({ memoryDir });
507
+ const catalog = new NamespaceCatalog(config);
508
+ await catalog.registerConfiguredNamespaces();
509
+ const report = await gatherMaintenanceHealth({
510
+ catalog,
511
+ qmdHealthProvider: async (ns) => qmdFixture(ns, { collectionState: "unknown" }),
512
+ });
513
+ // "unknown" is still counted degraded (matches the router health path) so
514
+ // aggregate alerting is unchanged, but it is now RENDERED honestly as a
515
+ // distinct state rather than collapsed into "missing".
516
+ assert.equal(report.degradedMode, true);
517
+ const entry = report.perNamespace.find((e) => e.namespace === "default");
518
+ assert.ok(entry);
519
+ assert.equal(entry!.qmdState, "unknown");
520
+ assert.equal(entry!.qmdDegraded, true);
521
+ assert.match(entry!.qmdStateReason, /transient/i);
522
+ } finally {
523
+ await rm(memoryDir, { recursive: true, force: true });
524
+ }
525
+ });
526
+
527
+ test("gatherMaintenanceHealth classifies a missing collection as an actionable state (issue #1658 thread 1)", async () => {
528
+ const memoryDir = await makeTempDir();
529
+ try {
530
+ const config = makeConfig({ memoryDir });
531
+ const catalog = new NamespaceCatalog(config);
532
+ await catalog.registerConfiguredNamespaces();
533
+ const report = await gatherMaintenanceHealth({
534
+ catalog,
535
+ qmdHealthProvider: async (ns) => qmdFixture(ns, { collectionState: "missing" }),
536
+ });
537
+ const entry = report.perNamespace.find((e) => e.namespace === "default");
538
+ assert.ok(entry);
539
+ assert.equal(entry!.qmdState, "missing");
540
+ assert.equal(entry!.qmdDegraded, true);
541
+ assert.match(entry!.qmdStateReason, /not built|index/i);
542
+ } finally {
543
+ await rm(memoryDir, { recursive: true, force: true });
544
+ }
545
+ });
546
+
547
+ test("gatherMaintenanceHealth classifies an unavailable backend distinctly (issue #1658 thread 1)", async () => {
548
+ const memoryDir = await makeTempDir();
549
+ try {
550
+ const config = makeConfig({ memoryDir });
551
+ const catalog = new NamespaceCatalog(config);
552
+ await catalog.registerConfiguredNamespaces();
553
+ const report = await gatherMaintenanceHealth({
554
+ catalog,
555
+ qmdHealthProvider: async (ns) => qmdFixture(ns, { available: false, collectionState: "unknown" }),
556
+ });
557
+ const entry = report.perNamespace.find((e) => e.namespace === "default");
558
+ assert.ok(entry);
559
+ assert.equal(entry!.qmdState, "unavailable");
560
+ assert.equal(entry!.qmdDegraded, true);
561
+ } finally {
562
+ await rm(memoryDir, { recursive: true, force: true });
563
+ }
564
+ });
565
+
566
+ test("gatherMaintenanceHealth reports not_probed when no provider is supplied (issue #1658 thread 1)", async () => {
567
+ const memoryDir = await makeTempDir();
568
+ try {
569
+ const config = makeConfig({ memoryDir });
570
+ const catalog = new NamespaceCatalog(config);
571
+ await catalog.registerConfiguredNamespaces();
572
+ const report = await gatherMaintenanceHealth({ catalog });
573
+ const entry = report.perNamespace.find((e) => e.namespace === "default");
574
+ assert.ok(entry);
575
+ assert.equal(entry!.qmdState, "not_probed");
576
+ assert.equal(entry!.qmdDegraded, false);
577
+ } finally {
578
+ await rm(memoryDir, { recursive: true, force: true });
579
+ }
580
+ });
581
+
582
+ // ---------------------------------------------------------------------------
583
+ // promoteMemory — honest project-context promotion reasons (issue #1658 thread 2)
584
+ // ---------------------------------------------------------------------------
585
+
586
+ test("promoteMemory reports an honest 'no active scope profile' reason for a project target without a profile (issue #1658 thread 2)", async () => {
587
+ const config = makeConfig();
588
+ const storage = makeMemoryStorageProvider();
589
+ const result = await promoteMemory({
590
+ config,
591
+ sourceMemoryId: "mem-1",
592
+ sourceNamespace: "default",
593
+ principal: "alice",
594
+ targets: [{ kind: "userProject" }],
595
+ reason: "promote project fact",
596
+ actor: "alice",
597
+ storage,
598
+ // scopeProfilePlan omitted → no active profile.
599
+ });
600
+ assert.equal(result.ok, false);
601
+ const target = result.targets[0]!;
602
+ assert.equal(target.authorized, false);
603
+ assert.equal(target.namespace, "");
604
+ assert.match(target.reason, /no active scope profile/);
605
+ assert.match(target.reason, /coding context/i);
606
+ assert.equal(storage.written.length, 0);
607
+ });
608
+
609
+ test("promoteMemory reports an honest 'no active scope profile' reason for a non-project target without a profile (issue #1658 thread 2)", async () => {
610
+ const config = makeConfig();
611
+ const storage = makeMemoryStorageProvider();
612
+ const result = await promoteMemory({
613
+ config,
614
+ sourceMemoryId: "mem-1",
615
+ sourceNamespace: "default",
616
+ principal: "alice",
617
+ targets: [{ kind: "serverShared" }],
618
+ reason: "promote shared fact",
619
+ actor: "alice",
620
+ storage,
621
+ });
622
+ assert.equal(result.ok, false);
623
+ const target = result.targets[0]!;
624
+ assert.match(target.reason, /no active scope profile/);
625
+ // Non-project targets must NOT mention coding context.
626
+ assert.doesNotMatch(target.reason, /coding context/i);
627
+ });
628
+
629
+ test("promoteMemory surfaces the profile layer's 'missing project context' reason when a project target is configured but unresolvable (issue #1658 thread 2)", async () => {
630
+ const config = makeConfig({
631
+ defaultScopeProfile: "team",
632
+ scopeProfiles: {
633
+ team: {
634
+ readOrder: ["userGlobal"],
635
+ writeDefault: "userGlobal",
636
+ promotionTargets: ["userProject"],
637
+ autoPromote: { enabled: false, targets: [], categories: ["fact"], minConfidenceTier: "speculative" },
638
+ },
639
+ },
640
+ principalFromSessionKeyMode: "prefix",
641
+ principalFromSessionKeyRules: [{ match: "alice:", principal: "alice" }],
642
+ namespacePolicies: [{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] }],
643
+ });
644
+ // No coding context → the userProject layer resolves "missing project context".
645
+ const scopeProfilePlan = resolveScopeProfilePlan({
646
+ config,
647
+ principal: "alice",
648
+ codingContext: null,
649
+ codingOverlay: null,
650
+ });
651
+ assert.ok(scopeProfilePlan, "scope profile must be active for this config");
652
+ const storage = makeMemoryStorageProvider();
653
+ const result = await promoteMemory({
654
+ config,
655
+ sourceMemoryId: "mem-1",
656
+ sourceNamespace: "alice",
657
+ principal: "alice",
658
+ targets: [{ kind: "userProject" }],
659
+ reason: "promote project fact",
660
+ actor: "alice",
661
+ storage,
662
+ scopeProfilePlan,
663
+ });
664
+ assert.equal(result.ok, false);
665
+ const target = result.targets[0]!;
666
+ assert.equal(target.authorized, false);
667
+ assert.match(target.reason, /missing project context/i);
668
+ });
669
+
670
+ // ---------------------------------------------------------------------------
671
+ // inspectScope — read-only override honest warning (issue #1658 thread 6)
672
+ // ---------------------------------------------------------------------------
673
+
674
+ test("inspectScope warns when an explicit override is readable but not writable (issue #1658 thread 6)", () => {
675
+ const config = makeConfig({
676
+ principalFromSessionKeyMode: "prefix",
677
+ principalFromSessionKeyRules: [{ match: "alice:", principal: "alice" }],
678
+ namespacePolicies: [
679
+ // alice can READ "audit-log" but cannot WRITE it.
680
+ { name: "audit-log", readPrincipals: ["alice", "bob"], writePrincipals: ["bob"] },
681
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
682
+ ],
683
+ });
684
+ const inspection = inspectScope({
685
+ config,
686
+ sessionKey: "alice:bot:x:1",
687
+ namespace: "audit-log",
688
+ });
689
+ // The override is readable → it becomes the effective write namespace...
690
+ assert.equal(inspection.namespaceOverride, "audit-log");
691
+ assert.equal(inspection.writeNamespace, "audit-log");
692
+ // ...but alice cannot write it → an honest warning must surface the mismatch.
693
+ assert.ok(
694
+ inspection.warnings.some(
695
+ (w) => w.includes("audit-log") && w.includes("not writable") && w.includes("writes will be rejected"),
696
+ ),
697
+ "expected a read-only-override warning; got: " + JSON.stringify(inspection.warnings),
698
+ );
699
+ });
700
+
701
+ test("inspectScope does not warn when an explicit override is writable (issue #1658 thread 6)", () => {
702
+ const config = makeConfig({
703
+ principalFromSessionKeyMode: "prefix",
704
+ principalFromSessionKeyRules: [{ match: "alice:", principal: "alice" }],
705
+ namespacePolicies: [
706
+ { name: "shared", readPrincipals: ["alice"], writePrincipals: ["alice"] },
707
+ { name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
708
+ ],
709
+ });
710
+ const inspection = inspectScope({
711
+ config,
712
+ sessionKey: "alice:bot:x:1",
713
+ namespace: "shared",
714
+ });
715
+ assert.equal(inspection.namespaceOverride, "shared");
716
+ assert.ok(
717
+ !inspection.warnings.some((w) => w.includes("not writable")),
718
+ "no read-only-override warning expected for a writable override",
719
+ );
720
+ });
721
+
722
+ test("gatherMaintenanceHealth preserves the original qmdDegraded invariant for an unrecognized collectionState (issue #1658 thread 1)", async () => {
723
+ // The original degraded formula was: !available || "missing" || "unknown".
724
+ // An available backend with any OTHER collectionState (present/skipped/and any
725
+ // unrecognized value) was NOT degraded. The classified qmdState surfaces the
726
+ // unrecognized value honestly, but the degraded bit must stay false so the
727
+ // aggregate alerting invariant is byte-for-byte unchanged.
728
+ const memoryDir = await makeTempDir();
729
+ try {
730
+ const config = makeConfig({ memoryDir });
731
+ const catalog = new NamespaceCatalog(config);
732
+ await catalog.registerConfiguredNamespaces();
733
+ const report = await gatherMaintenanceHealth({
734
+ catalog,
735
+ qmdHealthProvider: async (ns) =>
736
+ qmdFixture(ns, { collectionState: "future-state" }),
737
+ });
738
+ const entry = report.perNamespace.find((e) => e.namespace === "default");
739
+ assert.ok(entry);
740
+ assert.equal(entry!.qmdDegraded, false, "unrecognized collectionState must not flip degraded (unchanged semantics)");
741
+ assert.equal(entry!.qmdState, "unknown");
742
+ assert.match(entry!.qmdStateReason, /unrecognized collection state 'future-state'/);
743
+ assert.equal(report.degradedMode, false);
744
+ } finally {
745
+ await rm(memoryDir, { recursive: true, force: true });
746
+ }
747
+ });
@@ -213,6 +213,17 @@ export function inspectScope(options: InspectScopeOptions): ScopeInspection {
213
213
  `requested namespace override '${options.namespace}' is not readable by this principal and was ignored`
214
214
  );
215
215
  }
216
+ // Issue #1658 thread 6: when an explicit namespace override is readable but
217
+ // NOT writable, it still becomes the effective write namespace (baseNamespace
218
+ // in the scope plan). Surface the mismatch honestly so the operator knows
219
+ // writes will be rejected rather than reading writeNamespace as a usable
220
+ // target. (The per-layer writable flag already records this, but the
221
+ // top-level writeNamespace field does not.)
222
+ if (plan.namespaceOverride && !canWriteNamespace(plan.principal, plan.namespaceOverride, config)) {
223
+ warnings.push(
224
+ `explicit namespace override '${plan.namespaceOverride}' is readable but not writable by this principal; it is the effective write namespace but writes will be rejected`
225
+ );
226
+ }
216
227
 
217
228
  const layers: ScopeInspectionLayer[] = [];
218
229
  const scopeProfilePlan = plan.scopeProfilePlan;
@@ -417,6 +428,27 @@ function toAdminEntry(record: NamespaceRecord, staleBefore: Date): AdminNamespac
417
428
  // Maintenance and QMD health
418
429
  // ---------------------------------------------------------------------------
419
430
 
431
+ /**
432
+ * Honest, distinguishable classification of a namespace's QMD/search health
433
+ * (issue #1658 thread 1). A single `qmdDegraded` bit collapses the legitimate
434
+ * transient "unknown" (QMD reachable but collection status not yet populated,
435
+ * normal during startup/reindex) together with the actionable "missing"
436
+ * (collection not built — operator must act) and "unavailable" (backend down).
437
+ * Surfacing a classified state lets the dashboard render each case honestly
438
+ * instead of a flat degraded flag or a silent blank.
439
+ */
440
+ export type AdminNamespaceQmdState =
441
+ /** Backend reachable and collection present (or intentionally skipped). */
442
+ | "healthy"
443
+ /** Backend reachable but collection status not yet determined (transient). */
444
+ | "unknown"
445
+ /** Backend reachable but the collection is not built for this namespace. */
446
+ | "missing"
447
+ /** Backend unreachable / probe threw. */
448
+ | "unavailable"
449
+ /** No QMD probe was run for this report (provider omitted or returned null). */
450
+ | "not_probed";
451
+
420
452
  /** QMD/search health snapshot for one namespace (delegated to NamespaceSearchRouter). */
421
453
  export interface AdminNamespaceQmdHealth {
422
454
  namespace: string;
@@ -438,8 +470,20 @@ export interface AdminNamespaceHealth {
438
470
  lastMaintenanceAt?: Record<string, string>;
439
471
  /** True when no maintenance has ever been recorded. */
440
472
  maintenanceMissing: boolean;
441
- /** True when QMD is unavailable OR its collection is missing for this namespace. */
473
+ /**
474
+ * True when QMD is unavailable OR its collection is missing/unknown for this
475
+ * namespace. Matches the NamespaceSearchRouter health path so the aggregate
476
+ * `degradedMode` flag stays consistent with runtime alerting.
477
+ */
442
478
  qmdDegraded: boolean;
479
+ /**
480
+ * Honest, distinguishable QMD state (issue #1658 thread 1). Complements
481
+ * `qmdDegraded` so the dashboard can render "unknown" (transient) separately
482
+ * from "missing"/"unavailable" (action needed) instead of a flat bit.
483
+ */
484
+ qmdState: AdminNamespaceQmdState;
485
+ /** Human-readable explanation of {@link qmdState}, safe to show operators. */
486
+ qmdStateReason: string;
443
487
  qmd?: AdminNamespaceQmdHealth;
444
488
  /** Reason the QMD probe failed, when it did. */
445
489
  qmdError?: string;
@@ -473,6 +517,66 @@ export interface MaintenanceHealthOptions {
473
517
  * timestamps from the catalog and (optionally) QMD diagnostics from the
474
518
  * injected provider.
475
519
  */
520
+ /**
521
+ * Classify a QMD health snapshot into an honest, distinguishable state bucket
522
+ * (issue #1658 thread 1). The `degraded` bit mirrors the NamespaceSearchRouter
523
+ * health path (unavailable / missing / unknown) so the aggregate `degradedMode`
524
+ * flag and any downstream alerting stay consistent; the classified `state` +
525
+ * `reason` let the dashboard render each case distinctly instead of a flat bit.
526
+ */
527
+ function classifyQmdState(qmd: AdminNamespaceQmdHealth): {
528
+ state: AdminNamespaceQmdState;
529
+ reason: string;
530
+ degraded: boolean;
531
+ } {
532
+ if (!qmd.available) {
533
+ return {
534
+ state: "unavailable",
535
+ reason: "QMD backend is unavailable for this namespace",
536
+ degraded: true,
537
+ };
538
+ }
539
+ // collectionState is the router's string ("present" | "missing" | "unknown"
540
+ // | "skipped"); classify defensively — an unrecognized value is reported as
541
+ // "unknown" rather than silently treated as healthy.
542
+ switch (qmd.collectionState) {
543
+ case "present":
544
+ return { state: "healthy", reason: "QMD collection is ready", degraded: false };
545
+ case "skipped":
546
+ // "skipped" means QMD is intentionally disabled for this namespace; it is
547
+ // neither degraded nor a blank — surface it honestly as healthy/idle.
548
+ return {
549
+ state: "healthy",
550
+ reason: "QMD is skipped for this namespace (intentionally disabled)",
551
+ degraded: false,
552
+ };
553
+ case "missing":
554
+ return {
555
+ state: "missing",
556
+ reason: "QMD collection is not built for this namespace — add the collection to the QMD index",
557
+ degraded: true,
558
+ };
559
+ case "unknown":
560
+ return {
561
+ state: "unknown",
562
+ reason: "QMD collection status could not be determined (transient during startup/reindex)",
563
+ degraded: true,
564
+ };
565
+ default:
566
+ // Preserve the ORIGINAL qmdDegraded invariant exactly: an available
567
+ // backend with an unrecognized collectionState was NOT degraded before
568
+ // this PR (the pre-image only flagged missing/unknown/unavailable). We
569
+ // still surface the unrecognized value HONESTLY via state/reason so the
570
+ // dashboard can render it, but we do not flip the degraded bit — that
571
+ // would change alerting and contradict the "unchanged semantics" claim.
572
+ return {
573
+ state: "unknown",
574
+ reason: `QMD reported an unrecognized collection state '${qmd.collectionState}'`,
575
+ degraded: false,
576
+ };
577
+ }
578
+ }
579
+
476
580
  export async function gatherMaintenanceHealth(options: MaintenanceHealthOptions): Promise<MaintenanceHealthReport> {
477
581
  const { catalog, qmdHealthProvider } = options;
478
582
  if (!catalog.enabled) {
@@ -488,22 +592,32 @@ export async function gatherMaintenanceHealth(options: MaintenanceHealthOptions)
488
592
  lastMaintenanceAt: record.lastMaintenanceAt,
489
593
  maintenanceMissing: !record.lastMaintenanceAt || Object.keys(record.lastMaintenanceAt).length === 0,
490
594
  qmdDegraded: false,
595
+ qmdState: "not_probed",
596
+ qmdStateReason: "QMD health probe not run for this report",
491
597
  };
492
598
  if (qmdHealthProvider) {
493
599
  try {
494
600
  const qmd = await qmdHealthProvider(record.namespace);
495
601
  if (qmd) {
496
602
  entry.qmd = qmd;
497
- entry.qmdDegraded =
498
- !qmd.available ||
499
- qmd.collectionState === "missing" ||
500
- qmd.collectionState === "unknown";
603
+ // Classify the collection state into an honest, distinguishable
604
+ // bucket (issue #1658 thread 1) and derive the degraded bit from
605
+ // the same inputs the NamespaceSearchRouter health path uses.
606
+ const classified = classifyQmdState(qmd);
607
+ entry.qmdState = classified.state;
608
+ entry.qmdStateReason = classified.reason;
609
+ entry.qmdDegraded = classified.degraded;
610
+ } else {
611
+ entry.qmdState = "not_probed";
612
+ entry.qmdStateReason = "QMD health not available for this namespace";
501
613
  }
502
614
  } catch (err) {
503
- entry.qmdDegraded = true;
504
615
  // Sanitize: never echo raw error messages to the dashboard. The
505
616
  // generic diagnostic is sufficient for operators; the full error
506
617
  // is logged by the qmdHealthProvider's caller (access-service).
618
+ entry.qmdDegraded = true;
619
+ entry.qmdState = "unavailable";
620
+ entry.qmdStateReason = "QMD health probe failed";
507
621
  entry.qmdError = "QMD health probe failed";
508
622
  }
509
623
  }
@@ -735,7 +849,24 @@ export async function promoteMemory(options: PromoteMemoryOptions): Promise<Memo
735
849
  : "explicit namespace is not writable by this principal",
736
850
  };
737
851
  }
738
- const layer = scopeProfilePlan?.promotionTargets.find((t) => t.target === requested.kind);
852
+ if (!scopeProfilePlan) {
853
+ // Issue #1658 thread 2: distinguish "no active scope profile" from
854
+ // "profile does not configure this target". A project-scoped target
855
+ // (userProject/teamProject) cannot resolve without an active profile +
856
+ // coding context; report that honestly instead of a generic "not
857
+ // configured" message that implies a profile is active.
858
+ return {
859
+ target: requested.kind,
860
+ namespace: "",
861
+ authorized: false,
862
+ promoted: false,
863
+ reason:
864
+ requested.kind === "userProject" || requested.kind === "teamProject"
865
+ ? `no active scope profile; '${requested.kind}' promotion requires an active scope profile with a coding context (pass a sessionKey)`
866
+ : `no active scope profile; '${requested.kind}' promotion requires an active scope profile`,
867
+ };
868
+ }
869
+ const layer = scopeProfilePlan.promotionTargets.find((t) => t.target === requested.kind);
739
870
  if (!layer) {
740
871
  return {
741
872
  target: requested.kind,