@remnic/core 9.3.613 → 9.3.615

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 (386) hide show
  1. package/dist/access-cli.js +59 -58
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.d.ts +4 -2
  4. package/dist/access-http.js +23 -23
  5. package/dist/access-mcp.d.ts +9 -2
  6. package/dist/access-mcp.js +20 -20
  7. package/dist/access-schema.d.ts +26 -14
  8. package/dist/access-schema.js +3 -3
  9. package/dist/{access-service-D2J9dh_9.d.ts → access-service-CBNEKjzN.d.ts} +71 -6
  10. package/dist/access-service.d.ts +2 -2
  11. package/dist/access-service.js +17 -17
  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-TH67Q46T.js → chunk-5OHHEORR.js} +64 -21
  48. package/dist/chunk-5OHHEORR.js.map +1 -0
  49. package/dist/{chunk-CO7ZO4TU.js → chunk-5VDJMYTF.js} +2 -2
  50. package/dist/{chunk-BFBF3XEF.js → chunk-6BDVBBBY.js} +33 -25
  51. package/dist/{chunk-BFBF3XEF.js.map → chunk-6BDVBBBY.js.map} +1 -1
  52. package/dist/{chunk-EAZGEEG2.js → chunk-6L46YAEZ.js} +45 -9
  53. package/dist/chunk-6L46YAEZ.js.map +1 -0
  54. package/dist/{chunk-YFS5OEKO.js → chunk-7MLB4NCL.js} +2 -2
  55. package/dist/{chunk-LZ3VEOU5.js → chunk-AL4RAJL5.js} +22 -5
  56. package/dist/chunk-AL4RAJL5.js.map +1 -0
  57. package/dist/{chunk-557IAFPD.js → chunk-APRRL26Q.js} +2 -2
  58. package/dist/{chunk-QDDHYAKV.js → chunk-AZDOWD2L.js} +2 -2
  59. package/dist/{chunk-MLT75J5S.js → chunk-B6SU7YSE.js} +3 -3
  60. package/dist/{chunk-FXKPZ3H6.js → chunk-BPSGLMQ4.js} +2 -2
  61. package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
  62. package/dist/{chunk-NOMEVTUD.js → chunk-C6C7XVKG.js} +5 -4
  63. package/dist/chunk-C6C7XVKG.js.map +1 -0
  64. package/dist/{chunk-XKIQZXUB.js → chunk-CI7RKSRE.js} +7 -1
  65. package/dist/chunk-CI7RKSRE.js.map +1 -0
  66. package/dist/{chunk-IK34DVAC.js → chunk-CIOMS6DI.js} +2 -2
  67. package/dist/{chunk-2I5JGH3M.js → chunk-CYEPCZN5.js} +2 -2
  68. package/dist/{chunk-2I5JGH3M.js.map → chunk-CYEPCZN5.js.map} +1 -1
  69. package/dist/{chunk-JHMFYY7L.js → chunk-DCGT4FPP.js} +13 -5
  70. package/dist/chunk-DCGT4FPP.js.map +1 -0
  71. package/dist/{chunk-7DZRO2DC.js → chunk-DEPRLVLK.js} +2 -2
  72. package/dist/{chunk-CSKLPDN6.js → chunk-DEVUWMME.js} +52 -19
  73. package/dist/chunk-DEVUWMME.js.map +1 -0
  74. package/dist/{chunk-DHGSZ3UD.js → chunk-DGNQRNLL.js} +2 -2
  75. package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
  76. package/dist/{chunk-HJNQQICM.js → chunk-EXUAP5LH.js} +108 -51
  77. package/dist/chunk-EXUAP5LH.js.map +1 -0
  78. package/dist/chunk-FAV25DUZ.js +12 -0
  79. package/dist/chunk-FAV25DUZ.js.map +1 -0
  80. package/dist/{chunk-ETUPBUHB.js → chunk-GDASG7NC.js} +2 -2
  81. package/dist/{chunk-L227SKTB.js → chunk-GDB4J2H3.js} +17 -1
  82. package/dist/chunk-GDB4J2H3.js.map +1 -0
  83. package/dist/{chunk-IP73YCZP.js → chunk-GLPBYIXN.js} +4 -2
  84. package/dist/chunk-GLPBYIXN.js.map +1 -0
  85. package/dist/{chunk-4HP7HIE3.js → chunk-HP5FMB6L.js} +2 -2
  86. package/dist/{chunk-EVZFIAPG.js → chunk-IBTZEBUD.js} +23 -10
  87. package/dist/chunk-IBTZEBUD.js.map +1 -0
  88. package/dist/{chunk-DOX2CG6Y.js → chunk-IEUU7O4F.js} +2 -2
  89. package/dist/{chunk-EUML3N6B.js → chunk-IMA6GU4Y.js} +3 -3
  90. package/dist/chunk-IMA6GU4Y.js.map +1 -0
  91. package/dist/{chunk-JNANKJLN.js → chunk-JOASJWQR.js} +2 -2
  92. package/dist/chunk-JOASJWQR.js.map +1 -0
  93. package/dist/{chunk-WSGF57U2.js → chunk-JQDZQ4TB.js} +2 -2
  94. package/dist/{chunk-HINSGUA7.js → chunk-KBL3JJR6.js} +9 -13
  95. package/dist/chunk-KBL3JJR6.js.map +1 -0
  96. package/dist/{chunk-IOTENEVL.js → chunk-KGLPJROV.js} +57 -50
  97. package/dist/chunk-KGLPJROV.js.map +1 -0
  98. package/dist/{chunk-W7L6HXUC.js → chunk-LXOM6IQU.js} +2 -2
  99. package/dist/{chunk-G6R5UD3Q.js → chunk-MGN7VHWQ.js} +42 -1
  100. package/dist/{chunk-G6R5UD3Q.js.map → chunk-MGN7VHWQ.js.map} +1 -1
  101. package/dist/{chunk-DLJ4IR6M.js → chunk-MHQC2WU2.js} +2 -2
  102. package/dist/chunk-MHQC2WU2.js.map +1 -0
  103. package/dist/{chunk-5RPTH6AU.js → chunk-NM5NQYJE.js} +20 -19
  104. package/dist/chunk-NM5NQYJE.js.map +1 -0
  105. package/dist/{chunk-6JGNHWCI.js → chunk-OBIRVF36.js} +3 -3
  106. package/dist/{chunk-CHCA44C3.js → chunk-ODPLEWB6.js} +3 -3
  107. package/dist/chunk-ODPLEWB6.js.map +1 -0
  108. package/dist/{chunk-HENLZHIT.js → chunk-OIF36KGD.js} +7 -4
  109. package/dist/chunk-OIF36KGD.js.map +1 -0
  110. package/dist/{chunk-GUPISBV2.js → chunk-PP2JH3GP.js} +2 -2
  111. package/dist/{chunk-OXJBNGBK.js → chunk-PSUB67YB.js} +2 -2
  112. package/dist/{chunk-UWY7GIVS.js → chunk-PYIFUBRK.js} +45 -13
  113. package/dist/chunk-PYIFUBRK.js.map +1 -0
  114. package/dist/{chunk-KIB7SDIJ.js → chunk-Q6YIJGXJ.js} +2 -2
  115. package/dist/{chunk-ZT3EGNLR.js → chunk-QPD426WT.js} +2 -2
  116. package/dist/{chunk-RLV3PQGH.js → chunk-QVO4YOB7.js} +6 -6
  117. package/dist/{chunk-GMAG2HS4.js → chunk-RG3LBSGH.js} +46 -9
  118. package/dist/chunk-RG3LBSGH.js.map +1 -0
  119. package/dist/{chunk-XSWKORGM.js → chunk-S53OYO3F.js} +3 -1
  120. package/dist/chunk-S53OYO3F.js.map +1 -0
  121. package/dist/{chunk-YCN4BVDK.js → chunk-SCPFRKIT.js} +4 -2
  122. package/dist/chunk-SCPFRKIT.js.map +1 -0
  123. package/dist/{chunk-NZPF2SYV.js → chunk-T7N6KQGS.js} +138 -5
  124. package/dist/chunk-T7N6KQGS.js.map +1 -0
  125. package/dist/{chunk-VJXSUAO7.js → chunk-TNOWU6RP.js} +13 -10
  126. package/dist/chunk-TNOWU6RP.js.map +1 -0
  127. package/dist/{chunk-PCI747N2.js → chunk-TZVQQTG4.js} +48 -19
  128. package/dist/chunk-TZVQQTG4.js.map +1 -0
  129. package/dist/{chunk-KQAFEZQX.js → chunk-VDX2J7OX.js} +2 -2
  130. package/dist/{chunk-IK7DCC5H.js → chunk-VMGLYN42.js} +2 -2
  131. package/dist/{chunk-KM2A35EO.js → chunk-WB3LYXC5.js} +11 -7
  132. package/dist/chunk-WB3LYXC5.js.map +1 -0
  133. package/dist/{chunk-PPPZY2EU.js → chunk-WD2W4234.js} +9 -3
  134. package/dist/chunk-WD2W4234.js.map +1 -0
  135. package/dist/{chunk-NSKYFGDL.js → chunk-X4QQB7O6.js} +2 -2
  136. package/dist/{chunk-HPWVAEET.js → chunk-X6IRLNOO.js} +3 -7
  137. package/dist/chunk-X6IRLNOO.js.map +1 -0
  138. package/dist/{chunk-46GJIW5M.js → chunk-XAZOWLW4.js} +5 -5
  139. package/dist/{chunk-46GJIW5M.js.map → chunk-XAZOWLW4.js.map} +1 -1
  140. package/dist/{chunk-XPSVGJYA.js → chunk-YRMKDTKF.js} +12 -9
  141. package/dist/chunk-YRMKDTKF.js.map +1 -0
  142. package/dist/{chunk-6ZZP4EJF.js → chunk-ZJR7VG5L.js} +3 -3
  143. package/dist/{chunk-6ZZP4EJF.js.map → chunk-ZJR7VG5L.js.map} +1 -1
  144. package/dist/{chunk-2QANQKSQ.js → chunk-ZK32E74R.js} +156 -45
  145. package/dist/chunk-ZK32E74R.js.map +1 -0
  146. package/dist/{cli-OrfKXNU4.d.ts → cli-Cw729yLf.d.ts} +6 -2
  147. package/dist/cli.d.ts +3 -3
  148. package/dist/cli.js +61 -60
  149. package/dist/compounding/engine.js +3 -3
  150. package/dist/compounding/preference-consolidator.js +39 -11
  151. package/dist/compounding/preference-consolidator.js.map +1 -1
  152. package/dist/config.js +1 -1
  153. package/dist/connectors/codex-materialize-runner.js +3 -3
  154. package/dist/connectors/index.js +3 -3
  155. package/dist/consolidation-provenance-check.js +1 -1
  156. package/dist/contradiction/index.js +4 -4
  157. package/dist/conversation-index/backend.js +2 -2
  158. package/dist/conversation-index/indexer.js +1 -1
  159. package/dist/cross-namespace-budget.js +1 -1
  160. package/dist/enrichment/index.js +1 -1
  161. package/dist/entity-retrieval.js +3 -3
  162. package/dist/evals.js +1 -1
  163. package/dist/explicit-capture.d.ts +11 -1
  164. package/dist/explicit-capture.js +1 -1
  165. package/dist/extraction-judge.js +8 -1
  166. package/dist/extraction.js +2 -2
  167. package/dist/fallback-llm.d.ts +23 -6
  168. package/dist/fallback-llm.js +5 -3
  169. package/dist/{first-start-migration-GYJWIH36.js → first-start-migration-FF7YFGRP.js} +6 -6
  170. package/dist/index.d.ts +3 -3
  171. package/dist/index.js +95 -94
  172. package/dist/index.js.map +1 -1
  173. package/dist/lcm/archive.js +2 -2
  174. package/dist/lcm/engine.js +5 -5
  175. package/dist/lcm/index.js +7 -7
  176. package/dist/lcm/summarizer.js +3 -3
  177. package/dist/maintenance/memory-governance-cron.d.ts +6 -4
  178. package/dist/maintenance/memory-governance-cron.js +1 -1
  179. package/dist/maintenance/memory-governance.js +3 -3
  180. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  181. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  182. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  183. package/dist/mcp-memory-inspector-app.js +1 -1
  184. package/dist/migrate/from-engram.js +1 -1
  185. package/dist/namespaces/migrate.js +16 -15
  186. package/dist/namespaces/search.js +12 -11
  187. package/dist/namespaces/storage.js +3 -3
  188. package/dist/network/webdav.d.ts +2 -0
  189. package/dist/network/webdav.js +1 -1
  190. package/dist/objective-state-writers.js +2 -2
  191. package/dist/operator-toolkit.d.ts +3 -1
  192. package/dist/operator-toolkit.js +21 -20
  193. package/dist/{orchestrator-DTRQG75J.d.ts → orchestrator-CqWOjfgl.d.ts} +46 -3
  194. package/dist/orchestrator.d.ts +1 -1
  195. package/dist/orchestrator.js +48 -45
  196. package/dist/patterns-cli.js +1 -1
  197. package/dist/qmd-recall-cache.d.ts +2 -0
  198. package/dist/qmd-recall-cache.js +1 -1
  199. package/dist/qmd.d.ts +37 -2
  200. package/dist/qmd.js +4 -1
  201. package/dist/recall-explain-renderer.js +3 -3
  202. package/dist/recall-planner-llm.d.ts +57 -0
  203. package/dist/recall-planner-llm.js +167 -0
  204. package/dist/recall-planner-llm.js.map +1 -0
  205. package/dist/recall-xray-cli.js +4 -4
  206. package/dist/recall-xray-renderer.js +3 -3
  207. package/dist/recall-xray.js +2 -2
  208. package/dist/resume-bundles.js +2 -2
  209. package/dist/retrieval-agents.js +2 -2
  210. package/dist/routing/store.js +1 -1
  211. package/dist/schemas.d.ts +22 -22
  212. package/dist/search/factory.js +11 -10
  213. package/dist/search/index.js +11 -10
  214. package/dist/search/lancedb-backend.d.ts +1 -1
  215. package/dist/search/lancedb-backend.js +3 -2
  216. package/dist/search/meilisearch-backend.d.ts +1 -1
  217. package/dist/search/meilisearch-backend.js +3 -2
  218. package/dist/search/noop-backend.d.ts +1 -1
  219. package/dist/search/noop-backend.js +1 -1
  220. package/dist/search/orama-backend.d.ts +1 -1
  221. package/dist/search/orama-backend.js +3 -2
  222. package/dist/search/port.d.ts +6 -1
  223. package/dist/search/port.js +7 -0
  224. package/dist/search/remote-backend.d.ts +1 -1
  225. package/dist/search/remote-backend.js +1 -1
  226. package/dist/semantic-consolidation.js +4 -4
  227. package/dist/semantic-rule-promotion.js +3 -3
  228. package/dist/semantic-rule-verifier.js +3 -3
  229. package/dist/session-observer-state.js +1 -1
  230. package/dist/storage.js +2 -2
  231. package/dist/summarizer.js +2 -2
  232. package/dist/temporal-index.js +1 -1
  233. package/dist/{tier-stats-SKML2OSF.js → tier-stats-3LYQ3VV5.js} +3 -3
  234. package/dist/transfer/backup.js +2 -2
  235. package/dist/transfer/capsule-export.js +2 -2
  236. package/dist/transfer/capsule-import.js +2 -2
  237. package/dist/transfer/export-sqlite.js +1 -1
  238. package/dist/transfer/types.d.ts +12 -12
  239. package/dist/types.d.ts +32 -0
  240. package/dist/types.js +1 -1
  241. package/dist/utility-learner.js +1 -1
  242. package/dist/utility-runtime.js +2 -2
  243. package/dist/verified-recall.js +3 -3
  244. package/dist/work/board.js +2 -2
  245. package/dist/work/storage.d.ts +2 -0
  246. package/dist/work/storage.js +1 -1
  247. package/package.json +1 -1
  248. package/src/access-http.ts +24 -10
  249. package/src/access-mcp.test.ts +160 -0
  250. package/src/access-mcp.ts +72 -7
  251. package/src/access-schema.ts +11 -0
  252. package/src/access-service-coding-write.test.ts +478 -0
  253. package/src/access-service.ts +237 -32
  254. package/src/active-recall.test.ts +40 -0
  255. package/src/active-recall.ts +19 -2
  256. package/src/behavior-learner.ts +5 -3
  257. package/src/buffer-session.test.ts +58 -0
  258. package/src/buffer-surprise-trigger.test.ts +4 -18
  259. package/src/buffer.ts +39 -11
  260. package/src/calibration.ts +10 -4
  261. package/src/causal-consolidation.test.ts +47 -2
  262. package/src/causal-consolidation.ts +13 -9
  263. package/src/cli.ts +19 -4
  264. package/src/compounding/engine.ts +2 -0
  265. package/src/compounding/preference-consolidator.test.ts +292 -0
  266. package/src/compounding/preference-consolidator.ts +55 -19
  267. package/src/config.test.ts +213 -0
  268. package/src/config.ts +175 -4
  269. package/src/connectors/codex-materialize-runner.ts +7 -4
  270. package/src/consolidation-provenance-check.ts +24 -5
  271. package/src/conversation-index/indexer.test.ts +22 -0
  272. package/src/conversation-index/indexer.ts +7 -3
  273. package/src/cross-namespace-budget.test.ts +44 -21
  274. package/src/cross-namespace-budget.ts +2 -2
  275. package/src/enrichment/pipeline.ts +11 -16
  276. package/src/evals.ts +1 -1
  277. package/src/explicit-capture.ts +19 -2
  278. package/src/extraction-judge-chain.test.ts +55 -0
  279. package/src/extraction-judge.ts +7 -9
  280. package/src/extraction.ts +16 -5
  281. package/src/fallback-llm.test.ts +600 -1
  282. package/src/fallback-llm.ts +91 -22
  283. package/src/maintenance/memory-governance-cron.ts +39 -29
  284. package/src/mcp-memory-inspector-app.ts +54 -12
  285. package/src/message-parts/index.ts +6 -0
  286. package/src/message-parts/message-parts.test.ts +30 -0
  287. package/src/migrate/from-engram.ts +19 -5
  288. package/src/namespaces/search.test.ts +15 -2
  289. package/src/namespaces/search.ts +1 -1
  290. package/src/network/webdav.ts +61 -21
  291. package/src/operator-toolkit.ts +6 -2
  292. package/src/orchestrator.ts +173 -20
  293. package/src/qmd-client.test.ts +85 -0
  294. package/src/qmd-recall-cache.test.ts +16 -0
  295. package/src/qmd-recall-cache.ts +7 -0
  296. package/src/qmd.test.ts +54 -0
  297. package/src/qmd.ts +119 -19
  298. package/src/recall-planner-llm.test.ts +224 -0
  299. package/src/recall-planner-llm.ts +289 -0
  300. package/src/routing/store.ts +4 -8
  301. package/src/search/factory.ts +3 -0
  302. package/src/search/lancedb-backend.ts +15 -3
  303. package/src/search/meilisearch-backend.ts +70 -7
  304. package/src/search/noop-backend.ts +5 -1
  305. package/src/search/orama-backend.ts +15 -3
  306. package/src/search/port.ts +15 -0
  307. package/src/search/remote-backend.ts +5 -1
  308. package/src/session-observer-state.ts +1 -1
  309. package/src/summarizer.ts +3 -3
  310. package/src/temporal-index.test.ts +18 -0
  311. package/src/temporal-index.ts +45 -0
  312. package/src/training-export/cli-date-validation.test.ts +36 -0
  313. package/src/training-export/date-parse.ts +21 -2
  314. package/src/transfer/export-sqlite.ts +3 -0
  315. package/src/types.ts +35 -0
  316. package/src/utility-learner.ts +1 -0
  317. package/src/work/storage.ts +23 -0
  318. package/dist/chunk-2QANQKSQ.js.map +0 -1
  319. package/dist/chunk-5RPTH6AU.js.map +0 -1
  320. package/dist/chunk-AJA46VX5.js.map +0 -1
  321. package/dist/chunk-C4SQJZAF.js.map +0 -1
  322. package/dist/chunk-CHCA44C3.js.map +0 -1
  323. package/dist/chunk-CSKLPDN6.js.map +0 -1
  324. package/dist/chunk-DLJ4IR6M.js.map +0 -1
  325. package/dist/chunk-EAZGEEG2.js.map +0 -1
  326. package/dist/chunk-EUML3N6B.js.map +0 -1
  327. package/dist/chunk-EVZFIAPG.js.map +0 -1
  328. package/dist/chunk-G3Z3QEF5.js.map +0 -1
  329. package/dist/chunk-GMAG2HS4.js.map +0 -1
  330. package/dist/chunk-HENLZHIT.js.map +0 -1
  331. package/dist/chunk-HINSGUA7.js.map +0 -1
  332. package/dist/chunk-HJNQQICM.js.map +0 -1
  333. package/dist/chunk-HPWVAEET.js.map +0 -1
  334. package/dist/chunk-IOTENEVL.js.map +0 -1
  335. package/dist/chunk-IP73YCZP.js.map +0 -1
  336. package/dist/chunk-JHMFYY7L.js.map +0 -1
  337. package/dist/chunk-JNANKJLN.js.map +0 -1
  338. package/dist/chunk-KGK2QKWL.js.map +0 -1
  339. package/dist/chunk-KM2A35EO.js.map +0 -1
  340. package/dist/chunk-KVEVLBKC.js.map +0 -1
  341. package/dist/chunk-L227SKTB.js.map +0 -1
  342. package/dist/chunk-LZ3VEOU5.js.map +0 -1
  343. package/dist/chunk-NOMEVTUD.js.map +0 -1
  344. package/dist/chunk-NZPF2SYV.js.map +0 -1
  345. package/dist/chunk-PCI747N2.js.map +0 -1
  346. package/dist/chunk-PPPZY2EU.js.map +0 -1
  347. package/dist/chunk-TH67Q46T.js.map +0 -1
  348. package/dist/chunk-UWY7GIVS.js.map +0 -1
  349. package/dist/chunk-VJXSUAO7.js.map +0 -1
  350. package/dist/chunk-XKIQZXUB.js.map +0 -1
  351. package/dist/chunk-XPSVGJYA.js.map +0 -1
  352. package/dist/chunk-XSWKORGM.js.map +0 -1
  353. package/dist/chunk-YCN4BVDK.js.map +0 -1
  354. package/dist/chunk-ZDTVJXIP.js.map +0 -1
  355. /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
  356. /package/dist/{chunk-AU7Q3LSC.js.map → chunk-2QSZNTDO.js.map} +0 -0
  357. /package/dist/{chunk-HSVJGWYS.js.map → chunk-2ROPI5OE.js.map} +0 -0
  358. /package/dist/{chunk-CF3ZF2YU.js.map → chunk-3QSU4NFF.js.map} +0 -0
  359. /package/dist/{chunk-OI27U2HT.js.map → chunk-5BTCT236.js.map} +0 -0
  360. /package/dist/{chunk-CO7ZO4TU.js.map → chunk-5VDJMYTF.js.map} +0 -0
  361. /package/dist/{chunk-YFS5OEKO.js.map → chunk-7MLB4NCL.js.map} +0 -0
  362. /package/dist/{chunk-557IAFPD.js.map → chunk-APRRL26Q.js.map} +0 -0
  363. /package/dist/{chunk-QDDHYAKV.js.map → chunk-AZDOWD2L.js.map} +0 -0
  364. /package/dist/{chunk-MLT75J5S.js.map → chunk-B6SU7YSE.js.map} +0 -0
  365. /package/dist/{chunk-FXKPZ3H6.js.map → chunk-BPSGLMQ4.js.map} +0 -0
  366. /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
  367. /package/dist/{chunk-IK34DVAC.js.map → chunk-CIOMS6DI.js.map} +0 -0
  368. /package/dist/{chunk-7DZRO2DC.js.map → chunk-DEPRLVLK.js.map} +0 -0
  369. /package/dist/{chunk-DHGSZ3UD.js.map → chunk-DGNQRNLL.js.map} +0 -0
  370. /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.js.map} +0 -0
  371. /package/dist/{chunk-ETUPBUHB.js.map → chunk-GDASG7NC.js.map} +0 -0
  372. /package/dist/{chunk-4HP7HIE3.js.map → chunk-HP5FMB6L.js.map} +0 -0
  373. /package/dist/{chunk-DOX2CG6Y.js.map → chunk-IEUU7O4F.js.map} +0 -0
  374. /package/dist/{chunk-WSGF57U2.js.map → chunk-JQDZQ4TB.js.map} +0 -0
  375. /package/dist/{chunk-W7L6HXUC.js.map → chunk-LXOM6IQU.js.map} +0 -0
  376. /package/dist/{chunk-6JGNHWCI.js.map → chunk-OBIRVF36.js.map} +0 -0
  377. /package/dist/{chunk-GUPISBV2.js.map → chunk-PP2JH3GP.js.map} +0 -0
  378. /package/dist/{chunk-OXJBNGBK.js.map → chunk-PSUB67YB.js.map} +0 -0
  379. /package/dist/{chunk-KIB7SDIJ.js.map → chunk-Q6YIJGXJ.js.map} +0 -0
  380. /package/dist/{chunk-ZT3EGNLR.js.map → chunk-QPD426WT.js.map} +0 -0
  381. /package/dist/{chunk-RLV3PQGH.js.map → chunk-QVO4YOB7.js.map} +0 -0
  382. /package/dist/{chunk-KQAFEZQX.js.map → chunk-VDX2J7OX.js.map} +0 -0
  383. /package/dist/{chunk-IK7DCC5H.js.map → chunk-VMGLYN42.js.map} +0 -0
  384. /package/dist/{chunk-NSKYFGDL.js.map → chunk-X4QQB7O6.js.map} +0 -0
  385. /package/dist/{first-start-migration-GYJWIH36.js.map → first-start-migration-FF7YFGRP.js.map} +0 -0
  386. /package/dist/{tier-stats-SKML2OSF.js.map → tier-stats-3LYQ3VV5.js.map} +0 -0
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";
@@ -40,18 +40,47 @@ test("enabled budget warns past soft and denies past hard", () => {
40
40
  const limiter = new CrossNamespaceBudget({
41
41
  enabled: true,
42
42
  softLimit: 2,
43
- hardLimit: 3,
43
+ hardLimit: 4,
44
44
  windowMs: 10_000,
45
45
  });
46
46
  assert.equal(limiter.record("p1", 1).reason, "allowed-under-soft");
47
47
  assert.equal(limiter.record("p1", 2).reason, "allowed-under-soft");
48
48
  assert.equal(limiter.record("p1", 3).reason, "warn-over-soft");
49
- // 4th call crosses hardLimit (count would be 4 > 3) => deny.
49
+ // 4th call reaches hardLimit and is denied.
50
50
  const deny = limiter.record("p1", 4);
51
51
  assert.equal(deny.allowed, false);
52
52
  assert.equal(deny.reason, "deny-over-hard");
53
53
  });
