@remnic/core 9.3.612 → 9.3.614

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 (374) hide show
  1. package/dist/access-cli.js +58 -57
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.d.ts +4 -2
  4. package/dist/access-http.js +22 -22
  5. package/dist/access-mcp.d.ts +9 -2
  6. package/dist/access-mcp.js +19 -19
  7. package/dist/access-schema.d.ts +12 -12
  8. package/dist/access-schema.js +3 -3
  9. package/dist/{access-service-D2J9dh_9.d.ts → access-service-DGG_2xPK.d.ts} +1 -1
  10. package/dist/access-service.d.ts +2 -2
  11. package/dist/access-service.js +16 -16
  12. package/dist/active-recall.js +20 -3
  13. package/dist/active-recall.js.map +1 -1
  14. package/dist/adapters/index.js +4 -4
  15. package/dist/adapters/registry.js +2 -2
  16. package/dist/behavior-learner.js +2 -3
  17. package/dist/behavior-learner.js.map +1 -1
  18. package/dist/bootstrap.d.ts +1 -1
  19. package/dist/briefing.js +3 -3
  20. package/dist/buffer.d.ts +1 -1
  21. package/dist/buffer.js +1 -1
  22. package/dist/calibration.d.ts +5 -2
  23. package/dist/calibration.js +7 -5
  24. package/dist/calibration.js.map +1 -1
  25. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-YO5QJ6L3.js} +2 -2
  26. package/dist/causal-consolidation.d.ts +8 -2
  27. package/dist/causal-consolidation.js +13 -11
  28. package/dist/causal-consolidation.js.map +1 -1
  29. package/dist/{chunk-3BP57I6J.js → chunk-2F6NP3NT.js} +2 -1
  30. package/dist/{chunk-3BP57I6J.js.map → chunk-2F6NP3NT.js.map} +1 -1
  31. package/dist/{chunk-AU7Q3LSC.js → chunk-2QSZNTDO.js} +4 -4
  32. package/dist/{chunk-HSVJGWYS.js → chunk-2ROPI5OE.js} +2 -2
  33. package/dist/{chunk-C4SQJZAF.js → chunk-2SGJY2UY.js} +6 -3
  34. package/dist/chunk-2SGJY2UY.js.map +1 -0
  35. package/dist/{chunk-ZDTVJXIP.js → chunk-3MAONBX3.js} +13 -5
  36. package/dist/chunk-3MAONBX3.js.map +1 -0
  37. package/dist/{chunk-G3Z3QEF5.js → chunk-3PY7VHV7.js} +2 -2
  38. package/dist/chunk-3PY7VHV7.js.map +1 -0
  39. package/dist/{chunk-CF3ZF2YU.js → chunk-3QSU4NFF.js} +3 -3
  40. package/dist/{chunk-AJA46VX5.js → chunk-3T74IZB3.js} +11 -2
  41. package/dist/chunk-3T74IZB3.js.map +1 -0
  42. package/dist/{chunk-KVEVLBKC.js → chunk-4HFJQCJZ.js} +13 -8
  43. package/dist/chunk-4HFJQCJZ.js.map +1 -0
  44. package/dist/{chunk-KGK2QKWL.js → chunk-4R4KTDIE.js} +1 -1
  45. package/dist/chunk-4R4KTDIE.js.map +1 -0
  46. package/dist/{chunk-OI27U2HT.js → chunk-5BTCT236.js} +2 -2
  47. package/dist/{chunk-CO7ZO4TU.js → chunk-5VDJMYTF.js} +2 -2
  48. package/dist/{chunk-BFBF3XEF.js → chunk-6BDVBBBY.js} +33 -25
  49. package/dist/{chunk-BFBF3XEF.js.map → chunk-6BDVBBBY.js.map} +1 -1
  50. package/dist/{chunk-EAZGEEG2.js → chunk-6L46YAEZ.js} +45 -9
  51. package/dist/chunk-6L46YAEZ.js.map +1 -0
  52. package/dist/{chunk-YFS5OEKO.js → chunk-7MLB4NCL.js} +2 -2
  53. package/dist/{chunk-IOTENEVL.js → chunk-7YQFWOF7.js} +57 -50
  54. package/dist/chunk-7YQFWOF7.js.map +1 -0
  55. package/dist/{chunk-2QANQKSQ.js → chunk-ADNZVFXG.js} +15 -15
  56. package/dist/{chunk-LZ3VEOU5.js → chunk-AL4RAJL5.js} +22 -5
  57. package/dist/chunk-AL4RAJL5.js.map +1 -0
  58. package/dist/{chunk-557IAFPD.js → chunk-APRRL26Q.js} +2 -2
  59. package/dist/{chunk-QDDHYAKV.js → chunk-AZDOWD2L.js} +2 -2
  60. package/dist/{chunk-TH67Q46T.js → chunk-B6FDZPCF.js} +17 -9
  61. package/dist/chunk-B6FDZPCF.js.map +1 -0
  62. package/dist/{chunk-MLT75J5S.js → chunk-B6SU7YSE.js} +3 -3
  63. package/dist/{chunk-FXKPZ3H6.js → chunk-BPSGLMQ4.js} +2 -2
  64. package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
  65. package/dist/{chunk-NOMEVTUD.js → chunk-C6C7XVKG.js} +5 -4
  66. package/dist/chunk-C6C7XVKG.js.map +1 -0
  67. package/dist/{chunk-XKIQZXUB.js → chunk-CI7RKSRE.js} +7 -1
  68. package/dist/chunk-CI7RKSRE.js.map +1 -0
  69. package/dist/{chunk-IK34DVAC.js → chunk-CIOMS6DI.js} +2 -2
  70. package/dist/{chunk-2I5JGH3M.js → chunk-CYEPCZN5.js} +2 -2
  71. package/dist/{chunk-2I5JGH3M.js.map → chunk-CYEPCZN5.js.map} +1 -1
  72. package/dist/{chunk-JHMFYY7L.js → chunk-DCGT4FPP.js} +13 -5
  73. package/dist/chunk-DCGT4FPP.js.map +1 -0
  74. package/dist/{chunk-7DZRO2DC.js → chunk-DEPRLVLK.js} +2 -2
  75. package/dist/{chunk-CSKLPDN6.js → chunk-DEVUWMME.js} +52 -19
  76. package/dist/chunk-DEVUWMME.js.map +1 -0
  77. package/dist/{chunk-DHGSZ3UD.js → chunk-DGNQRNLL.js} +2 -2
  78. package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
  79. package/dist/chunk-FAV25DUZ.js +12 -0
  80. package/dist/chunk-FAV25DUZ.js.map +1 -0
  81. package/dist/{chunk-ETUPBUHB.js → chunk-GDASG7NC.js} +2 -2
  82. package/dist/{chunk-L227SKTB.js → chunk-GDB4J2H3.js} +17 -1
  83. package/dist/chunk-GDB4J2H3.js.map +1 -0
  84. package/dist/{chunk-IP73YCZP.js → chunk-GLPBYIXN.js} +4 -2
  85. package/dist/chunk-GLPBYIXN.js.map +1 -0
  86. package/dist/{chunk-4HP7HIE3.js → chunk-HP5FMB6L.js} +2 -2
  87. package/dist/{chunk-EVZFIAPG.js → chunk-IBTZEBUD.js} +23 -10
  88. package/dist/chunk-IBTZEBUD.js.map +1 -0
  89. package/dist/{chunk-DOX2CG6Y.js → chunk-IEUU7O4F.js} +2 -2
  90. package/dist/{chunk-JNANKJLN.js → chunk-JOASJWQR.js} +2 -2
  91. package/dist/chunk-JOASJWQR.js.map +1 -0
  92. package/dist/{chunk-WSGF57U2.js → chunk-JQDZQ4TB.js} +2 -2
  93. package/dist/{chunk-HINSGUA7.js → chunk-KBL3JJR6.js} +9 -13
  94. package/dist/chunk-KBL3JJR6.js.map +1 -0
  95. package/dist/{chunk-W7L6HXUC.js → chunk-LXOM6IQU.js} +2 -2
  96. package/dist/{chunk-G6R5UD3Q.js → chunk-MGN7VHWQ.js} +42 -1
  97. package/dist/{chunk-G6R5UD3Q.js.map → chunk-MGN7VHWQ.js.map} +1 -1
  98. package/dist/{chunk-DLJ4IR6M.js → chunk-MHQC2WU2.js} +2 -2
  99. package/dist/chunk-MHQC2WU2.js.map +1 -0
  100. package/dist/{chunk-6JGNHWCI.js → chunk-OBIRVF36.js} +3 -3
  101. package/dist/{chunk-CHCA44C3.js → chunk-ODPLEWB6.js} +3 -3
  102. package/dist/chunk-ODPLEWB6.js.map +1 -0
  103. package/dist/{chunk-HENLZHIT.js → chunk-OIF36KGD.js} +7 -4
  104. package/dist/chunk-OIF36KGD.js.map +1 -0
  105. package/dist/{chunk-GUPISBV2.js → chunk-PP2JH3GP.js} +2 -2
  106. package/dist/{chunk-OXJBNGBK.js → chunk-PSUB67YB.js} +2 -2
  107. package/dist/{chunk-UWY7GIVS.js → chunk-PYIFUBRK.js} +45 -13
  108. package/dist/chunk-PYIFUBRK.js.map +1 -0
  109. package/dist/{chunk-KIB7SDIJ.js → chunk-Q6YIJGXJ.js} +2 -2
  110. package/dist/{chunk-PPPZY2EU.js → chunk-QEMCQFDW.js} +2 -2
  111. package/dist/{chunk-ZT3EGNLR.js → chunk-QPD426WT.js} +2 -2
  112. package/dist/{chunk-RLV3PQGH.js → chunk-QVO4YOB7.js} +6 -6
  113. package/dist/{chunk-GMAG2HS4.js → chunk-RG3LBSGH.js} +46 -9
  114. package/dist/chunk-RG3LBSGH.js.map +1 -0
  115. package/dist/{chunk-XSWKORGM.js → chunk-S53OYO3F.js} +3 -1
  116. package/dist/chunk-S53OYO3F.js.map +1 -0
  117. package/dist/{chunk-YCN4BVDK.js → chunk-SCPFRKIT.js} +4 -2
  118. package/dist/chunk-SCPFRKIT.js.map +1 -0
  119. package/dist/{chunk-HJNQQICM.js → chunk-T5XWMMU2.js} +107 -50
  120. package/dist/chunk-T5XWMMU2.js.map +1 -0
  121. package/dist/{chunk-NZPF2SYV.js → chunk-T7N6KQGS.js} +138 -5
  122. package/dist/chunk-T7N6KQGS.js.map +1 -0
  123. package/dist/{chunk-VJXSUAO7.js → chunk-TNOWU6RP.js} +13 -10
  124. package/dist/chunk-TNOWU6RP.js.map +1 -0
  125. package/dist/{chunk-PCI747N2.js → chunk-TZVQQTG4.js} +48 -19
  126. package/dist/chunk-TZVQQTG4.js.map +1 -0
  127. package/dist/{chunk-KQAFEZQX.js → chunk-VDX2J7OX.js} +2 -2
  128. package/dist/{chunk-IK7DCC5H.js → chunk-VMGLYN42.js} +2 -2
  129. package/dist/{chunk-5RPTH6AU.js → chunk-VPGUMLBA.js} +8 -7
  130. package/dist/chunk-VPGUMLBA.js.map +1 -0
  131. package/dist/{chunk-KM2A35EO.js → chunk-WB3LYXC5.js} +11 -7
  132. package/dist/chunk-WB3LYXC5.js.map +1 -0
  133. package/dist/{chunk-NSKYFGDL.js → chunk-X4QQB7O6.js} +2 -2
  134. package/dist/{chunk-HPWVAEET.js → chunk-X6IRLNOO.js} +3 -7
  135. package/dist/chunk-X6IRLNOO.js.map +1 -0
  136. package/dist/{chunk-46GJIW5M.js → chunk-XAZOWLW4.js} +5 -5
  137. package/dist/{chunk-46GJIW5M.js.map → chunk-XAZOWLW4.js.map} +1 -1
  138. package/dist/{chunk-XPSVGJYA.js → chunk-YRMKDTKF.js} +12 -9
  139. package/dist/chunk-YRMKDTKF.js.map +1 -0
  140. package/dist/{chunk-6ZZP4EJF.js → chunk-ZJR7VG5L.js} +3 -3
  141. package/dist/{chunk-6ZZP4EJF.js.map → chunk-ZJR7VG5L.js.map} +1 -1
  142. package/dist/{cli-OrfKXNU4.d.ts → cli-DWeu7eTY.d.ts} +6 -2
  143. package/dist/cli.d.ts +3 -3
  144. package/dist/cli.js +60 -59
  145. package/dist/compounding/engine.js +3 -3
  146. package/dist/compounding/preference-consolidator.js +39 -11
  147. package/dist/compounding/preference-consolidator.js.map +1 -1
  148. package/dist/config.js +1 -1
  149. package/dist/connectors/codex-materialize-runner.js +3 -3
  150. package/dist/connectors/index.js +3 -3
  151. package/dist/consolidation-provenance-check.js +1 -1
  152. package/dist/contradiction/index.js +4 -4
  153. package/dist/conversation-index/backend.js +2 -2
  154. package/dist/conversation-index/indexer.js +1 -1
  155. package/dist/cross-namespace-budget.js +1 -1
  156. package/dist/enrichment/index.js +1 -1
  157. package/dist/entity-retrieval.js +3 -3
  158. package/dist/evals.js +1 -1
  159. package/dist/explicit-capture.d.ts +1 -1
  160. package/dist/extraction-judge.js +8 -1
  161. package/dist/extraction.js +2 -2
  162. package/dist/fallback-llm.d.ts +23 -6
  163. package/dist/fallback-llm.js +5 -3
  164. package/dist/{first-start-migration-GYJWIH36.js → first-start-migration-FF7YFGRP.js} +6 -6
  165. package/dist/index.d.ts +3 -3
  166. package/dist/index.js +94 -93
  167. package/dist/index.js.map +1 -1
  168. package/dist/lcm/archive.js +2 -2
  169. package/dist/lcm/engine.js +5 -5
  170. package/dist/lcm/index.js +7 -7
  171. package/dist/lcm/summarizer.js +3 -3
  172. package/dist/maintenance/memory-governance-cron.d.ts +6 -4
  173. package/dist/maintenance/memory-governance-cron.js +1 -1
  174. package/dist/maintenance/memory-governance.js +3 -3
  175. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  176. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  177. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  178. package/dist/mcp-memory-inspector-app.js +1 -1
  179. package/dist/migrate/from-engram.js +1 -1
  180. package/dist/namespaces/migrate.js +16 -15
  181. package/dist/namespaces/search.js +12 -11
  182. package/dist/namespaces/storage.js +3 -3
  183. package/dist/network/webdav.d.ts +2 -0
  184. package/dist/network/webdav.js +1 -1
  185. package/dist/objective-state-writers.js +2 -2
  186. package/dist/operator-toolkit.d.ts +3 -1
  187. package/dist/operator-toolkit.js +21 -20
  188. package/dist/{orchestrator-DTRQG75J.d.ts → orchestrator-CqWOjfgl.d.ts} +46 -3
  189. package/dist/orchestrator.d.ts +1 -1
  190. package/dist/orchestrator.js +47 -44
  191. package/dist/patterns-cli.js +1 -1
  192. package/dist/qmd-recall-cache.d.ts +2 -0
  193. package/dist/qmd-recall-cache.js +1 -1
  194. package/dist/qmd.d.ts +37 -2
  195. package/dist/qmd.js +4 -1
  196. package/dist/recall-explain-renderer.js +3 -3
  197. package/dist/recall-planner-llm.d.ts +57 -0
  198. package/dist/recall-planner-llm.js +167 -0
  199. package/dist/recall-planner-llm.js.map +1 -0
  200. package/dist/recall-xray-cli.js +4 -4
  201. package/dist/recall-xray-renderer.js +3 -3
  202. package/dist/recall-xray.js +2 -2
  203. package/dist/resume-bundles.js +2 -2
  204. package/dist/retrieval-agents.js +2 -2
  205. package/dist/routing/store.js +1 -1
  206. package/dist/search/factory.js +11 -10
  207. package/dist/search/index.js +11 -10
  208. package/dist/search/lancedb-backend.d.ts +1 -1
  209. package/dist/search/lancedb-backend.js +3 -2
  210. package/dist/search/meilisearch-backend.d.ts +1 -1
  211. package/dist/search/meilisearch-backend.js +3 -2
  212. package/dist/search/noop-backend.d.ts +1 -1
  213. package/dist/search/noop-backend.js +1 -1
  214. package/dist/search/orama-backend.d.ts +1 -1
  215. package/dist/search/orama-backend.js +3 -2
  216. package/dist/search/port.d.ts +6 -1
  217. package/dist/search/port.js +7 -0
  218. package/dist/search/remote-backend.d.ts +1 -1
  219. package/dist/search/remote-backend.js +1 -1
  220. package/dist/semantic-consolidation.js +4 -4
  221. package/dist/semantic-rule-promotion.js +3 -3
  222. package/dist/semantic-rule-verifier.js +3 -3
  223. package/dist/session-observer-state.js +1 -1
  224. package/dist/storage.js +2 -2
  225. package/dist/summarizer.js +2 -2
  226. package/dist/temporal-index.js +1 -1
  227. package/dist/{tier-stats-SKML2OSF.js → tier-stats-3LYQ3VV5.js} +3 -3
  228. package/dist/transfer/backup.js +2 -2
  229. package/dist/transfer/capsule-export.js +2 -2
  230. package/dist/transfer/capsule-import.js +2 -2
  231. package/dist/transfer/export-sqlite.js +1 -1
  232. package/dist/types.d.ts +32 -0
  233. package/dist/types.js +1 -1
  234. package/dist/utility-learner.js +1 -1
  235. package/dist/utility-runtime.js +2 -2
  236. package/dist/verified-recall.js +3 -3
  237. package/dist/work/board.js +2 -2
  238. package/dist/work/storage.d.ts +2 -0
  239. package/dist/work/storage.js +1 -1
  240. package/package.json +1 -1
  241. package/src/access-http.ts +3 -0
  242. package/src/access-mcp.test.ts +51 -0
  243. package/src/access-mcp.ts +26 -5
  244. package/src/active-recall.test.ts +40 -0
  245. package/src/active-recall.ts +19 -2
  246. package/src/behavior-learner.ts +5 -3
  247. package/src/buffer-session.test.ts +58 -0
  248. package/src/buffer-surprise-trigger.test.ts +4 -18
  249. package/src/buffer.ts +39 -11
  250. package/src/calibration.ts +10 -4
  251. package/src/causal-consolidation.test.ts +47 -2
  252. package/src/causal-consolidation.ts +13 -9
  253. package/src/cli.ts +19 -4
  254. package/src/compounding/engine.ts +2 -0
  255. package/src/compounding/preference-consolidator.test.ts +292 -0
  256. package/src/compounding/preference-consolidator.ts +55 -19
  257. package/src/config.test.ts +213 -0
  258. package/src/config.ts +175 -4
  259. package/src/connectors/codex-materialize-runner.ts +7 -4
  260. package/src/consolidation-provenance-check.ts +24 -5
  261. package/src/conversation-index/indexer.test.ts +22 -0
  262. package/src/conversation-index/indexer.ts +7 -3
  263. package/src/cross-namespace-budget.test.ts +44 -21
  264. package/src/cross-namespace-budget.ts +2 -2
  265. package/src/enrichment/pipeline.ts +11 -16
  266. package/src/evals.ts +1 -1
  267. package/src/extraction-judge-chain.test.ts +55 -0
  268. package/src/extraction-judge.ts +7 -9
  269. package/src/extraction.ts +16 -5
  270. package/src/fallback-llm.test.ts +600 -1
  271. package/src/fallback-llm.ts +91 -22
  272. package/src/maintenance/memory-governance-cron.ts +39 -29
  273. package/src/mcp-memory-inspector-app.ts +54 -12
  274. package/src/message-parts/index.ts +6 -0
  275. package/src/message-parts/message-parts.test.ts +30 -0
  276. package/src/migrate/from-engram.ts +19 -5
  277. package/src/namespaces/search.test.ts +15 -2
  278. package/src/namespaces/search.ts +1 -1
  279. package/src/network/webdav.ts +61 -21
  280. package/src/operator-toolkit.ts +6 -2
  281. package/src/orchestrator.ts +173 -20
  282. package/src/qmd-client.test.ts +85 -0
  283. package/src/qmd-recall-cache.test.ts +16 -0
  284. package/src/qmd-recall-cache.ts +7 -0
  285. package/src/qmd.test.ts +54 -0
  286. package/src/qmd.ts +119 -19
  287. package/src/recall-planner-llm.test.ts +224 -0
  288. package/src/recall-planner-llm.ts +289 -0
  289. package/src/routing/store.ts +4 -8
  290. package/src/search/factory.ts +3 -0
  291. package/src/search/lancedb-backend.ts +15 -3
  292. package/src/search/meilisearch-backend.ts +70 -7
  293. package/src/search/noop-backend.ts +5 -1
  294. package/src/search/orama-backend.ts +15 -3
  295. package/src/search/port.ts +15 -0
  296. package/src/search/remote-backend.ts +5 -1
  297. package/src/session-observer-state.ts +1 -1
  298. package/src/summarizer.ts +3 -3
  299. package/src/temporal-index.test.ts +18 -0
  300. package/src/temporal-index.ts +45 -0
  301. package/src/training-export/cli-date-validation.test.ts +36 -0
  302. package/src/training-export/date-parse.ts +21 -2
  303. package/src/transfer/export-sqlite.ts +3 -0
  304. package/src/types.ts +35 -0
  305. package/src/utility-learner.ts +1 -0
  306. package/src/work/storage.ts +23 -0
  307. package/dist/chunk-5RPTH6AU.js.map +0 -1
  308. package/dist/chunk-AJA46VX5.js.map +0 -1
  309. package/dist/chunk-C4SQJZAF.js.map +0 -1
  310. package/dist/chunk-CHCA44C3.js.map +0 -1
  311. package/dist/chunk-CSKLPDN6.js.map +0 -1
  312. package/dist/chunk-DLJ4IR6M.js.map +0 -1
  313. package/dist/chunk-EAZGEEG2.js.map +0 -1
  314. package/dist/chunk-EVZFIAPG.js.map +0 -1
  315. package/dist/chunk-G3Z3QEF5.js.map +0 -1
  316. package/dist/chunk-GMAG2HS4.js.map +0 -1
  317. package/dist/chunk-HENLZHIT.js.map +0 -1
  318. package/dist/chunk-HINSGUA7.js.map +0 -1
  319. package/dist/chunk-HJNQQICM.js.map +0 -1
  320. package/dist/chunk-HPWVAEET.js.map +0 -1
  321. package/dist/chunk-IOTENEVL.js.map +0 -1
  322. package/dist/chunk-IP73YCZP.js.map +0 -1
  323. package/dist/chunk-JHMFYY7L.js.map +0 -1
  324. package/dist/chunk-JNANKJLN.js.map +0 -1
  325. package/dist/chunk-KGK2QKWL.js.map +0 -1
  326. package/dist/chunk-KM2A35EO.js.map +0 -1
  327. package/dist/chunk-KVEVLBKC.js.map +0 -1
  328. package/dist/chunk-L227SKTB.js.map +0 -1
  329. package/dist/chunk-LZ3VEOU5.js.map +0 -1
  330. package/dist/chunk-NOMEVTUD.js.map +0 -1
  331. package/dist/chunk-NZPF2SYV.js.map +0 -1
  332. package/dist/chunk-PCI747N2.js.map +0 -1
  333. package/dist/chunk-TH67Q46T.js.map +0 -1
  334. package/dist/chunk-UWY7GIVS.js.map +0 -1
  335. package/dist/chunk-VJXSUAO7.js.map +0 -1
  336. package/dist/chunk-XKIQZXUB.js.map +0 -1
  337. package/dist/chunk-XPSVGJYA.js.map +0 -1
  338. package/dist/chunk-XSWKORGM.js.map +0 -1
  339. package/dist/chunk-YCN4BVDK.js.map +0 -1
  340. package/dist/chunk-ZDTVJXIP.js.map +0 -1
  341. /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
  342. /package/dist/{chunk-AU7Q3LSC.js.map → chunk-2QSZNTDO.js.map} +0 -0
  343. /package/dist/{chunk-HSVJGWYS.js.map → chunk-2ROPI5OE.js.map} +0 -0
  344. /package/dist/{chunk-CF3ZF2YU.js.map → chunk-3QSU4NFF.js.map} +0 -0
  345. /package/dist/{chunk-OI27U2HT.js.map → chunk-5BTCT236.js.map} +0 -0
  346. /package/dist/{chunk-CO7ZO4TU.js.map → chunk-5VDJMYTF.js.map} +0 -0
  347. /package/dist/{chunk-YFS5OEKO.js.map → chunk-7MLB4NCL.js.map} +0 -0
  348. /package/dist/{chunk-2QANQKSQ.js.map → chunk-ADNZVFXG.js.map} +0 -0
  349. /package/dist/{chunk-557IAFPD.js.map → chunk-APRRL26Q.js.map} +0 -0
  350. /package/dist/{chunk-QDDHYAKV.js.map → chunk-AZDOWD2L.js.map} +0 -0
  351. /package/dist/{chunk-MLT75J5S.js.map → chunk-B6SU7YSE.js.map} +0 -0
  352. /package/dist/{chunk-FXKPZ3H6.js.map → chunk-BPSGLMQ4.js.map} +0 -0
  353. /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
  354. /package/dist/{chunk-IK34DVAC.js.map → chunk-CIOMS6DI.js.map} +0 -0
  355. /package/dist/{chunk-7DZRO2DC.js.map → chunk-DEPRLVLK.js.map} +0 -0
  356. /package/dist/{chunk-DHGSZ3UD.js.map → chunk-DGNQRNLL.js.map} +0 -0
  357. /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.js.map} +0 -0
  358. /package/dist/{chunk-ETUPBUHB.js.map → chunk-GDASG7NC.js.map} +0 -0
  359. /package/dist/{chunk-4HP7HIE3.js.map → chunk-HP5FMB6L.js.map} +0 -0
  360. /package/dist/{chunk-DOX2CG6Y.js.map → chunk-IEUU7O4F.js.map} +0 -0
  361. /package/dist/{chunk-WSGF57U2.js.map → chunk-JQDZQ4TB.js.map} +0 -0
  362. /package/dist/{chunk-W7L6HXUC.js.map → chunk-LXOM6IQU.js.map} +0 -0
  363. /package/dist/{chunk-6JGNHWCI.js.map → chunk-OBIRVF36.js.map} +0 -0
  364. /package/dist/{chunk-GUPISBV2.js.map → chunk-PP2JH3GP.js.map} +0 -0
  365. /package/dist/{chunk-OXJBNGBK.js.map → chunk-PSUB67YB.js.map} +0 -0
  366. /package/dist/{chunk-KIB7SDIJ.js.map → chunk-Q6YIJGXJ.js.map} +0 -0
  367. /package/dist/{chunk-PPPZY2EU.js.map → chunk-QEMCQFDW.js.map} +0 -0
  368. /package/dist/{chunk-ZT3EGNLR.js.map → chunk-QPD426WT.js.map} +0 -0
  369. /package/dist/{chunk-RLV3PQGH.js.map → chunk-QVO4YOB7.js.map} +0 -0
  370. /package/dist/{chunk-KQAFEZQX.js.map → chunk-VDX2J7OX.js.map} +0 -0
  371. /package/dist/{chunk-IK7DCC5H.js.map → chunk-VMGLYN42.js.map} +0 -0
  372. /package/dist/{chunk-NSKYFGDL.js.map → chunk-X4QQB7O6.js.map} +0 -0
  373. /package/dist/{first-start-migration-GYJWIH36.js.map → first-start-migration-FF7YFGRP.js.map} +0 -0
  374. /package/dist/{tier-stats-SKML2OSF.js.map → tier-stats-3LYQ3VV5.js.map} +0 -0
