@remnic/core 10.0.73 → 10.0.75

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 (326) hide show
  1. package/dist/access-admin-ops-surface.js +49 -48
  2. package/dist/access-authorization-probe.js +50 -49
  3. package/dist/access-boundary.js +49 -48
  4. package/dist/access-cli.js +105 -104
  5. package/dist/access-cli.js.map +1 -1
  6. package/dist/access-extraction-force-flush.js +49 -48
  7. package/dist/access-http-briefing.js +50 -49
  8. package/dist/access-http-query.js +51 -50
  9. package/dist/access-http.js +68 -67
  10. package/dist/access-identity-continuity-surface.js +49 -48
  11. package/dist/access-lcm-surface.js +49 -48
  12. package/dist/access-mcp.js +58 -57
  13. package/dist/access-namespace-preflight.js +49 -48
  14. package/dist/access-observe-helpers.d.ts.map +1 -1
  15. package/dist/access-observe-helpers.js +1 -1
  16. package/dist/access-observe-write-category.js +6 -6
  17. package/dist/access-observe-write-surface.js +49 -48
  18. package/dist/access-offline-manifest.js +5 -5
  19. package/dist/access-operations-batch.js +50 -49
  20. package/dist/access-operations.js +57 -56
  21. package/dist/access-recall-concurrency.js +49 -48
  22. package/dist/access-recall-response.js +49 -48
  23. package/dist/access-recall-surface.js +49 -48
  24. package/dist/access-schema.js +5 -5
  25. package/dist/access-service.js +49 -48
  26. package/dist/access-surface-catalog.js +51 -50
  27. package/dist/access-surface-catalog.js.map +1 -1
  28. package/dist/active-recall.js +18 -18
  29. package/dist/adapters/index.js +4 -4
  30. package/dist/adapters/registry.js +2 -2
  31. package/dist/{auto-sync-ANOJDQD4.js → auto-sync-XHSS3XPL.js} +5 -5
  32. package/dist/briefing.js +15 -15
  33. package/dist/buffer-turn-helpers.d.ts +18 -0
  34. package/dist/buffer-turn-helpers.d.ts.map +1 -1
  35. package/dist/buffer-turn-helpers.js +5 -1
  36. package/dist/buffer.js +4 -4
  37. package/dist/calibration.js +5 -4
  38. package/dist/calibration.js.map +1 -1
  39. package/dist/causal-consolidation.js +16 -16
  40. package/dist/{chunk-7WFY2L5J.js → chunk-233S3MXC.js} +3 -3
  41. package/dist/{chunk-N7NAGECH.js → chunk-36B5VPRU.js} +2 -2
  42. package/dist/{chunk-5ORJHH43.js → chunk-3GGCMRO7.js} +2 -2
  43. package/dist/{chunk-RZED7FAU.js → chunk-3M7YP42P.js} +2 -2
  44. package/dist/{chunk-ICBUTKLL.js → chunk-3MBSD53O.js} +2 -2
  45. package/dist/{chunk-J72ZY7NU.js → chunk-4DLZ2ITE.js} +2 -2
  46. package/dist/{chunk-OUBEJWVI.js → chunk-4KLQI3L2.js} +42 -1
  47. package/dist/chunk-4KLQI3L2.js.map +1 -0
  48. package/dist/{chunk-WN2DASTS.js → chunk-5BE3FLKL.js} +4 -4
  49. package/dist/{chunk-S73ZVU37.js → chunk-5IQL4N37.js} +10 -10
  50. package/dist/{chunk-CIL4GHMD.js → chunk-6Q6AQ36B.js} +2 -2
  51. package/dist/{chunk-W4V7P5HP.js → chunk-6RPPZL7W.js} +2 -2
  52. package/dist/{chunk-EQ6EMJRG.js → chunk-6Y5ANLDH.js} +2 -2
  53. package/dist/{chunk-YPYSC3TX.js → chunk-7FFGWIBN.js} +2 -2
  54. package/dist/{chunk-VNTQU6KB.js → chunk-7PMOCQOL.js} +8 -13
  55. package/dist/chunk-7PMOCQOL.js.map +1 -0
  56. package/dist/{chunk-5RW247F5.js → chunk-AY2UK5XK.js} +14 -9
  57. package/dist/chunk-AY2UK5XK.js.map +1 -0
  58. package/dist/{chunk-5QV6CE6K.js → chunk-B36ACJG7.js} +2 -2
  59. package/dist/{chunk-ZBET3W26.js → chunk-BZTA7DR4.js} +3 -3
  60. package/dist/{chunk-JY4LXMZR.js → chunk-C44DRDZM.js} +2 -2
  61. package/dist/{chunk-UIFWZMO7.js → chunk-CLVILSBR.js} +7 -7
  62. package/dist/{chunk-OGQGDOYK.js → chunk-COIQ5N4J.js} +2 -2
  63. package/dist/{chunk-UAOJVQXC.js → chunk-DANFOVQG.js} +2 -2
  64. package/dist/{chunk-2R44BBPK.js → chunk-DEGE32C3.js} +2 -2
  65. package/dist/{chunk-YIJECSIX.js → chunk-DJDJBMFX.js} +9 -2
  66. package/dist/chunk-DJDJBMFX.js.map +1 -0
  67. package/dist/{chunk-LHMCZ6VW.js → chunk-E22CQZ3H.js} +14 -14
  68. package/dist/{chunk-JGLZ46JS.js → chunk-ELUG6NKQ.js} +2 -2
  69. package/dist/{chunk-X4DHAXFO.js → chunk-F3FNXE7C.js} +3 -3
  70. package/dist/{chunk-YIXWY4OZ.js → chunk-FETPTCQZ.js} +2 -2
  71. package/dist/{chunk-N2Y6COEM.js → chunk-FGUWCE5I.js} +2 -2
  72. package/dist/{chunk-FDUCIUQ5.js → chunk-FJ2VJTGA.js} +2 -2
  73. package/dist/{chunk-ENPJX3BB.js → chunk-FMFCQRDM.js} +3 -3
  74. package/dist/{chunk-BNU5TQLR.js → chunk-FOQ7TW2T.js} +2 -2
  75. package/dist/{chunk-4PMFSYH7.js → chunk-FP2AT4WL.js} +2 -2
  76. package/dist/{chunk-DLZGSZVC.js → chunk-FYQUUIPI.js} +4 -4
  77. package/dist/{chunk-XTN54NML.js → chunk-G3T6YMBO.js} +4 -4
  78. package/dist/{chunk-MUH7RR6T.js → chunk-G5NH3SSX.js} +5 -5
  79. package/dist/{chunk-DC3DWF2M.js → chunk-GCJA5KKT.js} +2 -2
  80. package/dist/{chunk-U6IL4724.js → chunk-GWOFCHPN.js} +2 -2
  81. package/dist/{chunk-TJ5YGCPV.js → chunk-GXVK754R.js} +2 -2
  82. package/dist/{chunk-FGGLVCHI.js → chunk-HB5TNQMM.js} +2 -2
  83. package/dist/{chunk-GXAZ4Q2S.js → chunk-HCFNZWFB.js} +2 -2
  84. package/dist/{chunk-F4ZHKDBW.js → chunk-HLWXCDUQ.js} +2 -2
  85. package/dist/{chunk-IQXQ3KWR.js → chunk-HTDXNIO6.js} +2 -2
  86. package/dist/{chunk-4RASUAN3.js → chunk-HXFIILGF.js} +4 -4
  87. package/dist/{chunk-S37O4S6E.js → chunk-I4LJJ44E.js} +2 -2
  88. package/dist/{chunk-USX5YIKB.js → chunk-IBQ7LGUJ.js} +6 -6
  89. package/dist/{chunk-UKQFCOGT.js → chunk-IPHIIRDN.js} +3 -3
  90. package/dist/chunk-IPHIIRDN.js.map +1 -0
  91. package/dist/{chunk-VSJ3ARFS.js → chunk-IRFQKR4Y.js} +2 -2
  92. package/dist/{chunk-BCBY5GYZ.js → chunk-IVXOYE43.js} +2 -2
  93. package/dist/{chunk-WXPBDEMW.js → chunk-JDQEQE2R.js} +2 -2
  94. package/dist/{chunk-2WPWEVAH.js → chunk-KAED7IBB.js} +2 -2
  95. package/dist/{chunk-OWZXDBV4.js → chunk-KNZDGZSY.js} +2 -2
  96. package/dist/{chunk-6KBCUIZP.js → chunk-L4QYW2HO.js} +2 -2
  97. package/dist/{chunk-LYOCAUI6.js → chunk-LJUQPT5G.js} +3 -3
  98. package/dist/{chunk-XPZM7GP6.js → chunk-MBENRPMY.js} +10 -10
  99. package/dist/chunk-MJFL75J6.js +42 -0
  100. package/dist/chunk-MJFL75J6.js.map +1 -0
  101. package/dist/{chunk-VK5HKVQ7.js → chunk-N6GEGRCT.js} +2 -2
  102. package/dist/{chunk-U2A56LLF.js → chunk-OBQ2NZAR.js} +10 -10
  103. package/dist/{chunk-WGRAB3KB.js → chunk-ORVPS4ZW.js} +2 -2
  104. package/dist/{chunk-PFUXPTBT.js → chunk-P3AQ2YSX.js} +4 -4
  105. package/dist/{chunk-DE4U5PTV.js → chunk-P4TEQSIZ.js} +2 -2
  106. package/dist/{chunk-UE5JYPU6.js → chunk-P7HNMNN2.js} +7 -7
  107. package/dist/{chunk-ZG4GNPRE.js → chunk-PBE7NFB5.js} +5 -5
  108. package/dist/{chunk-LN7CWLAU.js → chunk-PLGBPFEO.js} +2 -2
  109. package/dist/{chunk-5MA7ZLPD.js → chunk-QNUNAXBE.js} +35 -1
  110. package/dist/chunk-QNUNAXBE.js.map +1 -0
  111. package/dist/{chunk-23BA3USG.js → chunk-R24RYSQX.js} +43 -43
  112. package/dist/{chunk-WYOHQJKC.js → chunk-RXGFITWS.js} +4 -4
  113. package/dist/{chunk-JEX2JL3U.js → chunk-SHVLNI4L.js} +2 -2
  114. package/dist/{chunk-QUPTDKSX.js → chunk-SY4AFWUZ.js} +31 -31
  115. package/dist/{chunk-H2X32F2K.js → chunk-SYDPJUFR.js} +2 -2
  116. package/dist/{chunk-QIOQI7QZ.js → chunk-SZBZOUHR.js} +2 -2
  117. package/dist/{chunk-EGKDONTC.js → chunk-T6C3G67T.js} +10 -10
  118. package/dist/chunk-T6C3G67T.js.map +1 -0
  119. package/dist/{chunk-E3NBMTTX.js → chunk-TLTBACYX.js} +2 -2
  120. package/dist/{chunk-AVVCPA7U.js → chunk-UCPH56YX.js} +2 -2
  121. package/dist/{chunk-ZODKV27J.js → chunk-URS56HGW.js} +6 -6
  122. package/dist/{chunk-PJAMFIDE.js → chunk-VEQVXNWR.js} +4 -4
  123. package/dist/{chunk-AGRMXXVA.js → chunk-VEWTHVX2.js} +2 -2
  124. package/dist/{chunk-WNQ3B3Q5.js → chunk-VPEMGBTI.js} +2 -2
  125. package/dist/{chunk-DSSTOV5M.js → chunk-VYOAYFDV.js} +66 -49
  126. package/dist/chunk-VYOAYFDV.js.map +1 -0
  127. package/dist/{chunk-5JBVPJCT.js → chunk-WIAXVBQH.js} +2 -2
  128. package/dist/{chunk-BTU77RB5.js → chunk-X53HJENJ.js} +2 -2
  129. package/dist/{chunk-KY3KJUNN.js → chunk-XC7OYOQC.js} +6 -6
  130. package/dist/{chunk-43FLJLT2.js → chunk-Y5HZFIQO.js} +1 -1
  131. package/dist/{chunk-DOJBV6WR.js → chunk-YK6SFDP2.js} +4 -4
  132. package/dist/{chunk-QVZZE6VH.js → chunk-Z3CKK5RR.js} +3 -3
  133. package/dist/{chunk-AZNE2H6D.js → chunk-ZG57BEUS.js} +2 -2
  134. package/dist/{chunk-7P5VBRBS.js → chunk-ZZSFRWKD.js} +2 -2
  135. package/dist/cli.js +81 -80
  136. package/dist/coding/export-okf-codegraph.js +14 -14
  137. package/dist/compounding/engine.js +16 -16
  138. package/dist/config.js +18 -18
  139. package/dist/connectors/codex-materialize-runner.js +15 -15
  140. package/dist/connectors/index.js +15 -15
  141. package/dist/convergence-refresh.js +17 -17
  142. package/dist/corpus-watermark.js +17 -17
  143. package/dist/deep-recall-http-glue.js +6 -6
  144. package/dist/deep-recall-policy-llm.js +3 -3
  145. package/dist/deep-recall.js +2 -2
  146. package/dist/entity-retrieval.js +15 -15
  147. package/dist/external-wiki-access.js +50 -49
  148. package/dist/extraction-faithfulness.js +3 -3
  149. package/dist/extraction-judge.js +3 -3
  150. package/dist/extraction.js +23 -22
  151. package/dist/fallback-llm-codex-subscription.d.ts.map +1 -1
  152. package/dist/fallback-llm-codex-subscription.js +1 -1
  153. package/dist/fallback-llm.js +2 -2
  154. package/dist/{first-start-migration-A6PHIBUQ.js → first-start-migration-QVD22WWS.js} +4 -4
  155. package/dist/harmonic-retrieval.js +15 -15
  156. package/dist/index.js +130 -129
  157. package/dist/lcm/engine.js +2 -2
  158. package/dist/lcm/index.js +5 -5
  159. package/dist/local-llm-native-schema.d.ts +50 -0
  160. package/dist/local-llm-native-schema.d.ts.map +1 -0
  161. package/dist/local-llm-native-schema.js +8 -0
  162. package/dist/local-llm-native-schema.js.map +1 -0
  163. package/dist/local-llm.d.ts +1 -0
  164. package/dist/local-llm.d.ts.map +1 -1
  165. package/dist/local-llm.js +4 -3
  166. package/dist/location/index.js +17 -17
  167. package/dist/maintenance/memory-governance.js +15 -15
  168. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +15 -15
  169. package/dist/maintenance/rebuild-memory-projection-runner.js +17 -17
  170. package/dist/maintenance/rebuild-memory-projection.js +16 -16
  171. package/dist/memory-browse-operations.js +50 -49
  172. package/dist/memory-browse.js +3 -3
  173. package/dist/model-registry.d.ts +11 -0
  174. package/dist/model-registry.d.ts.map +1 -1
  175. package/dist/model-registry.js +1 -1
  176. package/dist/namespaces/migrate.js +16 -16
  177. package/dist/namespaces/search.js +7 -7
  178. package/dist/namespaces/storage.js +15 -15
  179. package/dist/offline-sync-changeset-generations.js +3 -3
  180. package/dist/offline-sync-embedding-generation.js +2 -2
  181. package/dist/offline-sync-snapshot-stream.js +5 -5
  182. package/dist/offline-sync.js +7 -7
  183. package/dist/operator-doctor-corpus.js +18 -18
  184. package/dist/operator-toolkit.js +27 -27
  185. package/dist/orchestration/backlog-scheduler.d.ts +8 -0
  186. package/dist/orchestration/backlog-scheduler.d.ts.map +1 -1
  187. package/dist/orchestration/backlog-scheduler.js +118 -106
  188. package/dist/orchestration/backlog-scheduler.js.map +1 -1
  189. package/dist/orchestration/buffer-drain-runner.js +106 -105
  190. package/dist/orchestration/extraction-queue-coordinator.d.ts +6 -0
  191. package/dist/orchestration/extraction-queue-coordinator.d.ts.map +1 -1
  192. package/dist/orchestration/maintenance.js +28 -27
  193. package/dist/orchestrator.js +105 -104
  194. package/dist/recall-navigation-operations.js +50 -49
  195. package/dist/recall-navigation.js +49 -48
  196. package/dist/recall-planner-llm.js +2 -2
  197. package/dist/recall-state-view-admission.js +3 -3
  198. package/dist/recall-why-service.js +3 -3
  199. package/dist/resume-bundles.js +18 -18
  200. package/dist/search/factory.js +6 -6
  201. package/dist/search/index.js +11 -11
  202. package/dist/search/lancedb-backend.js +2 -2
  203. package/dist/search/meilisearch-backend.js +2 -2
  204. package/dist/search/orama-backend.js +2 -2
  205. package/dist/semantic-consolidation.js +16 -16
  206. package/dist/semantic-rule-promotion.js +15 -15
  207. package/dist/semantic-rule-verifier.js +15 -15
  208. package/dist/source-agent-qualifier.js +18 -18
  209. package/dist/standup/build.js +15 -15
  210. package/dist/storage/buffer-drain-claims.d.ts.map +1 -1
  211. package/dist/storage/buffer-drain-claims.js +2 -2
  212. package/dist/storage/buffer-session-files.js +2 -2
  213. package/dist/storage.js +14 -14
  214. package/dist/summarizer.js +7 -6
  215. package/dist/support-passport/index.js +52 -51
  216. package/dist/transfer/export-okf.js +15 -15
  217. package/dist/verified-recall.js +15 -15
  218. package/dist/who-knows-cli.js +16 -16
  219. package/dist/who-knows.js +15 -15
  220. package/package.json +2 -2
  221. package/src/access-observe-acceptance-boundary.test.ts +24 -18
  222. package/src/access-observe-helpers.test.ts +72 -0
  223. package/src/access-observe-helpers.ts +5 -2
  224. package/src/access-service-observe-lcm-parity.test.ts +9 -2
  225. package/src/access-service-observe-scope.test.ts +8 -1
  226. package/src/backlog-scheduler.test.ts +104 -0
  227. package/src/buffer-record-snapshots.test.ts +195 -0
  228. package/src/buffer-turn-helpers.test.ts +205 -0
  229. package/src/buffer-turn-helpers.ts +62 -0
  230. package/src/buffer.ts +8 -8
  231. package/src/fallback-llm-codex-subscription.ts +10 -1
  232. package/src/local-llm-json-schema-fallback.test.ts +315 -0
  233. package/src/local-llm-native-schema.ts +78 -0
  234. package/src/local-llm.ts +8 -9
  235. package/src/model-registry.ts +40 -0
  236. package/src/orchestration/backlog-scheduler.ts +32 -0
  237. package/src/orchestration/extraction-queue-coordinator.ts +18 -1
  238. package/src/orchestrator-extraction-queue.test.ts +142 -0
  239. package/src/storage/buffer-drain-claims.ts +5 -11
  240. package/dist/chunk-5MA7ZLPD.js.map +0 -1
  241. package/dist/chunk-5RW247F5.js.map +0 -1
  242. package/dist/chunk-DSSTOV5M.js.map +0 -1
  243. package/dist/chunk-EGKDONTC.js.map +0 -1
  244. package/dist/chunk-OUBEJWVI.js.map +0 -1
  245. package/dist/chunk-UKQFCOGT.js.map +0 -1
  246. package/dist/chunk-VNTQU6KB.js.map +0 -1
  247. package/dist/chunk-YIJECSIX.js.map +0 -1
  248. /package/dist/{auto-sync-ANOJDQD4.js.map → auto-sync-XHSS3XPL.js.map} +0 -0
  249. /package/dist/{chunk-7WFY2L5J.js.map → chunk-233S3MXC.js.map} +0 -0
  250. /package/dist/{chunk-N7NAGECH.js.map → chunk-36B5VPRU.js.map} +0 -0
  251. /package/dist/{chunk-5ORJHH43.js.map → chunk-3GGCMRO7.js.map} +0 -0
  252. /package/dist/{chunk-RZED7FAU.js.map → chunk-3M7YP42P.js.map} +0 -0
  253. /package/dist/{chunk-ICBUTKLL.js.map → chunk-3MBSD53O.js.map} +0 -0
  254. /package/dist/{chunk-J72ZY7NU.js.map → chunk-4DLZ2ITE.js.map} +0 -0
  255. /package/dist/{chunk-WN2DASTS.js.map → chunk-5BE3FLKL.js.map} +0 -0
  256. /package/dist/{chunk-S73ZVU37.js.map → chunk-5IQL4N37.js.map} +0 -0
  257. /package/dist/{chunk-CIL4GHMD.js.map → chunk-6Q6AQ36B.js.map} +0 -0
  258. /package/dist/{chunk-W4V7P5HP.js.map → chunk-6RPPZL7W.js.map} +0 -0
  259. /package/dist/{chunk-EQ6EMJRG.js.map → chunk-6Y5ANLDH.js.map} +0 -0
  260. /package/dist/{chunk-YPYSC3TX.js.map → chunk-7FFGWIBN.js.map} +0 -0
  261. /package/dist/{chunk-5QV6CE6K.js.map → chunk-B36ACJG7.js.map} +0 -0
  262. /package/dist/{chunk-ZBET3W26.js.map → chunk-BZTA7DR4.js.map} +0 -0
  263. /package/dist/{chunk-JY4LXMZR.js.map → chunk-C44DRDZM.js.map} +0 -0
  264. /package/dist/{chunk-UIFWZMO7.js.map → chunk-CLVILSBR.js.map} +0 -0
  265. /package/dist/{chunk-OGQGDOYK.js.map → chunk-COIQ5N4J.js.map} +0 -0
  266. /package/dist/{chunk-UAOJVQXC.js.map → chunk-DANFOVQG.js.map} +0 -0
  267. /package/dist/{chunk-2R44BBPK.js.map → chunk-DEGE32C3.js.map} +0 -0
  268. /package/dist/{chunk-LHMCZ6VW.js.map → chunk-E22CQZ3H.js.map} +0 -0
  269. /package/dist/{chunk-JGLZ46JS.js.map → chunk-ELUG6NKQ.js.map} +0 -0
  270. /package/dist/{chunk-X4DHAXFO.js.map → chunk-F3FNXE7C.js.map} +0 -0
  271. /package/dist/{chunk-YIXWY4OZ.js.map → chunk-FETPTCQZ.js.map} +0 -0
  272. /package/dist/{chunk-N2Y6COEM.js.map → chunk-FGUWCE5I.js.map} +0 -0
  273. /package/dist/{chunk-FDUCIUQ5.js.map → chunk-FJ2VJTGA.js.map} +0 -0
  274. /package/dist/{chunk-ENPJX3BB.js.map → chunk-FMFCQRDM.js.map} +0 -0
  275. /package/dist/{chunk-BNU5TQLR.js.map → chunk-FOQ7TW2T.js.map} +0 -0
  276. /package/dist/{chunk-4PMFSYH7.js.map → chunk-FP2AT4WL.js.map} +0 -0
  277. /package/dist/{chunk-DLZGSZVC.js.map → chunk-FYQUUIPI.js.map} +0 -0
  278. /package/dist/{chunk-XTN54NML.js.map → chunk-G3T6YMBO.js.map} +0 -0
  279. /package/dist/{chunk-MUH7RR6T.js.map → chunk-G5NH3SSX.js.map} +0 -0
  280. /package/dist/{chunk-DC3DWF2M.js.map → chunk-GCJA5KKT.js.map} +0 -0
  281. /package/dist/{chunk-U6IL4724.js.map → chunk-GWOFCHPN.js.map} +0 -0
  282. /package/dist/{chunk-TJ5YGCPV.js.map → chunk-GXVK754R.js.map} +0 -0
  283. /package/dist/{chunk-FGGLVCHI.js.map → chunk-HB5TNQMM.js.map} +0 -0
  284. /package/dist/{chunk-GXAZ4Q2S.js.map → chunk-HCFNZWFB.js.map} +0 -0
  285. /package/dist/{chunk-F4ZHKDBW.js.map → chunk-HLWXCDUQ.js.map} +0 -0
  286. /package/dist/{chunk-IQXQ3KWR.js.map → chunk-HTDXNIO6.js.map} +0 -0
  287. /package/dist/{chunk-4RASUAN3.js.map → chunk-HXFIILGF.js.map} +0 -0
  288. /package/dist/{chunk-S37O4S6E.js.map → chunk-I4LJJ44E.js.map} +0 -0
  289. /package/dist/{chunk-USX5YIKB.js.map → chunk-IBQ7LGUJ.js.map} +0 -0
  290. /package/dist/{chunk-VSJ3ARFS.js.map → chunk-IRFQKR4Y.js.map} +0 -0
  291. /package/dist/{chunk-BCBY5GYZ.js.map → chunk-IVXOYE43.js.map} +0 -0
  292. /package/dist/{chunk-WXPBDEMW.js.map → chunk-JDQEQE2R.js.map} +0 -0
  293. /package/dist/{chunk-2WPWEVAH.js.map → chunk-KAED7IBB.js.map} +0 -0
  294. /package/dist/{chunk-OWZXDBV4.js.map → chunk-KNZDGZSY.js.map} +0 -0
  295. /package/dist/{chunk-6KBCUIZP.js.map → chunk-L4QYW2HO.js.map} +0 -0
  296. /package/dist/{chunk-LYOCAUI6.js.map → chunk-LJUQPT5G.js.map} +0 -0
  297. /package/dist/{chunk-XPZM7GP6.js.map → chunk-MBENRPMY.js.map} +0 -0
  298. /package/dist/{chunk-VK5HKVQ7.js.map → chunk-N6GEGRCT.js.map} +0 -0
  299. /package/dist/{chunk-U2A56LLF.js.map → chunk-OBQ2NZAR.js.map} +0 -0
  300. /package/dist/{chunk-WGRAB3KB.js.map → chunk-ORVPS4ZW.js.map} +0 -0
  301. /package/dist/{chunk-PFUXPTBT.js.map → chunk-P3AQ2YSX.js.map} +0 -0
  302. /package/dist/{chunk-DE4U5PTV.js.map → chunk-P4TEQSIZ.js.map} +0 -0
  303. /package/dist/{chunk-UE5JYPU6.js.map → chunk-P7HNMNN2.js.map} +0 -0
  304. /package/dist/{chunk-ZG4GNPRE.js.map → chunk-PBE7NFB5.js.map} +0 -0
  305. /package/dist/{chunk-LN7CWLAU.js.map → chunk-PLGBPFEO.js.map} +0 -0
  306. /package/dist/{chunk-23BA3USG.js.map → chunk-R24RYSQX.js.map} +0 -0
  307. /package/dist/{chunk-WYOHQJKC.js.map → chunk-RXGFITWS.js.map} +0 -0
  308. /package/dist/{chunk-JEX2JL3U.js.map → chunk-SHVLNI4L.js.map} +0 -0
  309. /package/dist/{chunk-QUPTDKSX.js.map → chunk-SY4AFWUZ.js.map} +0 -0
  310. /package/dist/{chunk-H2X32F2K.js.map → chunk-SYDPJUFR.js.map} +0 -0
  311. /package/dist/{chunk-QIOQI7QZ.js.map → chunk-SZBZOUHR.js.map} +0 -0
  312. /package/dist/{chunk-E3NBMTTX.js.map → chunk-TLTBACYX.js.map} +0 -0
  313. /package/dist/{chunk-AVVCPA7U.js.map → chunk-UCPH56YX.js.map} +0 -0
  314. /package/dist/{chunk-ZODKV27J.js.map → chunk-URS56HGW.js.map} +0 -0
  315. /package/dist/{chunk-PJAMFIDE.js.map → chunk-VEQVXNWR.js.map} +0 -0
  316. /package/dist/{chunk-AGRMXXVA.js.map → chunk-VEWTHVX2.js.map} +0 -0
  317. /package/dist/{chunk-WNQ3B3Q5.js.map → chunk-VPEMGBTI.js.map} +0 -0
  318. /package/dist/{chunk-5JBVPJCT.js.map → chunk-WIAXVBQH.js.map} +0 -0
  319. /package/dist/{chunk-BTU77RB5.js.map → chunk-X53HJENJ.js.map} +0 -0
  320. /package/dist/{chunk-KY3KJUNN.js.map → chunk-XC7OYOQC.js.map} +0 -0
  321. /package/dist/{chunk-43FLJLT2.js.map → chunk-Y5HZFIQO.js.map} +0 -0
  322. /package/dist/{chunk-DOJBV6WR.js.map → chunk-YK6SFDP2.js.map} +0 -0
  323. /package/dist/{chunk-QVZZE6VH.js.map → chunk-Z3CKK5RR.js.map} +0 -0
  324. /package/dist/{chunk-AZNE2H6D.js.map → chunk-ZG57BEUS.js.map} +0 -0
  325. /package/dist/{chunk-7P5VBRBS.js.map → chunk-ZZSFRWKD.js.map} +0 -0
  326. /package/dist/{first-start-migration-A6PHIBUQ.js.map → first-start-migration-QVD22WWS.js.map} +0 -0