54
54
 
55
+ test("enabled budget denies the threshold-crossing hard-limit request", () => {
56
+ const limiter = new CrossNamespaceBudget({
57
+ enabled: true,
58
+ softLimit: 0,
59
+ hardLimit: 2,
60
+ windowMs: 10_000,
61
+ });
62
+
63
+ const first = limiter.record("p1", 1);
64
+ assert.equal(first.allowed, true);
65
+ assert.equal(first.reason, "warn-over-soft");
66
+ assert.equal(first.count, 1);
67
+
68
+ const beforeSecond = limiter.peek({
69
+ principal: "p1",
70
+ principalNamespace: "alice",
71
+ queryNamespace: "bob",
72
+ now: 2,
73
+ });
74
+ assert.equal(beforeSecond.allowed, false);
75
+ assert.equal(beforeSecond.reason, "deny-over-hard");
76
+ assert.equal(beforeSecond.count, 1);
77
+
78
+ const second = limiter.record("p1", 2);
79
+ assert.equal(second.allowed, false);
80
+ assert.equal(second.reason, "deny-over-hard");
81
+ assert.equal(second.count, 1);
82
+ });
83
+
55
84
  test("sliding window drops old timestamps", () => {
56
85
  const limiter = new CrossNamespaceBudget({
57
86
  enabled: true,
@@ -61,10 +90,9 @@ test("sliding window drops old timestamps", () => {
61
90
  });
62
91
  // Fill to hard.
63
92
  limiter.record("p1", 0);
64
- limiter.record("p1", 50);
65
- assert.equal(limiter.record("p1", 80).reason, "deny-over-hard");
93
+ assert.equal(limiter.record("p1", 50).reason, "deny-over-hard");
66
94
 
67
- // Walk past the window so the first two slide out.
95
+ // Walk past the window so the first allowed timestamp slides out.
68
96
  const d = limiter.record("p1", 201);
69
97
  assert.equal(d.allowed, true);
70
98
  assert.equal(d.reason, "allowed-under-soft");
@@ -75,7 +103,7 @@ test("per-principal isolation: one principal's denial does not affect another",
75
103
  const limiter = new CrossNamespaceBudget({
76
104
  enabled: true,
77
105
  softLimit: 1,
78
- hardLimit: 1,
106
+ hardLimit: 2,
79
107
  windowMs: 10_000,
80
108
  });
81
109
  limiter.record("alice", 10);
@@ -107,7 +135,7 @@ test("check() engages on cross-namespace", () => {
107
135
  const limiter = new CrossNamespaceBudget({
108
136
  enabled: true,
109
137
  softLimit: 1,
110
- hardLimit: 1,
138
+ hardLimit: 2,
111
139
  windowMs: 10_000,
112
140
  });
113
141
  const d1 = limiter.check({
@@ -131,7 +159,7 @@ test("denied calls do not push bucket forward", () => {
131
159
  const limiter = new CrossNamespaceBudget({
132
160
  enabled: true,
133
161
  softLimit: 1,
134
- hardLimit: 1,
162
+ hardLimit: 2,
135
163
  windowMs: 100,
136
164
  });
137
165
  limiter.record("p1", 0);
@@ -150,7 +178,7 @@ test("missing principal is bucketed under __anonymous__ rather than failing open
150
178
  const limiter = new CrossNamespaceBudget({
151
179
  enabled: true,
152
180
  softLimit: 0,
153
- hardLimit: 1,
181
+ hardLimit: 2,
154
182
  windowMs: 10_000,
155
183
  });
156
184
  // An empty-string principal shares the anonymous bucket.
@@ -168,8 +196,7 @@ test("reset clears all state", () => {
168
196
  windowMs: 10_000,
169
197
  });
170
198
  limiter.record("p1", 1);
171
- limiter.record("p1", 2);
172
- assert.equal(limiter.record("p1", 3).reason, "deny-over-hard");
199
+ assert.equal(limiter.record("p1", 2).reason, "deny-over-hard");
173
200
  limiter.reset();
174
201
  const after = limiter.record("p1", 4);
175
202
  assert.equal(after.allowed, true);
@@ -231,7 +258,7 @@ test("record() normalizes non-finite clocks before mutating limiter state", () =
231
258
  const limiter = new CrossNamespaceBudget({
232
259
  enabled: true,
233
260
  softLimit: 1,
234
- hardLimit: 1,
261
+ hardLimit: 2,
235
262
  windowMs: 100,
236
263
  });
237
264
 
@@ -254,7 +281,7 @@ test("peek() with a non-finite clock is read-only and does not poison state", ()
254
281
  const limiter = new CrossNamespaceBudget({
255
282
  enabled: true,
256
283
  softLimit: 0,
257
- hardLimit: 1,
284
+ hardLimit: 2,
258
285
  windowMs: 100,
259
286
  });
260
287
 
@@ -293,14 +320,10 @@ test("bucket is evicted after a denial rolls the only timestamp back", () => {
293
320
  hardLimit: 1,
294
321
  windowMs: 100,
295
322
  });
296
- limiter.record("p1", 0);
297
- // Denial at t=150 after the earlier timestamp has slid out. The
298
- // timestamp just added gets rolled back; bucket becomes empty; must
299
- // be evicted.
300
- limiter.record("p1", 150);
301
- // (wait — the above is allowed because the earlier timestamp slid out.)
302
- // Force a deny path differently:
303
- assert.equal(limiter.bucketCount(), 1);
323
+ const denied = limiter.record("p1", 0);
324
+ assert.equal(denied.allowed, false);
325
+ assert.equal(denied.reason, "deny-over-hard");
326
+ assert.equal(limiter.bucketCount(), 0);
304
327
  });
305
328
 
306
329
  test("check() does NOT fail-open when both namespaces are empty or undefined", () => {
@@ -207,7 +207,7 @@ export class CrossNamespaceBudget {
207
207
  this.buckets.set(principal, bucket);
208
208
  const count = bucket.timestamps.length;
209
209
 
210
- if (count > hardLimit) {
210
+ if (count >= hardLimit) {
211
211
  // Denied: roll back the timestamp we just added so a repeated denied
212
212
  // call does not push the bucket further into the future. This keeps
213
213
  // the limiter stateless with respect to denied attempts.
@@ -290,7 +290,7 @@ export class CrossNamespaceBudget {
290
290
  if (ts >= cutoff) liveCount++;
291
291
  }
292
292
  const projected = liveCount + 1; // +1 for the current call
293
- if (projected > hardLimit) {
293
+ if (projected >= hardLimit) {
294
294
  return { allowed: false, reason: "deny-over-hard", count: liveCount, limit };
295
295
  }
296
296
  if (projected > softLimit) {
@@ -32,12 +32,12 @@ interface RateLimitBucket {
32
32
 
33
33
  const rateBuckets = new Map<string, RateLimitBucket>();
34
34
 
35
- function isRateLimited(
35
+ function reserveRateLimitSlot(
36
36
  provider: EnrichmentProvider,
37
37
  config: EnrichmentPipelineConfig,
38
38
  ): boolean {
39
39
  const providerCfg = config.providers.find((p) => p.id === provider.id);
40
- if (!providerCfg?.rateLimit) return false;
40
+ if (!providerCfg?.rateLimit) return true;
41
41
 
42
42
  const now = Date.now();
43
43
  let bucket = rateBuckets.get(provider.id);
@@ -62,17 +62,13 @@ function isRateLimited(
62
62
  }
63
63
 
64
64
  const { maxPerMinute, maxPerDay } = providerCfg.rateLimit;
65
- return bucket.minuteCount >= maxPerMinute || bucket.dayCount >= maxPerDay;
66
- }
67
-
68
- function recordCall(
69
- providerId: string,
70
- ): void {
71
- const bucket = rateBuckets.get(providerId);
72
- if (bucket) {
73
- bucket.minuteCount += 1;
74
- bucket.dayCount += 1;
65
+ if (bucket.minuteCount >= maxPerMinute || bucket.dayCount >= maxPerDay) {
66
+ return false;
75
67
  }
68
+
69
+ bucket.minuteCount += 1;
70
+ bucket.dayCount += 1;
71
+ return true;
76
72
  }
77
73
 
78
74
  // ---------------------------------------------------------------------------
@@ -129,8 +125,9 @@ export async function runEnrichmentPipeline(
129
125
  continue;
130
126
  }
131
127
 
132
- // Check rate limit
133
- if (isRateLimited(provider, config)) {
128
+ // Reserve quota before the awaited provider call so concurrent pipelines
129
+ // cannot all pass the same pre-await rate-limit check.
130
+ if (!reserveRateLimitSlot(provider, config)) {
134
131
  log.debug?.(
135
132
  `enrichment: skipping provider ${provider.id} for ${entity.name} — rate limited`,
136
133
  );
@@ -154,7 +151,6 @@ export async function runEnrichmentPipeline(
154
151
  try {
155
152
  candidates = await provider.enrich(entity);
156
153
  } catch (err) {
157
- recordCall(provider.id);
158
154
  log.error?.(
159
155
  `enrichment: provider ${provider.id} failed for ${entity.name}: ${err instanceof Error ? err.message : String(err)}`,
160
156
  );
@@ -169,7 +165,6 @@ export async function runEnrichmentPipeline(
169
165
  });
170
166
  continue;
171
167
  }
172
- recordCall(provider.id);
173
168
 
174
169
  // Tag each candidate with provider id
175
170
  for (const candidate of candidates) {
package/src/evals.ts CHANGED
@@ -359,7 +359,7 @@ export function validateEvalBenchmarkManifest(
359
359
 
360
360
  return {
361
361
  schemaVersion: 1,
362
- benchmarkId: assertString(raw.benchmarkId, "benchmarkId"),
362
+ benchmarkId: assertSafeBenchmarkId(assertString(raw.benchmarkId, "benchmarkId")),
363
363
  benchmarkType,
364
364
  title: assertString(raw.title, "title"),
365
365
  description:
@@ -25,6 +25,16 @@ export type ValidExplicitCapture = {
25
25
  entityRef?: string;
26
26
  expiresAt?: string;
27
27
  sourceReason?: string;
28
+ /**
29
+ * When true, `namespace` was already resolved AND authorized by the caller
30
+ * (the access service's `resolveCodingScopedWriteNamespace`, which auth-checks
31
+ * the base and derives a session-owned `project-*` overlay). The persist /
32
+ * queue layer then routes to it directly instead of re-validating against the
33
+ * static policy allow-list — which would otherwise reject legitimately-derived
34
+ * dynamic project namespaces (#1434). Callers that do NOT pre-authorize the
35
+ * namespace must leave this unset so the allow-list guard still applies.
36
+ */
37
+ namespacePreResolved?: boolean;
28
38
  };
29
39
 
30
40
  export type ExplicitCaptureSource = "memory_store" | "memory_capture" | "suggestion_submit" | "inline";
@@ -404,7 +414,9 @@ export async function persistExplicitCapture(
404
414
  candidate: ValidExplicitCapture,
405
415
  source: ExplicitCaptureSource,
406
416
  ): Promise<{ id: string; duplicateOf?: string }> {
407
- const resolvedNamespace = resolveExplicitCaptureNamespace(orchestrator, candidate.namespace);
417
+ const resolvedNamespace = candidate.namespacePreResolved
418
+ ? asTrimmed(candidate.namespace)
419
+ : resolveExplicitCaptureNamespace(orchestrator, candidate.namespace);
408
420
  const duplicateOf = await findDuplicateExplicitCapture(orchestrator, resolvedNamespace, candidate);
409
421
  if (duplicateOf) {
410
422
  return { id: duplicateOf, duplicateOf };
@@ -490,7 +502,12 @@ export async function queueExplicitCaptureForReview(
490
502
  ): Promise<{ id: string; duplicateOf?: string }> {
491
503
  const reason = sanitizeReviewText(normalizeExplicitCaptureError(error), "explicit capture failed");
492
504
  const requestedNamespace = asTrimmed(input.namespace);
493
- const queueNamespace = resolveExplicitCaptureReviewNamespace(orchestrator, requestedNamespace);
505
+ // A caller-pre-authorized namespace (e.g. a session-owned project overlay
506
+ // from the access service) routes directly; otherwise apply the static
507
+ // policy allow-list guard (#1434).
508
+ const queueNamespace = (input as { namespacePreResolved?: boolean }).namespacePreResolved
509
+ ? requestedNamespace
510
+ : resolveExplicitCaptureReviewNamespace(orchestrator, requestedNamespace);
494
511
  const content = buildExplicitCaptureReviewContent(input, reason);
495
512
  const duplicateOf = await findQueuedExplicitCaptureDuplicate(orchestrator, queueNamespace, content);
496
513
  if (duplicateOf) {