@remnic/core 10.0.26 → 10.0.28

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 (264) hide show
  1. package/dist/access-admin-ops-surface.js +28 -28
  2. package/dist/access-authorization-probe.js +29 -29
  3. package/dist/access-boundary.js +28 -28
  4. package/dist/access-cli.js +74 -74
  5. package/dist/access-extraction-force-flush.js +28 -28
  6. package/dist/access-http-briefing.js +29 -29
  7. package/dist/access-http-query.js +30 -30
  8. package/dist/access-http.js +45 -45
  9. package/dist/access-identity-continuity-surface.js +28 -28
  10. package/dist/access-lcm-surface.js +28 -28
  11. package/dist/access-mcp.js +38 -38
  12. package/dist/access-namespace-preflight.js +28 -28
  13. package/dist/access-observe-write-surface.js +28 -28
  14. package/dist/access-operations-batch.js +29 -29
  15. package/dist/access-operations.js +36 -36
  16. package/dist/access-recall-concurrency.js +28 -28
  17. package/dist/access-recall-diagnostics.js +2 -2
  18. package/dist/access-recall-response.js +28 -28
  19. package/dist/access-recall-surface.js +28 -28
  20. package/dist/access-service.js +28 -28
  21. package/dist/access-surface-catalog.js +30 -30
  22. package/dist/active-recall.js +5 -5
  23. package/dist/{auto-sync-7WWAKW6F.js → auto-sync-UQESPLCB.js} +5 -5
  24. package/dist/briefing.js +6 -6
  25. package/dist/buffer.js +3 -3
  26. package/dist/calibration.js +5 -5
  27. package/dist/causal-consolidation.js +7 -7
  28. package/dist/{chunk-AAQA6ZUP.js → chunk-23ZLN5X6.js} +13 -2
  29. package/dist/{chunk-AAQA6ZUP.js.map → chunk-23ZLN5X6.js.map} +1 -1
  30. package/dist/{chunk-2NQD4N42.js → chunk-2TGDT37F.js} +10 -10
  31. package/dist/{chunk-QNKEMFZY.js → chunk-2WDMUQOH.js} +2 -2
  32. package/dist/{chunk-B2BAZPVQ.js → chunk-364M2T3D.js} +2 -2
  33. package/dist/{chunk-D3I4P2SX.js → chunk-3EE7EHFX.js} +2 -2
  34. package/dist/{chunk-XSEVBFMD.js → chunk-3EJQXF3B.js} +22 -7
  35. package/dist/chunk-3EJQXF3B.js.map +1 -0
  36. package/dist/{chunk-E2I5FVEU.js → chunk-3LRBJQZE.js} +2 -2
  37. package/dist/{chunk-MRRLXXWA.js → chunk-3Z5LSDKR.js} +2 -2
  38. package/dist/{chunk-5ALVWXQG.js → chunk-4DQ4VEIQ.js} +2 -2
  39. package/dist/{chunk-DIHJ2QWP.js → chunk-4GGTC2AX.js} +2 -2
  40. package/dist/{chunk-HGSERGFX.js → chunk-4LJSCW24.js} +2 -2
  41. package/dist/{chunk-7RTNJ6M2.js → chunk-4QPW6JVI.js} +2 -2
  42. package/dist/{chunk-BO3F4X5U.js → chunk-67F6SBCJ.js} +2 -2
  43. package/dist/{chunk-LO76EF3Y.js → chunk-7EJCAFMS.js} +2 -2
  44. package/dist/{chunk-YUZRI3QU.js → chunk-AN3G5DZH.js} +2 -2
  45. package/dist/{chunk-ZXVLOKN4.js → chunk-AUPL775F.js} +2 -2
  46. package/dist/{chunk-XE5QLP3P.js → chunk-B6VPA3PF.js} +5 -5
  47. package/dist/{chunk-FT4UQYOV.js → chunk-BDLOEODO.js} +2 -2
  48. package/dist/{chunk-MKP4W52S.js → chunk-C2J4LBNB.js} +3 -3
  49. package/dist/{chunk-HNZTKT4J.js → chunk-C5JXYJHN.js} +3 -3
  50. package/dist/{chunk-JT7GSF3T.js → chunk-CQBNFDSG.js} +2 -2
  51. package/dist/{chunk-EQAIXC4Q.js → chunk-CRVL6SLC.js} +2 -2
  52. package/dist/{chunk-EP6BA7VV.js → chunk-CU5TILGD.js} +2 -5
  53. package/dist/chunk-CU5TILGD.js.map +1 -0
  54. package/dist/{chunk-2DT7PIDZ.js → chunk-EPOYPPOW.js} +2 -2
  55. package/dist/{chunk-NJAFC244.js → chunk-FDPE6OIK.js} +2 -2
  56. package/dist/{chunk-JNLEJE4A.js → chunk-GEHIRZXF.js} +2 -2
  57. package/dist/{chunk-V7KGDLBC.js → chunk-GQ7VWY6N.js} +2 -2
  58. package/dist/{chunk-IXUEFTK4.js → chunk-GV2V4CX6.js} +2 -2
  59. package/dist/{chunk-3VRMUNWL.js → chunk-HJ444KNS.js} +15 -7
  60. package/dist/chunk-HJ444KNS.js.map +1 -0
  61. package/dist/{chunk-I75TTE7D.js → chunk-HZKDYR3Z.js} +2 -2
  62. package/dist/{chunk-CT5YXLVB.js → chunk-I4MYLDRY.js} +2 -2
  63. package/dist/{chunk-ECBAENW7.js → chunk-IP5QGSNH.js} +12 -12
  64. package/dist/{chunk-G7QUQILS.js → chunk-J4PILQJG.js} +2 -2
  65. package/dist/{chunk-OL7LAGBG.js → chunk-JCPR47NY.js} +2 -2
  66. package/dist/{chunk-Z2XUE3VT.js → chunk-JSFU7AFZ.js} +2 -2
  67. package/dist/{chunk-B45WUQUU.js → chunk-JTLXVXYH.js} +2 -2
  68. package/dist/{chunk-HWMK3QHK.js → chunk-KNWC2EPP.js} +4 -4
  69. package/dist/{chunk-42KSEX6C.js → chunk-LDXNKEYB.js} +1 -1
  70. package/dist/chunk-LDXNKEYB.js.map +1 -0
  71. package/dist/{chunk-PIN5OPBA.js → chunk-LVU7FP27.js} +1 -1
  72. package/dist/{chunk-BGG5BP3M.js → chunk-LW6UHNWA.js} +2 -2
  73. package/dist/{chunk-BWOLINYY.js → chunk-MIIZAEJD.js} +2 -2
  74. package/dist/{chunk-QM2SPLKQ.js → chunk-MLED254J.js} +1 -1
  75. package/dist/chunk-MLED254J.js.map +1 -0
  76. package/dist/{chunk-HJQXHLQQ.js → chunk-MROA6OFU.js} +2 -2
  77. package/dist/{chunk-DFL3MBL5.js → chunk-NDZ7APNY.js} +3 -3
  78. package/dist/{chunk-DF2G4IXN.js → chunk-NPO4ZQFY.js} +3 -3
  79. package/dist/{chunk-X3ZLVGGX.js → chunk-NRG67KMO.js} +3 -3
  80. package/dist/{chunk-VBINQDWP.js → chunk-NRLWHZVS.js} +4 -4
  81. package/dist/{chunk-Q35L7TYB.js → chunk-NYPQHFC3.js} +2 -2
  82. package/dist/{chunk-G2OYFTUD.js → chunk-O6S5FLZB.js} +2 -2
  83. package/dist/{chunk-4B7JO3LG.js → chunk-OGOADX26.js} +2 -2
  84. package/dist/{chunk-US4WUDQR.js → chunk-OKNB2II7.js} +2 -2
  85. package/dist/{chunk-5BNDPLGI.js → chunk-P3NXZAV7.js} +2 -2
  86. package/dist/{chunk-KD73Q7EN.js → chunk-PCSPXMPD.js} +2 -2
  87. package/dist/{chunk-NKGI7JWP.js → chunk-PDN6MRWF.js} +2 -2
  88. package/dist/{chunk-YBO2WGCV.js → chunk-PM72NSEW.js} +2 -2
  89. package/dist/{chunk-KDMZZSL4.js → chunk-PRUXE3RC.js} +2 -2
  90. package/dist/{chunk-DUPHIZBY.js → chunk-PUDYANAX.js} +2 -2
  91. package/dist/{chunk-SA7NPDWJ.js → chunk-QLI74KWK.js} +9 -7
  92. package/dist/chunk-QLI74KWK.js.map +1 -0
  93. package/dist/{chunk-QP5GYLMS.js → chunk-QMYB3N7X.js} +20 -20
  94. package/dist/{chunk-FI6CUYMJ.js → chunk-RKB4JZEM.js} +34 -34
  95. package/dist/{chunk-3F3HA3R2.js → chunk-RN7HPFYS.js} +2 -2
  96. package/dist/{chunk-H53F22SJ.js → chunk-RQUF3FXK.js} +3 -3
  97. package/dist/{chunk-H3AW6QZF.js → chunk-RRJ462HP.js} +2 -2
  98. package/dist/{chunk-CZURNXGO.js → chunk-SSFO7B7H.js} +2 -2
  99. package/dist/{chunk-TRG3A2AW.js → chunk-SUZG7UUK.js} +2 -2
  100. package/dist/{chunk-ROF3JORU.js → chunk-TZZFGR4E.js} +2 -2
  101. package/dist/{chunk-TKC5LZ43.js → chunk-UIDWASOS.js} +2 -2
  102. package/dist/{chunk-M47LM554.js → chunk-UTGHW2Y5.js} +2 -2
  103. package/dist/{chunk-7BFE2UD4.js → chunk-XAJH5H3Z.js} +2 -2
  104. package/dist/{chunk-5A4QMATS.js → chunk-XALR4DOQ.js} +3 -3
  105. package/dist/{chunk-UFUWKKCV.js → chunk-XN6ABQGM.js} +21 -21
  106. package/dist/{chunk-CJRENHVZ.js → chunk-XNAOCY67.js} +2 -2
  107. package/dist/{chunk-DVE7B2IR.js → chunk-XOSCGZEV.js} +2 -2
  108. package/dist/{chunk-CA35LZAE.js → chunk-XOWR3I2W.js} +7 -7
  109. package/dist/cli.js +56 -56
  110. package/dist/coding/export-okf-codegraph.js +5 -5
  111. package/dist/compounding/engine.js +7 -7
  112. package/dist/config.js +5 -5
  113. package/dist/connectors/codex-materialize-runner.js +6 -6
  114. package/dist/connectors/index.js +6 -6
  115. package/dist/convergence-refresh.js +8 -8
  116. package/dist/corpus-watermark.js +8 -8
  117. package/dist/deep-recall-policy-llm.js +3 -3
  118. package/dist/entity-retrieval.js +6 -6
  119. package/dist/external-wiki-access.js +29 -29
  120. package/dist/extraction-faithfulness.js +3 -3
  121. package/dist/extraction-judge.js +3 -3
  122. package/dist/extraction-span-wire.d.ts +8 -0
  123. package/dist/extraction-span-wire.d.ts.map +1 -1
  124. package/dist/extraction-span-wire.js +5 -3
  125. package/dist/extraction.js +14 -14
  126. package/dist/fallback-llm-codex-subscription.d.ts.map +1 -1
  127. package/dist/fallback-llm-codex-subscription.js +1 -1
  128. package/dist/fallback-llm.js +2 -2
  129. package/dist/harmonic-retrieval.js +6 -6
  130. package/dist/index.js +74 -74
  131. package/dist/local-llm-helpers.d.ts +2 -1
  132. package/dist/local-llm-helpers.d.ts.map +1 -1
  133. package/dist/local-llm-helpers.js +1 -1
  134. package/dist/local-llm-rate-limit.d.ts +3 -1
  135. package/dist/local-llm-rate-limit.d.ts.map +1 -1
  136. package/dist/local-llm-rate-limit.js +1 -1
  137. package/dist/local-llm.d.ts.map +1 -1
  138. package/dist/local-llm.js +7 -3
  139. package/dist/location/index.js +8 -8
  140. package/dist/maintenance/memory-governance.js +6 -6
  141. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +6 -6
  142. package/dist/maintenance/rebuild-memory-projection-runner.js +8 -8
  143. package/dist/maintenance/rebuild-memory-projection.js +7 -7
  144. package/dist/memory-browse-operations.js +29 -29
  145. package/dist/memory-browse.js +3 -3
  146. package/dist/namespaces/migrate.js +7 -7
  147. package/dist/namespaces/storage.js +6 -6
  148. package/dist/operator-doctor-corpus.js +9 -9
  149. package/dist/operator-toolkit.js +14 -14
  150. package/dist/orchestration/maintenance.js +19 -19
  151. package/dist/orchestrator.js +74 -74
  152. package/dist/recall-explain-renderer.js +3 -3
  153. package/dist/recall-navigate-access.js +2 -2
  154. package/dist/recall-navigate-disclosure.js +2 -2
  155. package/dist/recall-navigate.js +2 -2
  156. package/dist/recall-navigation-operations.js +29 -29
  157. package/dist/recall-navigation.js +28 -28
  158. package/dist/recall-planner-llm.js +2 -2
  159. package/dist/recall-state-view-admission.js +3 -3
  160. package/dist/recall-why-service.js +3 -3
  161. package/dist/recall-xray-cli.js +4 -4
  162. package/dist/recall-xray-renderer.js +3 -3
  163. package/dist/recall-xray.js +2 -2
  164. package/dist/resume-bundles.js +5 -5
  165. package/dist/semantic-consolidation.js +7 -7
  166. package/dist/semantic-rule-promotion.js +6 -6
  167. package/dist/semantic-rule-verifier.js +6 -6
  168. package/dist/source-agent-qualifier.js +9 -9
  169. package/dist/standup/build.js +6 -6
  170. package/dist/storage.js +5 -5
  171. package/dist/summarizer.js +6 -6
  172. package/dist/support-passport/index.js +31 -31
  173. package/dist/transfer/export-okf.js +6 -6
  174. package/dist/types.d.ts +1 -1
  175. package/dist/types.d.ts.map +1 -1
  176. package/dist/types.js +1 -1
  177. package/dist/verified-recall.js +6 -6
  178. package/dist/who-knows-cli.js +7 -7
  179. package/dist/who-knows.js +6 -6
  180. package/package.json +2 -2
  181. package/src/ambient-provenance.test.ts +21 -3
  182. package/src/extraction-span-wire.ts +11 -0
  183. package/src/extraction.ts +2 -2
  184. package/src/fallback-llm-codex-subscription.ts +6 -9
  185. package/src/local-llm-helpers.ts +2 -1
  186. package/src/local-llm-json-schema-fallback.test.ts +278 -0
  187. package/src/local-llm-rate-limit.test.ts +59 -1
  188. package/src/local-llm-rate-limit.ts +22 -7
  189. package/src/local-llm.ts +22 -4
  190. package/src/types.ts +1 -1
  191. package/dist/chunk-3VRMUNWL.js.map +0 -1
  192. package/dist/chunk-42KSEX6C.js.map +0 -1
  193. package/dist/chunk-EP6BA7VV.js.map +0 -1
  194. package/dist/chunk-QM2SPLKQ.js.map +0 -1
  195. package/dist/chunk-SA7NPDWJ.js.map +0 -1
  196. package/dist/chunk-XSEVBFMD.js.map +0 -1
  197. /package/dist/{auto-sync-7WWAKW6F.js.map → auto-sync-UQESPLCB.js.map} +0 -0
  198. /package/dist/{chunk-2NQD4N42.js.map → chunk-2TGDT37F.js.map} +0 -0
  199. /package/dist/{chunk-QNKEMFZY.js.map → chunk-2WDMUQOH.js.map} +0 -0
  200. /package/dist/{chunk-B2BAZPVQ.js.map → chunk-364M2T3D.js.map} +0 -0
  201. /package/dist/{chunk-D3I4P2SX.js.map → chunk-3EE7EHFX.js.map} +0 -0
  202. /package/dist/{chunk-E2I5FVEU.js.map → chunk-3LRBJQZE.js.map} +0 -0
  203. /package/dist/{chunk-MRRLXXWA.js.map → chunk-3Z5LSDKR.js.map} +0 -0
  204. /package/dist/{chunk-5ALVWXQG.js.map → chunk-4DQ4VEIQ.js.map} +0 -0
  205. /package/dist/{chunk-DIHJ2QWP.js.map → chunk-4GGTC2AX.js.map} +0 -0
  206. /package/dist/{chunk-HGSERGFX.js.map → chunk-4LJSCW24.js.map} +0 -0
  207. /package/dist/{chunk-7RTNJ6M2.js.map → chunk-4QPW6JVI.js.map} +0 -0
  208. /package/dist/{chunk-BO3F4X5U.js.map → chunk-67F6SBCJ.js.map} +0 -0
  209. /package/dist/{chunk-LO76EF3Y.js.map → chunk-7EJCAFMS.js.map} +0 -0
  210. /package/dist/{chunk-YUZRI3QU.js.map → chunk-AN3G5DZH.js.map} +0 -0
  211. /package/dist/{chunk-ZXVLOKN4.js.map → chunk-AUPL775F.js.map} +0 -0
  212. /package/dist/{chunk-XE5QLP3P.js.map → chunk-B6VPA3PF.js.map} +0 -0
  213. /package/dist/{chunk-FT4UQYOV.js.map → chunk-BDLOEODO.js.map} +0 -0
  214. /package/dist/{chunk-MKP4W52S.js.map → chunk-C2J4LBNB.js.map} +0 -0
  215. /package/dist/{chunk-HNZTKT4J.js.map → chunk-C5JXYJHN.js.map} +0 -0
  216. /package/dist/{chunk-JT7GSF3T.js.map → chunk-CQBNFDSG.js.map} +0 -0
  217. /package/dist/{chunk-EQAIXC4Q.js.map → chunk-CRVL6SLC.js.map} +0 -0
  218. /package/dist/{chunk-2DT7PIDZ.js.map → chunk-EPOYPPOW.js.map} +0 -0
  219. /package/dist/{chunk-NJAFC244.js.map → chunk-FDPE6OIK.js.map} +0 -0
  220. /package/dist/{chunk-JNLEJE4A.js.map → chunk-GEHIRZXF.js.map} +0 -0
  221. /package/dist/{chunk-V7KGDLBC.js.map → chunk-GQ7VWY6N.js.map} +0 -0
  222. /package/dist/{chunk-IXUEFTK4.js.map → chunk-GV2V4CX6.js.map} +0 -0
  223. /package/dist/{chunk-I75TTE7D.js.map → chunk-HZKDYR3Z.js.map} +0 -0
  224. /package/dist/{chunk-CT5YXLVB.js.map → chunk-I4MYLDRY.js.map} +0 -0
  225. /package/dist/{chunk-ECBAENW7.js.map → chunk-IP5QGSNH.js.map} +0 -0
  226. /package/dist/{chunk-G7QUQILS.js.map → chunk-J4PILQJG.js.map} +0 -0
  227. /package/dist/{chunk-OL7LAGBG.js.map → chunk-JCPR47NY.js.map} +0 -0
  228. /package/dist/{chunk-Z2XUE3VT.js.map → chunk-JSFU7AFZ.js.map} +0 -0
  229. /package/dist/{chunk-B45WUQUU.js.map → chunk-JTLXVXYH.js.map} +0 -0
  230. /package/dist/{chunk-HWMK3QHK.js.map → chunk-KNWC2EPP.js.map} +0 -0
  231. /package/dist/{chunk-PIN5OPBA.js.map → chunk-LVU7FP27.js.map} +0 -0
  232. /package/dist/{chunk-BGG5BP3M.js.map → chunk-LW6UHNWA.js.map} +0 -0
  233. /package/dist/{chunk-BWOLINYY.js.map → chunk-MIIZAEJD.js.map} +0 -0
  234. /package/dist/{chunk-HJQXHLQQ.js.map → chunk-MROA6OFU.js.map} +0 -0
  235. /package/dist/{chunk-DFL3MBL5.js.map → chunk-NDZ7APNY.js.map} +0 -0
  236. /package/dist/{chunk-DF2G4IXN.js.map → chunk-NPO4ZQFY.js.map} +0 -0
  237. /package/dist/{chunk-X3ZLVGGX.js.map → chunk-NRG67KMO.js.map} +0 -0
  238. /package/dist/{chunk-VBINQDWP.js.map → chunk-NRLWHZVS.js.map} +0 -0
  239. /package/dist/{chunk-Q35L7TYB.js.map → chunk-NYPQHFC3.js.map} +0 -0
  240. /package/dist/{chunk-G2OYFTUD.js.map → chunk-O6S5FLZB.js.map} +0 -0
  241. /package/dist/{chunk-4B7JO3LG.js.map → chunk-OGOADX26.js.map} +0 -0
  242. /package/dist/{chunk-US4WUDQR.js.map → chunk-OKNB2II7.js.map} +0 -0
  243. /package/dist/{chunk-5BNDPLGI.js.map → chunk-P3NXZAV7.js.map} +0 -0
  244. /package/dist/{chunk-KD73Q7EN.js.map → chunk-PCSPXMPD.js.map} +0 -0
  245. /package/dist/{chunk-NKGI7JWP.js.map → chunk-PDN6MRWF.js.map} +0 -0
  246. /package/dist/{chunk-YBO2WGCV.js.map → chunk-PM72NSEW.js.map} +0 -0
  247. /package/dist/{chunk-KDMZZSL4.js.map → chunk-PRUXE3RC.js.map} +0 -0
  248. /package/dist/{chunk-DUPHIZBY.js.map → chunk-PUDYANAX.js.map} +0 -0
  249. /package/dist/{chunk-QP5GYLMS.js.map → chunk-QMYB3N7X.js.map} +0 -0
  250. /package/dist/{chunk-FI6CUYMJ.js.map → chunk-RKB4JZEM.js.map} +0 -0
  251. /package/dist/{chunk-3F3HA3R2.js.map → chunk-RN7HPFYS.js.map} +0 -0
  252. /package/dist/{chunk-H53F22SJ.js.map → chunk-RQUF3FXK.js.map} +0 -0
  253. /package/dist/{chunk-H3AW6QZF.js.map → chunk-RRJ462HP.js.map} +0 -0
  254. /package/dist/{chunk-CZURNXGO.js.map → chunk-SSFO7B7H.js.map} +0 -0
  255. /package/dist/{chunk-TRG3A2AW.js.map → chunk-SUZG7UUK.js.map} +0 -0
  256. /package/dist/{chunk-ROF3JORU.js.map → chunk-TZZFGR4E.js.map} +0 -0
  257. /package/dist/{chunk-TKC5LZ43.js.map → chunk-UIDWASOS.js.map} +0 -0
  258. /package/dist/{chunk-M47LM554.js.map → chunk-UTGHW2Y5.js.map} +0 -0
  259. /package/dist/{chunk-7BFE2UD4.js.map → chunk-XAJH5H3Z.js.map} +0 -0
  260. /package/dist/{chunk-5A4QMATS.js.map → chunk-XALR4DOQ.js.map} +0 -0
  261. /package/dist/{chunk-UFUWKKCV.js.map → chunk-XN6ABQGM.js.map} +0 -0
  262. /package/dist/{chunk-CJRENHVZ.js.map → chunk-XNAOCY67.js.map} +0 -0
  263. /package/dist/{chunk-DVE7B2IR.js.map → chunk-XOSCGZEV.js.map} +0 -0
  264. /package/dist/{chunk-CA35LZAE.js.map → chunk-XOWR3I2W.js.map} +0 -0
