@remnic/core 10.0.37 → 10.0.39

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 (440) hide show
  1. package/dist/access-admin-ops-surface.js +76 -76
  2. package/dist/access-audit.js +2 -2
  3. package/dist/access-authorization-probe.js +77 -77
  4. package/dist/access-boundary.js +76 -76
  5. package/dist/access-citation-namespace.js +2 -2
  6. package/dist/access-cli.js +165 -164
  7. package/dist/access-cli.js.map +1 -1
  8. package/dist/access-extraction-force-flush.js +76 -76
  9. package/dist/access-http-briefing.js +77 -77
  10. package/dist/access-http-query.js +78 -78
  11. package/dist/access-http.js +101 -101
  12. package/dist/access-identity-continuity-surface.js +76 -76
  13. package/dist/access-lcm-surface.js +76 -76
  14. package/dist/access-mcp.js +90 -90
  15. package/dist/access-namespace-preflight.js +76 -76
  16. package/dist/access-observe-write-category.js +15 -15
  17. package/dist/access-observe-write-surface.js +76 -76
  18. package/dist/access-offline-manifest.js +12 -12
  19. package/dist/access-operations-batch.js +80 -80
  20. package/dist/access-operations.js +87 -87
  21. package/dist/access-recall-concurrency.js +76 -76
  22. package/dist/access-recall-diagnostics.js +2 -2
  23. package/dist/access-recall-response.js +76 -76
  24. package/dist/access-recall-surface.js +76 -76
  25. package/dist/access-schema.js +10 -10
  26. package/dist/access-service.js +76 -76
  27. package/dist/access-surface-catalog.js +78 -78
  28. package/dist/active-recall.js +31 -31
  29. package/dist/adapters/index.js +4 -4
  30. package/dist/adapters/registry.js +2 -2
  31. package/dist/{auto-sync-R3XGJL5I.js → auto-sync-BNCBMU6H.js} +9 -9
  32. package/dist/briefing.js +21 -21
  33. package/dist/buffer-types.d.ts +59 -0
  34. package/dist/buffer-types.d.ts.map +1 -0
  35. package/dist/buffer-types.js +1 -0
  36. package/dist/buffer.d.ts +14 -22
  37. package/dist/buffer.d.ts.map +1 -1
  38. package/dist/buffer.js +11 -11
  39. package/dist/calibration.js +3 -3
  40. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-GWVG7LGC.js} +2 -2
  41. package/dist/causal-behavior.js +2 -2
  42. package/dist/causal-chain.js +2 -2
  43. package/dist/causal-consolidation.js +24 -24
  44. package/dist/causal-retrieval.js +2 -2
  45. package/dist/causal-trajectory-graph.js +2 -2
  46. package/dist/causal-trajectory.js +1 -1
  47. package/dist/{chunk-MH3LC7KB.js → chunk-22YJQTPN.js} +2 -2
  48. package/dist/{chunk-MKE5DP3S.js → chunk-23ZMQONL.js} +2 -2
  49. package/dist/{chunk-MGXFJOUR.js → chunk-2AV2U2SZ.js} +4 -4
  50. package/dist/{chunk-HJ3UTGIW.js → chunk-2JNBPOZ2.js} +3 -3
  51. package/dist/{chunk-EEOYF3IB.js → chunk-3RJDYZJE.js} +2 -2
  52. package/dist/{chunk-FBAHBFT5.js → chunk-4HJ2QK32.js} +2 -2
  53. package/dist/{chunk-UXXVOA4D.js → chunk-4LLV2N3R.js} +3 -3
  54. package/dist/{chunk-P5CUEB2Q.js → chunk-4QLKVZH6.js} +8 -8
  55. package/dist/{chunk-2RJO7VS4.js → chunk-4UT2NUAC.js} +2 -2
  56. package/dist/{chunk-R6TCM47S.js → chunk-5KZNWERU.js} +156 -143
  57. package/dist/chunk-5KZNWERU.js.map +1 -0
  58. package/dist/{chunk-MNDHKZZQ.js → chunk-5Q6CR65T.js} +2 -2
  59. package/dist/{chunk-XTBEXZFN.js → chunk-5SF7JOJB.js} +4 -4
  60. package/dist/{chunk-BQ6DGL6A.js → chunk-64WHWIPL.js} +2 -2
  61. package/dist/{chunk-TVIGN3XZ.js → chunk-6FRFVIKC.js} +10 -10
  62. package/dist/{chunk-A23DPFFI.js → chunk-6ITNEPJO.js} +2 -2
  63. package/dist/{chunk-UFBIOF7C.js → chunk-6X3J4NLZ.js} +10 -5
  64. package/dist/chunk-6X3J4NLZ.js.map +1 -0
  65. package/dist/{chunk-G6PPA4TH.js → chunk-6Z2ZWBOV.js} +6 -6
  66. package/dist/{chunk-QEGE5QCL.js → chunk-7IANFJ24.js} +58 -58
  67. package/dist/{chunk-PSQ2P2T3.js → chunk-7XZ7TOLQ.js} +3 -3
  68. package/dist/{chunk-Z5AQZRHF.js → chunk-AER3HD3B.js} +2 -2
  69. package/dist/{chunk-BPBUUGAO.js → chunk-ANV373S5.js} +2 -2
  70. package/dist/{chunk-GMMZ45C4.js → chunk-AVVCPA7U.js} +6 -8
  71. package/dist/chunk-AVVCPA7U.js.map +1 -0
  72. package/dist/{chunk-LSSCMZQ2.js → chunk-B3I2QQU7.js} +3 -3
  73. package/dist/{chunk-NSQSOLCE.js → chunk-B7VKRCAG.js} +2 -2
  74. package/dist/{chunk-HXFIILGF.js → chunk-BFPF5N3H.js} +4 -4
  75. package/dist/{chunk-KQAFEZQX.js → chunk-BUKK5SWA.js} +2 -2
  76. package/dist/{chunk-RBC4GIZU.js → chunk-BUKZ7MTN.js} +2 -2
  77. package/dist/{chunk-FO5AFCRI.js → chunk-CR72WZ3M.js} +9 -9
  78. package/dist/{chunk-YC2C67F7.js → chunk-CRXGQRNA.js} +3 -3
  79. package/dist/{chunk-WRDFYSUC.js → chunk-CZVNOTLR.js} +2 -2
  80. package/dist/{chunk-VPO53QMG.js → chunk-D6DOLQYK.js} +2 -2
  81. package/dist/{chunk-HRT2SJPC.js → chunk-E3NBMTTX.js} +2 -2
  82. package/dist/{chunk-HIMXDRSQ.js → chunk-E5IEI2MR.js} +2 -2
  83. package/dist/{chunk-Z2T5BPOZ.js → chunk-EIYJK7OD.js} +2 -2
  84. package/dist/{chunk-FRAL3EU4.js → chunk-EKPRLTAE.js} +2 -2
  85. package/dist/{chunk-EVVCE7S2.js → chunk-EKR6OO4P.js} +2 -2
  86. package/dist/{chunk-TYGLV6ZG.js → chunk-EYGOM6AI.js} +2 -2
  87. package/dist/{chunk-TYGLV6ZG.js.map → chunk-EYGOM6AI.js.map} +1 -1
  88. package/dist/{chunk-6R6CU3MR.js → chunk-F2IU6A2L.js} +57 -57
  89. package/dist/{chunk-FL6SLVAJ.js → chunk-FAKRTS3Y.js} +3 -3
  90. package/dist/{chunk-64IO6ZEE.js → chunk-FC3ZFEXF.js} +2 -2
  91. package/dist/{chunk-YGYT7S3G.js → chunk-FJ264QBP.js} +31 -9
  92. package/dist/chunk-FJ264QBP.js.map +1 -0
  93. package/dist/{chunk-DGBWPSM6.js → chunk-FLDXEJFX.js} +2 -2
  94. package/dist/{chunk-JH4FCVWJ.js → chunk-G6OFEGUR.js} +2 -2
  95. package/dist/{chunk-Z5QWUR3Y.js → chunk-GCW7UT5I.js} +2 -2
  96. package/dist/{chunk-ULUX6Q7R.js → chunk-GE5ZNDLE.js} +4 -4
  97. package/dist/{chunk-U4JXQCJ3.js → chunk-GLB6VV3J.js} +2 -2
  98. package/dist/{chunk-RWWYBE7E.js → chunk-GPVGAHXL.js} +3 -3
  99. package/dist/{chunk-3BPMYMPX.js → chunk-GSBSGT43.js} +2 -2
  100. package/dist/{chunk-645WOU5I.js → chunk-H7BZBSP6.js} +4 -4
  101. package/dist/{chunk-GFYFKXWZ.js → chunk-HUDPXTXW.js} +2 -2
  102. package/dist/{chunk-L2JQSG5M.js → chunk-I7TR2OCU.js} +2 -2
  103. package/dist/{chunk-HKEK27M4.js → chunk-IAOW2L6K.js} +2 -2
  104. package/dist/{chunk-STM4T335.js → chunk-IJH6METJ.js} +2 -2
  105. package/dist/{chunk-WADPRAZE.js → chunk-IZRNMXZC.js} +2 -2
  106. package/dist/{chunk-JWTUJVIF.js → chunk-JGS55BAS.js} +53 -2
  107. package/dist/chunk-JGS55BAS.js.map +1 -0
  108. package/dist/{chunk-LAQS5HYU.js → chunk-JM6U3SAE.js} +2 -2
  109. package/dist/{chunk-74AY3S7U.js → chunk-JNLYHJG7.js} +4 -4
  110. package/dist/{chunk-7ZBANI3M.js → chunk-JOOGPZJE.js} +2 -2
  111. package/dist/{chunk-V5RJCXLG.js → chunk-JORSMFL6.js} +6 -6
  112. package/dist/{chunk-VKTP3TZB.js → chunk-JQCP5QW3.js} +4 -4
  113. package/dist/{chunk-LRJKHXU4.js → chunk-JRTKYFFY.js} +2 -2
  114. package/dist/{chunk-UMFBHYPW.js → chunk-JT3IXJIL.js} +13 -13
  115. package/dist/{chunk-S53WZWGL.js → chunk-JZFJZLJY.js} +2 -2
  116. package/dist/{chunk-6QOL4G76.js → chunk-KQXRVELF.js} +2 -2
  117. package/dist/{chunk-USOXPN5H.js → chunk-L74NWAD4.js} +2 -2
  118. package/dist/{chunk-Y6Y3TRBN.js → chunk-LJXBFVI2.js} +4 -4
  119. package/dist/{chunk-3L5WH6CV.js → chunk-LPD6I2U6.js} +2 -2
  120. package/dist/{chunk-LDXS746V.js → chunk-LR7RW4T6.js} +3 -3
  121. package/dist/{chunk-FZVZYU5C.js → chunk-LXN737FX.js} +2 -2
  122. package/dist/{chunk-5U3NUHMA.js → chunk-MD3W2H3G.js} +301 -98
  123. package/dist/chunk-MD3W2H3G.js.map +1 -0
  124. package/dist/{chunk-ARFT6ZBE.js → chunk-MGYZENUY.js} +2 -2
  125. package/dist/{chunk-YQ6SLFNZ.js → chunk-MPJDQC4G.js} +3 -3
  126. package/dist/{chunk-5UWP5X3T.js → chunk-NJ2DZYEK.js} +2 -2
  127. package/dist/{chunk-MBMT5FUQ.js → chunk-O2O4FDAH.js} +2 -2
  128. package/dist/{chunk-GCVUBVEN.js → chunk-ODJ2HRFW.js} +10 -10
  129. package/dist/{chunk-2NLLXCJG.js → chunk-OWHERGF2.js} +2 -2
  130. package/dist/{chunk-I73ZSORJ.js → chunk-OXSTKCYK.js} +4 -4
  131. package/dist/{chunk-CNQQNYJ2.js → chunk-P3YUEPSQ.js} +2 -2
  132. package/dist/{chunk-A6PKEU7T.js → chunk-PARM6OYL.js} +1 -1
  133. package/dist/chunk-PARM6OYL.js.map +1 -0
  134. package/dist/{chunk-TPKB7T6Z.js → chunk-Q6UZREJK.js} +2 -2
  135. package/dist/{chunk-UJDTXWMT.js → chunk-QIMGULGN.js} +2 -2
  136. package/dist/{chunk-E24RUQEN.js → chunk-QNIYL5SK.js} +2 -2
  137. package/dist/{chunk-KOY25HUB.js → chunk-QYAXMXOB.js} +2 -2
  138. package/dist/{chunk-CU7WDDPV.js → chunk-RIAKNXZB.js} +4 -4
  139. package/dist/{chunk-UQWA5QDT.js → chunk-RO2NNWR5.js} +3 -3
  140. package/dist/{chunk-AYXRK264.js → chunk-RROFIAYI.js} +9 -9
  141. package/dist/{chunk-L46Y5FPE.js → chunk-RUAIO5UA.js} +2 -2
  142. package/dist/{chunk-QIC7YZ3D.js → chunk-RWW5KPIY.js} +18 -18
  143. package/dist/{chunk-E5PVMB3I.js → chunk-SDXBR5QO.js} +2 -2
  144. package/dist/{chunk-WSLNC5AL.js → chunk-SJHFU52W.js} +8 -8
  145. package/dist/{chunk-7PG7QBWD.js → chunk-SRPGA6WN.js} +2 -2
  146. package/dist/{chunk-CUWOSRCS.js → chunk-SUKPM6DV.js} +2 -2
  147. package/dist/{chunk-BBDASWNT.js → chunk-TJ5YGCPV.js} +81 -76
  148. package/dist/chunk-TJ5YGCPV.js.map +1 -0
  149. package/dist/{chunk-G2SRB62M.js → chunk-TTQYX7VU.js} +2 -2
  150. package/dist/{chunk-TDCR6PQL.js → chunk-TWRPWJJT.js} +2 -2
  151. package/dist/{chunk-KC5SVCUJ.js → chunk-U66CTIF5.js} +2 -2
  152. package/dist/{chunk-D2AKUKAQ.js → chunk-UIK3U36O.js} +2 -2
  153. package/dist/{chunk-XLEP7SS3.js → chunk-UMSH4I3E.js} +2 -2
  154. package/dist/{chunk-X7Y7WX73.js → chunk-UVYI6VIX.js} +1 -1
  155. package/dist/{chunk-LHBOFROT.js → chunk-V2XMG7GM.js} +2 -2
  156. package/dist/chunk-VA3GNAWQ.js +58 -0
  157. package/dist/chunk-VA3GNAWQ.js.map +1 -0
  158. package/dist/{chunk-NQOM6PHO.js → chunk-VCOCOBUS.js} +6 -6
  159. package/dist/{chunk-4P4VLPGB.js → chunk-VECRSYQG.js} +1 -1
  160. package/dist/{chunk-ID2PLENW.js → chunk-VHBGJAYC.js} +2 -2
  161. package/dist/{chunk-RVYXSS3Q.js → chunk-VIS23B6O.js} +8 -8
  162. package/dist/{chunk-NMKETOAX.js → chunk-VSHWPOUB.js} +2 -2
  163. package/dist/{chunk-UBN5AOLT.js → chunk-W5EO4USJ.js} +2 -2
  164. package/dist/{chunk-Y7427SL7.js → chunk-WCGJK4NQ.js} +4 -4
  165. package/dist/{chunk-CCYXCT4B.js → chunk-WQYS47G4.js} +2 -2
  166. package/dist/{chunk-JH2ALMPR.js → chunk-WR5POHQ3.js} +2 -2
  167. package/dist/{chunk-DK5VMJL3.js → chunk-WT2VVYBM.js} +2 -2
  168. package/dist/{chunk-EHIKY2IA.js → chunk-WU5WTCH5.js} +2 -2
  169. package/dist/{chunk-QKXPOLQD.js → chunk-WUBGMV3G.js} +6 -6
  170. package/dist/{chunk-O6YJI5VX.js → chunk-WUL6LDJ4.js} +2 -2
  171. package/dist/{chunk-5EILJ6W2.js → chunk-WXPBDEMW.js} +2 -2
  172. package/dist/{chunk-U7HJBODU.js → chunk-XKTHJHSQ.js} +5 -5
  173. package/dist/{chunk-JFI3ATK4.js → chunk-XMYWYJM2.js} +2 -2
  174. package/dist/{chunk-64S6X4IU.js → chunk-XTN54NML.js} +4 -4
  175. package/dist/{chunk-VGFNTY3Y.js → chunk-YFXRLRKP.js} +2 -2
  176. package/dist/{chunk-DAOMWGO2.js → chunk-ZX5N5D6M.js} +4 -4
  177. package/dist/{chunk-PTRPOJ7U.js → chunk-ZXFKG7VZ.js} +2 -2
  178. package/dist/cli.js +124 -124
  179. package/dist/coding/export-okf-codegraph.js +22 -22
  180. package/dist/coding/pre-action-gate.js +2 -2
  181. package/dist/compounding/engine.js +23 -23
  182. package/dist/config.d.ts.map +1 -1
  183. package/dist/config.js +31 -31
  184. package/dist/connectors/codex-materialize-runner.js +21 -21
  185. package/dist/connectors/index.js +21 -21
  186. package/dist/contradiction/index.js +5 -5
  187. package/dist/convergence-refresh.js +23 -23
  188. package/dist/corpus-watermark.js +23 -23
  189. package/dist/deep-recall-http-glue.js +11 -11
  190. package/dist/deep-recall-policy-llm.js +2 -2
  191. package/dist/deep-recall.js +2 -2
  192. package/dist/entity-retrieval.js +22 -22
  193. package/dist/event-order-recall.js +2 -2
  194. package/dist/explicit-capture.js +6 -6
  195. package/dist/external-wiki-access.js +77 -77
  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-window-drain.d.ts +48 -0
  201. package/dist/extraction-window-drain.d.ts.map +1 -0
  202. package/dist/extraction-window-drain.js +14 -0
  203. package/dist/extraction.js +29 -29
  204. package/dist/fallback-llm.d.ts +1 -0
  205. package/dist/fallback-llm.d.ts.map +1 -1
  206. package/dist/fallback-llm.js +1 -1
  207. package/dist/{graph-edge-decay-2BZTY2IM.js → graph-edge-decay-3HVLMT6U.js} +5 -5
  208. package/dist/graph-edge-decay-3HVLMT6U.js.map +1 -0
  209. package/dist/graph-jsonl.js +3 -3
  210. package/dist/graph-snapshot.js +3 -3
  211. package/dist/graph.js +4 -4
  212. package/dist/harmonic-retrieval.js +22 -22
  213. package/dist/harmonic-source-lookup.js +2 -2
  214. package/dist/index.js +212 -211
  215. package/dist/lcm/engine.js +2 -2
  216. package/dist/lcm/index.js +7 -7
  217. package/dist/local-llm.js +2 -2
  218. package/dist/location/index.js +24 -24
  219. package/dist/maintenance/memory-governance.js +21 -21
  220. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +21 -21
  221. package/dist/maintenance/rebuild-memory-projection-runner.js +23 -23
  222. package/dist/maintenance/rebuild-memory-projection.js +22 -22
  223. package/dist/memory-browse-operations.js +77 -77
  224. package/dist/memory-browse.js +6 -6
  225. package/dist/memory-subject.js +3 -3
  226. package/dist/namespaces/migrate.js +22 -22
  227. package/dist/namespaces/search.js +7 -7
  228. package/dist/namespaces/storage.js +21 -21
  229. package/dist/offline-sync-changeset-generations.js +3 -3
  230. package/dist/offline-sync-embedding-generation.js +2 -2
  231. package/dist/offline-sync-file-io.js +2 -2
  232. package/dist/offline-sync-impression-drain.js +3 -3
  233. package/dist/offline-sync-snapshot-stream.js +8 -8
  234. package/dist/offline-sync-upload-staging.js +3 -3
  235. package/dist/offline-sync.js +9 -9
  236. package/dist/operator-doctor-corpus.js +24 -24
  237. package/dist/operator-toolkit.js +42 -42
  238. package/dist/orchestration/extraction-drain.d.ts +90 -0
  239. package/dist/orchestration/extraction-drain.d.ts.map +1 -0
  240. package/dist/orchestration/extraction-run-helpers.d.ts +3 -0
  241. package/dist/orchestration/extraction-run-helpers.d.ts.map +1 -1
  242. package/dist/orchestration/extraction-run.d.ts +4 -1
  243. package/dist/orchestration/extraction-run.d.ts.map +1 -1
  244. package/dist/orchestration/maintenance.js +35 -35
  245. package/dist/orchestration/turn-ingestion.d.ts +3 -0
  246. package/dist/orchestration/turn-ingestion.d.ts.map +1 -1
  247. package/dist/orchestrator.d.ts +8 -0
  248. package/dist/orchestrator.d.ts.map +1 -1
  249. package/dist/orchestrator.js +165 -164
  250. package/dist/qmd.js +5 -5
  251. package/dist/recall-explain-renderer.js +3 -3
  252. package/dist/recall-navigate-access.js +2 -2
  253. package/dist/recall-navigate-disclosure.js +2 -2
  254. package/dist/recall-navigate.js +2 -2
  255. package/dist/recall-navigation-operations.js +77 -77
  256. package/dist/recall-navigation.js +76 -76
  257. package/dist/recall-planner-llm.js +1 -1
  258. package/dist/recall-state-view-admission.js +6 -6
  259. package/dist/recall-why-service.js +6 -6
  260. package/dist/recall-xray-cli.js +4 -4
  261. package/dist/recall-xray-renderer.js +3 -3
  262. package/dist/recall-xray.js +2 -2
  263. package/dist/reconcile/manifest.js +3 -3
  264. package/dist/{resolution-4ZEQPXUZ.js → resolution-NNYG37YB.js} +6 -6
  265. package/dist/resolution-NNYG37YB.js.map +1 -0
  266. package/dist/resume-bundles.js +32 -32
  267. package/dist/retrieval-agents.js +2 -2
  268. package/dist/salvage-envelope.js +5 -5
  269. package/dist/search/factory.js +6 -6
  270. package/dist/search/index.js +10 -10
  271. package/dist/secure-store/index.js +2 -2
  272. package/dist/semantic-consolidation.js +22 -22
  273. package/dist/semantic-rule-promotion.js +21 -21
  274. package/dist/semantic-rule-verifier.js +21 -21
  275. package/dist/source-agent-qualifier.js +25 -25
  276. package/dist/standup/build.js +21 -21
  277. package/dist/storage/dropped-extraction-chunk.d.ts +8 -0
  278. package/dist/storage/dropped-extraction-chunk.d.ts.map +1 -0
  279. package/dist/storage.d.ts +1 -0
  280. package/dist/storage.d.ts.map +1 -1
  281. package/dist/storage.js +20 -20
  282. package/dist/subject-config.js +4 -4
  283. package/dist/summarizer.js +4 -4
  284. package/dist/support-passport/index.js +79 -79
  285. package/dist/transfer/backup.js +2 -2
  286. package/dist/transfer/capsule-export.js +2 -2
  287. package/dist/transfer/capsule-import.js +2 -2
  288. package/dist/transfer/export-okf.js +22 -22
  289. package/dist/types.d.ts +19 -0
  290. package/dist/types.d.ts.map +1 -1
  291. package/dist/types.js +1 -1
  292. package/dist/verified-recall.js +21 -21
  293. package/dist/who-knows-cli.js +22 -22
  294. package/dist/who-knows.js +21 -21
  295. package/dist/write-envelope.js +4 -4
  296. package/package.json +2 -2
  297. package/src/buffer-drop-extraction-chunk.test.ts +196 -0
  298. package/src/buffer-types.ts +65 -0
  299. package/src/buffer.ts +60 -62
  300. package/src/config.test.ts +16 -0
  301. package/src/config.ts +22 -0
  302. package/src/extraction-judge-chain.test.ts +298 -2
  303. package/src/extraction-judge.ts +147 -82
  304. package/src/extraction-window-drain.test.ts +123 -0
  305. package/src/extraction-window-drain.ts +110 -0
  306. package/src/fallback-llm.test.ts +94 -0
  307. package/src/fallback-llm.ts +7 -7
  308. package/src/orchestration/extraction-drain.ts +261 -0
  309. package/src/orchestration/extraction-run-helpers.ts +8 -0
  310. package/src/orchestration/extraction-run.ts +5 -4
  311. package/src/orchestration/turn-ingestion.ts +9 -5
  312. package/src/orchestrator.ts +21 -0
  313. package/src/secure-store/secure-fs.ts +3 -1
  314. package/src/storage/dropped-extraction-chunk.ts +27 -0
  315. package/src/storage-dropped-chunk-filenames.test.ts +30 -0
  316. package/src/storage.ts +6 -7
  317. package/src/types.ts +19 -0
  318. package/dist/chunk-5U3NUHMA.js.map +0 -1
  319. package/dist/chunk-A6PKEU7T.js.map +0 -1
  320. package/dist/chunk-BBDASWNT.js.map +0 -1
  321. package/dist/chunk-GMMZ45C4.js.map +0 -1
  322. package/dist/chunk-JWTUJVIF.js.map +0 -1
  323. package/dist/chunk-R6TCM47S.js.map +0 -1
  324. package/dist/chunk-UFBIOF7C.js.map +0 -1
  325. package/dist/chunk-YGYT7S3G.js.map +0 -1
  326. /package/dist/{auto-sync-R3XGJL5I.js.map → auto-sync-BNCBMU6H.js.map} +0 -0
  327. /package/dist/{capsule-crypto-7FJQINUR.js.map → buffer-types.js.map} +0 -0
  328. /package/dist/{graph-edge-decay-2BZTY2IM.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
  329. /package/dist/{chunk-MH3LC7KB.js.map → chunk-22YJQTPN.js.map} +0 -0
  330. /package/dist/{chunk-MKE5DP3S.js.map → chunk-23ZMQONL.js.map} +0 -0
  331. /package/dist/{chunk-MGXFJOUR.js.map → chunk-2AV2U2SZ.js.map} +0 -0
  332. /package/dist/{chunk-HJ3UTGIW.js.map → chunk-2JNBPOZ2.js.map} +0 -0
  333. /package/dist/{chunk-EEOYF3IB.js.map → chunk-3RJDYZJE.js.map} +0 -0
  334. /package/dist/{chunk-FBAHBFT5.js.map → chunk-4HJ2QK32.js.map} +0 -0
  335. /package/dist/{chunk-UXXVOA4D.js.map → chunk-4LLV2N3R.js.map} +0 -0
  336. /package/dist/{chunk-P5CUEB2Q.js.map → chunk-4QLKVZH6.js.map} +0 -0
  337. /package/dist/{chunk-2RJO7VS4.js.map → chunk-4UT2NUAC.js.map} +0 -0
  338. /package/dist/{chunk-MNDHKZZQ.js.map → chunk-5Q6CR65T.js.map} +0 -0
  339. /package/dist/{chunk-XTBEXZFN.js.map → chunk-5SF7JOJB.js.map} +0 -0
  340. /package/dist/{chunk-BQ6DGL6A.js.map → chunk-64WHWIPL.js.map} +0 -0
  341. /package/dist/{chunk-TVIGN3XZ.js.map → chunk-6FRFVIKC.js.map} +0 -0
  342. /package/dist/{chunk-A23DPFFI.js.map → chunk-6ITNEPJO.js.map} +0 -0
  343. /package/dist/{chunk-G6PPA4TH.js.map → chunk-6Z2ZWBOV.js.map} +0 -0
  344. /package/dist/{chunk-QEGE5QCL.js.map → chunk-7IANFJ24.js.map} +0 -0
  345. /package/dist/{chunk-PSQ2P2T3.js.map → chunk-7XZ7TOLQ.js.map} +0 -0
  346. /package/dist/{chunk-Z5AQZRHF.js.map → chunk-AER3HD3B.js.map} +0 -0
  347. /package/dist/{chunk-BPBUUGAO.js.map → chunk-ANV373S5.js.map} +0 -0
  348. /package/dist/{chunk-LSSCMZQ2.js.map → chunk-B3I2QQU7.js.map} +0 -0
  349. /package/dist/{chunk-NSQSOLCE.js.map → chunk-B7VKRCAG.js.map} +0 -0
  350. /package/dist/{chunk-HXFIILGF.js.map → chunk-BFPF5N3H.js.map} +0 -0
  351. /package/dist/{chunk-KQAFEZQX.js.map → chunk-BUKK5SWA.js.map} +0 -0
  352. /package/dist/{chunk-RBC4GIZU.js.map → chunk-BUKZ7MTN.js.map} +0 -0
  353. /package/dist/{chunk-FO5AFCRI.js.map → chunk-CR72WZ3M.js.map} +0 -0
  354. /package/dist/{chunk-YC2C67F7.js.map → chunk-CRXGQRNA.js.map} +0 -0
  355. /package/dist/{chunk-WRDFYSUC.js.map → chunk-CZVNOTLR.js.map} +0 -0
  356. /package/dist/{chunk-VPO53QMG.js.map → chunk-D6DOLQYK.js.map} +0 -0
  357. /package/dist/{chunk-HRT2SJPC.js.map → chunk-E3NBMTTX.js.map} +0 -0
  358. /package/dist/{chunk-HIMXDRSQ.js.map → chunk-E5IEI2MR.js.map} +0 -0
  359. /package/dist/{chunk-Z2T5BPOZ.js.map → chunk-EIYJK7OD.js.map} +0 -0
  360. /package/dist/{chunk-FRAL3EU4.js.map → chunk-EKPRLTAE.js.map} +0 -0
  361. /package/dist/{chunk-EVVCE7S2.js.map → chunk-EKR6OO4P.js.map} +0 -0
  362. /package/dist/{chunk-6R6CU3MR.js.map → chunk-F2IU6A2L.js.map} +0 -0
  363. /package/dist/{chunk-FL6SLVAJ.js.map → chunk-FAKRTS3Y.js.map} +0 -0
  364. /package/dist/{chunk-64IO6ZEE.js.map → chunk-FC3ZFEXF.js.map} +0 -0
  365. /package/dist/{chunk-DGBWPSM6.js.map → chunk-FLDXEJFX.js.map} +0 -0
  366. /package/dist/{chunk-JH4FCVWJ.js.map → chunk-G6OFEGUR.js.map} +0 -0
  367. /package/dist/{chunk-Z5QWUR3Y.js.map → chunk-GCW7UT5I.js.map} +0 -0
  368. /package/dist/{chunk-ULUX6Q7R.js.map → chunk-GE5ZNDLE.js.map} +0 -0
  369. /package/dist/{chunk-U4JXQCJ3.js.map → chunk-GLB6VV3J.js.map} +0 -0
  370. /package/dist/{chunk-RWWYBE7E.js.map → chunk-GPVGAHXL.js.map} +0 -0
  371. /package/dist/{chunk-3BPMYMPX.js.map → chunk-GSBSGT43.js.map} +0 -0
  372. /package/dist/{chunk-645WOU5I.js.map → chunk-H7BZBSP6.js.map} +0 -0
  373. /package/dist/{chunk-GFYFKXWZ.js.map → chunk-HUDPXTXW.js.map} +0 -0
  374. /package/dist/{chunk-L2JQSG5M.js.map → chunk-I7TR2OCU.js.map} +0 -0
  375. /package/dist/{chunk-HKEK27M4.js.map → chunk-IAOW2L6K.js.map} +0 -0
  376. /package/dist/{chunk-STM4T335.js.map → chunk-IJH6METJ.js.map} +0 -0
  377. /package/dist/{chunk-WADPRAZE.js.map → chunk-IZRNMXZC.js.map} +0 -0
  378. /package/dist/{chunk-LAQS5HYU.js.map → chunk-JM6U3SAE.js.map} +0 -0
  379. /package/dist/{chunk-74AY3S7U.js.map → chunk-JNLYHJG7.js.map} +0 -0
  380. /package/dist/{chunk-7ZBANI3M.js.map → chunk-JOOGPZJE.js.map} +0 -0
  381. /package/dist/{chunk-V5RJCXLG.js.map → chunk-JORSMFL6.js.map} +0 -0
  382. /package/dist/{chunk-VKTP3TZB.js.map → chunk-JQCP5QW3.js.map} +0 -0
  383. /package/dist/{chunk-LRJKHXU4.js.map → chunk-JRTKYFFY.js.map} +0 -0
  384. /package/dist/{chunk-UMFBHYPW.js.map → chunk-JT3IXJIL.js.map} +0 -0
  385. /package/dist/{chunk-S53WZWGL.js.map → chunk-JZFJZLJY.js.map} +0 -0
  386. /package/dist/{chunk-6QOL4G76.js.map → chunk-KQXRVELF.js.map} +0 -0
  387. /package/dist/{chunk-USOXPN5H.js.map → chunk-L74NWAD4.js.map} +0 -0
  388. /package/dist/{chunk-Y6Y3TRBN.js.map → chunk-LJXBFVI2.js.map} +0 -0
  389. /package/dist/{chunk-3L5WH6CV.js.map → chunk-LPD6I2U6.js.map} +0 -0
  390. /package/dist/{chunk-LDXS746V.js.map → chunk-LR7RW4T6.js.map} +0 -0
  391. /package/dist/{chunk-FZVZYU5C.js.map → chunk-LXN737FX.js.map} +0 -0
  392. /package/dist/{chunk-ARFT6ZBE.js.map → chunk-MGYZENUY.js.map} +0 -0
  393. /package/dist/{chunk-YQ6SLFNZ.js.map → chunk-MPJDQC4G.js.map} +0 -0
  394. /package/dist/{chunk-5UWP5X3T.js.map → chunk-NJ2DZYEK.js.map} +0 -0
  395. /package/dist/{chunk-MBMT5FUQ.js.map → chunk-O2O4FDAH.js.map} +0 -0
  396. /package/dist/{chunk-GCVUBVEN.js.map → chunk-ODJ2HRFW.js.map} +0 -0
  397. /package/dist/{chunk-2NLLXCJG.js.map → chunk-OWHERGF2.js.map} +0 -0
  398. /package/dist/{chunk-I73ZSORJ.js.map → chunk-OXSTKCYK.js.map} +0 -0
  399. /package/dist/{chunk-CNQQNYJ2.js.map → chunk-P3YUEPSQ.js.map} +0 -0
  400. /package/dist/{chunk-TPKB7T6Z.js.map → chunk-Q6UZREJK.js.map} +0 -0
  401. /package/dist/{chunk-UJDTXWMT.js.map → chunk-QIMGULGN.js.map} +0 -0
  402. /package/dist/{chunk-E24RUQEN.js.map → chunk-QNIYL5SK.js.map} +0 -0
  403. /package/dist/{chunk-KOY25HUB.js.map → chunk-QYAXMXOB.js.map} +0 -0
  404. /package/dist/{chunk-CU7WDDPV.js.map → chunk-RIAKNXZB.js.map} +0 -0
  405. /package/dist/{chunk-UQWA5QDT.js.map → chunk-RO2NNWR5.js.map} +0 -0
  406. /package/dist/{chunk-AYXRK264.js.map → chunk-RROFIAYI.js.map} +0 -0
  407. /package/dist/{chunk-L46Y5FPE.js.map → chunk-RUAIO5UA.js.map} +0 -0
  408. /package/dist/{chunk-QIC7YZ3D.js.map → chunk-RWW5KPIY.js.map} +0 -0
  409. /package/dist/{chunk-E5PVMB3I.js.map → chunk-SDXBR5QO.js.map} +0 -0
  410. /package/dist/{chunk-WSLNC5AL.js.map → chunk-SJHFU52W.js.map} +0 -0
  411. /package/dist/{chunk-7PG7QBWD.js.map → chunk-SRPGA6WN.js.map} +0 -0
  412. /package/dist/{chunk-CUWOSRCS.js.map → chunk-SUKPM6DV.js.map} +0 -0
  413. /package/dist/{chunk-G2SRB62M.js.map → chunk-TTQYX7VU.js.map} +0 -0
  414. /package/dist/{chunk-TDCR6PQL.js.map → chunk-TWRPWJJT.js.map} +0 -0
  415. /package/dist/{chunk-KC5SVCUJ.js.map → chunk-U66CTIF5.js.map} +0 -0
  416. /package/dist/{chunk-D2AKUKAQ.js.map → chunk-UIK3U36O.js.map} +0 -0
  417. /package/dist/{chunk-XLEP7SS3.js.map → chunk-UMSH4I3E.js.map} +0 -0
  418. /package/dist/{chunk-X7Y7WX73.js.map → chunk-UVYI6VIX.js.map} +0 -0
  419. /package/dist/{chunk-LHBOFROT.js.map → chunk-V2XMG7GM.js.map} +0 -0
  420. /package/dist/{chunk-NQOM6PHO.js.map → chunk-VCOCOBUS.js.map} +0 -0
  421. /package/dist/{chunk-4P4VLPGB.js.map → chunk-VECRSYQG.js.map} +0 -0
  422. /package/dist/{chunk-ID2PLENW.js.map → chunk-VHBGJAYC.js.map} +0 -0
  423. /package/dist/{chunk-RVYXSS3Q.js.map → chunk-VIS23B6O.js.map} +0 -0
  424. /package/dist/{chunk-NMKETOAX.js.map → chunk-VSHWPOUB.js.map} +0 -0
  425. /package/dist/{chunk-UBN5AOLT.js.map → chunk-W5EO4USJ.js.map} +0 -0
  426. /package/dist/{chunk-Y7427SL7.js.map → chunk-WCGJK4NQ.js.map} +0 -0
  427. /package/dist/{chunk-CCYXCT4B.js.map → chunk-WQYS47G4.js.map} +0 -0
  428. /package/dist/{chunk-JH2ALMPR.js.map → chunk-WR5POHQ3.js.map} +0 -0
  429. /package/dist/{chunk-DK5VMJL3.js.map → chunk-WT2VVYBM.js.map} +0 -0
  430. /package/dist/{chunk-EHIKY2IA.js.map → chunk-WU5WTCH5.js.map} +0 -0
  431. /package/dist/{chunk-QKXPOLQD.js.map → chunk-WUBGMV3G.js.map} +0 -0
  432. /package/dist/{chunk-O6YJI5VX.js.map → chunk-WUL6LDJ4.js.map} +0 -0
  433. /package/dist/{chunk-5EILJ6W2.js.map → chunk-WXPBDEMW.js.map} +0 -0
  434. /package/dist/{chunk-U7HJBODU.js.map → chunk-XKTHJHSQ.js.map} +0 -0
  435. /package/dist/{chunk-JFI3ATK4.js.map → chunk-XMYWYJM2.js.map} +0 -0
  436. /package/dist/{chunk-64S6X4IU.js.map → chunk-XTN54NML.js.map} +0 -0
  437. /package/dist/{chunk-VGFNTY3Y.js.map → chunk-YFXRLRKP.js.map} +0 -0
  438. /package/dist/{chunk-DAOMWGO2.js.map → chunk-ZX5N5D6M.js.map} +0 -0
  439. /package/dist/{chunk-PTRPOJ7U.js.map → chunk-ZXFKG7VZ.js.map} +0 -0
  440. /package/dist/{resolution-4ZEQPXUZ.js.map → extraction-window-drain.js.map} +0 -0
@@ -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
+ });
@@ -10,7 +10,11 @@
10
10
  * - Critical-importance facts are auto-approved.