@@ -1,9 +1,14 @@
1
1
  import assert from "node:assert/strict";
2
+ import fs from "node:fs";
2
3
  import http from "node:http";
4
+ import os from "node:os";
3
5
  import type { AddressInfo } from "node:net";
6
+ import { join } from "node:path";
4
7
  import test, { mock } from "node:test";
5
8
 
6
9
  import { LocalLlmClient } from "./local-llm.js";
10
+ import { ModelRegistry } from "./model-registry.js";
11
+ import { NativeSchemaGate, type LocalLlmChatRequestBody } from "./local-llm-native-schema.js";
7
12
  import { ChatTransport } from "./local-llm-transport.js";
8
13
  import { parseConfig } from "./config.js";
9
14
  import type { LocalLlmChatCompletionOptions } from "./local-llm-helpers.js";
@@ -276,3 +281,313 @@ test("request without response_format is unchanged", async () => {
276
281
  }
277
282
  });
278
283
 
284
+ // ── Schema-validation rejections (the provider parses our json_schema) ──────
285
+ //
286
+ // Production signature (2026-11-05, MiniMax-M3.1-Flash via local proxy): a
287
+ // 400 "invalid params — Mismatch type string with value array" quoting a
288
+ // schema fragment like `"episodeTitle": {"type": ["string","null"]}`. The
289
+ // provider accepts structured output in principle but its strict parser
290
+ // rejects our draft-7 nullable type unions. Unlike capability rejections
291
+ // these errors never name response_format, so nothing stripped the format,
292
+ // every drain attempt re-sent the identical payload, and the 400 breaker
293
+ // opened. The zod draft-7 schema legitimately contains `type` arrays
294
+ // (nullable fields), so the provider dialect, not our request, is the
295
+ // constraint: degrade to the pre-schema request shape and remember it.
296
+
297
+ const schemaValidationRejection = {
298
+ error: {
299
+ // Verbatim production body shape (2026-11-05): "Mismatch type" (no "ed"),
300
+ // JSON-escaped quotes around the schema fragment, and no mention of
301
+ // response_format anywhere.
302
+ message:
303
+ 'invalid params, Mismatch type string with value array "at index 13779: \\"episodeTitle\\": {\\"type\\": [\\"string\\",\\"null\\"]}"',
304
+ },
305
+ };
306
+
307
+ test("a schema-validation 400 strips the format and retries without it", async () => {
308
+ const server = await startRecordingServer((_req, count) =>
309
+ count === 1
310
+ ? { status: 400, body: schemaValidationRejection }
311
+ : { status: 200, body: okCompletion },
312
+ );
313
+ try {
314
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
315
+ primeClient(client);
316
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }], {
317
+ responseFormat,
318
+ });
319
+ assert.equal(result?.content, "done");
320
+ assert.equal(server.requests.length, 2, "exactly one strip-and-retry");
321
+ assert.ok(server.requests[0].body.response_format, "the schema-shaped request went out first");
322
+ assert.equal(server.requests[1].body.response_format, undefined);
323
+ } finally {
324
+ await server.close();
325
+ }
326
+ });
327
+
328
+ test("a rejected schema format is not re-sent on later requests", async () => {
329
+ const server = await startRecordingServer((_req, count) =>
330
+ count === 1
331
+ ? { status: 400, body: schemaValidationRejection }
332
+ : { status: 200, body: okCompletion },
333
+ );
334
+ try {
335
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
336
+ primeClient(client);
337
+ assert.equal(
338
+ (await client.chatCompletion([{ role: "user", content: "hello" }], { responseFormat }))?.content,
339
+ "done",
340
+ );
341
+ assert.equal(
342
+ (await client.chatCompletion([{ role: "user", content: "hello again" }], { responseFormat }))?.content,
343
+ "done",
344
+ );
345
+ assert.equal(server.requests.length, 3, "second call sends one request, not a new 400");
346
+ assert.equal(
347
+ server.requests[2].body.response_format,
348
+ undefined,
349
+ "the learned rejection skips the format entirely",
350
+ );
351
+ } finally {
352
+ await server.close();
353
+ }
354
+ });
355
+
356
+ test("a learned schema rejection persists in the model registry for fresh clients", async () => {
357
+ const memoryDir = fs.mkdtempSync(join(os.tmpdir(), "remnic-schema-cap-"));
358
+ const server = await startRecordingServer((_req, count) =>
359
+ count === 1
360
+ ? { status: 400, body: schemaValidationRejection }
361
+ : { status: 200, body: okCompletion },
362
+ );
363
+ try {
364
+ const registry = new ModelRegistry(memoryDir);
365
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }), registry);
366
+ primeClient(client);
367
+ assert.equal(
368
+ (await client.chatCompletion([{ role: "user", content: "hello" }], { responseFormat }))?.content,
369
+ "done",
370
+ );
371
+ // SAFETY: we wrote model-capabilities.json through ModelRegistry above; the
372
+ // document is an object keyed by normalized model id.
373
+ const persisted = JSON.parse(
374
+ fs.readFileSync(join(memoryDir, ".registry", "model-capabilities.json"), "utf8"),
375
+ ) as { models: Record<string, { structuredOutput?: string }> };
376
+ assert.equal(persisted.models["test-local-model"]?.structuredOutput, "none");
377
+
378
+ const fresh = new LocalLlmClient(createConfig({ localLlmUrl: server.url }), new ModelRegistry(memoryDir));
379
+ primeClient(fresh);
380
+ assert.equal(
381
+ (await fresh.chatCompletion([{ role: "user", content: "hello" }], { responseFormat }))?.content,
382
+ "done",
383
+ );
384
+ const formatRequests = server.requests.filter((r) => r.body.response_format);
385
+ assert.equal(formatRequests.length, 1, "only the first-ever request carries the format");
386
+ } finally {
387
+ await server.close();
388
+ fs.rmSync(memoryDir, { recursive: true, force: true });
389
+ }
390
+ });
391
+
392
+ test("a schema-carrying success records native and keeps sending the format", async () => {
393
+ const memoryDir = fs.mkdtempSync(join(os.tmpdir(), "remnic-schema-cap-"));
394
+ const server = await startRecordingServer(() => ({ status: 200, body: okCompletion }));
395
+ try {
396
+ const registry = new ModelRegistry(memoryDir);
397
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }), registry);
398
+ primeClient(client);
399
+ await client.chatCompletion([{ role: "user", content: "hello" }], { responseFormat });
400
+ await client.chatCompletion([{ role: "user", content: "hello again" }], { responseFormat });
401
+ // SAFETY: we wrote model-capabilities.json through ModelRegistry above; the
402
+ // document is an object keyed by normalized model id.
403
+ const persisted = JSON.parse(
404
+ fs.readFileSync(join(memoryDir, ".registry", "model-capabilities.json"), "utf8"),
405
+ ) as { models: Record<string, { structuredOutput?: string }> };
406
+ assert.equal(persisted.models["test-local-model"]?.structuredOutput, "native");
407
+ assert.equal(server.requests.length, 2);
408
+ assert.ok(server.requests[1].body.response_format, "structured output remains for supporting providers");
409
+ } finally {
410
+ await server.close();
411
+ fs.rmSync(memoryDir, { recursive: true, force: true });
412
+ }
413
+ });
414
+
415
+ test("an expired registry rejection is re-probed instead of honored forever", async () => {
416
+ const memoryDir = fs.mkdtempSync(join(os.tmpdir(), "remnic-schema-cap-"));
417
+ const server = await startRecordingServer(() => ({ status: 200, body: okCompletion }));
418
+ try {
419
+ const registry = new ModelRegistry(memoryDir);
420
+ registry.setStructuredOutput("test-local-model", "none");
421
+ // Age the entry past the 7-day registry TTL, as a long-lived deployment
422
+ // would: the learned rejection must not be honored forever.
423
+ const registryPath = join(memoryDir, ".registry", "model-capabilities.json");
424
+ // SAFETY: we wrote model-capabilities.json through ModelRegistry above; the
425
+ // document is an object keyed by normalized model id.
426
+ const persisted = JSON.parse(fs.readFileSync(registryPath, "utf8")) as {
427
+ models: Record<string, { fetchedAt: string }>;
428
+ };
429
+ persisted.models["test-local-model"].fetchedAt = new Date(
430
+ Date.now() - 8 * 24 * 60 * 60 * 1000,
431
+ ).toISOString();
432
+ fs.writeFileSync(registryPath, JSON.stringify(persisted));
433
+
434
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }), new ModelRegistry(memoryDir));
435
+ primeClient(client);
436
+ const result = await client.chatCompletion([{ role: "user", content: "hello" }], {
437
+ responseFormat,
438
+ });
439
+ assert.equal(result?.content, "done");
440
+ assert.ok(server.requests[0].body.response_format, "expired rejection is re-probed with the schema");
441
+ // The provider accepts it this time: the native record refreshes.
442
+ // SAFETY: the file above was written by ModelRegistry's own persistence;
443
+ // the document is an object keyed by normalized model id.
444
+ const refreshed = JSON.parse(fs.readFileSync(registryPath, "utf8")) as {
445
+ models: Record<string, { structuredOutput?: string }>;
446
+ };
447
+ assert.equal(refreshed.models["test-local-model"]?.structuredOutput, "native");
448
+ } finally {
449
+ await server.close();
450
+ fs.rmSync(memoryDir, { recursive: true, force: true });
451
+ }
452
+ });
453
+
454
+ test("a later capabilities write preserves the learned structuredOutput", async () => {
455
+ const memoryDir = fs.mkdtempSync(join(os.tmpdir(), "remnic-schema-cap-"));
456
+ try {
457
+ const registry = new ModelRegistry(memoryDir);
458
+ registry.setStructuredOutput("test-local-model", "none");
459
+ // Mirrors getCachedContextWindow's LM Studio detection replacing the record.
460
+ registry.setCapabilities("test-local-model", {
461
+ maxPositionEmbeddings: 131072,
462
+ contextWindow: 131072,
463
+ supportsExtendedContext: true,
464
+ typicalOutputTokens: 8192,
465
+ source: "lmstudio",
466
+ });
467
+ assert.equal(registry.getStructuredOutput("test-local-model"), "none");
468
+ } finally {
469
+ fs.rmSync(memoryDir, { recursive: true, force: true });
470
+ }
471
+ });
472
+
473
+ test("an expired structuredOutput decision is not renewed by a capability refresh", async () => {
474
+ const memoryDir = fs.mkdtempSync(join(os.tmpdir(), "remnic-schema-cap-"));
475
+ try {
476
+ const registry = new ModelRegistry(memoryDir);
477
+ registry.setStructuredOutput("test-local-model", "none");
478
+ // Age the learned decision past the registry TTL exactly as a long-lived
479
+ // deployment would.
480
+ const registryPath = join(memoryDir, ".registry", "model-capabilities.json");
481
+ // SAFETY: we wrote model-capabilities.json through ModelRegistry above; the
482
+ // document is an object keyed by normalized model id.
483
+ const persisted = JSON.parse(fs.readFileSync(registryPath, "utf8")) as {
484
+ models: Record<string, { fetchedAt: string }>;
485
+ };
486
+ persisted.models["test-local-model"].fetchedAt = new Date(
487
+ Date.now() - 8 * 24 * 60 * 60 * 1000,
488
+ ).toISOString();
489
+ fs.writeFileSync(registryPath, JSON.stringify(persisted));
490
+ // The refresh happens in a process that loaded the AGED state from disk
491
+ // (a restart or another runtime sharing the memory dir).
492
+ const refreshedRegistry = new ModelRegistry(memoryDir);
493
+ refreshedRegistry.setCapabilities("test-local-model", {
494
+ maxPositionEmbeddings: 131072,
495
+ contextWindow: 131072,
496
+ supportsExtendedContext: true,
497
+ typicalOutputTokens: 8192,
498
+ source: "lmstudio",
499
+ });
500
+ assert.equal(
501
+ refreshedRegistry.getStructuredOutput("test-local-model"),
502
+ undefined,
503
+ "an expired decision must not be renewed into a fresh record — the re-probe stays due",
504
+ );
505
+ } finally {
506
+ fs.rmSync(memoryDir, { recursive: true, force: true });
507
+ }
508
+ });
509
+
510
+ test("a stale in-flight schema success does not overwrite a current rejection", () => {
511
+ const memoryDir = fs.mkdtempSync(join(os.tmpdir(), "remnic-schema-cap-"));
512
+ try {
513
+ const registry = new ModelRegistry(memoryDir);
514
+ const gate = new NativeSchemaGate(() => registry);
515
+ // Two schema-bearing requests attach before either settles.
516
+ const body: LocalLlmChatRequestBody = {
517
+ model: "test-local-model",
518
+ messages: [{ role: "user", content: "hello" }],
519
+ temperature: 0.1,
520
+ max_tokens: 16,
521
+ stream: false,
522
+ };
523
+ gate.attach(body, { responseFormat }, "test-local-model");
524
+ // The rejection settles first: "none" is the newest evidence.
525
+ gate.rememberRejection("test-local-model");
526
+ // The older in-flight success settles last: it must not flip the
527
+ // decision back, or later requests re-send the rejected schema.
528
+ gate.recordSuccess(body, "test-local-model");
529
+ assert.equal(registry.getStructuredOutput("test-local-model"), "none");
530
+ // A fresh success with no rejection standing still records native.
531
+ const otherBody: LocalLlmChatRequestBody = { ...body, model: "other-model" };
532
+ const freshGate = new NativeSchemaGate(() => registry);
533
+ freshGate.attach(otherBody, { responseFormat }, "other-model");
534
+ freshGate.recordSuccess(otherBody, "other-model");
535
+ assert.equal(registry.getStructuredOutput("other-model"), "native");
536
+ } finally {
537
+ fs.rmSync(memoryDir, { recursive: true, force: true });
538
+ }
539
+ });
540
+
541
+ test("invalid params without schema evidence is not treated as a schema rejection", async () => {
542
+ const server = await startRecordingServer(() => ({
543
+ status: 400,
544
+ body: { error: { message: "invalid params: temperature must be between 0 and 2" } },
545
+ }));
546
+ try {
547
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
548
+ primeClient(client);
549
+ assert.equal(await client.chatCompletion([{ role: "user", content: "hello" }], { responseFormat }), null);
550
+ assert.equal(server.requests.length, 1, "unrelated invalid-param errors must not strip the format");
551
+ } finally {
552
+ await server.close();
553
+ }
554
+ });
555
+
556
+ test("an echoed format name inside an unrelated invalid-param error does not strip", async () => {
557
+ const server = await startRecordingServer(() => ({
558
+ status: 400,
559
+ body: {
560
+ error: {
561
+ message: "invalid params: quota exceeded; request included response_format json_schema",
562
+ },
563
+ },
564
+ }));
565
+ try {
566
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
567
+ primeClient(client);
568
+ assert.equal(await client.chatCompletion([{ role: "user", content: "hello" }], { responseFormat }), null);
569
+ assert.equal(server.requests.length, 1, "an echo of the format name is not schema-rejection evidence");
570
+ } finally {
571
+ await server.close();
572
+ }
573
+ });
574
+
575
+ test("a type mismatch on an unrelated parameter does not strip the schema", async () => {
576
+ const server = await startRecordingServer(() => ({
577
+ status: 400,
578
+ body: { error: { message: "invalid params: mismatch type for temperature, expected number" } },
579
+ }));
580
+ try {
581
+ const client = new LocalLlmClient(createConfig({ localLlmUrl: server.url }));
582
+ primeClient(client);
583
+ assert.equal(await client.chatCompletion([{ role: "user", content: "hello" }], { responseFormat }), null);
584
+ assert.equal(
585
+ server.requests.length,
586
+ 1,
587
+ "no schema fragment quoted: the mismatch is not evidence our schema was rejected",
588
+ );
589
+ } finally {
590
+ await server.close();
591
+ }
592
+ });
593
+
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Native json_schema capability shaping for the local LLM client.
3
+ *
4
+ * A provider that rejects the schema PAYLOAD itself (strict parsers that die
5
+ * on draft-7 nullable `type` unions, e.g. MiniMax-M3.1-Flash on 2026-11-05:
6
+ * "invalid params, Mismatch type string with value array") gets the pre-#3248
7
+ * shape: no response_format, the JSON contract stays in the prompt. The
8
+ * rejection is learned per model and persisted through the ModelRegistry so
9
+ * fresh runtimes shape requests correctly without re-proving it; a
10
+ * schema-carrying success persists `structuredOutput: "native"`.
11
+ */
12
+
13
+ import type { ResponseFormatJSONSchema } from "openai/resources/shared";
14
+ import type { LocalLlmChatCompletionOptions } from "./local-llm-helpers.js";
15
+ import type { ModelRegistry } from "./model-registry.js";
16
+
17
+ /** The chat-completions wire body; optional keys are capability-gated additions. */
18
+ export interface LocalLlmChatRequestBody {
19
+ model: string;
20
+ messages: Array<{ role: string; content: string }>;
21
+ temperature: number;
22
+ max_tokens: number;
23
+ stream: boolean;
24
+ response_format?: { type: string } | ResponseFormatJSONSchema;
25
+ chat_template_kwargs?: { enable_thinking: boolean };
26
+ reasoning_effort?: string;
27
+ }
28
+
29
+ /** Learned structured-output shaping for one local endpoint. */
30
+ export class NativeSchemaGate {
31
+ /** Models rejected in this process — the fallback for registry-less clients. */
32
+ private readonly rejectedByProcess = new Set<string>();
33
+
34
+ constructor(private readonly registry: () => ModelRegistry | undefined) {}
35
+
36
+ /** False when the model is known to reject the native json_schema payload. */
37
+ usable(modelId: string): boolean {
38
+ const registry = this.registry();
39
+ // Registry-backed decisions honor the registry TTL: an expired entry is
40
+ // re-probed instead of being honored forever. The in-memory set only
41
+ // serves clients without a registry.
42
+ if (registry) return registry.getStructuredOutput(modelId) !== "none";
43
+ return !this.rejectedByProcess.has(modelId);
44
+ }
45
+
46
+ rememberRejection(modelId: string): void {
47
+ const registry = this.registry();
48
+ if (registry) registry.setStructuredOutput(modelId, "none");
49
+ else this.rejectedByProcess.add(modelId);
50
+ }
51
+
52
+ /** Attach the schema format unless the gate suppresses it for this model. */
53
+ attach(
54
+ requestBody: LocalLlmChatRequestBody,
55
+ options: Pick<LocalLlmChatCompletionOptions, "responseFormat">,
56
+ modelId: string,
57
+ ): void {
58
+ if (options.responseFormat?.type === "json_schema" && this.usable(modelId)) {
59
+ requestBody.response_format = options.responseFormat;
60
+ }
61
+ }
62
+
63
+ /** A schema-carrying 200: persist the native capability once. A CURRENT
64
+ * "none" stands — that success is stale in-flight evidence from before the
65
+ * rejection, and overwriting it would make later requests re-send the
66
+ * rejected schema. */
67
+ recordSuccess(requestBody: LocalLlmChatRequestBody, modelId: string): void {
68
+ const registry = this.registry();
69
+ if (
70
+ !requestBody.response_format ||
71
+ !registry ||
72
+ registry.getStructuredOutput(modelId) !== undefined
73
+ ) {
74
+ return;
75
+ }
76
+ registry.setStructuredOutput(modelId, "native");
77
+ }
78
+ }
package/src/local-llm.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  type LocalLlmChatCompletionOptions,
25
25
  type LocalLlmChatCompletionResult,