@@ -4,9 +4,9 @@ import {
4
4
  renderWhoKnows,
5
5
  renderWhoKnowsJson,
6
6
  renderWhoKnowsText
7
- } from "./chunk-HGSERGFX.js";
8
- import "./chunk-4B7JO3LG.js";
9
- import "./chunk-5A4QMATS.js";
7
+ } from "./chunk-4LJSCW24.js";
8
+ import "./chunk-OGOADX26.js";
9
+ import "./chunk-XALR4DOQ.js";
10
10
  import "./chunk-UHVXE24Y.js";
11
11
  import "./chunk-TRMJ6BYC.js";
12
12
  import "./chunk-V4JZCTMK.js";
@@ -31,7 +31,7 @@ import "./chunk-S2HLHAZ4.js";
31
31
  import "./chunk-MRPOZBJC.js";
32
32
  import "./chunk-DM2T26WE.js";
33
33
  import "./chunk-LDXUBPMO.js";
34
- import "./chunk-2DT7PIDZ.js";
34
+ import "./chunk-EPOYPPOW.js";
35
35
  import "./chunk-AAD2ZS6P.js";
36
36
  import "./chunk-FF6N7BXC.js";
37
37
  import "./chunk-JNIVZNIB.js";
@@ -60,11 +60,11 @@ import "./chunk-IUM3VSHI.js";
60
60
  import "./chunk-C64XATK7.js";