11
11
  * - Batches respect extractionJudgeBatchSize.
12
12
  * - Content-hash caching avoids redundant LLM calls.
13
- * - Performance budget: <= 1.5s per batch.
13
+ * - LLM calls use extractionJudgeTimeoutMs (default 20s: measured 2.1-2.5 s
14
+ * per compact verdict request with a batch-scaled completion budget on a
15
+ * small fast judge model; 13-22 s per compact call, 41 s verbose, on the
16
+ * big shared core model — raise it there; the old 150s default only
17
+ * papered over 59-305s verbose replies).
14
18
  */
15
19
 
16
20
  import { createHash } from "node:crypto";
@@ -215,48 +219,79 @@ export const JUDGE_SYSTEM_PROMPT = `You are a memory curator evaluating whether
215
219
 
216
220
  A fact is **durable** if it will still be useful 30+ days from now and is relevant across multiple sessions, not just the current task.
217
221
 
218
- Return one of three verdicts per candidate:
222
+ The user message is a JSON array of candidates: [{"index": 0, "text": "...", "category": "...", "confidence": 0.5}, ...].
219
223
 
220
- ACCEPT — the fact is durable, persist it:
221
- - Personal preferences, identities, or relationships
222
- - Decisions with rationale that affect future work
223
- - Corrections to previously held beliefs
224
- - Principles, rules, or constraints the user wants respected
225
- - Stable facts about projects, tools, or workflows
226
- - Commitments, deadlines, or obligations
224
+ Verdicts:
225
+ - "accept" — durable. Personal preferences, identities, relationships; decisions with rationale that affect future work; corrections to previously held beliefs; principles, rules, or constraints the user wants respected; stable facts about projects, tools, or workflows; commitments, deadlines, or obligations.
226
+ - "reject" — not durable. Transient task details ("currently debugging line 42"); ephemeral state ("the build is running now"); routine operations ("ran npm install"); conversational filler; anything stale within hours; step-by-step instructions for a one-time task.
227
+ - "defer" — MIGHT be durable but the candidate alone cannot say: ambiguous referents ("he said they'd follow up"), in-progress statements that may become durable once completed, future commitments with unclear subject or timeline. Defer is NOT a soft reject: use it only when another turn of context would genuinely change the verdict; when torn between accept and reject, lean accept — losing a useful fact is worse than keeping a marginal one.
227
228
 
228
- REJECT — the fact is not durable, drop it:
229
- - Transient task details ("currently debugging line 42")
230
- - Ephemeral state ("the build is running now")
231
- - Routine operations ("ran npm install")
232
- - Conversational filler or acknowledgements
233
- - Information that will be stale within hours
234
- - Step-by-step instructions for a one-time task
229
+ Reply with a JSON object with a single "verdicts" key, exactly one object per input candidate:
230
+ {"verdicts": [{"index": <candidate index>, "kind": "accept" | "reject" | "defer"}]}
235
231
 
236
- DEFER — the fact MIGHT be durable but needs more context to decide. The candidate will be re-evaluated on a later extraction pass with fresh context; if it cannot be resolved within a small number of re-evaluations it will be rejected.
237
- - Ambiguous referents ("he said they'd follow up on it")
238
- - Partial or in-progress statements that might become durable once completed
239
- - Future-tense commitments whose subject or timeline is unclear
240
- - Facts whose durability hinges on context not present in the candidate text
232
+ Rules:
233
+ 1. The top level must be one object {"verdicts": [...]} — JSON mode rejects a bare top-level array.
234
+ 2. Copy each object's "index" from the input candidate. One object per candidate, exactly.
235
+ 3. No other fields — no "reason", no "durable", no commentary.
236
+ 4. Output valid JSON only. No markdown fences.
237
+ 5. ${OUTPUT_LANGUAGE_POLICY}
241
238
 
242
- Do NOT use defer as a soft reject. Reject facts you are confident are transient. Only defer when another turn of context would genuinely change the verdict.
239
+ Example output:
240
+ {"verdicts": [{"index": 0, "kind": "accept"}, {"index": 1, "kind": "reject"}, {"index": 2, "kind": "defer"}]}`;
243
241
 
