@remnic/core 10.0.36 → 10.0.38

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 (422) hide show
  1. package/dist/access-admin-ops-surface.js +84 -84
  2. package/dist/access-authorization-probe.js +85 -85
  3. package/dist/access-boundary.js +84 -84
  4. package/dist/access-citation-namespace.js +2 -2
  5. package/dist/access-cli.js +178 -176
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-extraction-force-flush.js +84 -84
  8. package/dist/access-http-briefing.js +85 -85
  9. package/dist/access-http-query.js +86 -86
  10. package/dist/access-http.js +108 -108
  11. package/dist/access-identity-continuity-surface.js +84 -84
  12. package/dist/access-lcm-surface.js +84 -84
  13. package/dist/access-mcp.js +97 -97
  14. package/dist/access-namespace-preflight.js +84 -84
  15. package/dist/access-observe-write-category.js +13 -13
  16. package/dist/access-observe-write-surface.js +84 -84
  17. package/dist/access-offline-manifest.js +12 -12
  18. package/dist/access-operations-batch.js +88 -88
  19. package/dist/access-operations.js +95 -95
  20. package/dist/access-recall-concurrency.js +84 -84
  21. package/dist/access-recall-diagnostics.js +2 -2
  22. package/dist/access-recall-response.js +84 -84
  23. package/dist/access-recall-surface.js +84 -84
  24. package/dist/access-schema.js +8 -8
  25. package/dist/access-service.js +84 -84
  26. package/dist/access-surface-catalog.js +86 -86
  27. package/dist/active-recall.js +43 -43
  28. package/dist/adapters/index.js +7 -7
  29. package/dist/adapters/registry.js +3 -3
  30. package/dist/{auto-sync-VPHR3FBI.js → auto-sync-432USQDH.js} +9 -9
  31. package/dist/briefing.js +32 -32
  32. package/dist/buffer-eviction.d.ts +16 -0
  33. package/dist/buffer-eviction.d.ts.map +1 -0
  34. package/dist/buffer-eviction.js +8 -0
  35. package/dist/buffer-persistence.d.ts +45 -0
  36. package/dist/buffer-persistence.d.ts.map +1 -0
  37. package/dist/buffer-persistence.js +8 -0
  38. package/dist/buffer.d.ts +5 -2
  39. package/dist/buffer.d.ts.map +1 -1
  40. package/dist/buffer.js +13 -11
  41. package/dist/calibration.js +1 -1
  42. package/dist/causal-behavior.js +2 -2
  43. package/dist/causal-chain.js +2 -2
  44. package/dist/causal-consolidation.js +35 -35
  45. package/dist/causal-retrieval.js +2 -2
  46. package/dist/causal-trajectory-graph.js +2 -2
  47. package/dist/causal-trajectory.js +1 -1
  48. package/dist/{chunk-FL6SLVAJ.js → chunk-2AKIVSHP.js} +2 -2
  49. package/dist/{chunk-ORWDCX5Q.js → chunk-3AODCBKX.js} +11 -11
  50. package/dist/{chunk-XFDHL252.js → chunk-3K5IEJTK.js} +4 -4
  51. package/dist/{chunk-6FIFEXFF.js → chunk-3PLAL2KP.js} +21 -7
  52. package/dist/chunk-3PLAL2KP.js.map +1 -0
  53. package/dist/{chunk-XVZA6VXC.js → chunk-3SL52JKD.js} +4 -4
  54. package/dist/{chunk-ZSAQSLKL.js → chunk-3XVBGGF3.js} +2 -2
  55. package/dist/{chunk-XO4HOBM2.js → chunk-4B37KNMP.js} +2 -2
  56. package/dist/{chunk-NIANATGL.js → chunk-4IZ6AC2U.js} +2 -2
  57. package/dist/chunk-4OFXLQJD.js +58 -0
  58. package/dist/chunk-4OFXLQJD.js.map +1 -0
  59. package/dist/{chunk-S2PDFSXS.js → chunk-4P4VLPGB.js} +1 -1
  60. package/dist/{chunk-G5UR4DXQ.js → chunk-53BLBDV6.js} +2 -2
  61. package/dist/{chunk-PDYP7SUC.js → chunk-5ANUG4JZ.js} +2 -2
  62. package/dist/{chunk-2ZYIAUYU.js → chunk-5CWH2LKL.js} +4 -4
  63. package/dist/{chunk-S2S4U4G3.js → chunk-5DCS4C4S.js} +4 -4
  64. package/dist/{chunk-7WTTE5FW.js → chunk-5K23NQRI.js} +2 -2
  65. package/dist/{chunk-V3JZZJ2E.js → chunk-5UPOKIFP.js} +12 -12
  66. package/dist/{chunk-K567EIW3.js → chunk-63YPKHAU.js} +2 -2
  67. package/dist/{chunk-G3T6YMBO.js → chunk-64S6X4IU.js} +7 -7
  68. package/dist/{chunk-HJM23E65.js → chunk-6DTIYAAN.js} +2 -2
  69. package/dist/{chunk-G7XAZU2L.js → chunk-6F5BGIAF.js} +3 -3
  70. package/dist/{chunk-IHTKBQBS.js → chunk-6JEEY42N.js} +2 -2
  71. package/dist/{chunk-NHB4RC3G.js → chunk-6QOL4G76.js} +2 -2
  72. package/dist/{chunk-AD3ZB72W.js → chunk-6SYS5YX3.js} +2 -2
  73. package/dist/{chunk-DFP37GUN.js → chunk-7HXZOWTR.js} +2 -2
  74. package/dist/{chunk-DGBWPSM6.js → chunk-7VFNGQ4V.js} +2 -2
  75. package/dist/{chunk-77ZK533I.js → chunk-7ZRRJ6RG.js} +2 -2
  76. package/dist/{chunk-GMMZ45C4.js → chunk-AVVCPA7U.js} +6 -8
  77. package/dist/chunk-AVVCPA7U.js.map +1 -0
  78. package/dist/{chunk-QISYYK4L.js → chunk-AYBAC2N4.js} +3 -3
  79. package/dist/{chunk-V44DWTFA.js → chunk-AYXRK264.js} +6 -6
  80. package/dist/{chunk-LJSBF6PW.js → chunk-C2KOW2UQ.js} +2 -2
  81. package/dist/{chunk-GY4R4P2A.js → chunk-CUWOSRCS.js} +2 -2
  82. package/dist/{chunk-VNAIUNXI.js → chunk-CVMAOPAH.js} +10 -10
  83. package/dist/{chunk-CJKHLACF.js → chunk-DG4H56SK.js} +2 -2
  84. package/dist/{chunk-BQ6DGL6A.js → chunk-DKHX2FHI.js} +2 -2
  85. package/dist/{chunk-77IXBNV6.js → chunk-E24RUQEN.js} +2 -2
  86. package/dist/{chunk-HRT2SJPC.js → chunk-E3NBMTTX.js} +2 -2
  87. package/dist/{chunk-PTQZPVRQ.js → chunk-E5PVMB3I.js} +2 -2
  88. package/dist/{chunk-AHS3QEEA.js → chunk-EEOYF3IB.js} +2 -2
  89. package/dist/{chunk-5XTSIZ3L.js → chunk-EGNONTVT.js} +6 -6
  90. package/dist/{chunk-PH33R7GN.js → chunk-EHIKY2IA.js} +2 -2
  91. package/dist/{chunk-4ISHIPOC.js → chunk-FGEY2AGV.js} +2 -2
  92. package/dist/{chunk-O6YJI5VX.js → chunk-FKTMWDN2.js} +2 -2
  93. package/dist/{chunk-KQUN4RSX.js → chunk-FQFMLOAC.js} +18 -18
  94. package/dist/{chunk-5LWODP5R.js → chunk-FRAL3EU4.js} +2 -2
  95. package/dist/{chunk-AAANTXCQ.js → chunk-FZVZYU5C.js} +2 -2
  96. package/dist/{chunk-TGAXRT7N.js → chunk-GFYFKXWZ.js} +2 -2
  97. package/dist/{chunk-ID2PLENW.js → chunk-GGI6FLO6.js} +2 -2
  98. package/dist/{chunk-GDYLZ6FR.js → chunk-HJ3UTGIW.js} +3 -3
  99. package/dist/{chunk-X3IFRQKS.js → chunk-I73ZSORJ.js} +4 -4
  100. package/dist/{chunk-X73SCHKR.js → chunk-I75BWXRZ.js} +2 -2
  101. package/dist/{chunk-M3GKSCFN.js → chunk-J7G656MY.js} +4 -4
  102. package/dist/{chunk-EH6KLYIE.js → chunk-JF3MEIHO.js} +4 -4
  103. package/dist/{chunk-XUB4I2N6.js → chunk-JFI3ATK4.js} +2 -2
  104. package/dist/{chunk-R4SATHCV.js → chunk-JH2ALMPR.js} +2 -2
  105. package/dist/{chunk-TKBW4YLS.js → chunk-JH4FCVWJ.js} +2 -2
  106. package/dist/{chunk-DG5UKN2Y.js → chunk-JIA435P3.js} +2 -2
  107. package/dist/chunk-KASJJGRG.js +29 -0
  108. package/dist/chunk-KASJJGRG.js.map +1 -0
  109. package/dist/{chunk-4Z7O2QVJ.js → chunk-KOAVX2IT.js} +2 -2
  110. package/dist/{chunk-6NXHX6MI.js → chunk-KOY25HUB.js} +2 -2
  111. package/dist/{chunk-TRW7N4FS.js → chunk-L2JQSG5M.js} +2 -2
  112. package/dist/{chunk-QQ4T622E.js → chunk-L7PGPH4E.js} +2 -2
  113. package/dist/{chunk-TYT4TEQV.js → chunk-LDWE7OZS.js} +6 -2
  114. package/dist/chunk-LDWE7OZS.js.map +1 -0
  115. package/dist/{chunk-23VGZ65V.js → chunk-LHBOFROT.js} +2 -2
  116. package/dist/{chunk-VKB2FGNW.js → chunk-LOMSBYNS.js} +90 -90
  117. package/dist/{chunk-NDJ42GKB.js → chunk-MCPSMWUC.js} +6 -6
  118. package/dist/{chunk-ERMT5WQM.js → chunk-MKE5DP3S.js} +2 -2
  119. package/dist/{chunk-64IO6ZEE.js → chunk-MZWHIAOL.js} +2 -2
  120. package/dist/{chunk-LDXS746V.js → chunk-N7DVBHU3.js} +3 -3
  121. package/dist/{chunk-F2JLBXMG.js → chunk-NH6GZEQN.js} +8 -8
  122. package/dist/{chunk-7FG2JSU6.js → chunk-NMKETOAX.js} +2 -2
  123. package/dist/{chunk-GAARPWMN.js → chunk-OKKBOTRA.js} +2 -2
  124. package/dist/{chunk-PCIIKQXW.js → chunk-OLH4XCLU.js} +2 -2
  125. package/dist/{chunk-XA32GZ6B.js → chunk-OLHN4ZNT.js} +3 -3
  126. package/dist/{chunk-UR25IQVT.js → chunk-P5CUEB2Q.js} +8 -8
  127. package/dist/{chunk-LO775P3S.js → chunk-POGPI5GR.js} +2 -2
  128. package/dist/{chunk-FXO43NBW.js → chunk-PSQ2P2T3.js} +3 -3
  129. package/dist/{chunk-KRAMCBIW.js → chunk-QKXPOLQD.js} +4 -4
  130. package/dist/{chunk-65JLKIVA.js → chunk-QO5KDQGG.js} +8 -8
  131. package/dist/{chunk-A4B7WL5N.js → chunk-QOUKZ2N3.js} +2 -2
  132. package/dist/{chunk-ZABEDRGQ.js → chunk-RGKXF4W3.js} +2 -2
  133. package/dist/{chunk-IKCGNTGS.js → chunk-RWRHGE75.js} +2 -2
  134. package/dist/{chunk-NBH7IRME.js → chunk-SWZOAQUM.js} +54 -54
  135. package/dist/{chunk-FQ52AJXX.js → chunk-SZPPCEYT.js} +2 -2
  136. package/dist/{chunk-4YYIZ475.js → chunk-TAEJ2MRR.js} +52 -52
  137. package/dist/{chunk-3ODEGTVV.js → chunk-TDCR6PQL.js} +2 -2
  138. package/dist/{chunk-QCP75JGO.js → chunk-TFA6HPOA.js} +5 -5
  139. package/dist/{chunk-BBDASWNT.js → chunk-TJ5YGCPV.js} +81 -76
  140. package/dist/chunk-TJ5YGCPV.js.map +1 -0
  141. package/dist/{chunk-SE3JAD22.js → chunk-TPKB7T6Z.js} +2 -2
  142. package/dist/{chunk-6VYY2XFK.js → chunk-TVIGN3XZ.js} +19 -19
  143. package/dist/{chunk-OHMGR6QN.js → chunk-TXP7Q7T5.js} +2 -2
  144. package/dist/{chunk-ZG3HZZ6K.js → chunk-TYGLV6ZG.js} +5 -1
  145. package/dist/{chunk-ZG3HZZ6K.js.map → chunk-TYGLV6ZG.js.map} +1 -1
  146. package/dist/{chunk-AI2VXOSO.js → chunk-TYLNWE4D.js} +2 -2
  147. package/dist/{chunk-WI6MIJYK.js → chunk-U7HJBODU.js} +5 -5
  148. package/dist/{chunk-IS4HKNWP.js → chunk-UBN5AOLT.js} +2 -2
  149. package/dist/{chunk-A6PKEU7T.js → chunk-UFM3PJDT.js} +1 -1
  150. package/dist/chunk-UFM3PJDT.js.map +1 -0
  151. package/dist/{chunk-JNELS2TW.js → chunk-UJDTXWMT.js} +2 -2
  152. package/dist/{chunk-DJMC4W6N.js → chunk-VC3X7XP3.js} +2 -2
  153. package/dist/{chunk-A2D3WICT.js → chunk-VGFNTY3Y.js} +2 -2
  154. package/dist/{chunk-X544TW54.js → chunk-VGRM43X6.js} +2 -2
  155. package/dist/{chunk-CSGFGQBI.js → chunk-VPO53QMG.js} +2 -2
  156. package/dist/{chunk-BRZRP5WD.js → chunk-VSSQIBVU.js} +2 -2
  157. package/dist/{chunk-O3VKQMWC.js → chunk-WCBLPKG3.js} +366 -168
  158. package/dist/chunk-WCBLPKG3.js.map +1 -0
  159. package/dist/{chunk-GKNX4QO3.js → chunk-WCIDCJOQ.js} +3 -3
  160. package/dist/{chunk-7VJEZQYF.js → chunk-WEK3OUGO.js} +3 -3
  161. package/dist/{chunk-IVYAPAHG.js → chunk-WRDFYSUC.js} +2 -2
  162. package/dist/{chunk-5EILJ6W2.js → chunk-WXPBDEMW.js} +2 -2
  163. package/dist/{chunk-QZP3RMYP.js → chunk-X247CYL6.js} +4 -4
  164. package/dist/{chunk-UAYA4NJN.js → chunk-XHVO3CGX.js} +28 -36
  165. package/dist/chunk-XHVO3CGX.js.map +1 -0
  166. package/dist/{chunk-YLF42JQ7.js → chunk-XHX67LX6.js} +2 -2
  167. package/dist/{chunk-C6S2Y7HE.js → chunk-XKDQTDXN.js} +4 -4
  168. package/dist/{chunk-4AHKNZW7.js → chunk-XLEP7SS3.js} +2 -2
  169. package/dist/{chunk-B6L7BKA7.js → chunk-XRFLR3XF.js} +4 -4
  170. package/dist/{chunk-H7QC7WCU.js → chunk-XTBEXZFN.js} +4 -4
  171. package/dist/{chunk-CIJRM6SQ.js → chunk-Y5ZHZKZD.js} +2 -2
  172. package/dist/{chunk-H6SKBCNI.js → chunk-YC2C67F7.js} +2 -2
  173. package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
  174. package/dist/{chunk-YOL56HY5.js → chunk-YQ6SLFNZ.js} +3 -3
  175. package/dist/{chunk-SIIWHIBK.js → chunk-YYGZ4EIY.js} +2 -2
  176. package/dist/{chunk-G22A6WJQ.js → chunk-Z2T5BPOZ.js} +2 -2
  177. package/dist/{chunk-RYUSWAAU.js → chunk-ZAP7BKTZ.js} +6 -6
  178. package/dist/cli.js +136 -136
  179. package/dist/coding/export-okf-codegraph.js +33 -33
  180. package/dist/coding/pre-action-gate.js +2 -2
  181. package/dist/compounding/engine.js +34 -34
  182. package/dist/config.d.ts.map +1 -1
  183. package/dist/config.js +43 -43
  184. package/dist/connectors/codex-materialize-runner.js +32 -32
  185. package/dist/connectors/index.js +32 -32
  186. package/dist/contradiction/index.js +5 -5
  187. package/dist/convergence-refresh.js +34 -34
  188. package/dist/conversation-index/backend.js +2 -2
  189. package/dist/corpus-watermark.js +34 -34
  190. package/dist/dashboard-runtime.js +2 -2
  191. package/dist/deep-recall-http-glue.js +9 -9
  192. package/dist/deep-recall-policy-llm.js +2 -2
  193. package/dist/entity-retrieval.js +34 -34
  194. package/dist/explicit-capture.js +7 -7
  195. package/dist/external-wiki-access.js +85 -85
  196. package/dist/extraction-faithfulness.js +2 -2
  197. package/dist/extraction-judge.d.ts +42 -2
  198. package/dist/extraction-judge.d.ts.map +1 -1
  199. package/dist/extraction-judge.js +12 -2
  200. package/dist/extraction-span-materialize.js +2 -2
  201. package/dist/extraction.js +47 -47
  202. package/dist/fallback-llm.d.ts +1 -0
  203. package/dist/fallback-llm.d.ts.map +1 -1
  204. package/dist/fallback-llm.js +1 -1
  205. package/dist/{graph-edge-decay-IU75NV42.js → graph-edge-decay-2BZTY2IM.js} +5 -5
  206. package/dist/graph-edge-decay-2BZTY2IM.js.map +1 -0
  207. package/dist/graph-jsonl.js +3 -3
  208. package/dist/graph-snapshot.js +3 -3
  209. package/dist/graph.js +2 -2
  210. package/dist/harmonic-retrieval.js +33 -33
  211. package/dist/harmonic-source-lookup.js +2 -2
  212. package/dist/index.js +236 -234
  213. package/dist/lcm/index.js +3 -3
  214. package/dist/location/index.js +35 -35
  215. package/dist/maintenance/memory-governance.js +32 -32
  216. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +32 -32
  217. package/dist/maintenance/rebuild-memory-projection-runner.js +34 -34
  218. package/dist/maintenance/rebuild-memory-projection.js +33 -33
  219. package/dist/memory-browse-operations.js +85 -85
  220. package/dist/memory-browse.js +5 -5
  221. package/dist/memory-subject.js +3 -3
  222. package/dist/namespaces/migrate.js +33 -33
  223. package/dist/namespaces/search.js +11 -11
  224. package/dist/namespaces/storage.js +32 -32
  225. package/dist/offline-sync-changeset-generations.js +3 -3
  226. package/dist/offline-sync-embedding-generation.js +2 -2
  227. package/dist/offline-sync-file-io.js +2 -2
  228. package/dist/offline-sync-impression-drain.js +3 -3
  229. package/dist/offline-sync-runtime-state.d.ts.map +1 -1
  230. package/dist/offline-sync-runtime-state.js +1 -1
  231. package/dist/offline-sync-snapshot-stream.js +8 -8
  232. package/dist/offline-sync-upload-staging.js +3 -3
  233. package/dist/offline-sync.js +8 -8
  234. package/dist/operator-doctor-corpus.js +35 -35
  235. package/dist/operator-toolkit.js +54 -54
  236. package/dist/orchestration/maintenance.js +44 -44
  237. package/dist/orchestrator.js +183 -181
  238. package/dist/qmd.js +4 -4
  239. package/dist/recall-explain-renderer.js +3 -3
  240. package/dist/recall-navigate-access.js +2 -2
  241. package/dist/recall-navigate-disclosure.js +2 -2
  242. package/dist/recall-navigate.js +2 -2
  243. package/dist/recall-navigation-operations.js +85 -85
  244. package/dist/recall-navigation.js +84 -84
  245. package/dist/recall-planner-llm.js +1 -1
  246. package/dist/recall-state-view-admission.js +5 -5
  247. package/dist/recall-why-service.js +5 -5
  248. package/dist/recall-xray-cli.js +4 -4
  249. package/dist/recall-xray-renderer.js +3 -3
  250. package/dist/recall-xray.js +2 -2
  251. package/dist/reconcile/manifest.js +3 -3
  252. package/dist/{resolution-V6JBMW7U.js → resolution-4ZEQPXUZ.js} +6 -6
  253. package/dist/resolution-4ZEQPXUZ.js.map +1 -0
  254. package/dist/resume-bundles.js +44 -44
  255. package/dist/retrieval-agents.js +2 -2
  256. package/dist/salvage-envelope.js +5 -5
  257. package/dist/search/factory.js +10 -10
  258. package/dist/search/index.js +14 -14
  259. package/dist/search/lancedb-backend.js +2 -2
  260. package/dist/search/meilisearch-backend.js +2 -2
  261. package/dist/search/orama-backend.js +2 -2
  262. package/dist/secure-store/index.js +2 -2
  263. package/dist/semantic-consolidation.js +33 -33
  264. package/dist/semantic-rule-promotion.js +32 -32
  265. package/dist/semantic-rule-verifier.js +32 -32
  266. package/dist/source-agent-qualifier.js +37 -37
  267. package/dist/standup/build.js +32 -32
  268. package/dist/storage/buffer-session-files.d.ts +69 -0
  269. package/dist/storage/buffer-session-files.d.ts.map +1 -0
  270. package/dist/storage.d.ts +5 -1
  271. package/dist/storage.d.ts.map +1 -1
  272. package/dist/storage.js +31 -31
  273. package/dist/subject-config.js +4 -4
  274. package/dist/summarizer.js +2 -2
  275. package/dist/support-passport/index.js +87 -87
  276. package/dist/transfer/export-okf.js +33 -33
  277. package/dist/types.d.ts +12 -0
  278. package/dist/types.d.ts.map +1 -1
  279. package/dist/types.js +1 -1
  280. package/dist/verified-recall.js +32 -32
  281. package/dist/who-knows-cli.js +33 -33
  282. package/dist/who-knows.js +32 -32
  283. package/dist/write-envelope.js +4 -4
  284. package/package.json +2 -2
  285. package/src/buffer-eviction.ts +51 -0
  286. package/src/buffer-incremental-persistence.test.ts +256 -0
  287. package/src/buffer-persistence.ts +89 -0
  288. package/src/buffer.ts +32 -61
  289. package/src/config.test.ts +16 -0
  290. package/src/config.ts +14 -0
  291. package/src/extraction-judge-chain.test.ts +298 -2
  292. package/src/extraction-judge.ts +147 -82
  293. package/src/fallback-llm.test.ts +94 -0
  294. package/src/fallback-llm.ts +7 -7
  295. package/src/offline-sync-runtime-state.ts +24 -2
  296. package/src/offline-sync.test.ts +9 -0
  297. package/src/secure-store/secure-fs.ts +11 -0
  298. package/src/secure-store/secure-store.test.ts +22 -0
  299. package/src/storage/buffer-session-files.ts +249 -0
  300. package/src/storage-buffer-sessions.test.ts +369 -0
  301. package/src/storage.ts +11 -16
  302. package/src/types.ts +12 -0
  303. package/dist/chunk-6FIFEXFF.js.map +0 -1
  304. package/dist/chunk-A6PKEU7T.js.map +0 -1
  305. package/dist/chunk-BBDASWNT.js.map +0 -1
  306. package/dist/chunk-GMMZ45C4.js.map +0 -1
  307. package/dist/chunk-O3VKQMWC.js.map +0 -1
  308. package/dist/chunk-TYT4TEQV.js.map +0 -1
  309. package/dist/chunk-UAYA4NJN.js.map +0 -1
  310. /package/dist/{auto-sync-VPHR3FBI.js.map → auto-sync-432USQDH.js.map} +0 -0
  311. /package/dist/{graph-edge-decay-IU75NV42.js.map → buffer-eviction.js.map} +0 -0
  312. /package/dist/{resolution-V6JBMW7U.js.map → buffer-persistence.js.map} +0 -0
  313. /package/dist/{chunk-FL6SLVAJ.js.map → chunk-2AKIVSHP.js.map} +0 -0
  314. /package/dist/{chunk-ORWDCX5Q.js.map → chunk-3AODCBKX.js.map} +0 -0
  315. /package/dist/{chunk-XFDHL252.js.map → chunk-3K5IEJTK.js.map} +0 -0
  316. /package/dist/{chunk-XVZA6VXC.js.map → chunk-3SL52JKD.js.map} +0 -0
  317. /package/dist/{chunk-ZSAQSLKL.js.map → chunk-3XVBGGF3.js.map} +0 -0
  318. /package/dist/{chunk-XO4HOBM2.js.map → chunk-4B37KNMP.js.map} +0 -0
  319. /package/dist/{chunk-NIANATGL.js.map → chunk-4IZ6AC2U.js.map} +0 -0
  320. /package/dist/{chunk-S2PDFSXS.js.map → chunk-4P4VLPGB.js.map} +0 -0
  321. /package/dist/{chunk-G5UR4DXQ.js.map → chunk-53BLBDV6.js.map} +0 -0
  322. /package/dist/{chunk-PDYP7SUC.js.map → chunk-5ANUG4JZ.js.map} +0 -0
  323. /package/dist/{chunk-2ZYIAUYU.js.map → chunk-5CWH2LKL.js.map} +0 -0
  324. /package/dist/{chunk-S2S4U4G3.js.map → chunk-5DCS4C4S.js.map} +0 -0
  325. /package/dist/{chunk-7WTTE5FW.js.map → chunk-5K23NQRI.js.map} +0 -0
  326. /package/dist/{chunk-V3JZZJ2E.js.map → chunk-5UPOKIFP.js.map} +0 -0
  327. /package/dist/{chunk-K567EIW3.js.map → chunk-63YPKHAU.js.map} +0 -0
  328. /package/dist/{chunk-G3T6YMBO.js.map → chunk-64S6X4IU.js.map} +0 -0
  329. /package/dist/{chunk-HJM23E65.js.map → chunk-6DTIYAAN.js.map} +0 -0
  330. /package/dist/{chunk-G7XAZU2L.js.map → chunk-6F5BGIAF.js.map} +0 -0
  331. /package/dist/{chunk-IHTKBQBS.js.map → chunk-6JEEY42N.js.map} +0 -0
  332. /package/dist/{chunk-NHB4RC3G.js.map → chunk-6QOL4G76.js.map} +0 -0
  333. /package/dist/{chunk-AD3ZB72W.js.map → chunk-6SYS5YX3.js.map} +0 -0
  334. /package/dist/{chunk-DFP37GUN.js.map → chunk-7HXZOWTR.js.map} +0 -0
  335. /package/dist/{chunk-DGBWPSM6.js.map → chunk-7VFNGQ4V.js.map} +0 -0
  336. /package/dist/{chunk-77ZK533I.js.map → chunk-7ZRRJ6RG.js.map} +0 -0
  337. /package/dist/{chunk-QISYYK4L.js.map → chunk-AYBAC2N4.js.map} +0 -0
  338. /package/dist/{chunk-V44DWTFA.js.map → chunk-AYXRK264.js.map} +0 -0
  339. /package/dist/{chunk-LJSBF6PW.js.map → chunk-C2KOW2UQ.js.map} +0 -0
  340. /package/dist/{chunk-GY4R4P2A.js.map → chunk-CUWOSRCS.js.map} +0 -0
  341. /package/dist/{chunk-VNAIUNXI.js.map → chunk-CVMAOPAH.js.map} +0 -0
  342. /package/dist/{chunk-CJKHLACF.js.map → chunk-DG4H56SK.js.map} +0 -0
  343. /package/dist/{chunk-BQ6DGL6A.js.map → chunk-DKHX2FHI.js.map} +0 -0
  344. /package/dist/{chunk-77IXBNV6.js.map → chunk-E24RUQEN.js.map} +0 -0
  345. /package/dist/{chunk-HRT2SJPC.js.map → chunk-E3NBMTTX.js.map} +0 -0
  346. /package/dist/{chunk-PTQZPVRQ.js.map → chunk-E5PVMB3I.js.map} +0 -0
  347. /package/dist/{chunk-AHS3QEEA.js.map → chunk-EEOYF3IB.js.map} +0 -0
  348. /package/dist/{chunk-5XTSIZ3L.js.map → chunk-EGNONTVT.js.map} +0 -0
  349. /package/dist/{chunk-PH33R7GN.js.map → chunk-EHIKY2IA.js.map} +0 -0
  350. /package/dist/{chunk-4ISHIPOC.js.map → chunk-FGEY2AGV.js.map} +0 -0
  351. /package/dist/{chunk-O6YJI5VX.js.map → chunk-FKTMWDN2.js.map} +0 -0
  352. /package/dist/{chunk-KQUN4RSX.js.map → chunk-FQFMLOAC.js.map} +0 -0
  353. /package/dist/{chunk-5LWODP5R.js.map → chunk-FRAL3EU4.js.map} +0 -0
  354. /package/dist/{chunk-AAANTXCQ.js.map → chunk-FZVZYU5C.js.map} +0 -0
  355. /package/dist/{chunk-TGAXRT7N.js.map → chunk-GFYFKXWZ.js.map} +0 -0
  356. /package/dist/{chunk-ID2PLENW.js.map → chunk-GGI6FLO6.js.map} +0 -0
  357. /package/dist/{chunk-GDYLZ6FR.js.map → chunk-HJ3UTGIW.js.map} +0 -0
  358. /package/dist/{chunk-X3IFRQKS.js.map → chunk-I73ZSORJ.js.map} +0 -0
  359. /package/dist/{chunk-X73SCHKR.js.map → chunk-I75BWXRZ.js.map} +0 -0
  360. /package/dist/{chunk-M3GKSCFN.js.map → chunk-J7G656MY.js.map} +0 -0
  361. /package/dist/{chunk-EH6KLYIE.js.map → chunk-JF3MEIHO.js.map} +0 -0
  362. /package/dist/{chunk-XUB4I2N6.js.map → chunk-JFI3ATK4.js.map} +0 -0
  363. /package/dist/{chunk-R4SATHCV.js.map → chunk-JH2ALMPR.js.map} +0 -0
  364. /package/dist/{chunk-TKBW4YLS.js.map → chunk-JH4FCVWJ.js.map} +0 -0
  365. /package/dist/{chunk-DG5UKN2Y.js.map → chunk-JIA435P3.js.map} +0 -0
  366. /package/dist/{chunk-4Z7O2QVJ.js.map → chunk-KOAVX2IT.js.map} +0 -0
  367. /package/dist/{chunk-6NXHX6MI.js.map → chunk-KOY25HUB.js.map} +0 -0
  368. /package/dist/{chunk-TRW7N4FS.js.map → chunk-L2JQSG5M.js.map} +0 -0
  369. /package/dist/{chunk-QQ4T622E.js.map → chunk-L7PGPH4E.js.map} +0 -0
  370. /package/dist/{chunk-23VGZ65V.js.map → chunk-LHBOFROT.js.map} +0 -0
  371. /package/dist/{chunk-VKB2FGNW.js.map → chunk-LOMSBYNS.js.map} +0 -0
  372. /package/dist/{chunk-NDJ42GKB.js.map → chunk-MCPSMWUC.js.map} +0 -0
  373. /package/dist/{chunk-ERMT5WQM.js.map → chunk-MKE5DP3S.js.map} +0 -0
  374. /package/dist/{chunk-64IO6ZEE.js.map → chunk-MZWHIAOL.js.map} +0 -0
  375. /package/dist/{chunk-LDXS746V.js.map → chunk-N7DVBHU3.js.map} +0 -0
  376. /package/dist/{chunk-F2JLBXMG.js.map → chunk-NH6GZEQN.js.map} +0 -0
  377. /package/dist/{chunk-7FG2JSU6.js.map → chunk-NMKETOAX.js.map} +0 -0
  378. /package/dist/{chunk-GAARPWMN.js.map → chunk-OKKBOTRA.js.map} +0 -0
  379. /package/dist/{chunk-PCIIKQXW.js.map → chunk-OLH4XCLU.js.map} +0 -0
  380. /package/dist/{chunk-XA32GZ6B.js.map → chunk-OLHN4ZNT.js.map} +0 -0
  381. /package/dist/{chunk-UR25IQVT.js.map → chunk-P5CUEB2Q.js.map} +0 -0
  382. /package/dist/{chunk-LO775P3S.js.map → chunk-POGPI5GR.js.map} +0 -0
  383. /package/dist/{chunk-FXO43NBW.js.map → chunk-PSQ2P2T3.js.map} +0 -0
  384. /package/dist/{chunk-KRAMCBIW.js.map → chunk-QKXPOLQD.js.map} +0 -0
  385. /package/dist/{chunk-65JLKIVA.js.map → chunk-QO5KDQGG.js.map} +0 -0
  386. /package/dist/{chunk-A4B7WL5N.js.map → chunk-QOUKZ2N3.js.map} +0 -0
  387. /package/dist/{chunk-ZABEDRGQ.js.map → chunk-RGKXF4W3.js.map} +0 -0
  388. /package/dist/{chunk-IKCGNTGS.js.map → chunk-RWRHGE75.js.map} +0 -0
  389. /package/dist/{chunk-NBH7IRME.js.map → chunk-SWZOAQUM.js.map} +0 -0
  390. /package/dist/{chunk-FQ52AJXX.js.map → chunk-SZPPCEYT.js.map} +0 -0
  391. /package/dist/{chunk-4YYIZ475.js.map → chunk-TAEJ2MRR.js.map} +0 -0
  392. /package/dist/{chunk-3ODEGTVV.js.map → chunk-TDCR6PQL.js.map} +0 -0
  393. /package/dist/{chunk-QCP75JGO.js.map → chunk-TFA6HPOA.js.map} +0 -0
  394. /package/dist/{chunk-SE3JAD22.js.map → chunk-TPKB7T6Z.js.map} +0 -0
  395. /package/dist/{chunk-6VYY2XFK.js.map → chunk-TVIGN3XZ.js.map} +0 -0
  396. /package/dist/{chunk-OHMGR6QN.js.map → chunk-TXP7Q7T5.js.map} +0 -0
  397. /package/dist/{chunk-AI2VXOSO.js.map → chunk-TYLNWE4D.js.map} +0 -0
  398. /package/dist/{chunk-WI6MIJYK.js.map → chunk-U7HJBODU.js.map} +0 -0
  399. /package/dist/{chunk-IS4HKNWP.js.map → chunk-UBN5AOLT.js.map} +0 -0
  400. /package/dist/{chunk-JNELS2TW.js.map → chunk-UJDTXWMT.js.map} +0 -0
  401. /package/dist/{chunk-DJMC4W6N.js.map → chunk-VC3X7XP3.js.map} +0 -0
  402. /package/dist/{chunk-A2D3WICT.js.map → chunk-VGFNTY3Y.js.map} +0 -0
  403. /package/dist/{chunk-X544TW54.js.map → chunk-VGRM43X6.js.map} +0 -0
  404. /package/dist/{chunk-CSGFGQBI.js.map → chunk-VPO53QMG.js.map} +0 -0
  405. /package/dist/{chunk-BRZRP5WD.js.map → chunk-VSSQIBVU.js.map} +0 -0
  406. /package/dist/{chunk-GKNX4QO3.js.map → chunk-WCIDCJOQ.js.map} +0 -0
  407. /package/dist/{chunk-7VJEZQYF.js.map → chunk-WEK3OUGO.js.map} +0 -0
  408. /package/dist/{chunk-IVYAPAHG.js.map → chunk-WRDFYSUC.js.map} +0 -0
  409. /package/dist/{chunk-5EILJ6W2.js.map → chunk-WXPBDEMW.js.map} +0 -0
  410. /package/dist/{chunk-QZP3RMYP.js.map → chunk-X247CYL6.js.map} +0 -0
  411. /package/dist/{chunk-YLF42JQ7.js.map → chunk-XHX67LX6.js.map} +0 -0
  412. /package/dist/{chunk-C6S2Y7HE.js.map → chunk-XKDQTDXN.js.map} +0 -0
  413. /package/dist/{chunk-4AHKNZW7.js.map → chunk-XLEP7SS3.js.map} +0 -0
  414. /package/dist/{chunk-B6L7BKA7.js.map → chunk-XRFLR3XF.js.map} +0 -0
  415. /package/dist/{chunk-H7QC7WCU.js.map → chunk-XTBEXZFN.js.map} +0 -0
  416. /package/dist/{chunk-CIJRM6SQ.js.map → chunk-Y5ZHZKZD.js.map} +0 -0
  417. /package/dist/{chunk-H6SKBCNI.js.map → chunk-YC2C67F7.js.map} +0 -0
  418. /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
  419. /package/dist/{chunk-YOL56HY5.js.map → chunk-YQ6SLFNZ.js.map} +0 -0
  420. /package/dist/{chunk-SIIWHIBK.js.map → chunk-YYGZ4EIY.js.map} +0 -0
  421. /package/dist/{chunk-G22A6WJQ.js.map → chunk-Z2T5BPOZ.js.map} +0 -0
  422. /package/dist/{chunk-RYUSWAAU.js.map → chunk-ZAP7BKTZ.js.map} +0 -0
