@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
package/src/qmd.ts CHANGED
@@ -9,7 +9,12 @@ import {
9
9
  throwIfAborted,
10
10
  } from "./abort-error.js";
11
11
  import type { QmdSearchExplain, QmdSearchResult } from "./types.js";
12
- import type { SearchBackend, SearchExecutionOptions, SearchQueryOptions } from "./search/port.js";
12
+ import {
13
+ resolveEnsureCollectionArgs,
14
+ type SearchBackend,
15
+ type SearchExecutionOptions,
16
+ type SearchQueryOptions,
17
+ } from "./search/port.js";
13
18
  import { launchProcess, type CommandChildProcess } from "./runtime/child-process.js";
14
19
  import { mergeEnv } from "./runtime/env.js";
15
20
 
@@ -33,11 +38,32 @@ export interface QmdClientOptions {
33
38
  qmdEmbedModel?: string;
34
39
  qmdRerankModel?: string;
35
40
  qmdGenerateModel?: string;
41
+ /** Daemon search plan; default "hybrid" preserves lex+vec+hyde. Issue #1335. */
42
+ qmdSearchStrategy?: QmdSearchStrategy;
43
+ /** Subprocess fallback command; default "query" keeps LLM expansion. Issue #1335. */
44
+ qmdSubprocessStrategy?: QmdSubprocessStrategy;
45
+ /** Per-call daemon search timeout in ms; default 8000. Issue #1335. */
46
+ qmdDaemonTimeoutMs?: number;
36
47
  }
37
48
 
38
49
  export type QmdVersionTuple = [number, number, number];
39
50
  export type QmdChunkStrategy = "auto" | "regex";
40
51
  export type QmdStructuredSearchType = "lex" | "vec" | "hyde";