244
- Return a JSON array of objects with these fields:
245
- - index: number (the candidate index)
246
- - kind: string — one of "accept", "reject", "defer"
247
- - reason: string (brief explanation, under 80 characters)
242
+ // ---------------------------------------------------------------------------
243
+ // Judge request assembly (PR 3264 follow-up)
244
+ // ---------------------------------------------------------------------------
248
245
 
249
- You may also include durable (boolean) for backwards compatibility: true for accept, false for reject or defer. If kind is omitted, durable determines the verdict.
246
+ /** Completion tokens reserved per candidate verdict (measured 13 on a small judge model). */
247
+ const JUDGE_TOKENS_PER_CANDIDATE = 24;
248
+ /** Ceiling on one judge reply: the old flat budget. */
249
+ export const MAX_JUDGE_TOKENS = 2048;
250
+ /**
251
+ * Extra completion tokens for a judge call routed through the Responses API. Reasoning tokens count against
252
+ * `max_output_tokens`, so the compact verdict budget alone can be spent before a reasoning-enabled model answers; the
253
+ * unparsed batch would then fail open as durable. A cap, not a target: a non-reasoning model still writes a short reply.
254
+ */
255
+ export const JUDGE_REASONING_HEADROOM_TOKENS = 1024;
256
+ /** Largest batch whose verdict array fits MAX_JUDGE_TOKENS; a larger configured batch is split. */
257
+ export const MAX_JUDGE_BATCH = Math.floor(MAX_JUDGE_TOKENS / JUDGE_TOKENS_PER_CANDIDATE);
250
258
 