61
61
  import "./chunk-42JKGUFJ.js";
62
62
  import "./chunk-23G5P7TA.js";
63
- import "./chunk-TKC5LZ43.js";
63
+ import "./chunk-UIDWASOS.js";
64
64
  import "./chunk-I55KAVET.js";
65
65
  import "./chunk-L2EXJQJP.js";
66
66
  import "./chunk-45GF7Y3V.js";
67
- import "./chunk-EP6BA7VV.js";
67
+ import "./chunk-CU5TILGD.js";
68
68
  import "./chunk-E2AWJ7KQ.js";
69
69
  import "./chunk-Z7L72ZHL.js";
70
70
  import "./chunk-4O4TKIXQ.js";
@@ -72,7 +72,7 @@ import "./chunk-YCVB7X56.js";
72
72
  import "./chunk-PQVCH2AE.js";
73
73
  import "./chunk-PJKTFXE2.js";
74
74
  import "./chunk-W5BIQOAJ.js";
75
- import "./chunk-42KSEX6C.js";
75
+ import "./chunk-LDXNKEYB.js";
76
76
  import "./chunk-YBA52OKG.js";
77
77
  import "./chunk-5LRV557D.js";
78
78
  import "./chunk-BT7NVCML.js";
package/dist/who-knows.js CHANGED
@@ -9,8 +9,8 @@ import {
9
9
  loadWhoKnowsEntities,
10
10
  tokenizeTopic,
11
11
  validateWhoKnowsInput
12
- } from "./chunk-4B7JO3LG.js";
13
- import "./chunk-5A4QMATS.js";
12
+ } from "./chunk-OGOADX26.js";
13
+ import "./chunk-XALR4DOQ.js";
14
14
  import "./chunk-UHVXE24Y.js";