52
+ /**
53
+ * Daemon search plan. Issue #1335.
54
+ * - "hybrid" → lex + vec + hyde (DEFAULT — full recall; runs an embedding pass
55
+ * plus a HyDE generate pass, which dominates latency on CPU-only models).
56
+ * - "lex-vec" → lex + vec (drops the expensive HyDE generate leg).
57
+ * - "lex" → lex only (BM25; fastest, no model inference).
58
+ */
59
+ export type QmdSearchStrategy = "hybrid" | "lex-vec" | "lex";
60
+ /**
61
+ * Subprocess fallback command (used only when the daemon is unavailable). Issue #1335.
62
+ * - "query" → `qmd query` (DEFAULT — LLM query expansion + rerank; see CLAUDE.md
63
+ * gotcha #7, this is intentional and must remain the default).
64
+ * - "search" → `qmd search` (BM25-only; fast, but no expansion/rerank).
65
+ */
66
+ export type QmdSubprocessStrategy = "query" | "search";
41
67
  export interface QmdStructuredSearch {
42
68
  type: QmdStructuredSearchType;
43
69
  query: string;
@@ -100,6 +126,9 @@ const QMD_TIMEOUT_MS = 30_000;
100
126
  // During the loading window, searches will timeout/return [] quickly — this is preferable to
101
127
  // blocking the full 75s on every recall request.
102
128
  // Note: keep this ≥ 5s to allow normal searches (post-load) to complete reliably.
129
+ // This is the DEFAULT only — operators can override per-client via the
130
+ // `qmdDaemonTimeoutMs` config knob (issue #1335), e.g. to give CPU-only HyDE
131
+ // queries more headroom. The effective value lives in `this.daemonTimeoutMs`.
103
132
  const QMD_DAEMON_TIMEOUT_MS = 8_000;
104
133
  const QMD_PROBE_TIMEOUT_MS = 8_000;
105
134
  const QMD_UPDATE_BACKOFF_MS = 15 * 60 * 1000; // 15m
@@ -404,17 +433,32 @@ function buildSyntheticHydeQuery(query: string, intent?: string): string {
404
433
  : base;
405
434
  }
406
435
 
407
- function buildDefaultStructuredSearches(
436
+ /**
437
+ * Build the structured sub-queries the daemon `query` tool runs in one request.
438
+ *
439
+ * The default `"hybrid"` plan (lex + vec + hyde) intentionally exercises QMD's
440
+ * full RRF + rerank path for best recall. On CPU-only models the `hyde` leg
441
+ * (1.7B generate + embed) dominates wall-clock — operators on constrained
442
+ * hardware can trade recall for latency via `qmdSearchStrategy` (issue #1335)
443
+ * without losing the default behavior. A per-call `structuredSearches` override
444
+ * always wins so callers with stronger query-document structure stay in control.
445
+ */
446
+ export function buildDefaultStructuredSearches(
408
447
  query: string,
409
448
  options?: SearchQueryOptions,
449
+ strategy: QmdSearchStrategy = "hybrid",
410
450
  ): QmdStructuredSearch[] {
411
451
  const explicit = normalizeStructuredSearches(options?.structuredSearches);
412
452
  if (explicit.length > 0) return explicit;
413
453
  const trimmed = query.trim();
414
454
  if (!trimmed) return [];
455
+ const lex: QmdStructuredSearch = { type: "lex", query: trimmed };
456
+ if (strategy === "lex") return [lex];
457
+ const vec: QmdStructuredSearch = { type: "vec", query: trimmed };
458
+ if (strategy === "lex-vec") return [lex, vec];
415
459
  return [
416
- { type: "lex", query: trimmed },
417
- { type: "vec", query: trimmed },
460
+ lex,
461
+ vec,
418
462
  { type: "hyde", query: buildSyntheticHydeQuery(trimmed, options?.intent) },
419
463
  ];
420
464
  }
@@ -1202,6 +1246,9 @@ export class QmdClient implements SearchBackend {
1202
1246
  private readonly qmdCandidateLimit?: number;
1203
1247
  private readonly qmdQueryRerankEnabled: boolean;
1204
1248
  private readonly qmdIndexName?: string;
1249
+ private readonly qmdSearchStrategy: QmdSearchStrategy;
1250
+ private readonly qmdSubprocessStrategy: QmdSubprocessStrategy;
1251
+ private readonly daemonTimeoutMs: number;
1205
1252
  private readonly qmdRuntimeEnv: QmdRuntimeEnv;
1206
1253
  private qmdPathSource: "auto-path" | "auto-fallback" | "configured" = "auto-path";
1207
1254
  private cliVersion: string | null = null;
@@ -1249,6 +1296,20 @@ export class QmdClient implements SearchBackend {
1249
1296
  : undefined;
1250
1297
  this.qmdQueryRerankEnabled = opts?.qmdQueryRerankEnabled !== false;
1251
1298
  this.qmdIndexName = opts?.qmdIndexName?.trim() || undefined;
1299
+ // Default "hybrid" preserves the historical lex+vec+hyde daemon plan. Issue #1335.
1300
+ this.qmdSearchStrategy =
1301
+ opts?.qmdSearchStrategy === "lex" || opts?.qmdSearchStrategy === "lex-vec"
1302
+ ? opts.qmdSearchStrategy
1303
+ : "hybrid";
1304
+ // Default "query" keeps `qmd query` (LLM expansion + rerank) per gotcha #7. Issue #1335.
1305
+ this.qmdSubprocessStrategy = opts?.qmdSubprocessStrategy === "search" ? "search" : "query";
1306
+ // Default 8000ms preserves the historical hardcoded daemon timeout. Issue #1335.
1307
+ // Floor of 1000ms avoids absurdly small values; callers wanting CPU-only HyDE
1308
+ // headroom can raise this (e.g. 20000) without code changes.
1309
+ this.daemonTimeoutMs =
1310
+ typeof opts?.qmdDaemonTimeoutMs === "number" && Number.isFinite(opts.qmdDaemonTimeoutMs)
1311
+ ? Math.max(1_000, Math.floor(opts.qmdDaemonTimeoutMs))
1312
+ : QMD_DAEMON_TIMEOUT_MS;
1252
1313
  this.qmdRuntimeEnv = this.buildRuntimeEnv(opts);
1253
1314
  if (this.configuredQmdPath) {
1254
1315
  this.qmdPath = this.configuredQmdPath;
@@ -1823,7 +1884,15 @@ export class QmdClient implements SearchBackend {
1823
1884
  // repeated queries within the same recall cycle (e.g., primary + hybrid
1824
1885
  // top-up, or conversation recall using the same collection).
1825
1886
  const optionsFingerprint = searchOptions ? JSON.stringify(searchOptions) : "";
1826
- const cacheKey = createHash("sha256").update(`${col}:${n}:${optionsFingerprint}:${trimmed}`).digest("hex");
1887
+ // The QMD search cache is a process-global keyed map (memory-cache.ts), so the
1888
+ // key must capture every input that changes the result — including the daemon
1889
+ // and subprocess strategies. Otherwise two QmdClient instances (or a reloaded
1890
+ // config) with different strategies collide within the TTL and one plan serves
1891
+ // another plan's cached results. Issue #1335 (codex review on #1422).
1892
+ const strategyFingerprint = `${this.qmdSearchStrategy}:${this.qmdSubprocessStrategy}`;
1893
+ const cacheKey = createHash("sha256")
1894
+ .update(`${strategyFingerprint}:${col}:${n}:${optionsFingerprint}:${trimmed}`)
1895
+ .digest("hex");
1827
1896
  const cached = getCachedQmdSearch(cacheKey);
1828
1897
  if (cached) {
1829
1898
  log.debug(`QMD search cache hit (${cached.length} results)`);
@@ -2066,8 +2135,9 @@ export class QmdClient implements SearchBackend {
2066
2135
  // QMD v2: query tool expects { searches: [...], collections?: [...] }
2067
2136
  // The MCP tool is structured-only in 2.x; use lex+vec+hyde by default
2068
2137
  // to exercise QMD's RRF + rerank path and let callers override when
2069
- // they have stronger query-document structure.
2070
- const searches = buildDefaultStructuredSearches(query, options);
2138
+ // they have stronger query-document structure. Operators on CPU-only
2139
+ // models can downgrade the plan via qmdSearchStrategy (issue #1335).
2140
+ const searches = buildDefaultStructuredSearches(query, options, this.qmdSearchStrategy);
2071
2141
  args = { searches, limit: maxResults };
2072
2142
  if (collection) {
2073
2143
  args.collections = [collection];
@@ -2082,7 +2152,7 @@ export class QmdClient implements SearchBackend {
2082
2152
  this.addResolvedSearchOptionsToMcpArgs(args, options);
2083
2153
  }
2084
2154
 
2085
- const result = await this.daemonSession.callTool("query", args, QMD_DAEMON_TIMEOUT_MS, signal);
2155
+ const result = await this.daemonSession.callTool("query", args, this.daemonTimeoutMs, signal);
2086
2156
  const durationMs = Date.now() - startedAtMs;
2087
2157
 
2088
2158
  if (this.slowLog?.enabled && durationMs >= this.slowLog.thresholdMs) {
@@ -2134,7 +2204,7 @@ export class QmdClient implements SearchBackend {
2134
2204
  collections: [collection],
2135
2205
  limit: maxResults,
2136
2206
  },
2137
- QMD_DAEMON_TIMEOUT_MS,
2207
+ this.daemonTimeoutMs,
2138
2208
  signal,
2139
2209
  );
2140
2210
  } else {
@@ -2142,7 +2212,7 @@ export class QmdClient implements SearchBackend {
2142
2212
  result = await this.daemonSession.callTool(
2143
2213
  "search",
2144
2214
  { query, limit: maxResults, collection },
2145
- QMD_DAEMON_TIMEOUT_MS,
2215
+ this.daemonTimeoutMs,
2146
2216
  signal,
2147
2217
  );
2148
2218
  }
@@ -2187,7 +2257,7 @@ export class QmdClient implements SearchBackend {
2187
2257
  collections: [collection],
2188
2258
  limit: maxResults,
2189
2259
  },
2190
- QMD_DAEMON_TIMEOUT_MS,
2260
+ this.daemonTimeoutMs,
2191
2261
  signal,
2192
2262
  );
2193
2263
  } else {
@@ -2195,7 +2265,7 @@ export class QmdClient implements SearchBackend {
2195
2265
  result = await this.daemonSession.callTool(
2196
2266
  "vsearch",
2197
2267
  { query, limit: maxResults, collection },
2198
- QMD_DAEMON_TIMEOUT_MS,
2268
+ this.daemonTimeoutMs,
2199
2269
  signal,
2200
2270
  );
2201
2271
  }
@@ -2228,6 +2298,17 @@ export class QmdClient implements SearchBackend {
2228
2298
  ): Promise<QmdSearchResult[]> {
2229
2299
  if (this.available === false) return [];
2230
2300
 
2301
+ // INTENTIONAL — DO NOT change the default command from `query` to `search`.
2302
+ // `qmd query` performs LLM query expansion + reranking that Remnic depends on
2303
+ // (Remnic's own reranking is disabled because `qmd query` handles it). This is
2304
+ // CLAUDE.md gotcha #7. On very large collections `qmd query` can be slow/hang
2305
+ // (issue #1335), so operators may opt into BM25-only `qmd search` by setting
2306
+ // `qmdSubprocessStrategy: "search"` — but that trades away expansion + rerank,
2307
+ // so it stays opt-in and the default remains `query`.
2308
+ if (this.qmdSubprocessStrategy === "search") {
2309
+ return this.bm25SearchViaSubprocess(query, collection, maxResults, signal);
2310
+ }
2311
+
2231
2312
  const startedAtMs = Date.now();
2232
2313
  try {
2233
2314
  const args = ["query", query, "-c", collection];
@@ -2311,15 +2392,24 @@ export class QmdClient implements SearchBackend {
2311
2392
 
2312
2393
  const startedAtMs = Date.now();
2313
2394
  try {
2314
- const args = ["query", query];
2395
+ // Mirror searchViaSubprocess: default `qmd query` keeps LLM expansion +
2396
+ // rerank (gotcha #7); `qmdSubprocessStrategy: "search"` opts into BM25-only
2397
+ // `qmd search` for both scoped AND global recall so the gate stays uniform
2398
+ // across every subprocess path (gotcha #39). Issue #1335.
2399
+ const bm25 = this.qmdSubprocessStrategy === "search";
2400
+ const args = bm25 ? ["search", query] : ["query", query];
2315
2401
  this.addQmdJsonOutputArgs(args);
2316
2402
  args.push("-n", String(maxResults));
2317
- this.addResolvedSearchOptionsToArgs(args, options);
2403
+ // BM25 `qmd search` takes no expansion/rerank options — only forward them
2404
+ // for the `query` command.
2405
+ if (!bm25) {
2406
+ this.addResolvedSearchOptionsToArgs(args, options);
2407
+ }
2318
2408
  const { stdout } = await this.runQmdCommand(args, QMD_TIMEOUT_MS, signal);
2319
2409
  const durationMs = Date.now() - startedAtMs;
2320
2410
  if (this.slowLog?.enabled && durationMs >= this.slowLog.thresholdMs) {
2321
2411
  log.warn(
2322
- `SLOW QMD global query: durationMs=${durationMs} maxResults=${maxResults} queryChars=${query.length}`,
2412
+ `SLOW QMD global ${bm25 ? "search" : "query"}: durationMs=${durationMs} maxResults=${maxResults} queryChars=${query.length}`,
2323
2413
  );
2324
2414
  }
2325
2415
 
@@ -2597,16 +2687,22 @@ export class QmdClient implements SearchBackend {
2597
2687
 
2598
2688
  async ensureCollection(
2599
2689
  memoryDir: string,
2690
+ collectionOrExecution?: string | SearchExecutionOptions,
2600
2691
  execution?: SearchExecutionOptions,
2601
2692
  ): Promise<"present" | "missing" | "unknown" | "skipped"> {
2693
+ const { collection, execution: effectiveExecution } = resolveEnsureCollectionArgs(
2694
+ collectionOrExecution,
2695
+ execution,
2696
+ );
2602
2697
  if (this.available === false && !this.daemonAvailable) return "unknown";
2603
2698
  // If only daemon is available (no CLI), skip collection check
2604
2699
  if (this.available === false) return "skipped";
2700
+ const targetCollection = collection ?? this.collection;
2605
2701
  try {
2606
- const { stdout } = await this.runQmdCommand(["collection", "list"], QMD_TIMEOUT_MS, execution?.signal);
2702
+ const { stdout } = await this.runQmdCommand(["collection", "list"], QMD_TIMEOUT_MS, effectiveExecution?.signal);
2607
2703
  // Parse text output: "openclaw-engram (qmd://openclaw-engram/)"
2608
2704
  const collectionRegex = new RegExp(
2609
- `^${this.collection}\\s+\\(qmd://`,
2705
+ `^${escapeRegExp(targetCollection)}\\s+\\(qmd://`,
2610
2706
  "m",
2611
2707
  );
2612
2708
  if (collectionRegex.test(stdout)) {
@@ -2616,15 +2712,19 @@ export class QmdClient implements SearchBackend {
2616
2712
  // Treat command/probe failures as unknown so callers do not disable features
2617
2713
  // permanently after a transient CLI or daemon hiccup.
2618
2714
  log.debug(
2619
- `QMD collection check unavailable for "${this.collection}" (will not disable features): ${err instanceof Error ? err.message : String(err)}`,
2715
+ `QMD collection check unavailable for "${targetCollection}" (will not disable features): ${err instanceof Error ? err.message : String(err)}`,
2620
2716
  );
2621
2717
  return "unknown";
2622
2718
  }
2623
2719
 
2624
2720
  log.info(
2625
- `QMD collection "${this.collection}" not found. ` +
2721
+ `QMD collection "${targetCollection}" not found. ` +
2626
2722
  `Add it to ~/.config/qmd/index.yml pointing at ${memoryDir}`,
2627
2723
  );
2628
2724
  return "missing";
2629
2725
  }
2630
2726
  }
2727
+
2728
+ function escapeRegExp(value: string): string {
2729
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2730
+ }
@@ -0,0 +1,224 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { parseConfig } from "./config.js";
5
+ import {
6
+ planRecallModeLLM,
7
+ resolveRecallPlannerLlmOptions,
8
+ } from "./recall-planner-llm.js";
9
+ import type { FallbackLlmClient } from "./fallback-llm.js";
10
+ import type { RecallPlanMode } from "./types.js";
11
+
12
+ // A stub FallbackLlmClient that records the options it was called with and
13
+ // returns a scripted classification (or simulates a failure).
14
+ function stubLlm(opts: {
15
+ available?: boolean;
16
+ capturedOptions?: Array<Record<string, unknown>>;
17
+ result?: { mode: RecallPlanMode; reason?: string | null } | null;
18
+ modelUsed?: string;
19
+ throwError?: string;
20
+ }): FallbackLlmClient {
21
+ return {
22
+ isAvailable: () => opts.available !== false,
23
+ parseWithSchemaDetailed: async (
24
+ _messages: unknown,
25
+ schema: { parse: (v: unknown) => unknown },
26
+ options: Record<string, unknown>,
27
+ ) => {
28
+ opts.capturedOptions?.push(options);
29
+ if (opts.throwError) throw new Error(opts.throwError);
30
+ if (opts.result === null || opts.result === undefined) return null;
31
+ // Exercise the real schema so malformed scripted output is caught too.
32
+ const parsed = schema.parse(opts.result);
33
+ return { result: parsed, modelUsed: opts.modelUsed ?? "test/model" };
34
+ },
35
+ } as unknown as FallbackLlmClient;
36
+ }
37
+
38
+ test("returns heuristic without calling the LLM when recallPlannerLlmEnabled is false", async () => {
39
+ const config = parseConfig({ recallPlannerLlmEnabled: false });
40
+ const captured: Array<Record<string, unknown>> = [];
41
+ const llm = stubLlm({ capturedOptions: captured, result: { mode: "no_recall" } });
42
+
43
+ const result = await planRecallModeLLM("what did we decide about auth?", undefined, config, llm);
44
+
45
+ assert.equal(captured.length, 0, "LLM must not be contacted when disabled");
46
+ assert.equal(result.source, "heuristic");
47
+ assert.equal(result.fallbackUsed, false);
48
+ // Memory-seeking question → heuristic "full".
49
+ assert.equal(result.mode, "full");
50
+ assert.equal(result.heuristicMode, "full");
51
+ });
52
+
53
+ test("uses the LLM classification when enabled", async () => {
54
+ const config = parseConfig({ recallPlannerLlmEnabled: true });
55
+ const llm = stubLlm({ result: { mode: "graph_mode", reason: "asks for root cause" }, modelUsed: "anthropic/claude" });
56
+
57
+ const result = await planRecallModeLLM("restart the gateway", undefined, config, llm);
58
+
59
+ assert.equal(result.source, "llm");
60
+ assert.equal(result.mode, "graph_mode");
61
+ assert.equal(result.reason, "asks for root cause");
62
+ assert.equal(result.modelUsed, "anthropic/claude");
63
+ assert.equal(result.fallbackUsed, false);
64
+ });
65
+
66
+ test("forwards taskModelChain AND recallPlannerModel in gateway mode (provider-agnostic routing)", async () => {
67
+ const config = parseConfig({
68
+ recallPlannerLlmEnabled: true,
69
+ modelSource: "gateway",
70
+ gatewayAgentId: "persona-agent",
71
+ taskModelChain: { primary: "zai/glm-4.7-flash", fallbacks: ["fireworks/x/glm-5p1"] },
72
+ recallPlannerModel: "anthropic/claude-haiku-4-5",
73
+ });
74
+ const captured: Array<Record<string, unknown>> = [];
75
+ const llm = stubLlm({ capturedOptions: captured, result: { mode: "minimal" } });
76
+
77
+ await planRecallModeLLM("check status", undefined, config, llm);
78
+
79
+ assert.equal(captured.length, 1);
80
+ // recallPlannerModel is tried first (prepended), taskModelChain is the fallback chain.
81
+ assert.equal(captured[0]?.model, "anthropic/claude-haiku-4-5");
82
+ assert.deepEqual(captured[0]?.modelChain, {
83
+ primary: "zai/glm-4.7-flash",
84
+ fallbacks: ["fireworks/x/glm-5p1"],
85
+ });
86
+ // taskModelChain wins over the agent persona (gotcha #22).
87
+ assert.equal(captured[0]?.agentId, undefined);
88
+ assert.equal(captured[0]?.timeoutMs, config.recallPlannerTimeoutMs);
89
+ });
90
+
91
+ test("plugin mode passes only the explicit model, no gateway chain", async () => {
92
+ const config = parseConfig({
93
+ recallPlannerLlmEnabled: true,
94
+ modelSource: "plugin",
95
+ recallPlannerModel: "openai/gpt-5.5",
96
+ });
97
+ const captured: Array<Record<string, unknown>> = [];
98
+ const llm = stubLlm({ capturedOptions: captured, result: { mode: "full" } });
99
+
100
+ await planRecallModeLLM("summarize the project", undefined, config, llm);
101
+
102
+ assert.equal(captured.length, 1);
103
+ assert.equal(captured[0]?.model, "openai/gpt-5.5");
104
+ assert.equal(captured[0]?.modelChain, undefined);
105
+ assert.equal(captured[0]?.agentId, undefined);
106
+ });
107
+
108
+ test("falls back to heuristic when the LLM throws", async () => {
109
+ const config = parseConfig({ recallPlannerLlmEnabled: true });
110
+ const llm = stubLlm({ throwError: "boom" });
111
+
112
+ const result = await planRecallModeLLM("what happened during the outage?", undefined, config, llm);
113
+
114
+ assert.equal(result.source, "heuristic-fallback");
115
+ assert.equal(result.fallbackUsed, true);
116
+ assert.match(result.reason, /llm-error:boom/);
117
+ // "what happened" → heuristic graph_mode.
118
+ assert.equal(result.mode, "graph_mode");
119
+ assert.equal(result.mode, result.heuristicMode);
120
+ });
121
+
122
+ test("falls back to heuristic when the LLM returns no parseable result", async () => {
123
+ const config = parseConfig({ recallPlannerLlmEnabled: true });
124
+ const llm = stubLlm({ result: null });
125
+
126
+ const result = await planRecallModeLLM("how did we get here?", undefined, config, llm);
127
+
128
+ assert.equal(result.source, "heuristic-fallback");
129
+ assert.equal(result.fallbackUsed, true);
130
+ assert.equal(result.reason, "llm-empty");
131
+ });
132
+
133
+ test("falls back without a network attempt when the chain is empty and the model is bare (default gpt-5.5)", async () => {
134
+ // The legacy default recallPlannerModel "gpt-5.5" is bare (no provider/),
135
+ // which FallbackLlmClient cannot resolve — so with no gateway chain there is
136
+ // nothing routable and the planner must short-circuit to the heuristic
137
+ // (issue #1367 review on PR #1428), not log an invalid-model warning per call.
138
+ const config = parseConfig({ recallPlannerLlmEnabled: true });
139
+ const captured: Array<Record<string, unknown>> = [];
140
+ const llm = stubLlm({ available: false, capturedOptions: captured, result: { mode: "full" } });
141
+
142
+ const result = await planRecallModeLLM("anything", undefined, config, llm);
143
+
144
+ assert.equal(captured.length, 0, "no network attempt when nothing is routable");
145
+ assert.equal(result.source, "heuristic-fallback");
146
+ assert.equal(result.fallbackUsed, true);
147
+ assert.equal(result.reason, "llm-no-model");
148
+ });
149
+
150
+ test("attempts the call (and falls back) when a provider-qualified model override is set even if the chain is empty", async () => {
151
+ // A qualified `provider/model` override is genuinely routable, so we attempt
152
+ // it even when the chain probe reports unavailable, then fall back on a null
153
+ // response.
154
+ const config = parseConfig({ recallPlannerLlmEnabled: true, recallPlannerModel: "openai/gpt-5.5" });
155
+ const captured: Array<Record<string, unknown>> = [];
156
+ const llm = stubLlm({ available: false, capturedOptions: captured, result: null });
157
+
158
+ const result = await planRecallModeLLM("anything", undefined, config, llm);
159
+
160
+ assert.equal(captured.length, 1, "qualified model override → still attempt the call");
161
+ assert.equal(captured[0]?.model, "openai/gpt-5.5");
162
+ assert.equal(result.source, "heuristic-fallback");
163
+ assert.equal(result.reason, "llm-empty");
164
+ });
165
+
166
+ test("an already-aborted recall short-circuits to the heuristic without an LLM call", async () => {
167
+ const config = parseConfig({ recallPlannerLlmEnabled: true, recallPlannerModel: "openai/gpt-5.5" });
168
+ const captured: Array<Record<string, unknown>> = [];
169
+ const llm = stubLlm({ capturedOptions: captured, result: { mode: "full" } });
170
+ const ac = new AbortController();
171
+ ac.abort();
172
+
173
+ const result = await planRecallModeLLM("what did we decide?", undefined, config, llm, ac.signal);
174
+
175
+ assert.equal(captured.length, 0, "no LLM call when the recall is already aborted");
176
+ assert.equal(result.source, "heuristic-fallback");
177
+ assert.equal(result.reason, "aborted");
178
+ assert.equal(result.fallbackUsed, true);
179
+ });
180
+
181
+ test("forwards the abort signal into the LLM call (cancellation contract)", async () => {
182
+ const config = parseConfig({ recallPlannerLlmEnabled: true, recallPlannerModel: "openai/gpt-5.5" });
183
+ const captured: Array<Record<string, unknown>> = [];
184
+ const llm = stubLlm({ capturedOptions: captured, result: { mode: "minimal" } });
185
+ const ac = new AbortController();
186
+
187
+ await planRecallModeLLM("check status", undefined, config, llm, ac.signal);
188
+
189
+ assert.equal(captured.length, 1);
190
+ assert.equal(captured[0]?.signal, ac.signal, "recall abort signal must reach FallbackLlmClient");
191
+ });
192
+
193
+ test("empty prompts skip the LLM entirely", async () => {
194
+ const config = parseConfig({ recallPlannerLlmEnabled: true });
195
+ const captured: Array<Record<string, unknown>> = [];
196
+ const llm = stubLlm({ capturedOptions: captured, result: { mode: "full" } });
197
+
198
+ const result = await planRecallModeLLM(" ", undefined, config, llm);
199
+
200
+ assert.equal(captured.length, 0);
201
+ assert.equal(result.mode, "no_recall"); // heuristic returns no_recall for empty
202
+ assert.equal(result.source, "heuristic");
203
+ });
204
+
205
+ test("resolveRecallPlannerLlmOptions clamps timeout and sets deterministic decoding", () => {
206
+ const config = parseConfig({ recallPlannerLlmEnabled: true, recallPlannerTimeoutMs: 0 });
207
+ const options = resolveRecallPlannerLlmOptions(config);
208
+ assert.equal(options.temperature, 0);
209
+ assert.equal(options.maxTokens, 64);
210
+ assert.equal(options.timeoutMs, 1500, "non-positive timeout falls back to 1500");
211
+ });
212
+
213
+ test("resolveRecallPlannerLlmOptions drops bare model names but keeps provider-qualified ones", () => {
214
+ // Bare "gpt-5.5" is unresolvable by FallbackLlmClient → dropped (routing falls
215
+ // through to the chain); a qualified value is forwarded as the override.
216
+ const bare = resolveRecallPlannerLlmOptions(
217
+ parseConfig({ recallPlannerLlmEnabled: true, recallPlannerModel: "gpt-5.5" }),
218
+ );
219
+ assert.equal(bare.model, undefined);
220
+ const qualified = resolveRecallPlannerLlmOptions(
221
+ parseConfig({ recallPlannerLlmEnabled: true, recallPlannerModel: "anthropic/claude-haiku-4-5" }),
222
+ );
223
+ assert.equal(qualified.model, "anthropic/claude-haiku-4-5");
224
+ });