251
- Rules:
252
- 1. Return exactly one verdict per input candidate, matched by index.
253
- 2. When in doubt between accept and reject, lean toward accept — false negatives (losing a useful fact) are worse than false positives (keeping a marginal one).
254
- 3. Use defer only when another turn of context would genuinely change the verdict.
255
- 4. Output valid JSON only. No markdown fences, no commentary.
256
- 5. ${OUTPUT_LANGUAGE_POLICY}
259
+ /**
260
+ * Compact completion budget for one judge call. A verdict is
261
+ * `{"index":12,"kind":"reject"}` (~14 tokens), so the old flat 2048 only gave
262
+ * a slow model room to produce 59-305s replies that hid inside the timeout.
263
+ * Scales with the batch size (about 24 tokens per candidate verdict, measured
264
+ * 13 per verdict on a small judge model) so a large batch is never truncated;
265
+ * capped at 2048, the old flat budget.
266
+ */
267
+ export function judgeMaxTokens(batchSize: number): number {
268
+ return Math.min(MAX_JUDGE_TOKENS, Math.max(64, batchSize * JUDGE_TOKENS_PER_CANDIDATE));
269
+ }
257
270
 
258
- Example output:
259
- [{"index": 0, "kind": "accept", "durable": true, "reason": "Stable personal preference"}, {"index": 1, "kind": "reject", "durable": false, "reason": "Ephemeral build status"}, {"index": 2, "kind": "defer", "durable": false, "reason": "Ambiguous pronoun"}]`;
271
+ /** One indexed candidate entry in a judge request payload. */
272
+ export interface JudgeRequestEntry {
273
+ index: number;
274
+ text: string;
275
+ category: string;
276
+ confidence: number;
277
+ }
278
+
279
+ /** Full judge LLM request: messages plus the completion budget. */
280
+ export interface JudgeRequest {
281
+ messages: Array<{ role: "system" | "user"; content: string }>;
282
+ maxTokens: number;
283
+ }
284
+
285
+ /** Build a full judge request body: messages plus the completion budget. */
286
+ export function buildJudgeRequest(entries: readonly JudgeRequestEntry[]): JudgeRequest {
287
+ return {
288
+ messages: [
289
+ { role: "system", content: JUDGE_SYSTEM_PROMPT },
290
+ { role: "user", content: JSON.stringify(entries) },
291
+ ],
292
+ maxTokens: judgeMaxTokens(entries.length),
293
+ };
294
+ }
260
295
 
261
296
  // ---------------------------------------------------------------------------
262
297
  // Content-hash cache (in-memory, per-process fallback)
@@ -492,40 +527,39 @@ export async function judgeFactDurability(
492
527
  }
493
528
 
494
529
  // Batch the pending candidates up to batchSize
495
- const batchSize = config.extractionJudgeBatchSize;
530
+ const batchSize = Math.min(config.extractionJudgeBatchSize, MAX_JUDGE_BATCH);
531
+ // Defer bookkeeping spans the WHOLE call, not one batch: identical candidates can land in different batches, and each key
532
+ // must advance the defer cap once and decide against the same prior count (the snapshot taken at its first sighting).
533
+ const deferredThisCall = new Set<string>();
534
+ const priorDefersSeen = new Map<string, number>();
496
535
  for (let batchStart = 0; batchStart < pendingIndices.length; batchStart += batchSize) {
497
536
  const batchIndices = pendingIndices.slice(batchStart, batchStart + batchSize);
498
- const batchPayload = batchIndices.map((idx) => ({
499
- index: idx,
500
- text: candidates[idx].text,
501
- category: candidates[idx].category,
502
- confidence: candidates[idx].confidence,
503
- }));
504
-
505
- const userPrompt = JSON.stringify(batchPayload);
537
+ const request = buildJudgeRequest(
538
+ batchIndices.map((idx) => ({
539
+ index: idx,
540
+ text: candidates[idx].text,
541
+ category: candidates[idx].category,
542
+ confidence: candidates[idx].confidence,
543
+ })),
544
+ );
506
545
 
507
546
  try {
508
- const llmResponse = await callJudgeLlm(
509
- userPrompt,
510
- config,
511
- localLlm,
512
- fallbackLlm,
513
- );
547
+ const llmResponse = await callJudgeLlm(request, config, localLlm, fallbackLlm);
514
548
 
515
549
  if (llmResponse) {
516
550
  const parsed = parseJudgeResponse(llmResponse, batchIndices);
517
- // Per-batch defer-increment dedupe (codex P2): a single extraction
551
+ // Per-call defer-increment dedupe (codex P2): a single extraction
518
552
  // pass must not advance the cap more than once for identical
519
- // candidate content, even if the same text appears multiple times
520
- // in the same LLM response. Duplicate hits share the first
521
- // increment's `priorDeferrals` snapshot.
522
- const deferredThisBatch = new Set<string>();
553
+ // candidate content, even if the same text appears several times in
554
+ // one LLM response or in different batches of one call. Duplicate
555
+ // hits share the first sighting's `priorDeferrals` snapshot.
523
556
  for (const [idx, rawVerdict] of parsed.entries()) {
524
557
  const c = candidates[idx];
525
558
  const key = cacheKey(c.text, c.category);
526
559
  let verdict = rawVerdict;
527
560
  let source: JudgeVerdictObservation["source"] = "llm";
528
- const priorDefers = deferCountMap.get(key) ?? 0;
561
+ if (!priorDefersSeen.has(key)) priorDefersSeen.set(key, deferCountMap.get(key) ?? 0);
562
+ const priorDefers = priorDefersSeen.get(key) ?? 0;
529
563
 
530
564
  // Defer cap (issue #562, PR 2). A candidate that has already been
531
565
  // deferred `deferCap` times is forcibly rejected so a pathological
@@ -541,15 +575,15 @@ export async function judgeFactDurability(
541
575
  // Only clear + count the cap conversion once per batch for
542
576
  // this key — duplicates in the same response all resolve to
543
577
  // reject but should not inflate the cap-rejection counter.
544
- if (!deferredThisBatch.has(key)) {
578
+ if (!deferredThisCall.has(key)) {
545
579
  deferCountMap.delete(key);
546
580
  deferredCappedToReject++;
547
- deferredThisBatch.add(key);
581
+ deferredThisCall.add(key);
548
582
  }
549
- } else if (!deferredThisBatch.has(key)) {
583
+ } else if (!deferredThisCall.has(key)) {
550
584
  deferCountMap.set(key, priorDefers + 1);
551
585
  deferred++;
552
- deferredThisBatch.add(key);
586
+ deferredThisCall.add(key);
553
587
  // Bound the per-process defer-counter map.
554
588
  if (deferCountMap.size > DEFER_COUNT_MAX_SIZE) {
555
589
  const drop = Math.floor(deferCountMap.size / 2);
@@ -561,12 +595,14 @@ export async function judgeFactDurability(
561
595
  }
562
596
  }
563
597
  }
564
- // else: duplicate defer for a key already counted this batch —
598
+ // else: duplicate defer for a key already counted this call —
565
599
  // no additional counter increment, verdict still marked defer.
566
600
  } else {
567
601
  // On accept/reject, clear any outstanding defer counter so a
568
602
  // future reappearance of the same text starts fresh.
569
603
  deferCountMap.delete(key);
604
+ priorDefersSeen.set(key, 0);
605
+ deferredThisCall.delete(key);
570
606
  }
571
607
 
572
608
  verdicts.set(idx, verdict);
@@ -607,6 +643,8 @@ export async function judgeFactDurability(
607
643
  // telemetry event reflects the true state rather than always 0.
608
644
  const priorDefers = deferCountMap.get(hash) ?? 0;
609
645
  deferCountMap.delete(hash);
646
+ priorDefersSeen.set(hash, 0);
647
+ deferredThisCall.delete(hash);
610
648
  const v: JudgeVerdict = {
611
649
  durable: true,
612
650
  reason: "Approved by default (judge unavailable or parse error)",
@@ -641,15 +679,12 @@ export async function judgeFactDurability(
641
679
  // ---------------------------------------------------------------------------
642
680
 
643
681
  async function callJudgeLlm(
644
- userPrompt: string,
682
+ request: JudgeRequest,
645
683
  config: PluginConfig,
646
684
  localLlm: LocalLlmClient | null,
647
685
  fallbackLlm: FallbackLlmClient | null,
648
686
  ): Promise<string | null> {
649
- const messages: Array<{ role: "system" | "user"; content: string }> = [
650
- { role: "system", content: JUDGE_SYSTEM_PROMPT },
651
- { role: "user", content: userPrompt },
652
- ];
687
+ const { messages, maxTokens } = request;
653
688
 
654
689
  const modelOverride = config.extractionJudgeModel || undefined;
655
690
 
@@ -669,9 +704,14 @@ async function callJudgeLlm(
669
704
  try {
670
705
  const result = await (localLlm as any).chatCompletion(messages, {
671
706
  temperature: 0.1,
672
- maxTokens: 2048,
707
+ maxTokens,
708
+ // A verdict needs no reasoning: keep a thinking-capable model from spending the compact budget on it.
709
+ disableThinking: true,
673
710
  responseFormat: { type: "json_object" },
674
- timeoutMs: 1500,
711
+ timeoutMs: config.extractionJudgeTimeoutMs,
712
+ // The client applies timeoutMs per attempt and retries an abort once:
713
+ // one deadline signal bounds the whole call to the configured time.
714
+ signal: AbortSignal.timeout(config.extractionJudgeTimeoutMs),
675
715
  operation: "extraction-judge",
676
716
  ...(modelOverride ? { model: modelOverride } : {}),
677
717
  });
@@ -692,8 +732,9 @@ async function callJudgeLlm(
692
732
  messages as Array<{ role: "system" | "user" | "assistant"; content: string }>,
693
733
  {
694
734
  temperature: 0.1,
695
- maxTokens: 2048,
696
- timeoutMs: 1500,
735
+ maxTokens,
736
+ reasoningHeadroomTokens: JUDGE_REASONING_HEADROOM_TOKENS,
737
+ timeoutMs: config.extractionJudgeTimeoutMs,
697
738
  ...(modelOverride ? { model: modelOverride } : {}),
698
739
  ...gatewayChain,
699
740
  },
@@ -711,6 +752,38 @@ async function callJudgeLlm(
711
752
  return null;
712
753
  }
713
754
 
755
+ interface JudgeVerdictItem {
756
+ index?: number;
757
+ kind?: string;
758
+ action?: string;
759
+ durable?: boolean;
760
+ reason?: string;
761
+ }
762
+
763
+ /** What a judge model may return: a bare array, or one object wrapping verdicts. */
764
+ interface JudgeEnvelope {
765
+ verdicts?: JudgeVerdictItem[];
766
+ index?: number;
767
+ }
768
+
769
+ type ParsedJudgeJson = JudgeVerdictItem[] | JudgeEnvelope | null | undefined;
770
+
771
+ /**
772
+ * Normalise the object shapes a judge model returns to a verdict array.
773
+ * Requested shape {"verdicts": [...]} (response_format json_object forces a
774
+ * top-level object); a single verdict object counts as one verdict at its
775
+ * index, never the whole batch; legacy: any array-valued key.
776
+ */
777
+ function unwrapJudgeEnvelope(parsed: ParsedJudgeJson): JudgeVerdictItem[] | null {
778
+ if (!parsed || Array.isArray(parsed)) return null;
779
+ if (Array.isArray(parsed.verdicts)) return parsed.verdicts;
780
+ if (Number.isInteger(parsed.index)) return [parsed];
781
+ for (const v of Object.values(parsed)) {
782
+ if (Array.isArray(v)) return v;
783
+ }
784
+ return null;
785
+ }
786
+
714
787
  // ---------------------------------------------------------------------------
715
788
  // Response parsing
716
789
  // ---------------------------------------------------------------------------
@@ -724,7 +797,7 @@ function parseJudgeResponse(
724
797
 
725
798
  try {
726
799
  // Try direct parse first, then fall back to JSON extraction
727
- let parsed: unknown;
800
+ let parsed: ParsedJudgeJson;
728
801
  try {
729
802
  parsed = JSON.parse(raw);
730
803
  } catch {
@@ -735,20 +808,12 @@ function parseJudgeResponse(
735
808
  }
736
809
 
737
810
  if (!Array.isArray(parsed)) {
738
- // Might be wrapped in an object with a key
739
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
740
- const values = Object.values(parsed as Record<string, unknown>);
741
- for (const v of values) {
742
- if (Array.isArray(v)) {
743
- parsed = v;
744
- break;
745
- }
746
- }
747
- }
748
- if (!Array.isArray(parsed)) {
811
+ const unwrapped = unwrapJudgeEnvelope(parsed);
812
+ if (unwrapped === null) {
749
813
  log.debug("extraction-judge: response is not an array, cannot parse");
750
814
  return result;
751
815
  }
816
+ parsed = unwrapped;
752
817
  }
753
818
 
754
819
  for (const item of parsed) {