15
15
  import "./chunk-TRMJ6BYC.js";
16
16
  import "./chunk-V4JZCTMK.js";
@@ -35,7 +35,7 @@ import "./chunk-S2HLHAZ4.js";
35
35
  import "./chunk-MRPOZBJC.js";
36
36
  import "./chunk-DM2T26WE.js";
37
37
  import "./chunk-LDXUBPMO.js";
38
- import "./chunk-2DT7PIDZ.js";
38
+ import "./chunk-EPOYPPOW.js";
39
39
  import "./chunk-AAD2ZS6P.js";
40
40
  import "./chunk-FF6N7BXC.js";
41
41
  import "./chunk-JNIVZNIB.js";
@@ -64,11 +64,11 @@ import "./chunk-IUM3VSHI.js";
64
64
  import "./chunk-C64XATK7.js";
65
65
  import "./chunk-42JKGUFJ.js";
66
66
  import "./chunk-23G5P7TA.js";
67
- import "./chunk-TKC5LZ43.js";
67
+ import "./chunk-UIDWASOS.js";
68
68
  import "./chunk-I55KAVET.js";
69
69
  import "./chunk-L2EXJQJP.js";
70
70
  import "./chunk-45GF7Y3V.js";
71
- import "./chunk-EP6BA7VV.js";
71
+ import "./chunk-CU5TILGD.js";
72
72
  import "./chunk-E2AWJ7KQ.js";