26
26
  } from "./local-llm-helpers.js";
27
+ import { NativeSchemaGate, type LocalLlmChatRequestBody } from "./local-llm-native-schema.js";
27
28
  import {
28
29
  isObjectRecord,
29
30
  orderedLocalServers,
@@ -189,6 +190,7 @@ export class LocalLlmClient {
189
190
  private consecutive400s: number = 0;
190
191
  private cooldownUntilMs: number = 0;
191
192
  private modelRegistry?: ModelRegistry;
193
+ private readonly nativeSchemaGate = new NativeSchemaGate(() => this.modelRegistry);
192
194
  private _disableThinking: boolean = false;
193
195
  private readonly requestQueues: Record<LocalLlmRequestPriority, LocalLlmQueuedRequest[]> = {
194
196
  "recall-critical": [],
@@ -887,21 +889,16 @@ export class LocalLlmClient {
887
889
  }
888
890
 
889
891
  const promptChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);
890
- const requestBody: Record<string, unknown> = {
891
- model: options.model ?? this.config.localLlmModel,
892
+ const requestModel = options.model ?? this.config.localLlmModel;
893
+ const requestBody: LocalLlmChatRequestBody = {
894
+ model: requestModel,
892
895
  messages,
893
896
  temperature: options.temperature ?? 0.7,
894
897
  // Use max_tokens consistent with cloud models
895
898
  max_tokens: options.maxTokens ?? 4096,
896
899
  stream: false,
897
900
  };
898
-
899
- // Skip response_format for local LLMs - they don't support json_object type
900
- // The prompts already instruct the model to output JSON
901
- // Only send if it's json_schema type which some local LLMs support
902
- if (options.responseFormat?.type === "json_schema") {
903
- requestBody.response_format = options.responseFormat;
904
- }
901
+ this.nativeSchemaGate.attach(requestBody, options, requestModel);
905
902
 
906
903
  // Suppress thinking/reasoning for operations that benefit from terse,
907
904
  // structured output. Thinking-capable models (Qwen 3.5, Gemma 4,
@@ -1016,6 +1013,7 @@ export class LocalLlmClient {
1016
1013
  requestBody.response_format &&
1017
1014
  isUnsupportedJsonSchemaError(`400 ${responseBody}`)
1018
1015
  ) {
1016
+ this.nativeSchemaGate.rememberRejection(requestModel);
1019
1017
  // Match gateway degradation without extending this attempt's deadline.
1020
1018
  delete requestBody.response_format;
1021
1019
  requestBodyJson = JSON.stringify(requestBody);
@@ -1150,6 +1148,7 @@ export class LocalLlmClient {
1150
1148
  return null;
1151
1149
  }
1152
1150
  this.consecutive400s = 0;
1151
+ this.nativeSchemaGate.recordSuccess(requestBody, requestModel);
1153
1152
 
1154
1153
  const data = JSON.parse(responseBody) as {
1155
1154
  choices?: Array<{
@@ -19,6 +19,8 @@ export interface ModelCapabilities {
19
19
  };
20
20
  typicalOutputTokens: number;
21
21
  source: "huggingface" | "lmstudio" | "manual" | "default";
22
+ /** Learned structured-output shaping (see ModelRegistry.getStructuredOutput). */
23
+ structuredOutput?: "native" | "none";
22
24
  fetchedAt: string;
23
25
  }
24
26
 
@@ -193,6 +195,9 @@ export class ModelRegistry {
193
195
  ...capabilities,
194
196
  modelId: normalizedId,
195
197
  source: "default",
198
+ // Reachable only with a missing or expired cached record, so any
199
+ // learned structuredOutput is stale here — leave it unset and let
200
+ // getStructuredOutput's freshness rules drive the re-probe.
196
201
  fetchedAt: new Date().toISOString(),
197
202
  };
198
203
  this.data.models[normalizedId] = caps;
@@ -214,8 +219,15 @@ export class ModelRegistry {
214
219
  */
215
220
  setCapabilities(modelId: string, capabilities: Omit<ModelCapabilities, "modelId" | "fetchedAt">): void {
216
221
  const normalizedId = this.normalizeModelId(modelId);
222
+ const previous = this.data.models[normalizedId];
217
223
  const caps: ModelCapabilities = {
218
224
  ...capabilities,
225
+ // A replacement record (e.g. fresh LM Studio detection) must not erase a
226
+ // learned structuredOutput shaping — but an EXPIRED decision must not be
227
+ // renewed either: drop it so the re-probe stays due.
228
+ structuredOutput:
229
+ capabilities.structuredOutput ??
230
+ (previous && !this.isCacheExpired(previous) ? previous.structuredOutput : undefined),
219
231
  modelId: normalizedId,
220
232
  fetchedAt: new Date().toISOString(),
221
233
  };
@@ -224,6 +236,34 @@ export class ModelRegistry {
224
236
  log.info(`ModelRegistry: stored capabilities for ${modelId}`);
225
237
  }
226
238
 
239
+ /**
240
+ * Learned structured-output shaping for a model, persisted so fresh
241
+ * runtimes shape requests correctly without re-proving a rejection.
242
+ * Honors the cache TTL: a provider that later gains schema support is
243
+ * re-probed (one degraded request) after the window lapses.
244
+ */
245
+ getStructuredOutput(modelId: string): "native" | "none" | undefined {
246
+ const cached = this.data.models[this.normalizeModelId(modelId)];
247
+ if (!cached || this.isCacheExpired(cached)) return undefined;
248
+ return cached.structuredOutput;
249
+ }
250
+
251
+ /** Merge-and-persist: preserves any previously learned capabilities. */
252
+ setStructuredOutput(modelId: string, mode: "native" | "none"): void {
253
+ const normalizedId = this.normalizeModelId(modelId);
254
+ const base = this.data.models[normalizedId] ?? {
255
+ ...DEFAULT_CAPABILITIES,
256
+ modelId: normalizedId,
257
+ };
258
+ this.data.models[normalizedId] = {
259
+ ...base,
260
+ structuredOutput: mode,
261
+ fetchedAt: new Date().toISOString(),
262
+ };
263
+ this.saveRegistry();
264
+ log.info(`ModelRegistry: stored structuredOutput=${mode} for ${modelId}`);
265
+ }
266
+
227
267
  /**
228
268
  * Calculate optimal input/output sizes for a model
229
269
  * @param maxContextOverride - Optional override for max context (e.g., if LLM server defaults to smaller window)
@@ -32,6 +32,7 @@ import type { BufferDrainClaims, DrainLease } from "../storage/buffer-drain-clai
32
32
  import { BufferDrainRunner } from "./buffer-drain-runner.js";
33
33
  import type { DrainRunnerEvent, DrainSessionOutcome } from "./buffer-drain-runner.js";
34
34
  import type { ExtractionQueueCoordinator } from "./extraction-queue-coordinator.js";
35
+ import type { ProviderBreakerSnapshot } from "./provider-breaker.js";
35
36
 
36
37
  /** What the scheduler needs from its host (structurally satisfied by the
37
38
  * orchestrator wiring); injectable so tests drive ticks deterministically. */
@@ -50,6 +51,13 @@ export interface BacklogSchedulerDeps {
50
51
  key: string,
51
52
  options: { ownerId: string; signal: AbortSignal; onEvent?: (event: DrainRunnerEvent) => void }
52
53
  ): Promise<DrainSessionOutcome>;
54
+ /** Shared provider circuit-breaker snapshot (the same instance the run
55
+ * coordinator gates attempts on). While OPEN inside its cooldown window
56
+ * the scheduler skips submission entirely — claims stay durable, and the
57
+ * breaker's single half-open probe (one attempt through once the window
58
+ * lapses) keeps recovery automatic. Optional: hosts without a breaker
59
+ * never skip. */
60
+ breakerSnapshot?(): ProviderBreakerSnapshot;
53
61
  /** Test seam: monotonic-ish clock for backoff windows. */
54
62
  now?(): number;
55
63
  }
@@ -211,6 +219,18 @@ export class BacklogScheduler {
211
219
  this.ticking = true;
212
220
  const cursorBeforeTick = this.claimCursor;
213
221
  try {
222
+ // Fast bail: a breaker already open must not pay discovery I/O
223
+ // (listPage reads and parses claim bodies on every poll). The
224
+ // submitCandidates guard below still catches a breaker that opens
225
+ // while discovery awaits.
226
+ const breakerBeforeDiscovery = this.deps.breakerSnapshot?.();
227
+ if (
228
+ breakerBeforeDiscovery &&
229
+ breakerBeforeDiscovery.state === "open" &&
230
+ this.now() < breakerBeforeDiscovery.openUntilMs
231
+ ) {
232
+ return;
233
+ }
214
234
  // stop() may land while discovery's I/O is pending: re-check before
215
235
  // submitting anything, and again per candidate.
216
236
  // A discovery failure (EACCES/EIO on the claims dir) must not reject
@@ -269,6 +289,15 @@ export class BacklogScheduler {
269
289
 
270
290
  /** Submit discovery candidates within the queue's concurrency bound. */
271
291
  private submitCandidates(candidates: { key: string; needsClaim: boolean }[]): boolean {
292
+ // Breaker-open churn guard: a full attempt reads the claim, rebuilds the
293
+ // buffer/coordinator and re-renders chunks before the breaker fails it
294
+ // fast — pure heap pressure per poll during a provider stall. Skip while
295
+ // the window is open; once it lapses, submissions resume and the
296
+ // breaker's half-open probe runs as usual.
297
+ const breaker = this.deps.breakerSnapshot?.();
298
+ if (breaker && breaker.state === "open" && this.now() < breaker.openUntilMs) {
299
+ return true;
300
+ }
272
301
  let dropped = false;
273
302
  for (const candidate of candidates) {
274
303
  if (this.stopped || this.inFlight.size >= this.submissionLimit()) {
@@ -475,6 +504,9 @@ export function wireBacklogScheduler(
475
504
  };
476
505
  },
477
506
  drainKey: (key, drainOptions) => runner.drainSession(key, drainOptions),
507
+ // The orchestrator's one shared provider breaker (foreground and every
508
+ // claim-backed drain accumulate toward the same threshold).
509
+ breakerSnapshot: () => orchestrator.getExtractionResilienceStatus().breaker,
478
510
  },
479
511
  { ...options, ownerId }
480
512
  );
@@ -115,10 +115,27 @@ export class ExtractionQueueCoordinator {
115
115
  * Enqueue a task and start the processor when capacity is free. This is
116
116
  * the production entry point — the orchestrator's `queueBufferedExtraction`
117
117
  * builds each task closure and hands it here with its buffer key.
118
+ *
119
+ * Per-key coalescing: a queued-not-started task for key K is superseded by
120
+ * a newer enqueue for K instead of stacking another full turn snapshot
121
+ * behind it (extraction reads live state at run time, so the newest
122
+ * closure is the authoritative one). Keyless tasks are independent work
123
+ * and never coalesce.
118
124
  */
119
125
  enqueue(task: ExtractionQueueTask, options?: ExtractionQueueEnqueueOptions): boolean {
120
126
  if (!this.accepting) return false;
121
- const entry: QueueEntry = { run: task, key: options?.key };
127
+ const key = options?.key;
128
+ if (key !== undefined) {
129
+ const queuedAt = this.queue.findIndex((entry) => entry.key === key);
130
+ if (!options?.front && queuedAt >= 0) {
131
+ this.queue[queuedAt] = { run: task, key };
132
+ return true;
133
+ }
134
+ if (options?.front && queuedAt >= 0) {
135
+ this.queue.splice(queuedAt, 1);
136
+ }
137
+ }
138
+ const entry: QueueEntry = { run: task, key };
122
139
  if (options?.front) {
123
140
  this.preemptRunning(options.key);
124
141
  this.queue.unshift(entry);