package/src/buffer.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { log } from "./logger.js";
2
2
  import { throwIfAborted } from "./abort-error.js";
3
3
  import { scanSignals } from "./signal.js";
4
- import type { StorageManager } from "./storage.js";
4
+ import { BufferSessionPersistence, type BufferStorage } from "./buffer-persistence.js";
5
+ import { selectEvictedBufferKeys } from "./buffer-eviction.js";
5
6
  import type {
6
7
  BufferEntryState,
7
8
  BufferState,
@@ -175,9 +176,12 @@ export class SmartBuffer {
175
176
  * activity (see BUFFER_SAVE_MAX_DEFER_MULTIPLIER). */
176
177
  private firstPendingAtMs: number | null = null;
177
178
 
179
+ /** Which sessions' persisted files are stale (per-session incremental saves). */
180
+ private readonly persistence = new BufferSessionPersistence();
181
+
178
182
  constructor(
179
183
  private readonly config: PluginConfig,
180
- private readonly storage: StorageManager,
184
+ private readonly storage: BufferStorage,
181
185
  surpriseProbe: BufferSurpriseProbe | null = null,
182
186
  ) {
183
187
  this.state = { turns: [], lastExtractionAt: null, extractionCount: 0 };
@@ -261,60 +265,20 @@ export class SmartBuffer {
261
265
  );
262
266
  }
263
267
 
268
+ /** Evict oldest entries past MAX_BUFFER_ENTRY_COUNT and queue their session files for deletion. */
264
269
  private pruneEntries(retainKeys: string[]): void {
265
270
  const entries = this.state.entries;
266
271
  if (!entries) return;
267
- const keys = Object.keys(entries);
268
- if (keys.length <= MAX_BUFFER_ENTRY_COUNT) return;
269
-
270
- const insertionOrder = new Map(keys.map((key, index) => [key, index]));
271
- const byOldestActivity = (left: string, right: string): number => {
272
- const leftAt = this.entryActivityAt(entries[left] ?? {
273
- turns: [],
274
- lastExtractionAt: null,
275
- extractionCount: 0,
276
- });
277
- const rightAt = this.entryActivityAt(entries[right] ?? {
278
- turns: [],
279
- lastExtractionAt: null,
280
- extractionCount: 0,
281
- });
282
- if (leftAt !== rightAt) return leftAt - rightAt;
283
- return (insertionOrder.get(left) ?? 0) - (insertionOrder.get(right) ?? 0);
284
- };
285
- const candidates = keys.filter(
286
- (key) =>
287
- key !== "default" &&
288
- !retainKeys.includes(key) &&
289
- // #3251: accepted captures exit via refusal/backpressure, never eviction.
290
- !this.entryHoldsAcceptedCapture(entries[key]),
291
- );
292
- const emptyRemovable = candidates
293
- .filter((key) => (entries[key]?.turns.length ?? 0) === 0)
294
- .sort(byOldestActivity);
295
-
296
- const removableCount = Math.max(0, keys.length - MAX_BUFFER_ENTRY_COUNT);
297
- let evicted = emptyRemovable.slice(0, removableCount);
298
- if (evicted.length < removableCount) {
299
- // Retained failed extractions (#1908 retry retention) keep their turns in
300
- // the buffer, so empty entries alone may not bring the map back under the
301
- // cap during a provider outage. The cap is the documented bound — evict
302
- // the oldest NON-empty sessions too rather than growing unboundedly
303
- // (codex review).
304
- const nonEmptyRemovable = candidates
305
- .filter((key) => (entries[key]?.turns.length ?? 0) > 0)
306
- .sort(byOldestActivity);
307
- evicted = evicted.concat(nonEmptyRemovable.slice(0, removableCount - evicted.length));
308
- }
309
- for (const key of evicted) {
310
- delete entries[key];
311
- }
272
+ const evicted = selectEvictedBufferKeys(entries, retainKeys, MAX_BUFFER_ENTRY_COUNT, {
273
+ activityAt: (entry) => this.entryActivityAt(entry),
274
+ holdsAcceptedCapture: (entry) => this.entryHoldsAcceptedCapture(entry),
275
+ });
276
+ for (const key of evicted) delete entries[key];
277
+ this.persistence.markEvicted(evicted);
312
278
  if (evicted.length > 0) {
313
- // Loud degradation (extraction hot-loop hardening, Step 7): during a
314
- // prolonged provider outage the breaker holds extraction off and buffered
315
- // sessions accumulate. Eviction of the oldest empty session entries past
316
- // MAX_BUFFER_ENTRY_COUNT is bounded and expected, but must be observable —
317
- // not silent — so operators can see buffer pressure.
279
+ // Loud degradation (extraction hot-loop hardening, Step 7): buffer
280
+ // pressure during a provider outage is bounded and expected, but must
281
+ // be observable, not silent.
318
282
  log.warn(
319
283
  `buffer: pruned ${evicted.length} oldest session entr${evicted.length === 1 ? "y" : "ies"} ` +
320
284
  `past MAX_BUFFER_ENTRY_COUNT=${MAX_BUFFER_ENTRY_COUNT}; buffered turn data for those sessions was dropped`,
@@ -376,12 +340,15 @@ export class SmartBuffer {
376
340
  * so the buffer can still accept new turns for the rest of the session.
377
341
  */
378
342
  resetToEmpty(): void {
343
+ for (const key of Object.keys(this.state.entries ?? {})) {
344
+ this.persistence.markDeleted(key);
345
+ }
379
346
  this.state = { turns: [], lastExtractionAt: null, extractionCount: 0 };
380
347
  this.loaded = true;
381
348
  }
382
349
 
383
350
  private async saveUnlocked(): Promise<void> {
384
- await this.storage.saveBuffer(this.state);
351
+ await this.persistence.persist(this.storage, this.state, (key) => this.peekEntry(key) ?? undefined);
385
352
  }
386
353
 
387
354
  async save(): Promise<void> {
@@ -495,13 +462,16 @@ export class SmartBuffer {
495
462
  clearTimeout(this.saveTimer);
496
463
  this.saveTimer = null;
497
464
  }
498
- if (!this.pendingSave) return;
465
+ // resetToEmpty-only deletions are pending work too, even with no
466
+ // pending-save flag set.
467
+ if (!this.pendingSave && !this.persistence.hasPending()) return;
499
468
  try {
500
469
  await this.enqueueMutation(async () => {
501
470
  // Re-check under the serializer: a concurrent flush may have already
502
471
  // persisted this pending save. Skip the redundant write (no phantom
503
- // writes).
504
- if (!this.pendingSave) return;
472
+ // writes). resetToEmpty-only deletions also land here without a
473
+ // pending-save flag.
474
+ if (!this.pendingSave && !this.persistence.hasPending()) return;
505
475
  await this.saveUnlocked();
506
476
  // Clear the pending flag INSIDE the same serialized mutation as the
507
477
  // write (issue #1909 review). Clearing it after the enqueue await
@@ -579,6 +549,7 @@ export class SmartBuffer {
579
549
  for (const key of emptyRemovable) {
580
550
  if (Object.keys(entries).length < MAX_BUFFER_ENTRY_COUNT) break;
581
551
  delete entries[key];
552
+ this.persistence.markDeleted(key);
582
553
  }
583
554
  }
584
555
 
@@ -714,6 +685,7 @@ export class SmartBuffer {
714
685
  const decision = this.evaluate(entry, signal.level);
715
686
  const turnCountInWindow = entry.turns.length;
716
687
 
688
+ this.persistence.markDirty(bufferKey);
717
689
  this.pruneEntries([bufferKey]);
718
690
  if (decision === "keep_buffering" && this.config.bufferSaveDebounceMs > 0) {
719
691
  // Steady-state buffering: coalesce the whole-state serialize onto a
@@ -792,11 +764,7 @@ export class SmartBuffer {
792
764
  private async emitSurpriseEventSafe(
793
765
  params: SurpriseTelemetryQueueEntry,
794
766
  ): Promise<void> {
795
- const storage = this.storage as StorageManager & {
796
- appendBufferSurpriseEvents?: (
797
- events: BufferSurpriseEvent[],
798
- ) => Promise<number>;
799
- };
767
+ const storage = this.storage;
800
768
  if (typeof storage.appendBufferSurpriseEvents !== "function") {
801
769
  // Older StorageManager / test double without the telemetry sink.
802
770
  // Silently skip — core path is still covered by the log line above.
@@ -955,6 +923,7 @@ export class SmartBuffer {
955
923
  );
956
924
  if (retained.length > 0) entry.retainedTurns = retained;
957
925
  else delete entry.retainedTurns;
926
+ this.persistence.markDirty(bufferKey);
958
927
  await this.saveUnlocked();
959
928
  });
960
929
  }
@@ -1062,6 +1031,7 @@ export class SmartBuffer {
1062
1031
  if (remainingTurns.length > 0) entry.retainedTurns = remainingTurns;
1063
1032
  else delete entry.retainedTurns;
1064
1033
  if (bufferKey === "default") this.state.turns = entry.turns;
1034
+ this.persistence.markDirty(bufferKey);
1065
1035
  }
1066
1036
  if (changed || hadPendingSave) {
1067
1037
  assertLifecycle();
@@ -1172,6 +1142,7 @@ export class SmartBuffer {
1172
1142
  this.state.lastExtractionAt = entry.lastExtractionAt;
1173
1143
  this.state.extractionCount = entry.extractionCount;
1174
1144
  }
1145
+ this.persistence.markDirty(bufferKey);
1175
1146
  this.pruneEntries([bufferKey]);
1176
1147
  await this.saveNowRetainingPendingOnFailure("clearAfterExtraction");
1177
1148
  });
@@ -1060,6 +1060,22 @@ test("parseConfig localLlmTimeoutMs clamps invalid values to a positive timeout"
1060
1060
  assert.equal(parseConfig({ localLlmTimeoutMs: Number.NaN }).localLlmTimeoutMs, 180_000);
1061
1061
  });
1062
1062
 
1063
+ test("parseConfig extractionJudgeTimeoutMs defaults to 20000 — compact verdict measures 2.1-2.5 s on a small fast judge model", () => {
1064
+ assert.equal(parseConfig({}).extractionJudgeTimeoutMs, 20_000);
1065
+ });
1066
+
1067
+ test("parseConfig extractionJudgeTimeoutMs accepts numbers and CLI-style numeric strings", () => {
1068
+ assert.equal(parseConfig({ extractionJudgeTimeoutMs: 45_000 }).extractionJudgeTimeoutMs, 45_000);
1069
+ assert.equal(parseConfig({ extractionJudgeTimeoutMs: "90000" }).extractionJudgeTimeoutMs, 90_000);
1070
+ });
1071
+
1072
+ test("parseConfig extractionJudgeTimeoutMs falls back to the default on unusable values and caps the top", () => {
1073
+ for (const value of [0, -1, Number.NaN, "abc"]) {
1074
+ assert.equal(parseConfig({ extractionJudgeTimeoutMs: value }).extractionJudgeTimeoutMs, 20_000);
1075
+ }
1076
+ assert.equal(parseConfig({ extractionJudgeTimeoutMs: 10_000_000 }).extractionJudgeTimeoutMs, 3_600_000);
1077
+ });
1078
+
1063
1079
  test("parseConfig validates localLlmMaxContext as a usable context window", () => {
1064
1080
  assert.equal(parseConfig({ localLlmMaxContext: 4096 }).localLlmMaxContext, 4096);
1065
1081
  assert.equal(parseConfig({ localLlmMaxContext: "8192" }).localLlmMaxContext, 8192);
package/src/config.ts CHANGED
@@ -2731,6 +2731,20 @@ export function parseConfig(
2731
2731
  typeof cfg.extractionJudgeBatchSize === "number" && Number.isFinite(cfg.extractionJudgeBatchSize)
2732
2732
  ? Math.max(1, Math.round(cfg.extractionJudgeBatchSize))
2733
2733
  : 20,
2734
+ // Judge LLM timeout (issue #376 follow-up, Studio 2026-10-29; tightened in
2735
+ // PR 3264): the judge requests a compact verdict with a batch-scaled
2736
+ // completion budget (24 tokens per candidate, up to 2048). Measured
2737
+ // 2.1-2.5 s per call on a small fast judge
2738
+ // model; the big shared core model measured 13-22 s per compact call,
2739
+ // 41 s verbose. The 20s default assumes the fast model, stays fail-open
2740
+ // on timeout, and needs a raise if extractionJudgeModel points at the
2741
+ // slow core model. Non-positive / unusable values fall back to the
2742
+ // default rather than a 1ms timeout that would silently disable judging.
2743
+ extractionJudgeTimeoutMs: ((): number => {
2744
+ const coerced = coerceNumber(cfg.extractionJudgeTimeoutMs);
2745
+ if (coerced === undefined || coerced < 1) return 20_000;
2746
+ return Math.min(3_600_000, Math.floor(coerced));
2747
+ })(),
2734
2748
  extractionJudgeShadow: cfg.extractionJudgeShadow === true,
2735
2749
  // Defer cap (issue #562 PR 2): max re-deferrals for the same candidate
2736
2750
  // text before the verdict is forcibly converted to reject.
@@ -2,8 +2,11 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
 
4
4
  import { parseConfig } from "./config.js";
5
- import { judgeFactDurability } from "./extraction-judge.js";
6
- import type { FallbackLlmClient } from "./fallback-llm.js";
5
+ import { judgeFactDurability, JUDGE_SYSTEM_PROMPT, judgeMaxTokens, MAX_JUDGE_BATCH, MAX_JUDGE_TOKENS } from "./extraction-judge.js";
6
+ import { FallbackLlmClient } from "./fallback-llm.js";
7
+ import type { FallbackLlmOptions, FallbackLlmResponse } from "./fallback-llm.js";
8
+ import { LocalLlmClient } from "./local-llm.js";
9
+ import type { LocalLlmChatCompletionOptions, LocalLlmChatCompletionResult } from "./local-llm-helpers.js";
7
10
 
8
11
  // Regression for cursor review on #1425: the extraction judge must route gateway
9
12
  // fallback calls through the SAME precedence as ExtractionEngine.withGatewayAgent
@@ -53,3 +56,296 @@ test("judge falls back to gatewayAgentId when no taskModelChain is set", async (
53
56
  assert.equal(captured[0]?.agentId, "persona-agent");
54
57
  assert.equal(captured[0]?.modelChain, undefined);
55
58
  });
59
+
60
+ type ChatOptions = LocalLlmChatCompletionOptions;
61
+
62
+ /** A real FallbackLlmClient whose gateway call is replaced: records the options the judge passes. */
63
+ class RecordingFallbackLlm extends FallbackLlmClient {
64
+ constructor(private readonly sink: FallbackLlmOptions[]) {
65
+ super();
66
+ }
67
+
68
+ override isAvailable(): boolean {
69
+ return true;
70
+ }
71
+
72
+ override async chatCompletion(
73
+ _messages: Array<{ role: "system" | "user" | "assistant"; content: string }>,
74
+ options: FallbackLlmOptions = {},
75
+ ): Promise<FallbackLlmResponse | null> {
76
+ this.sink.push(options);
77
+ return { content: JSON.stringify([{ index: 0, durable: true, reason: "ok" }]), modelUsed: "recording-fake" };
78
+ }
79
+ }
80
+
81
+ /** A real LocalLlmClient whose network call is replaced: records the options the judge passes. */
82
+ class RecordingLocalLlm extends LocalLlmClient {
83
+ constructor(
84
+ private readonly sink: ChatOptions[],
85
+ private readonly reply: string,
86
+ ) {
87
+ super(parseConfig({}));
88
+ }
89
+
90
+ override async chatCompletion(
91
+ _messages: Array<{ role: string; content: string }>,
92
+ options: ChatOptions = {},
93
+ ): Promise<LocalLlmChatCompletionResult | null> {
94
+ this.sink.push(options);
95
+ return { content: this.reply };
96
+ }
97
+ }
98
+
99
+ interface ExampleVerdict {
100
+ index?: number;
101
+ kind?: string;
102
+ reason?: string;
103
+ durable?: boolean;
104
+ }
105
+
106
+ // Regression (Studio 2026-10-29): the judge hardcoded timeoutMs: 1500 while the
107
+ // deployed local model needs tens of seconds — 94 straight aborts, 0 completions.
108
+ // The timeout must come from config so operators can set one the model can meet.
109
+
110
+ test("judge forwards the configured timeout to the local LLM call", async () => {
111
+ const config = parseConfig({ extractionJudgeTimeoutMs: 45_000 });
112
+ const captured: ChatOptions[] = [];
113
+ const localLlm = new RecordingLocalLlm(captured, JSON.stringify([{ index: 0, durable: true, reason: "ok" }]));
114
+
115
+ await judgeFactDurability([candidate], config, localLlm, null, new Map(), new Map());
116
+
117
+ assert.equal(captured.length, 1, "judge should call the local LLM once");
118
+ assert.equal(captured[0]?.timeoutMs, 45_000);
119
+ });
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Compact verdict request (PR 3264 follow-up): the judge asks for a minimal
123
+ // `[{"index":N,"kind":"accept|reject|defer"}]` reply with a batch-scaled
124
+ // completion budget (24 tokens per candidate, up to 2048) instead of a flat 2048. The big budget let
125
+ // a verbose 59-305s reply hide inside the old 150s timeout; the compact one
126
+ // measures 2.1-2.5 s per call on a small fast judge model (13-22 s, 41 s
127
+ // verbose on the big shared core model), inside the 20s default.
128
+ // ---------------------------------------------------------------------------
129
+
130
+ function captureLocal(captured: ChatOptions[], content?: string): LocalLlmClient {
131
+ return new RecordingLocalLlm(captured, content ?? JSON.stringify([{ index: 0, kind: "reject" }]));
132
+ }
133
+
134
+ const EIGHT_CANDIDATES = Array.from({ length: 8 }, (_, i) => ({
135
+ text: `sample candidate fact ${i}`,
136
+ category: "preference",
137
+ confidence: 0.5,
138
+ }));
139
+
140
+ test("judge uses a compact completion budget scaled to the batch size", async () => {
141
+ const config = parseConfig({});
142
+ const captured: ChatOptions[] = [];
143
+
144
+ await judgeFactDurability(EIGHT_CANDIDATES, config, captureLocal(captured), null, new Map(), new Map());
145
+
146
+ assert.equal(captured.length, 1, "judge should call the local LLM once");
147
+ assert.equal(captured[0]?.maxTokens, judgeMaxTokens(8));
148
+ assert.ok(
149
+ (captured[0]?.maxTokens ?? Number.POSITIVE_INFINITY) <= judgeMaxTokens(8),
150
+ `completion budget must stay small, got ${captured[0]?.maxTokens}`,
151
+ );
152
+ });
153
+
154
+ test("judgeMaxTokens leaves room for one verdict per candidate, up to a cap", () => {
155
+ assert.equal(judgeMaxTokens(1), 64);
156
+ assert.equal(judgeMaxTokens(8), 192);
157
+ assert.ok(judgeMaxTokens(50) >= 50 * 16, "a 50-candidate batch needs a verdict array that fits");
158
+ assert.equal(judgeMaxTokens(500), 2048, "cap");
159
+ });
160
+
161
+ test("judge bounds the whole local call, retries included, with one deadline signal", async () => {
162
+ const config = parseConfig({ extractionJudgeTimeoutMs: 45_000 });
163
+ const captured: ChatOptions[] = [];
164
+
165
+ await judgeFactDurability([candidate], config, captureLocal(captured), null, new Map(), new Map());
166
+
167
+ assert.ok(captured[0]?.signal instanceof AbortSignal, "judge passes an AbortSignal so retries share one deadline");
168
+ assert.equal(captured[0]?.signal?.aborted, false);
169
+ });
170
+
171
+
172
+ test("system prompt requests the compact verdict shape only", () => {
173
+ const lines = JUDGE_SYSTEM_PROMPT.split("\n");
174
+ const exampleIdx = lines.findIndex((l) => l.trim() === "Example output:");
175
+ assert.ok(exampleIdx >= 0, "prompt must contain an 'Example output:' line");
176
+ // SAFETY: the line is the example JSON checked into the prompt text this test pins.
177
+ const example = JSON.parse(lines[exampleIdx + 1].trim()) as { verdicts?: ExampleVerdict[] };
178
+ assert.ok(
179
+ Array.isArray(example.verdicts) && example.verdicts.length > 0,
180
+ 'example must use the {"verdicts":[...]} wrapper — response_format json_object rejects a bare top-level array',
181
+ );
182
+ for (const item of example.verdicts) {
183
+ assert.ok(Number.isInteger(item.index), "example index must be an integer");
184
+ assert.ok(
185
+ item.kind === "accept" || item.kind === "reject" || item.kind === "defer",
186
+ `example kind must be a parser-accepted verdict, got ${item.kind}`,
187
+ );
188
+ assert.equal(item.reason, undefined, "compact request must not ask for reasons");
189
+ assert.equal(item.durable, undefined, "compact request must not ask for the durable flag");
190
+ }
191
+ });
192
+
193
+ test("judge still accepts the legacy verbose verdict shape", async () => {
194
+ const config = parseConfig({});
195
+ const captured: ChatOptions[] = [];
196
+ const llm = captureLocal(
197
+ captured,
198
+ JSON.stringify([
199
+ { index: 0, kind: "accept", durable: true, reason: "Stable preference" },
200
+ { index: 1, durable: false, reason: "Ephemeral build status" },
201
+ { index: 2, kind: "defer", durable: false, reason: "Ambiguous pronoun" },
202
+ ]),
203
+ );
204
+ const candidates = [
205
+ { text: "user prefers dark mode", category: "preference", confidence: 0.5 },
206
+ { text: "the build is running now", category: "task", confidence: 0.5 },
207
+ { text: "he said they'd follow up", category: "task", confidence: 0.5 },
208
+ ];
209
+
210
+ const result = await judgeFactDurability(candidates, config, llm, null, new Map(), new Map());
211
+
212
+ assert.deepEqual(
213
+ { ...result.verdicts.get(0) },
214
+ { durable: true, reason: "Stable preference", kind: "accept" },
215
+ );
216
+ assert.deepEqual(
217
+ { ...result.verdicts.get(1) },
218
+ { durable: false, reason: "Ephemeral build status" },
219
+ );
220
+ assert.deepEqual(
221
+ { ...result.verdicts.get(2) },
222
+ { durable: false, reason: "Ambiguous pronoun", kind: "defer" },
223
+ );
224
+ });
225
+
226
+ test("judge parses the {\"verdicts\":[...]} wrapper object the prompt requests", async () => {
227
+ const config = parseConfig({});
228
+ const captured: ChatOptions[] = [];
229
+ const llm = captureLocal(
230
+ captured,
231
+ JSON.stringify({
232
+ verdicts: [
233
+ { index: 0, kind: "accept" },
234
+ { index: 1, kind: "reject" },
235
+ ],
236
+ }),
237
+ );
238
+ const candidates = [
239
+ { text: "user prefers dark mode", category: "preference", confidence: 0.5 },
240
+ { text: "the build is running now", category: "task", confidence: 0.5 },
241
+ ];
242
+
243
+ const result = await judgeFactDurability(candidates, config, llm, null, new Map(), new Map());
244
+
245
+ assert.equal(result.judged, 2, "both verdicts must resolve from the wrapper");
246
+ assert.equal(result.verdicts.get(0)?.kind, "accept");
247
+ assert.equal(result.verdicts.get(1)?.kind, "reject");
248
+ });
249
+
250
+ test("judge still parses the compact bare-array shape", async () => {
251
+ const config = parseConfig({});
252
+ const captured: ChatOptions[] = [];
253
+ const llm = captureLocal(
254
+ captured,
255
+ JSON.stringify([
256
+ { index: 0, kind: "reject" },
257
+ { index: 1, kind: "accept" },
258
+ ]),
259
+ );
260
+ const candidates = [
261
+ { text: "the build is running now", category: "task", confidence: 0.5 },
262
+ { text: "user prefers dark mode", category: "preference", confidence: 0.5 },
263
+ ];
264
+
265
+ const result = await judgeFactDurability(candidates, config, llm, null, new Map(), new Map());
266
+
267
+ assert.equal(result.judged, 2, "both verdicts must resolve from the bare array");
268
+ assert.equal(result.verdicts.get(0)?.kind, "reject");
269
+ assert.equal(result.verdicts.get(1)?.kind, "accept");
270
+ });
271
+
272
+ test("judge treats a single verdict object as one verdict at its index, not the whole batch", async () => {
273
+ const config = parseConfig({});
274
+ const captured: ChatOptions[] = [];
275
+ const llm = captureLocal(captured, JSON.stringify({ index: 1, kind: "reject" }));
276
+ const candidates = [
277
+ { text: "user prefers dark mode", category: "preference", confidence: 0.5 },
278
+ { text: "the build is running now", category: "task", confidence: 0.5 },
279
+ { text: "he said they'd follow up", category: "task", confidence: 0.5 },
280
+ ];
281
+
282
+ const result = await judgeFactDurability(candidates, config, llm, null, new Map(), new Map());
283
+
284
+ assert.equal(result.judged, 1, "only the indexed candidate comes from the LLM");
285
+ assert.equal(result.verdicts.get(1)?.kind, "reject");
286
+ assert.equal(result.verdicts.get(1)?.durable, false);
287
+ assert.equal(result.verdicts.get(0)?.kind, undefined, "index 0 must not inherit the reject");
288
+ assert.equal(result.verdicts.get(0)?.durable, true, "index 0 fail-open approves");
289
+ assert.equal(result.verdicts.get(2)?.kind, undefined, "index 2 must not inherit the reject");
290
+ assert.equal(result.verdicts.get(2)?.durable, true, "index 2 fail-open approves");
291
+ });
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // Judge model override: extractionJudgeModel must reach the LLM call options
295
+ // (options.model) on both the local and fallback paths, and stay absent when
296
+ // unset so routing keeps the default model.
297
+ // ---------------------------------------------------------------------------
298
+
299
+ test("judge forwards extractionJudgeModel to the local LLM options", async () => {
300
+ const config = parseConfig({ extractionJudgeModel: "qwen3.8-27b-64k-nothink" });
301
+ const captured: ChatOptions[] = [];
302
+
303
+ await judgeFactDurability([candidate], config, captureLocal(captured), null, new Map(), new Map());
304
+
305
+ assert.equal(captured[0]?.model, "qwen3.8-27b-64k-nothink");
306
+ });
307
+
308
+ test("judge omits model from local LLM options when extractionJudgeModel is unset", async () => {
309
+ const config = parseConfig({});
310
+ const captured: ChatOptions[] = [];
311
+
312
+ await judgeFactDurability([candidate], config, captureLocal(captured), null, new Map(), new Map());
313
+
314
+ assert.equal(captured[0]?.model, undefined);
315
+ });
316
+
317
+ test("judge forwards extractionJudgeModel to the fallback LLM options in gateway mode", async () => {
318
+ const config = parseConfig({ modelSource: "gateway", extractionJudgeModel: "fast-judge" });
319
+ const captured: FallbackLlmOptions[] = [];
320
+
321
+ await judgeFactDurability([candidate], config, null, new RecordingFallbackLlm(captured), new Map(), new Map());
322
+
323
+ assert.equal(captured[0]?.model, "fast-judge");
324
+ });
325
+
326
+ test("judge defaults to a 20s timeout when none is configured", async () => {
327
+ const config = parseConfig({});
328
+ const captured: FallbackLlmOptions[] = [];
329
+
330
+ await judgeFactDurability([candidate], config, null, new RecordingFallbackLlm(captured), new Map(), new Map());
331
+
332
+ assert.equal(captured[0]?.timeoutMs, 20_000);
333
+ });
334
+
335
+ test("a configured batch size above what the token cap can answer is split", async () => {
336
+ const config = parseConfig({ extractionJudgeBatchSize: 200 });
337
+ const captured: ChatOptions[] = [];
338
+ const candidates = Array.from({ length: 200 }, (_, i) => ({
339
+ text: `distinct candidate fact number ${i}`,
340
+ category: "preference",
341
+ confidence: 0.5,
342
+ }));
343
+
344
+ await judgeFactDurability(candidates, config, captureLocal(captured), null, new Map(), new Map());
345
+
346
+ assert.equal(captured.length, 3, "200 candidates at 85 per request");
347
+ for (const call of captured) {
348
+ assert.ok((call.maxTokens ?? 0) <= MAX_JUDGE_TOKENS, "never above the cap");
349
+ }
350
+ assert.equal(MAX_JUDGE_BATCH * 24 <= MAX_JUDGE_TOKENS, true, "a full batch fits its own budget");
351
+ });