73
73
  import "./chunk-Z7L72ZHL.js";
74
74
  import "./chunk-4O4TKIXQ.js";
@@ -76,7 +76,7 @@ import "./chunk-YCVB7X56.js";
76
76
  import "./chunk-PQVCH2AE.js";
77
77
  import "./chunk-PJKTFXE2.js";
78
78
  import "./chunk-W5BIQOAJ.js";
79
- import "./chunk-42KSEX6C.js";
79
+ import "./chunk-LDXNKEYB.js";
80
80
  import "./chunk-YBA52OKG.js";
81
81
  import "./chunk-5LRV557D.js";
82
82
  import "./chunk-BT7NVCML.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "10.0.26",
3
+ "version": "10.0.28",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3347,7 +3347,7 @@
3347
3347
  "core"
3348
3348
  ],
3349
3349
  "peerDependencies": {
3350
- "@remnic/coding-graph": "^10.0.26"
3350
+ "@remnic/coding-graph": "^10.0.28"
3351
3351
  },
3352
3352
  "peerDependenciesMeta": {
3353
3353
  "@remnic/coding-graph": {
@@ -11,6 +11,8 @@ import {
11
11
  import { parseConfig } from "./config.js";
12
12
  import { bufferTurnsEqual, copyBufferTurn } from "./buffer-turn-helpers.js";
13
13
  import { ExtractionEngine } from "./extraction.js";
14
+ import { extractionProviderJsonSchema } from "./extraction-span-wire.js";
15
+ import type { LocalLlmChatCompletionOptions } from "./local-llm-helpers.js";
14
16
  import { SUMMARY_SYSTEM_PROMPT } from "./meetings/summary-extractor.js";
15
17
  import { emptySpeakerRegistry } from "./wearables/speakers.js";
16
18
  import { buildExtractionTurns } from "./wearables/memory-gen.js";
@@ -77,20 +79,26 @@ const MODEL_REGISTRY_FIXTURE = {
77
79
  };
78
80
 
79
81
  /** Run the local-LLM path, returning the prompt the model saw + the result. */
80
- async function runLocal(turns: BufferTurn[]): Promise<{ prompt: string; result: ExtractionResult }> {
82
+ async function runLocal(turns: BufferTurn[]): Promise<{
83
+ prompt: string;
84
+ result: ExtractionResult;
85
+ options: LocalLlmChatCompletionOptions;
86
+ }> {
81
87
  const engine = new ExtractionEngine(
82
88
  parseConfig({ localLlmEnabled: true, localLlmModel: "fixture-local", localLlmFallback: false }),
83
89
  );
84
90
  let prompt = "";
91
+ let options: LocalLlmChatCompletionOptions = {};
85
92
  const localLlm = {
86
- async chatCompletion(messages: ChatMessage[], _options: { signal?: AbortSignal } = {}) {
93
+ async chatCompletion(messages: ChatMessage[], seen: LocalLlmChatCompletionOptions = {}) {
87
94
  prompt = messages[1]?.content ?? "";
95
+ options = seen;
88
96
  return { content: JSON.stringify(HIGH_IMPACT_EXTRACTION) };
89
97
  },
90
98
  };
91
99
  assert.equal(Reflect.set(engine, "localLlm", localLlm), true);
92
100
  assert.equal(Reflect.set(engine, "modelRegistry", MODEL_REGISTRY_FIXTURE), true);
93
- return { result: await engine.extract(turns), prompt };
101
+ return { result: await engine.extract(turns), prompt, options };
94
102
  }
95
103
 
96
104
  /** Run the gateway-fallback path. */
@@ -169,6 +177,16 @@ for (const [name, run] of PATHS) {
169
177
  });
170
178
  }
171
179
 
180
+ test("local extraction supplies the provider JSON schema as responseFormat with an object root", async () => {
181
+ const { options } = await runLocal([TYPED_TURN]);
182
+ const schema = extractionProviderJsonSchema("off");
183
+ assert.deepEqual(options.responseFormat, {
184
+ type: "json_schema",
185
+ json_schema: { name: "extraction_result", strict: false, schema },
186
+ });
187
+ assert.equal(schema.type, "object");
188
+ });
189
+
172
190
  test("an ambient turn marked context-only does not trigger the ambient prompt", async () => {
173
191
  const { prompt } = await runGateway([
174
192
  { ...AMBIENT_TURN, extractionContextOnly: true },
@@ -58,3 +58,14 @@ export function extractionProviderJsonSchema(
58
58
  ? EXTRACTION_PROVIDER_JSON_SCHEMA_WITHOUT_SPAN
59
59
  : EXTRACTION_PROVIDER_JSON_SCHEMA_WITH_SPAN;
60
60
  }
61
+
62
+ export function extractionProviderResponseFormat(spanMode: ExtractionSpanConfig["spanMode"] = "off") {
63
+ return {
64
+ type: "json_schema" as const,
65
+ json_schema: {
66
+ name: "extraction_result",
67
+ schema: extractionProviderJsonSchema(spanMode),
68
+ strict: false,
69
+ },
70
+ };
71
+ }
package/src/extraction.ts CHANGED
@@ -54,7 +54,7 @@ import {
54
54
  } from "./extraction-prompt.js";
55
55
  import { containsExtractionPlaceholder, extractionAttributes, extractionCueAnchors, extractionText, isPlainRecord } from "./extraction-normalization.js";
56
56
  import { applyExtractionSpanMaterialization, bindLocalExtractionPrompt, buildSpanMaterializeTurns, stripUntrustedFactSpans, truncateLocalExtractionConversation, type SpanMaterializeTurn } from "./extraction-span-materialize.js";
57
- import { extractionProviderJsonSchema } from "./extraction-span-wire.js";
57
+ import { extractionProviderJsonSchema, extractionProviderResponseFormat } from "./extraction-span-wire.js";
58
58
  import { resolveLocalLlmCapabilities, resolveMemoryLifecycleCapabilities, resolvePipelineProcessingCapabilities, resolveRecallAuxiliaryCapabilities } from "./capabilities.js";
59
59
  type ExtractionQuestion = ExtractionResult["questions"][number];
60
60
  type ExtractedFactResult = ExtractionResult["facts"][number];
@@ -1506,6 +1506,7 @@ ${truncatedConversation}`;
1506
1506
  temperature: 0.1,
1507
1507
  maxTokens: contextSizes.maxOutputTokens,
1508
1508
  operation: "extraction",
1509
+ responseFormat: extractionProviderResponseFormat(this.config.extraction?.spanMode ?? "off"),
1509
1510
  disableThinking: shouldEnableLocalExtractionThinking(this.config, conversation.length)
1510
1511
  ? false
1511
1512
  : undefined,
@@ -1513,7 +1514,6 @@ ${truncatedConversation}`;
1513
1514
  signal,
1514
1515
  },
1515
1516
  );
1516
-
1517
1517
  if (!response?.content) {
1518
1518
  log.debug("extractWithLocalLlm: chatCompletion returned null or empty content");
1519
1519
  return null;
@@ -194,18 +194,15 @@ export function withCodexRuntimeShutdown<T extends { signal?: AbortSignal }>(
194
194
 
195
195
  export function isUnsupportedJsonSchemaError(error: unknown): boolean {
196
196
  const message = error instanceof Error ? error.message : String(error);
197
- // Require an HTTP status plus explicit schema/format context. A bare
198
- // "unsupported" (e.g. "'temperature' is unsupported with this model") must
199
- // NOT trigger the schema-stripping retry, or we pay a duplicate request
200
- // for unrelated provider errors.
197
+ // An echoed request format is not a capability rejection.
201
198
  if (!/\b(?:400|404|422)\b/.test(message)) {
202
199
  return false;
203
200
  }
204
- if (/(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output)/i.test(message)) {
205
- return true;
206
- }
207
- // "unsupported" only counts when adjacent to schema/format terminology.
208
- return /\bunsupported\b[\s\S]{0,40}\b(?:schema|format)\b/i.test(message);
201
+ return (
202
+ /(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output)\b[^;,\n]{0,60}\b(?:unsupported|not supported|not implemented|not available)\b/i.test(message) ||
203
+ /\b(?:unsupported|unknown|unrecognized)(?:[\s:"'=]|parameter|argument|request|supplied|field|type|value){1,40}\b(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output|schema|format)\b/i.test(message) ||
204
+ /\b(?:does not|doesn't|cannot|can't) support(?:[\s:"'=]|the|native|type){1,20}\b(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output|schema|format)\b/i.test(message)
205
+ );
209
206
  }
210
207
 
211
208
  export function extractResponsesOutputText(data: {
@@ -1,10 +1,11 @@
1
+ import type { ResponseFormatJSONSchema } from "openai/resources/shared";
1
2
  import { log } from "./logger.js";
2
3
  import type { LocalLlmRequestPriority } from "./local-llm.js";
3
4
 
4
5
  export interface LocalLlmChatCompletionOptions {
5
6
  temperature?: number;
6
7
  maxTokens?: number;
7
- responseFormat?: { type: string };
8
+ responseFormat?: { type: string } | ResponseFormatJSONSchema;
8
9
  timeoutMs?: number;
9
10
  operation?: string;
10
11
  forceDisableThinking?: boolean;
@@ -0,0 +1,278 @@
1
+ import assert from "node:assert/strict";
2
+ import http from "node:http";
3
+ import type { AddressInfo } from "node:net";
4
+ import test, { mock } from "node:test";
5
+
6
+ import { LocalLlmClient } from "./local-llm.js";
7
+ import { ChatTransport } from "./local-llm-transport.js";
8
+ import { parseConfig } from "./config.js";
9
+ import type { LocalLlmChatCompletionOptions } from "./local-llm-helpers.js";
10
+ import type { PluginConfig } from "./types.js";
11
+
12
+ function createConfig(overrides: Partial<PluginConfig> = {}): PluginConfig {
13
+ return parseConfig({
14
+ localLlmEnabled: true,
15
+ localLlmModel: "test-local-model",
16
+ localLlmUrl: "http://127.0.0.1:1234",
17
+ localLlmTimeoutMs: 5_000,
18
+ localLlmRetry5xxCount: 0,
19
+ localLlmRateLimitRetryCount: 0,
20
+ localLlmRetryBackoffMs: 1,
21
+ localLlmHeaders: { "X-Test": "yes" },
22
+ localLlmApiKey: undefined,
23
+ localLlmAuthHeader: false,
24
+ localLlm400TripThreshold: 50,
25
+ localLlm400CooldownMs: 60_000,
26
+ debug: false,
27
+ localLlmReasoningEffort: "none",
28
+ slowLogEnabled: false,
29
+ slowLogThresholdMs: 60_000,
30
+ ...overrides,
31
+ });
32
+ }
33
+
34
+ function primeClient(client: LocalLlmClient): void {
35
+ mock.method(client, "checkAvailability", async () => true);
36
+ }
37
+
38
+ interface RecordedRequest {
39
+ headers: http.IncomingHttpHeaders;
40
+ body: { model: string; response_format?: LocalLlmChatCompletionOptions["responseFormat"] };
41
+ }
42
+
43
+ async function startRecordingServer(
44
+ respond: (req: http.IncomingMessage, requestCount: number) => {
45
+ status: number;
46
+ body: unknown;
47
+ },
48
+ ): Promise<{
49
+ url: string;
50
+ requests: RecordedRequest[];
51
+ close: () => Promise<void>;
52
+ }> {
53
+ const requests: RecordedRequest[] = [];
54
+ const server = http.createServer((req, res) => {
55
+ let raw = "";
56
+ req.setEncoding("utf8");
57
+ req.on("data", (chunk: string) => {
58
+ raw += chunk;
59
+ });
60
+ req.on("end", () => {
61
+ requests.push({ headers: req.headers, body: JSON.parse(raw) });
62
+ const { status, body } = respond(req, requests.length);
63
+ res.writeHead(status, { "content-type": "application/json" });
64
+ res.end(JSON.stringify(body));
65
+ });
66
+ });
67
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
68
+ // SAFETY: listen(0, "127.0.0.1") above binds TCP, so its address is AddressInfo.
69
+ const { port } = server.address() as AddressInfo;
70
+ return {
71
+ url: `http://127.0.0.1:${port}/v1`,
72
+ requests,
73
+ close: async () => {
74
+ await new Promise<void>((resolve) => server.close(() => resolve()));
75
+ },
76
+ };
77
+ }
78
+
79
+ const okCompletion = {
80
+ choices: [{ message: { content: "done" } }],
81
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
82
+ };
83
+
84
+ const responseFormat = {
85
+ type: "json_schema",
86
+ json_schema: {
87
+ name: "result",
88
+ schema: { type: "object", properties: { ok: { type: "boolean" } } },
89
+ },
90
+ };
91
+
92
+ test("forwards json_schema response_format when the backend accepts it", async () => {
93
+ const server = await startRecordingServer(() => ({ status: 200, body: okCompletion }));
94
+ try {
95
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
96
+ primeClient(client);
97
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }], {
98
+ responseFormat,
99
+ });
100
+ assert.equal(result?.content, "done");
101
+ assert.equal(server.requests.length, 1);
102
+ assert.deepEqual(server.requests[0].body.response_format, responseFormat);
103
+ } finally {
104
+ await server.close();
105
+ }
106
+ });
107
+
108
+ test("400 rejecting json_schema triggers exactly one retry without the format", async (t) => {
109
+ const server = await startRecordingServer((_req, count) =>
110
+ count === 1
111
+ ? {
112
+ status: 400,
113
+ body: {
114
+ error: { message: "response_format type json_schema is not supported" },
115
+ },
116
+ }
117
+ : { status: 200, body: okCompletion },
118
+ );
119
+ try {
120
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
121
+ primeClient(client);
122
+ const post = ChatTransport.prototype.post;
123
+ const attempts: Array<Parameters<ChatTransport["post"]>[0]> = [];
124
+ t.mock.method(ChatTransport.prototype, "post", async function (this: ChatTransport, request: Parameters<ChatTransport["post"]>[0]) {
125
+ attempts.push(request);
126
+ return post.call(this, request);
127
+ });
128
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }], {
129
+ responseFormat,
130
+ });
131
+ assert.equal(result?.content, "done");
132
+ assert.equal(server.requests.length, 2, "exactly one strip-and-retry");
133
+ assert.deepEqual(server.requests[0].body.response_format, responseFormat);
134
+ assert.equal(server.requests[1].body.response_format, undefined);
135
+ // headers/model/deadline preserved on the retry
136
+ assert.equal(server.requests[1].headers["x-test"], "yes");
137
+ assert.equal(server.requests[1].body.model, "test-local-model");
138
+ assert.equal(attempts[0].signal, attempts[1].signal, "retry shares the attempt deadline and caller abort");
139
+ assert.equal(attempts[0].budgetMs, attempts[1].budgetMs);
140
+ assert.deepEqual(attempts[0].headers, attempts[1].headers);
141
+ } finally {
142
+ await server.close();
143
+ }
144
+ });
145
+
146
+ test("timeout before strip-retry headers uses the configured transport retry", async (t) => {
147
+ const bodies: RecordedRequest["body"][] = [];
148
+ t.mock.method(ChatTransport.prototype, "post", async (request: Parameters<ChatTransport["post"]>[0]) => {
149
+ bodies.push(JSON.parse(request.body));
150
+ if (bodies.length === 1) return new Response("json_schema is not supported", { status: 400 });
151
+ if (bodies.length === 2) throw new DOMException("Timed out before headers", "AbortError");
152
+ return new Response(JSON.stringify(okCompletion), { status: 200 });
153
+ });
154
+ const client = new LocalLlmClient(createConfig({ localLlmRetry5xxCount: 1, localLlm400TripThreshold: 1 }));
155
+ primeClient(client);
156
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }], { responseFormat });
157
+ assert.equal(result?.content, "done");
158
+ assert.equal(bodies.length, 3);
159
+ assert.deepEqual(bodies[0].response_format, responseFormat);
160
+ assert.equal(bodies[1].response_format, undefined);
161
+ assert.equal(bodies[2].response_format, undefined);
162
+ });
163
+
164
+ test("a diagnostic echo of the format is not a schema capability rejection", async () => {
165
+ for (const message of [
166
+ "Context length exceeded; request included response_format json_schema",
167
+ "Unsupported parameter temperature; request included response_format json_schema",
168
+ ]) {
169
+ const server = await startRecordingServer(() => ({ status: 400, body: { error: { message } } }));
170
+ try {
171
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
172
+ primeClient(client);
173
+ assert.equal(await client.chatCompletion([{ role: "user", content: "hello" }], { responseFormat }), null);
174
+ assert.equal(server.requests.length, 1, "unrelated provider errors must not strip the format");
175
+ } finally {
176
+ await server.close();
177
+ }
178
+ }
179
+ });
180
+
181
+ test("an explicit unknown response_format field retries without the format", async () => {
182
+ const server = await startRecordingServer((_req, count) => count === 1
183
+ ? { status: 400, body: { error: { message: "Unknown field response_format" } } }
184
+ : { status: 200, body: okCompletion });
185
+ try {
186
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
187
+ primeClient(client);
188
+ assert.equal((await client.chatCompletion([{ role: "user", content: "hello" }], { responseFormat }))?.content, "done");
189
+ assert.equal(server.requests.length, 2);
190
+ assert.deepEqual(server.requests[0].body.response_format, responseFormat);
191
+ assert.equal(server.requests[1].body.response_format, undefined);
192
+ } finally {
193
+ await server.close();
194
+ }
195
+ });
196
+
197
+ test("format removal persists for later 5xx attempts", async () => {
198
+ const server = await startRecordingServer((_req, count) =>
199
+ count === 1
200
+ ? { status: 400, body: { error: { message: "json_schema unsupported by backend" } } }
201
+ : count === 2
202
+ ? { status: 500, body: { error: { message: "boom" } } }
203
+ : { status: 200, body: okCompletion },
204
+ );
205
+ try {
206
+ const client = new LocalLlmClient(
207
+ createConfig({ localLlmUrl: server.url, localLlmRetry5xxCount: 2 }),
208
+ );
209
+ primeClient(client);
210
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }], {
211
+ responseFormat,
212
+ });
213
+ assert.equal(result?.content, "done");
214
+ assert.equal(server.requests.length, 3);
215
+ assert.equal(server.requests[2].body.response_format, undefined);
216
+ } finally {
217
+ await server.close();
218
+ }
219
+ });
220
+
221
+ test("unrelated 400 and 429 are not retried with a stripped format", async () => {
222
+ for (const status of [400, 429]) {
223
+ const server = await startRecordingServer(() => ({
224
+ status,
225
+ body: { error: { message: "some other problem" } },
226
+ }));
227
+ try {
228
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
229
+ primeClient(client);
230
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }], {
231
+ responseFormat,
232
+ });
233
+ assert.equal(result, null);
234
+ assert.equal(server.requests.length, 1);
235
+ assert.ok(server.requests[0].body.response_format, "format still present");
236
+ } finally {
237
+ await server.close();
238
+ }
239
+ }
240
+ });
241
+
242
+ test("caller abort still fails closed during the strip retry", async () => {
243
+ const controller = new AbortController();
244
+ const server = await startRecordingServer((_req, count) => {
245
+ if (count === 2) controller.abort();
246
+ return count === 1
247
+ ? { status: 400, body: { error: { message: "response_format json_schema is not supported" } } }
248
+ : { status: 200, body: okCompletion };
249
+ });
250
+ try {
251
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
252
+ primeClient(client);
253
+ const pending = client.chatCompletion([{ role: "user", content: "hello" }], {
254
+ responseFormat,
255
+ signal: controller.signal,
256
+ });
257
+ const result = await pending;
258
+ assert.equal(result, null);
259
+ assert.equal(server.requests.length, 2, "caller abort happens during the strip retry, not before the first request");
260
+ } finally {
261
+ await server.close();
262
+ }
263
+ });
264
+
265
+ test("request without response_format is unchanged", async () => {
266
+ const server = await startRecordingServer(() => ({ status: 200, body: okCompletion }));
267
+ try {
268
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
269
+ primeClient(client);
270
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }]);
271
+ assert.equal(result?.content, "done");
272
+ assert.equal(server.requests.length, 1);
273
+ assert.equal(server.requests[0].body.response_format, undefined);
274
+ } finally {
275
+ await server.close();
276
+ }
277
+ });
278
+
@@ -339,7 +339,10 @@ test("process-wide concurrency cap bounds in-flight requests across clients", as
339
339
  const config = createConfig({ localLlmUrl: "http://127.0.0.1:1007", localLlmMaxConcurrency: 2 });
340
340
  const clients = [new LocalLlmClient(config), new LocalLlmClient(config), new LocalLlmClient(config)];
341
341
  const results = await Promise.all(
342
- Array.from({ length: 7 }, (_, i) => clients[i % 3].chatCompletion([{ role: "user", content: `q${i}` }]))
342
+ Array.from({ length: 7 }, (_, i) => clients[i % 3].chatCompletion(
343
+ [{ role: "user", content: `q${i}` }],
344
+ { priority: i % 2 === 0 ? "recall-critical" : "background" }
345
+ ))
343
346
  );
344
347
  assert.equal(results.filter((r) => r?.content === "ok").length, 7);
345
348
  assert.equal(peak, 2);
@@ -404,3 +407,58 @@ test("waiting for a concurrency slot is bounded by the wait budget", async () =>
404
407
  }
405
408
  );