@@ -3,6 +3,63 @@ import test from "node:test";
3
3
 
4
4
  import { parseConfig } from "./config.js";
5
5
 
6
+ test("parseConfig emitLegacyTools defaults to true and coerces config/env (issue #1427)", () => {
7
+ // Default: legacy aliases on, for backward compatibility.
8
+ assert.equal(parseConfig({}).emitLegacyTools, true);
9
+ // `null` means "unset → use default", consistent with the repo convention for
10
+ // optional fields (e.g. taskModelChain: null → undefined). Not a hard error.
11
+ assert.equal(parseConfig({ emitLegacyTools: null }).emitLegacyTools, true);
12
+ // Boolean + boolean-like string config values.
13
+ assert.equal(parseConfig({ emitLegacyTools: false }).emitLegacyTools, false);
14
+ assert.equal(parseConfig({ emitLegacyTools: "false" }).emitLegacyTools, false);
15
+ assert.equal(parseConfig({ emitLegacyTools: "0" }).emitLegacyTools, false);
16
+ assert.equal(parseConfig({ emitLegacyTools: "true" }).emitLegacyTools, true);
17
+
18
+ // Env var fallback (REMNIC_ preferred, ENGRAM_ legacy) when config field absent.
19
+ const prevRemnic = process.env.REMNIC_EMIT_LEGACY_TOOLS;
20
+ const prevEngram = process.env.ENGRAM_EMIT_LEGACY_TOOLS;
21
+ try {
22
+ process.env.REMNIC_EMIT_LEGACY_TOOLS = "false";
23
+ assert.equal(parseConfig({}).emitLegacyTools, false, "REMNIC_ env disables");
24
+ // Explicit config field wins over env.
25
+ assert.equal(parseConfig({ emitLegacyTools: true }).emitLegacyTools, true, "config field wins over env");
26
+ delete process.env.REMNIC_EMIT_LEGACY_TOOLS;
27
+ process.env.ENGRAM_EMIT_LEGACY_TOOLS = "false";
28
+ assert.equal(parseConfig({}).emitLegacyTools, false, "ENGRAM_ env fallback disables");
29
+ } finally {
30
+ if (prevRemnic === undefined) delete process.env.REMNIC_EMIT_LEGACY_TOOLS;
31
+ else process.env.REMNIC_EMIT_LEGACY_TOOLS = prevRemnic;
32
+ if (prevEngram === undefined) delete process.env.ENGRAM_EMIT_LEGACY_TOOLS;
33
+ else process.env.ENGRAM_EMIT_LEGACY_TOOLS = prevEngram;
34
+ }
35
+ });
36
+
37
+ test("parseConfig rejects a present-but-malformed emitLegacyTools (gotcha #51, #1427)", () => {
38
+ // A typo must fail fast, not silently fall through to the default (true) and
39
+ // re-enable legacy tool advertising.
40
+ for (const bad of ["fales", "maybe", 2, "2", "enabled"]) {
41
+ assert.throws(
42
+ () => parseConfig({ emitLegacyTools: bad }),
43
+ /emitLegacyTools must be a boolean-like value/,
44
+ `emitLegacyTools=${JSON.stringify(bad)} should throw`,
45
+ );
46
+ }
47
+ // Malformed env var also fails fast (only when the config field is absent).
48
+ const prev = process.env.REMNIC_EMIT_LEGACY_TOOLS;
49
+ try {
50
+ process.env.REMNIC_EMIT_LEGACY_TOOLS = "maybe";
51
+ assert.throws(
52
+ () => parseConfig({}),
53
+ /REMNIC_EMIT_LEGACY_TOOLS must be a boolean-like value/,
54
+ );
55
+ // An explicit valid config field overrides a malformed env (field wins first).
56
+ assert.equal(parseConfig({ emitLegacyTools: false }).emitLegacyTools, false);
57
+ } finally {
58
+ if (prev === undefined) delete process.env.REMNIC_EMIT_LEGACY_TOOLS;
59
+ else process.env.REMNIC_EMIT_LEGACY_TOOLS = prev;
60
+ }
61
+ });
62
+
6
63
  test("parseConfig expands tilde paths for core storage directories", () => {
7
64
  const previousHome = process.env.HOME;
8
65
  process.env.HOME = "/Users/remnic-test";
@@ -72,6 +129,33 @@ test("parseConfig codex missing entirely → installExtension defaults to true",
72
129
  assert.equal(result.codex.installExtension, true);
73
130
  });
74
131
 
132
+ test("parseConfig recallPlannerLlmEnabled defaults to false and coerces boolean-like strings (opt-in, issue #1367)", () => {
133
+ assert.equal(parseConfig({}).recallPlannerLlmEnabled, false);
134
+ assert.equal(parseConfig({ recallPlannerLlmEnabled: true }).recallPlannerLlmEnabled, true);
135
+ // CLI/env surfaces pass strings — these must enable the gate (gotcha #36).
136
+ assert.equal(parseConfig({ recallPlannerLlmEnabled: "true" }).recallPlannerLlmEnabled, true);
137
+ assert.equal(parseConfig({ recallPlannerLlmEnabled: "1" }).recallPlannerLlmEnabled, true);
138
+ assert.equal(parseConfig({ recallPlannerLlmEnabled: "on" }).recallPlannerLlmEnabled, true);
139
+ // Boolean-like falses and junk stay off.
140
+ assert.equal(parseConfig({ recallPlannerLlmEnabled: "false" }).recallPlannerLlmEnabled, false);
141
+ assert.equal(parseConfig({ recallPlannerLlmEnabled: "0" }).recallPlannerLlmEnabled, false);
142
+ });
143
+
144
+ test("parseConfig coerces boolean-like strings for all recallPlanner gates (issue #1367, gotcha #36)", () => {
145
+ // Rollout switches must honor string config from CLI/env surfaces.
146
+ assert.equal(parseConfig({ recallPlannerShadowMode: "true" }).recallPlannerShadowMode, true);
147
+ assert.equal(parseConfig({ recallPlannerShadowMode: "off" }).recallPlannerShadowMode, false);
148
+ assert.equal(parseConfig({}).recallPlannerShadowMode, false);
149
+
150
+ assert.equal(parseConfig({ recallPlannerTelemetryEnabled: "false" }).recallPlannerTelemetryEnabled, false);
151
+ assert.equal(parseConfig({}).recallPlannerTelemetryEnabled, true);
152
+
153
+ // The enable gate must be disableable via string "false" (the old `!== false`
154
+ // check treated "false" as truthy → could not disable).
155
+ assert.equal(parseConfig({ recallPlannerEnabled: "false" }).recallPlannerEnabled, false);
156
+ assert.equal(parseConfig({}).recallPlannerEnabled, true);
157
+ });
158
+
75
159
  test("parseConfig dreaming.maxEntries=0 preserves the runtime disable switch", () => {
76
160
  const result = parseConfig({ dreaming: { maxEntries: 0 } });
77
161
  assert.equal(result.dreaming.maxEntries, 0);
@@ -116,6 +200,59 @@ test("parseConfig initGateTimeoutMs defaults to OpenClaw cold-start budget", ()
116
200
  assert.equal(result.initGateTimeoutMs, 30_000);
117
201
  });
118
202
 
203
+ test("parseConfig qmdSearchStrategy defaults to hybrid and validates the enum", () => {
204
+ // Default must equal the historical lex+vec+hyde behavior. Issue #1335.
205
+ assert.equal(parseConfig({}).qmdSearchStrategy, "hybrid");
206
+ assert.equal(parseConfig({ qmdSearchStrategy: "hybrid" }).qmdSearchStrategy, "hybrid");
207
+ assert.equal(parseConfig({ qmdSearchStrategy: "lex-vec" }).qmdSearchStrategy, "lex-vec");
208
+ assert.equal(parseConfig({ qmdSearchStrategy: "lex" }).qmdSearchStrategy, "lex");
209
+ assert.equal(parseConfig({ qmdSearchStrategy: "LEX" }).qmdSearchStrategy, "lex");
210
+
211
+ for (const value of ["hyde", "vec", "bm25", "", 42]) {
212
+ assert.throws(
213
+ () => parseConfig({ qmdSearchStrategy: value }),
214
+ /qmdSearchStrategy must be one of/,
215
+ `invalid qmdSearchStrategy ${String(value)} should throw`,
216
+ );
217
+ }
218
+ });
219
+
220
+ test("parseConfig qmdSubprocessStrategy defaults to query (honors QMD query intent)", () => {
221
+ // Default must remain `qmd query` (LLM expansion + rerank) per CLAUDE.md gotcha #7.
222
+ assert.equal(parseConfig({}).qmdSubprocessStrategy, "query");
223
+ assert.equal(parseConfig({ qmdSubprocessStrategy: "query" }).qmdSubprocessStrategy, "query");
224
+ assert.equal(parseConfig({ qmdSubprocessStrategy: "search" }).qmdSubprocessStrategy, "search");
225
+ assert.equal(parseConfig({ qmdSubprocessStrategy: "SEARCH" }).qmdSubprocessStrategy, "search");
226
+
227
+ for (const value of ["bm25", "vsearch", "", 7]) {
228
+ assert.throws(
229
+ () => parseConfig({ qmdSubprocessStrategy: value }),
230
+ /qmdSubprocessStrategy must be one of/,
231
+ `invalid qmdSubprocessStrategy ${String(value)} should throw`,
232
+ );
233
+ }
234
+ });
235
+
236
+ test("parseConfig qmdDaemonTimeoutMs defaults to 8000 and clamps valid integers", () => {
237
+ assert.equal(parseConfig({}).qmdDaemonTimeoutMs, 8_000);
238
+ assert.equal(parseConfig({ qmdDaemonTimeoutMs: 20_000 }).qmdDaemonTimeoutMs, 20_000);
239
+ assert.equal(parseConfig({ qmdDaemonTimeoutMs: "20000" }).qmdDaemonTimeoutMs, 20_000);
240
+ // Below floor clamps up; above ceiling clamps down.
241
+ assert.equal(parseConfig({ qmdDaemonTimeoutMs: 100 }).qmdDaemonTimeoutMs, 1_000);
242
+ assert.equal(parseConfig({ qmdDaemonTimeoutMs: 999_999 }).qmdDaemonTimeoutMs, 120_000);
243
+ });
244
+
245
+ test("parseConfig qmdDaemonTimeoutMs rejects non-numeric and non-integer input", () => {
246
+ // gotcha #51 + codex review on #1422: silent coercion hides config mistakes.
247
+ for (const value of ["abc", "", 2500.9, "2500.9", Number.NaN, Infinity, true, {}]) {
248
+ assert.throws(
249
+ () => parseConfig({ qmdDaemonTimeoutMs: value }),
250
+ /qmdDaemonTimeoutMs must be an integer/,
251
+ `invalid qmdDaemonTimeoutMs ${String(value)} should throw`,
252
+ );
253
+ }
254
+ });
255
+
119
256
  test("parseConfig initGateTimeoutMs accepts CLI-style numeric strings", () => {
120
257
  const result = parseConfig({ initGateTimeoutMs: "45000" });
121
258
  assert.equal(result.initGateTimeoutMs, 45_000);
@@ -140,6 +277,82 @@ test("parseConfig modelSource=gateway does not inherit OPENAI_API_KEY from the p
140
277
  }
141
278
  });
142
279
 
280
+ test("parseConfig normalizes taskModelChain", () => {
281
+ const cfg = parseConfig({
282
+ taskModelChain: {
283
+ primary: " openai/cheap-primary ",
284
+ fallbacks: ["openai/cheap-primary", " fireworks/accounts/fireworks/models/glm-5p1 ", ""],
285
+ },
286
+ });
287
+
288
+ assert.deepEqual(cfg.taskModelChain, {
289
+ primary: "openai/cheap-primary",
290
+ fallbacks: ["fireworks/accounts/fireworks/models/glm-5p1"],
291
+ });
292
+ });
293
+
294
+ test("parseConfig treats an absent taskModelChain as not configured", () => {
295
+ assert.equal(parseConfig({}).taskModelChain, undefined);
296
+ assert.equal(parseConfig({ taskModelChain: null }).taskModelChain, undefined);
297
+ assert.equal(parseConfig({ taskModelChain: undefined }).taskModelChain, undefined);
298
+ });
299
+
300
+ test("parseConfig rejects a present-but-malformed taskModelChain (gotcha #51)", () => {
301
+ // A typo'd chain must surface loudly instead of silently reverting to defaults.
302
+ assert.throws(() => parseConfig({ taskModelChain: [] }), /taskModelChain must be an object/);
303
+ assert.throws(() => parseConfig({ taskModelChain: "openai/x" }), /taskModelChain must be an object/);
304
+ assert.throws(() => parseConfig({ taskModelChain: { primary: " " } }), /taskModelChain\.primary is required/);
305
+ assert.throws(() => parseConfig({ taskModelChain: { fallbacks: ["openai/fallback-only"] } }), /taskModelChain\.primary is required/);
306
+ assert.throws(
307
+ () => parseConfig({ taskModelChain: { primary: "openai/p", fallbacks: "not-an-array" } }),
308
+ /taskModelChain\.fallbacks must be an array/,
309
+ );
310
+ assert.throws(
311
+ () => parseConfig({ taskModelChain: { primary: "openai/p", fallbacks: [123] } }),
312
+ /taskModelChain\.fallbacks must contain only strings/,
313
+ );
314
+ });
315
+
316
+ test("parseConfig rejects unqualified taskModelChain model strings (codex review #1425)", () => {
317
+ // A slash-less id like "gpt-4.1" parses here but FallbackLlmClient.parseModelString
318
+ // drops it, leaving the chain silently using a different model — reject at parse.
319
+ assert.throws(
320
+ () => parseConfig({ taskModelChain: { primary: "gpt-4.1" } }),
321
+ /taskModelChain\.primary must be in "provider\/model" form/,
322
+ );
323
+ assert.throws(
324
+ () => parseConfig({ taskModelChain: { primary: "openai/" } }),
325
+ /taskModelChain\.primary must be in "provider\/model" form/,
326
+ );
327
+ assert.throws(
328
+ () => parseConfig({ taskModelChain: { primary: "/gpt-4.1" } }),
329
+ /taskModelChain\.primary must be in "provider\/model" form/,
330
+ );
331
+ assert.throws(
332
+ () => parseConfig({ taskModelChain: { primary: "openai/gpt", fallbacks: ["bare-model"] } }),
333
+ /taskModelChain\.fallbacks entries must be in "provider\/model" form/,
334
+ );
335
+ // Multi-slash provider/model paths remain valid.
336
+ assert.deepEqual(
337
+ parseConfig({
338
+ taskModelChain: { primary: "fireworks/accounts/fireworks/models/glm-5p1" },
339
+ }).taskModelChain,
340
+ { primary: "fireworks/accounts/fireworks/models/glm-5p1" },
341
+ );
342
+ });
343
+
344
+ test("parseConfig rejects unknown taskModelChain keys (codex review #1425)", () => {
345
+ // A misspelled "fallback" must not silently drop the fallback chain.
346
+ assert.throws(
347
+ () => parseConfig({ taskModelChain: { primary: "openai/p", fallback: ["openai/q"] } }),
348
+ /taskModelChain has unknown property: fallback/,
349
+ );
350
+ assert.throws(
351
+ () => parseConfig({ taskModelChain: { primary: "openai/p", fallbackModels: ["openai/q"], extra: 1 } }),
352
+ /taskModelChain has unknown properties:/,
353
+ );
354
+ });
355
+
143
356
  test("parseConfig modelSource=gateway still honors an explicit openaiApiKey override", () => {
144
357
  const original = process.env.OPENAI_API_KEY;
145
358
  process.env.OPENAI_API_KEY = "sk-env-should-not-be-used";
package/src/config.ts CHANGED
@@ -13,6 +13,7 @@ import type {
13
13
  HeartbeatConfig,
14
14
  IdentityInjectionMode,
15
15
  MemoryOsPresetName,
16
+ AgentPersonaModelConfig,
16
17
  PluginConfig,
17
18
  PrincipalRule,
18
19
  RecallPipelineConfig,
@@ -69,6 +70,81 @@ function parseBoundedIntegerMs(
69
70
  return Math.min(max, Math.max(min, Math.floor(coerced)));
70
71
  }
71
72
 
73
+ // A gateway model string must be "provider/model" — at least one "/" with a
74
+ // non-empty provider segment and a non-empty model segment. This mirrors
75
+ // FallbackLlmClient.parseModelString (which requires >= 2 slash-parts), so a
76
+ // model the runtime would silently drop is rejected at config time instead.
77
+ function isQualifiedModelString(value: string): boolean {
78
+ const slash = value.indexOf("/");
79
+ return slash > 0 && slash < value.length - 1;
80
+ }
81
+
82
+ function parseModelChainConfig(
83
+ value: unknown,
84
+ keyName: string,
85
+ ): AgentPersonaModelConfig | undefined {
86
+ // Absent → not configured (no error).
87
+ if (value === undefined || value === null) return undefined;
88
+
89
+ // Present but malformed → reject loudly rather than silently dropping it,
90
+ // so a typo'd chain surfaces instead of quietly reverting to defaults
91
+ // (gotcha #51). Issue #1365 / PR #1370.
92
+ if (typeof value !== "object" || Array.isArray(value)) {
93
+ throw new Error(
94
+ `${keyName} must be an object like { "primary": "provider/model", "fallbacks": ["provider/model", ...] }; got ${JSON.stringify(value)}`,
95
+ );
96
+ }
97
+ const raw = value as Record<string, unknown>;
98
+ // Reject unknown keys (matches the manifest's additionalProperties:false) so a
99
+ // misspelled "fallback"/"fallbackModels" doesn't silently drop the fallback
100
+ // chain (gotcha #51, codex review #1425).
101
+ const unknownKeys = Object.keys(raw).filter((k) => k !== "primary" && k !== "fallbacks");
102
+ if (unknownKeys.length > 0) {
103
+ throw new Error(
104
+ `${keyName} has unknown propert${unknownKeys.length === 1 ? "y" : "ies"}: ${unknownKeys.join(", ")}. Allowed: "primary", "fallbacks".`,
105
+ );
106
+ }
107
+ if (typeof raw.primary !== "string" || raw.primary.trim().length === 0) {
108
+ throw new Error(
109
+ `${keyName}.primary is required and must be a non-empty "provider/model" string; got ${JSON.stringify(raw.primary)}`,
110
+ );
111
+ }
112
+ const primary = raw.primary.trim();
113
+ if (!isQualifiedModelString(primary)) {
114
+ throw new Error(
115
+ `${keyName}.primary must be in "provider/model" form (e.g. "zai/glm-4.7-flash"); got ${JSON.stringify(primary)}`,
116
+ );
117
+ }
118
+
119
+ let dedupedFallbacks: string[] | undefined;
120
+ if (raw.fallbacks !== undefined) {
121
+ if (!Array.isArray(raw.fallbacks)) {
122
+ throw new Error(
123
+ `${keyName}.fallbacks must be an array of "provider/model" strings; got ${JSON.stringify(raw.fallbacks)}`,
124
+ );
125
+ }
126
+ if (raw.fallbacks.some((item) => typeof item !== "string")) {
127
+ throw new Error(`${keyName}.fallbacks must contain only strings`);
128
+ }
129
+ const trimmed = raw.fallbacks
130
+ .map((item) => (item as string).trim())
131
+ .filter((item) => item.length > 0 && item !== primary);
132
+ for (const fb of trimmed) {
133
+ if (!isQualifiedModelString(fb)) {
134
+ throw new Error(
135
+ `${keyName}.fallbacks entries must be in "provider/model" form; got ${JSON.stringify(fb)}`,
136
+ );
137
+ }
138
+ }
139
+ dedupedFallbacks = [...new Set(trimmed)];
140
+ }
141
+
142
+ return {
143
+ primary,
144
+ ...(dedupedFallbacks && dedupedFallbacks.length > 0 ? { fallbacks: dedupedFallbacks } : {}),
145
+ };
146
+ }
147
+
72
148
  function parsePositiveInteger(value: unknown, keyName: string): number | undefined {
73
149
  if (value === undefined || value === null) return undefined;
74
150
  const coerced = coerceNumber(value);
@@ -139,6 +215,51 @@ function parseQmdChunkStrategy(value: unknown): "auto" | "regex" {
139
215
  throw new Error(`qmdChunkStrategy must be "auto" or "regex"; got ${JSON.stringify(value)}`);
140
216
  }
141
217
 
218
+ // Issue #1335. Default "hybrid" preserves the historical lex+vec+hyde daemon plan.
219
+ function parseQmdSearchStrategy(value: unknown): "hybrid" | "lex-vec" | "lex" {
220
+ if (value === undefined || value === null) return "hybrid";
221
+ if (typeof value !== "string") {
222
+ throw new Error(
223
+ `qmdSearchStrategy must be one of "hybrid", "lex-vec", or "lex"; got ${JSON.stringify(value)}`,
224
+ );
225
+ }
226
+ const normalized = value.trim().toLowerCase();
227
+ if (normalized === "hybrid" || normalized === "lex-vec" || normalized === "lex") {
228
+ return normalized;
229
+ }
230
+ throw new Error(
231
+ `qmdSearchStrategy must be one of "hybrid", "lex-vec", or "lex"; got ${JSON.stringify(value)}`,
232
+ );
233
+ }
234
+
235
+ // Issue #1335. Reject non-numeric / non-integer timeouts rather than silently
236
+ // coercing them (gotcha #51), then clamp valid integers to the documented bounds.
237
+ function parseQmdDaemonTimeoutMs(value: unknown): number {
238
+ if (value === undefined || value === null) return 8_000;
239
+ const coerced = coerceNumber(value);
240
+ if (coerced === undefined || !Number.isInteger(coerced)) {
241
+ throw new Error(
242
+ `qmdDaemonTimeoutMs must be an integer number of milliseconds between 1000 and 120000; got ${JSON.stringify(value)}`,
243
+ );
244
+ }
245
+ return Math.min(120_000, Math.max(1_000, coerced));
246
+ }
247
+
248
+ // Issue #1335. Default "query" keeps `qmd query` (LLM expansion + rerank) per gotcha #7.
249
+ function parseQmdSubprocessStrategy(value: unknown): "query" | "search" {
250
+ if (value === undefined || value === null) return "query";
251
+ if (typeof value !== "string") {
252
+ throw new Error(
253
+ `qmdSubprocessStrategy must be one of "query" or "search"; got ${JSON.stringify(value)}`,
254
+ );
255
+ }
256
+ const normalized = value.trim().toLowerCase();
257
+ if (normalized === "query" || normalized === "search") return normalized;
258
+ throw new Error(
259
+ `qmdSubprocessStrategy must be one of "query" or "search"; got ${JSON.stringify(value)}`,
260
+ );
261
+ }
262
+
142
263
  function parseOptionalNonEmptyString(value: unknown): string | undefined {
143
264
  if (typeof value !== "string") return undefined;
144
265
  const normalized = value.trim();
@@ -230,6 +351,38 @@ function coerceBooleanLike(value: unknown): boolean | undefined {
230
351
  return undefined;
231
352
  }
232
353
 
354
+ /**
355
+ * Resolve the `emitLegacyTools` opt-out (issue #1427): config field wins, then
356
+ * the REMNIC_/ENGRAM_ env var, then default true. A *present-but-malformed*
357
+ * value fails fast rather than silently re-enabling legacy aliases — this knob
358
+ * controls the advertised MCP `tools/list` surface, so a typo like
359
+ * `emitLegacyTools=fales` must not be misread as `true` (gotcha #51).
360
+ */
361
+ function resolveEmitLegacyTools(configValue: unknown): boolean {
362
+ const ACCEPTED = "true/false/1/0/yes/no/on/off";
363
+ if (configValue !== undefined && configValue !== null) {
364
+ const coerced = coerceBooleanLike(configValue);
365
+ if (coerced === undefined) {
366
+ throw new Error(
367
+ `emitLegacyTools must be a boolean-like value (${ACCEPTED}); got ${JSON.stringify(configValue)}`,
368
+ );
369
+ }
370
+ return coerced;
371
+ }
372
+ const envRaw =
373
+ readEnvVar("REMNIC_EMIT_LEGACY_TOOLS") ?? readEnvVar("ENGRAM_EMIT_LEGACY_TOOLS");
374
+ if (envRaw !== undefined) {
375
+ const coerced = coerceBooleanLike(envRaw);
376
+ if (coerced === undefined) {
377
+ throw new Error(
378
+ `REMNIC_EMIT_LEGACY_TOOLS must be a boolean-like value (${ACCEPTED}); got "${envRaw}"`,
379
+ );
380
+ }
381
+ return coerced;
382
+ }
383
+ return true;
384
+ }
385
+
233
386
  export function isOpenaiApiKeyDisabled(value: unknown): boolean {
234
387
  return value === false || (typeof value === "string" && value.trim().toLowerCase() === "false");
235
388
  }
@@ -1379,6 +1532,9 @@ export function parseConfig(raw: unknown): PluginConfig {
1379
1532
  qmdChunkStrategy: parseQmdChunkStrategy(cfg.qmdChunkStrategy),
1380
1533
  qmdCandidateLimit: parsePositiveInteger(cfg.qmdCandidateLimit, "qmdCandidateLimit"),
1381
1534
  qmdQueryRerankEnabled: coerceBooleanLike(cfg.qmdQueryRerankEnabled) ?? true,
1535
+ qmdSearchStrategy: parseQmdSearchStrategy(cfg.qmdSearchStrategy),
1536
+ qmdSubprocessStrategy: parseQmdSubprocessStrategy(cfg.qmdSubprocessStrategy),
1537
+ qmdDaemonTimeoutMs: parseQmdDaemonTimeoutMs(cfg.qmdDaemonTimeoutMs),
1382
1538
  qmdIndexName: parseOptionalNonEmptyString(cfg.qmdIndexName),
1383
1539
  qmdForceCpu: coerceBooleanLike(cfg.qmdForceCpu) ?? false,
1384
1540
  qmdGpuBackend: parseQmdGpuBackend(cfg.qmdGpuBackend),
@@ -2377,6 +2533,7 @@ export function parseConfig(raw: unknown): PluginConfig {
2377
2533
  typeof cfg.fastGatewayAgentId === "string" && cfg.fastGatewayAgentId.length > 0
2378
2534
  ? cfg.fastGatewayAgentId
2379
2535
  : "",
2536
+ taskModelChain: parseModelChainConfig(cfg.taskModelChain, "taskModelChain"),
2380
2537
 
2381
2538
  // v3.0 namespaces (default off)
2382
2539
  namespacesEnabled: cfg.namespacesEnabled === true,
@@ -2716,20 +2873,30 @@ export function parseConfig(raw: unknown): PluginConfig {
2716
2873
  .filter((param): param is string => typeof param === "string" && param.trim().length > 0)
2717
2874
  : [...DEFAULT_BEHAVIOR_LOOP_PROTECTED_PARAMS],
2718
2875
  // v8.0 phase 1
2719
- recallPlannerEnabled: cfg.recallPlannerEnabled !== false,
2876
+ // All recallPlanner boolean gates coerce boolean-like strings so CLI/env
2877
+ // surfaces (`--config recallPlanner*=true|false`) behave correctly — the
2878
+ // shadow-mode/telemetry/enable flags are documented rollout switches and
2879
+ // must not silently ignore string values (gotcha #36, #1428 review).
2880
+ recallPlannerEnabled: coerceBooleanLike(cfg.recallPlannerEnabled) ?? true,
2881
+ // Issue #1367 / Option C: LLM-based recall planning is opt-in so the
2882
+ // default recall path stays heuristic (no added latency / LLM call unless
2883
+ // the operator asks for it — gotcha #30). Coerce boolean-like strings so
2884
+ // CLI/env surfaces (`--config recallPlannerLlmEnabled=true`) actually
2885
+ // enable it (gotcha #36); defaults off.
2886
+ recallPlannerLlmEnabled: coerceBooleanLike(cfg.recallPlannerLlmEnabled) ?? false,
2720
2887
  recallPlannerModel:
2721
2888
  typeof cfg.recallPlannerModel === "string" && cfg.recallPlannerModel.trim().length > 0
2722
2889
  ? cfg.recallPlannerModel.trim()
2723
2890
  : DEFAULT_REASONING_MODEL,
2724
2891
  recallPlannerTimeoutMs:
2725
2892
  typeof cfg.recallPlannerTimeoutMs === "number" ? cfg.recallPlannerTimeoutMs : 1500,
2726
- recallPlannerUseResponsesApi: cfg.recallPlannerUseResponsesApi !== false,
2893
+ recallPlannerUseResponsesApi: coerceBooleanLike(cfg.recallPlannerUseResponsesApi) ?? true,
2727
2894
  recallPlannerMaxPromptChars:
2728
2895
  typeof cfg.recallPlannerMaxPromptChars === "number" ? cfg.recallPlannerMaxPromptChars : 4000,
2729
2896
  recallPlannerMaxMemoryHints:
2730
2897
  typeof cfg.recallPlannerMaxMemoryHints === "number" ? cfg.recallPlannerMaxMemoryHints : 24,
2731
- recallPlannerShadowMode: cfg.recallPlannerShadowMode === true,
2732
- recallPlannerTelemetryEnabled: cfg.recallPlannerTelemetryEnabled !== false,
2898
+ recallPlannerShadowMode: coerceBooleanLike(cfg.recallPlannerShadowMode) ?? false,
2899
+ recallPlannerTelemetryEnabled: coerceBooleanLike(cfg.recallPlannerTelemetryEnabled) ?? true,
2733
2900
  recallPlannerMaxQmdResultsMinimal:
2734
2901
  typeof cfg.recallPlannerMaxQmdResultsMinimal === "number"
2735
2902
  ? cfg.recallPlannerMaxQmdResultsMinimal
@@ -3422,6 +3589,10 @@ export function parseConfig(raw: unknown): PluginConfig {
3422
3589
  ? cfg.binaryLifecycleBackendPath.trim()
3423
3590
  : "",
3424
3591
 
3592
+ // Legacy MCP tool aliases opt-out (issue #1427). Config field wins; then
3593
+ // the REMNIC_/ENGRAM_ env var (gotcha #9); default true for back-compat.
3594
+ // Malformed values fail fast rather than silently defaulting (gotcha #51).
3595
+ emitLegacyTools: resolveEmitLegacyTools(cfg.emitLegacyTools),
3425
3596
  // Codex citation parity (issue #379)
3426
3597
  citationsEnabled: cfg.citationsEnabled === true,
3427
3598
  citationsAutoDetect: cfg.citationsAutoDetect !== false,
@@ -70,8 +70,9 @@ export async function runCodexMaterialize(
70
70
  }
71
71
 
72
72
  // Per-trigger gate: session-end runs must honor codexMaterializeOnSessionEnd.
73
- // session-end.sh passes reason="session_end"; when the user has turned off the
74
- // session-end trigger we short-circuit here without touching disk.
73
+ // The Codex Stop hook (remnic-codex-hook.cjs, session-end event) passes
74
+ // reason="session_end"; when the user has turned off the session-end trigger
75
+ // we short-circuit here without touching disk.
75
76
  if (options.reason === "session_end" && cfg.codexMaterializeOnSessionEnd === false) {
76
77
  log.debug(
77
78
  `[codex-materialize] skipped — session-end disabled via codexMaterializeOnSessionEnd=false`,
@@ -199,8 +200,10 @@ function resolveNamespaceDir(
199
200
  ): string {
200
201
  if (!cfg.namespacesEnabled) return memoryDir;
201
202
 
202
- const defaultNamespace = (cfg.defaultNamespace ?? "").trim();
203
- const ns = (namespace || defaultNamespace || "default").trim();
203
+ const configuredDefaultNamespace = (cfg.defaultNamespace ?? "").trim();
204
+ const defaultNamespace =
205
+ configuredDefaultNamespace.length > 0 ? configuredDefaultNamespace : "default";
206
+ const ns = (namespace || defaultNamespace).trim();
204
207
  if (!isSafeRouteNamespace(ns)) {
205
208
  throw new Error(`invalid materialize namespace: ${ns}`);
206
209
  }
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import path from "node:path";
21
- import { access, readdir, readFile, stat } from "node:fs/promises";
21
+ import { lstat, readdir, readFile, realpath, stat } from "node:fs/promises";
22
22
  import { constants as fsConstants } from "node:fs";
23
23
  import type { StorageManager } from "./storage.js";
24
24
  import {
@@ -499,7 +499,7 @@ export async function runConsolidationProvenanceCheck(options: {
499
499
  const scanRoots = ["facts", "corrections", "procedures", "reasoning-traces"];
500
500
  for (const rootName of scanRoots) {
501
501
  const rootPath = path.join(memoryDir, rootName);
502
- for await (const file of walkMarkdownFiles(rootPath)) {
502
+ for await (const file of walkMarkdownFiles(rootPath, memoryDir)) {
503
503
  if (seenPaths.has(file)) continue;
504
504
  try {
505
505
  const raw = await readFile(file, "utf-8");
@@ -531,21 +531,40 @@ export async function runConsolidationProvenanceCheck(options: {
531
531
  /**
532
532
  * Recursively yield all `.md` file paths under `root`. Silent on
533
533
  * missing directories — the facts/corrections dirs may not exist in
534
- * fresh installs.
534
+ * fresh installs. Symlinked roots/directories are skipped so the
535
+ * best-effort parse-failure pass cannot escape `memoryDir`.
535
536
  */
536
- async function* walkMarkdownFiles(root: string): AsyncGenerator<string> {
537
+ async function* walkMarkdownFiles(root: string, memoryDir: string): AsyncGenerator<string> {
537
538
  let entries;
539
+ let memoryDirReal: string;
538
540
  try {
541
+ const rootStat = await lstat(root);
542
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) return;
543
+ memoryDirReal = await realpath(memoryDir);
544
+ const rootReal = await realpath(root);
545
+ if (!isPathWithin(rootReal, memoryDirReal)) return;
539
546
  entries = await readdir(root, { withFileTypes: true });
540
547
  } catch {
541
548
  return;
542
549
  }
543
550
  for (const entry of entries) {
544
551
  const full = path.join(root, entry.name);
552
+ if (entry.isSymbolicLink()) continue;
545
553
  if (entry.isDirectory()) {
546
- yield* walkMarkdownFiles(full);
554
+ yield* walkMarkdownFiles(full, memoryDirReal);
547
555
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
556
+ try {
557
+ const fileReal = await realpath(full);
558
+ if (!isPathWithin(fileReal, memoryDirReal)) continue;
559
+ } catch {
560
+ continue;
561
+ }
548
562
  yield full;
549
563
  }
550
564
  }
551
565
  }
566
+
567
+ function isPathWithin(candidate: string, root: string): boolean {
568
+ const relative = path.relative(root, candidate);
569
+ return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
570
+ }
@@ -98,6 +98,28 @@ test("writeConversationChunks keeps distinct raw session keys from overwriting a
98
98
  }
99
99
  });
100
100
 
101
+ test("writeConversationChunks quotes metadata scalars in front matter", async () => {
102
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-conversation-index-"));
103
+ try {
104
+ const sessionKey = "agent\nkind: other\n---\ncolon: value";
105
+ const written = await writeConversationChunks(root, [
106
+ sampleChunk({
107
+ sessionKey,
108
+ startTs: "2026-05-17T00:00:00.000Z",
109
+ endTs: "2026-05-17T00:01:00.000Z",
110
+ }),
111
+ ]);
112
+
113
+ const content = await readFile(written[0]!, "utf-8");
114
+ assert.match(content, /^sessionKey: "agent\\nkind: other\\n---\\ncolon: value"$/m);
115
+ assert.match(content, /^startTs: "2026-05-17T00:00:00.000Z"$/m);
116
+ assert.match(content, /^endTs: "2026-05-17T00:01:00.000Z"$/m);
117
+ assert.doesNotMatch(content, /^kind: other$/m);
118
+ } finally {
119
+ await rm(root, { recursive: true, force: true });
120
+ }
121
+ });
122
+
101
123
  test("writeConversationChunks rejects invalid chunk timestamps before deriving paths", async () => {
102
124
  const root = await mkdtemp(path.join(os.tmpdir(), "remnic-conversation-index-"));
103
125
  try {
@@ -40,6 +40,10 @@ function sanitizeChunkId(id: string): string {
40
40
  return sanitizePathComponent(id, "chunk");
41
41
  }
42
42
 
43
+ function yamlQuotedScalar(value: string): string {
44
+ return JSON.stringify(value);
45
+ }
46
+
43
47
  function datePathComponent(startTs: string): string {
44
48
  const match = typeof startTs === "string"
45
49
  ? /^(\d{4})-(\d{2})-(\d{2})T/.exec(startTs)
@@ -153,9 +157,9 @@ export async function writeConversationChunks(
153
157
  const content =
154
158
  `---\n` +
155
159
  `kind: conversation_chunk\n` +
156
- `sessionKey: ${c.sessionKey}\n` +
157
- `startTs: ${c.startTs}\n` +
158
- `endTs: ${c.endTs}\n` +
160
+ `sessionKey: ${yamlQuotedScalar(c.sessionKey)}\n` +
161
+ `startTs: ${yamlQuotedScalar(c.startTs)}\n` +
162
+ `endTs: ${yamlQuotedScalar(c.endTs)}\n` +
159
163
  `---\n\n` +
160
164
  c.text +
161
165
  "\n";