406
409
  });
410
+
411
+ test("limiter reserves one finite permit for recall-critical work", async () => {
412
+ const limiter = new LocalLlmConcurrencyLimiter(2);
413
+ const background = await limiter.acquire();
414
+ const secondBackground = await limiter.acquire({ timeoutMs: 0 });
415
+ try {
416
+ assert.ok(background);
417
+ assert.equal(secondBackground, null, "background must not consume the recall permit");
418
+ const foreground = await limiter.acquire({ highPriority: true, timeoutMs: 0 });
419
+ assert.ok(foreground, "recall must start without waiting for background completion");
420
+ foreground();
421
+ foreground();
422
+ assert.equal(limiter.inFlight, 1, "release remains idempotent");
423
+ } finally {
424
+ secondBackground?.();
425
+ background?.();
426
+ }
427
+ });
428
+
429
+ test("reserved background queue survives aborts and responds to resizing", async () => {
430
+ const limiter = new LocalLlmConcurrencyLimiter(2);
431
+ const background = await limiter.acquire();
432
+ assert.ok(background);
433
+ const controller = new AbortController();
434
+ const aborted = limiter.acquire({ signal: controller.signal });
435
+ controller.abort();
436
+ const abortedRelease = await aborted;
437
+ abortedRelease?.();
438
+ assert.equal(abortedRelease, null);
439
+ const queued = limiter.acquire();
440
+ const foreground = await limiter.acquire({ highPriority: true, timeoutMs: 0 });
441
+ assert.ok(foreground);
442
+ assert.equal(limiter.queued, 1);
443
+ limiter.setMax(3);
444
+ const nextBackground = await queued;
445
+ assert.ok(nextBackground);
446
+ assert.equal(limiter.inFlight, 3);
447
+ nextBackground();
448
+ background();
449
+ foreground();
450
+ assert.equal(limiter.inFlight, 0);
451
+ });
452
+
453
+ test("one-permit and unlimited limits still admit background work", async () => {
454
+ for (const max of [1, 0]) {
455
+ const limiter = new LocalLlmConcurrencyLimiter(max);
456
+ const first = await limiter.acquire({ timeoutMs: 0 });
457
+ assert.ok(first);
458
+ const second = await limiter.acquire({ timeoutMs: 0 });
459
+ assert.equal(second === null, max === 1);
460
+ second?.();
461
+ first();
462
+ assert.equal(limiter.inFlight, 0);
463
+ }
464
+ });
@@ -151,9 +151,10 @@ export function computeRateLimitDelayMs(
151
151
 
152
152
  type Waiter = { highPriority: boolean; grant: () => void };
153
153
 
154
- /** FIFO counting semaphore with a high-priority lane (recall-critical first). */
154
+ /** FIFO semaphore; finite caps above one reserve a permit for recall-critical work. */
155
155
  export class LocalLlmConcurrencyLimiter {
156
156
  private active = 0;
157
+ private activeBackground = 0;
157
158
  private max: number;
158
159
  private readonly waiters: Waiter[] = [];
159
160
 
@@ -182,20 +183,25 @@ export class LocalLlmConcurrencyLimiter {
182
183
  options: { highPriority?: boolean; signal?: AbortSignal; timeoutMs?: number } = {}
183
184
  ): Promise<(() => void) | null> {
184
185
  const { signal, timeoutMs } = options;
186
+ const highPriority = options.highPriority === true;
185
187
  if (signal?.aborted) return Promise.resolve(null);
186
- if (this.max <= 0 || (this.active < this.max && this.waiters.length === 0)) {
188
+ if (
189
+ this.canAcquire(highPriority) &&
190
+ (this.waiters.length === 0 || (highPriority && !this.waiters[0].highPriority))
191
+ ) {
187
192
  this.active += 1;
188
- return Promise.resolve(this.releaser());
193
+ if (!highPriority) this.activeBackground += 1;
194
+ return Promise.resolve(this.releaser(highPriority));
189
195
  }
190
196
  if (timeoutMs !== undefined && timeoutMs <= 0) return Promise.resolve(null);
191
197
  return new Promise((resolve) => {
192
198
  let timer: NodeJS.Timeout | undefined;
193
199
  const waiter: Waiter = {
194
- highPriority: options.highPriority === true,
200
+ highPriority,
195
201
  grant: () => {
196
202
  signal?.removeEventListener("abort", onAbort);
197
203
  if (timer) clearTimeout(timer);
198
- resolve(this.releaser());
204
+ resolve(this.releaser(highPriority));
199
205
  },
200
206
  };
201
207
  const onAbort = (): void => {
@@ -217,21 +223,30 @@ export class LocalLlmConcurrencyLimiter {
217
223
  });
218
224
  }
219
225
 
220
- private releaser(): () => void {
226
+ private canAcquire(highPriority: boolean): boolean {
227
+ return this.max <= 0 || (
228
+ this.active < this.max &&
229
+ (highPriority || this.max === 1 || this.activeBackground < this.max - 1)
230
+ );
231
+ }
232
+
233
+ private releaser(highPriority: boolean): () => void {
221
234
  let released = false;
222
235
  return () => {
223
236
  if (released) return;
224
237
  released = true;
225
238
  this.active -= 1;
239
+ if (!highPriority) this.activeBackground -= 1;
226
240
  this.pump();
227
241
  };
228
242
  }
229
243
 
230
244
  private pump(): void {
231
- while (this.waiters.length > 0 && (this.max <= 0 || this.active < this.max)) {
245
+ while (this.waiters.length > 0 && this.canAcquire(this.waiters[0].highPriority)) {
232
246
  const next = this.waiters.shift();
233
247
  if (!next) break;
234
248
  this.active += 1;
249
+ if (!next.highPriority) this.activeBackground += 1;
235
250
  next.grant();
236
251
  }
237
252
  }