mlx-bun 0.2.0 → 0.3.0

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 (364) hide show
  1. package/README.md +71 -588
  2. package/docs/README.md +91 -0
  3. package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
  4. package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
  5. package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
  6. package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
  7. package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
  8. package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
  9. package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
  10. package/docs/design/batching.md +569 -0
  11. package/docs/design/decode-speed-program.md +3178 -169
  12. package/docs/design/dreaming-nightly-pipeline.md +675 -282
  13. package/docs/design/generic-model-support.md +579 -334
  14. package/docs/design/kv-cache.md +680 -0
  15. package/docs/design/orpo-training.md +673 -1239
  16. package/docs/design/speculative-decoding.md +1627 -0
  17. package/docs/design/turboquant.md +2102 -0
  18. package/docs/design/unified-engine-frontier-plan.md +1831 -727
  19. package/docs/design/web-chat-redesign.md +666 -1083
  20. package/docs/planning/PRODUCT_ROADMAP.md +34 -1
  21. package/docs/planning/release-notes-v0.3.0.md +35 -0
  22. package/docs/planning/video-series.md +2 -2
  23. package/docs/reference/benchmarks.md +1162 -0
  24. package/docs/reference/cli.md +394 -159
  25. package/docs/reference/distribution.md +321 -182
  26. package/docs/reference/environment.md +348 -0
  27. package/docs/reference/glossary.md +39 -0
  28. package/docs/reference/library-api.md +566 -168
  29. package/docs/reference/memory.md +227 -64
  30. package/docs/reference/models.md +104 -6
  31. package/docs/reference/server-api.md +886 -425
  32. package/docs/reference/server-config.md +638 -330
  33. package/docs/reference/training.md +393 -254
  34. package/docs/reference/troubleshooting.md +166 -0
  35. package/package.json +9 -4
  36. package/src/audio/decode.ts +2 -2
  37. package/src/audio/features.ts +2 -2
  38. package/src/backends/mlx/autoregressive.ts +108 -0
  39. package/src/backends/mlx/batch-group.ts +1646 -0
  40. package/src/backends/mlx/diffusion.ts +39 -0
  41. package/src/backends/mlx/draft-target.ts +47 -0
  42. package/src/backends/mlx/evaluation.ts +105 -0
  43. package/src/backends/mlx/fill-append.ts +45 -0
  44. package/src/backends/mlx/gateway-binding.ts +71 -0
  45. package/src/backends/mlx/graph.ts +38 -0
  46. package/src/backends/mlx/memory-client.ts +189 -0
  47. package/src/backends/mlx/memory-guard.ts +40 -0
  48. package/src/backends/mlx/methods.ts +90 -0
  49. package/src/backends/mlx/model-prompt.ts +164 -0
  50. package/src/backends/mlx/model-serving.ts +79 -0
  51. package/src/backends/mlx/prefill.ts +33 -0
  52. package/src/backends/mlx/request-policy.ts +20 -0
  53. package/src/backends/mlx/rollback.ts +51 -0
  54. package/src/backends/mlx/serial-executor.ts +273 -0
  55. package/src/backends/mlx/speculative.ts +114 -0
  56. package/src/backends/mlx/state-views.ts +26 -0
  57. package/src/backends/mlx/text-engine.ts +73 -0
  58. package/src/cli.ts +58 -25
  59. package/src/client.ts +66 -0
  60. package/src/config.ts +21 -4
  61. package/src/contracts/completion.ts +16 -0
  62. package/src/contracts/execution.ts +49 -0
  63. package/src/contracts/generation.ts +103 -0
  64. package/src/contracts/host.ts +7 -0
  65. package/src/contracts/jobs.ts +66 -0
  66. package/src/contracts/pi-provider.ts +30 -0
  67. package/src/contracts/pi.ts +259 -0
  68. package/src/contracts/resources.ts +18 -0
  69. package/src/contracts/scheduling.ts +27 -0
  70. package/src/contracts/tsconfig.json +14 -0
  71. package/src/diffusion/diffusion-generate.ts +218 -153
  72. package/src/embed.ts +1 -1
  73. package/src/engine/admission.ts +59 -0
  74. package/src/engine/cancellation.ts +38 -0
  75. package/src/engine/engine.ts +56 -0
  76. package/src/engine/execution-coordinator.ts +70 -0
  77. package/src/engine/execution-plan.ts +51 -0
  78. package/src/engine/resources.ts +42 -0
  79. package/src/engine/scheduler.ts +67 -0
  80. package/src/engine/session.ts +247 -0
  81. package/src/eval/README.md +44 -1
  82. package/src/eval/kl.ts +236 -3
  83. package/src/eval/reference-logits.ts +207 -0
  84. package/src/eval/runner.ts +8 -96
  85. package/src/fill/echo-index.ts +188 -0
  86. package/src/fill/fill-session.ts +452 -0
  87. package/src/fill/proposal.ts +68 -0
  88. package/src/fill/schema-rows.ts +506 -0
  89. package/src/fill/tool-boundary.ts +103 -0
  90. package/src/fit.ts +1 -1
  91. package/src/generate.ts +656 -268
  92. package/src/grammar.ts +12 -7
  93. package/src/harness-pi.ts +14 -33
  94. package/src/hub-rest.ts +4 -4
  95. package/src/index.ts +7 -0
  96. package/src/inference/denoising.ts +17 -0
  97. package/src/inference/graph.ts +33 -0
  98. package/src/inference/prefill.ts +39 -0
  99. package/src/inference/rollback.ts +7 -0
  100. package/src/jobs/db.ts +4 -0
  101. package/src/jobs/index.ts +2 -0
  102. package/src/jobs/job-entry.ts +12 -12
  103. package/src/jobs/runner.ts +98 -13
  104. package/src/jobs/task-client.ts +19 -0
  105. package/src/jobs/types.ts +2 -58
  106. package/src/kv-store.ts +109 -49
  107. package/src/lab/README.md +18 -0
  108. package/src/{assets → lab/curve}/curve-designer.html +2 -2
  109. package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
  110. package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +5 -5
  111. package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
  112. package/src/library.ts +40 -0
  113. package/src/lora.ts +18 -0
  114. package/src/media-fetch.ts +1 -1
  115. package/src/memory/article.ts +1 -1
  116. package/src/memory/chunk.ts +1 -1
  117. package/src/memory/model.ts +30 -169
  118. package/src/memory/prompts.ts +1 -1
  119. package/src/memory/rest.ts +1 -1
  120. package/src/memory/schedule.ts +1 -1
  121. package/src/memory/stages.ts +4 -1
  122. package/src/memory/vault.ts +1 -3
  123. package/src/mlx/array.ts +9 -10
  124. package/src/mlx/compile.ts +12 -2
  125. package/src/mlx/ffi.ts +58 -11
  126. package/src/mlx/materialize.ts +23 -0
  127. package/src/mlx/metal-kernel.ts +10 -11
  128. package/src/mlx/ops.ts +33 -13
  129. package/src/mlx/quantized-rows.ts +36 -0
  130. package/src/mlx/token-bitmask.ts +52 -0
  131. package/src/mlx/turboquant-kv-decode.ts +82 -0
  132. package/src/mlx/turboquant-kv-inverse.ts +122 -0
  133. package/src/mlx/turboquant-ops.ts +33 -3
  134. package/src/mlx/turboquant-tables.ts +2 -2
  135. package/src/model/batched-mask.ts +8 -2
  136. package/src/model/batched-quant.ts +1 -1
  137. package/src/model/batched-rotating-quant.ts +2 -2
  138. package/src/model/batched-rotating.ts +1 -1
  139. package/src/model/compiled-decode.ts +1 -1
  140. package/src/model/diffusion-gemma.ts +8 -8
  141. package/src/model/factory.ts +76 -33
  142. package/src/model/fingerprint.ts +1 -1
  143. package/src/model/flash-attention.ts +3 -3
  144. package/src/model/gemma4-base.ts +26 -9
  145. package/src/model/gemma4.ts +6 -6
  146. package/src/model/generated/gemma4-12b.ts +2 -2
  147. package/src/model/generated/gemma4-26b.ts +2 -2
  148. package/src/model/generated/gemma4-e4b.ts +2 -2
  149. package/src/model/glm52-dsa.ts +13 -9
  150. package/src/model/glm52-memory.ts +1 -1
  151. package/src/model/implementation.ts +69 -0
  152. package/src/model/minicpm5.ts +6 -8
  153. package/src/model/profile.ts +12 -2
  154. package/src/model/qwen-append.ts +12 -0
  155. package/src/model/qwen-conv.ts +52 -0
  156. package/src/model/qwen3_5-checkpoint.ts +164 -0
  157. package/src/model/qwen3_5.ts +140 -56
  158. package/src/model/support.ts +1 -1
  159. package/src/model/trellis-balanced-scatter.ts +51 -0
  160. package/src/model/trellis-linear.ts +672 -0
  161. package/src/model/trellis-shared-m.ts +151 -0
  162. package/src/model/trellis-shared-scatter.ts +144 -0
  163. package/src/model/trellis-splitk-prefill.ts +158 -0
  164. package/src/model/trellis-tiled-prefill.ts +136 -0
  165. package/src/model/trellis-vector-expand.ts +62 -0
  166. package/src/model/trellis-wide-prefill.ts +94 -0
  167. package/src/model/universal/dense.ts +1 -1
  168. package/src/model/universal/modules.ts +8 -1
  169. package/src/native-pack.ts +6 -5
  170. package/src/pi-provider.ts +6 -64
  171. package/src/pi-web.ts +8 -315
  172. package/src/prompt-cache.ts +96 -53
  173. package/src/quantize/allocator.ts +1 -1
  174. package/src/quantize/config-writer.ts +10 -3
  175. package/src/quantize/rotate.ts +4 -4
  176. package/src/quantize/trellis.ts +586 -0
  177. package/src/responses.ts +28 -1
  178. package/src/runtime-config.ts +18 -4
  179. package/src/sampler.ts +4 -4
  180. package/src/serve/batch-scheduler.ts +8 -1671
  181. package/src/serve/chat-request.ts +391 -0
  182. package/src/serve/chat-stage.ts +195 -0
  183. package/src/serve/checkpoint-identity.ts +64 -0
  184. package/src/serve/completion-executor.ts +15 -7
  185. package/src/serve/discovery-routes.ts +12 -13
  186. package/src/serve/generation-gateway.ts +154 -193
  187. package/src/serve/http.ts +159 -0
  188. package/src/serve/inference-request.ts +79 -0
  189. package/src/serve/isolate.ts +297 -105
  190. package/src/serve/lab-routes.ts +3 -0
  191. package/src/serve/lane-registry.ts +2 -1
  192. package/src/serve/model-admin-routes.ts +7 -5
  193. package/src/serve/model-binding.ts +43 -0
  194. package/src/serve/model-host.ts +512 -0
  195. package/src/serve/openai-wire.ts +103 -0
  196. package/src/serve/parent-application.ts +56 -0
  197. package/src/serve/pipeline.ts +33 -0
  198. package/src/serve/preparation.ts +33 -0
  199. package/src/serve/request-plan.ts +19 -15
  200. package/src/serve/request-prep.ts +346 -0
  201. package/src/serve/response-lease.ts +24 -0
  202. package/src/serve/responses-client.ts +69 -0
  203. package/src/serve/session-completion-engine.ts +110 -0
  204. package/src/serve/session-search.ts +1 -1
  205. package/src/serve/static-routes.ts +5 -5
  206. package/src/serve/text-completion-stage.ts +104 -0
  207. package/src/serve/token-streams.ts +519 -0
  208. package/src/server.ts +317 -2649
  209. package/src/spec/assistant-source.ts +10 -44
  210. package/src/spec/deepspec-source.ts +6 -6
  211. package/src/spec/dflash-source.ts +8 -8
  212. package/src/spec/dspark/ab-stats.ts +3 -3
  213. package/src/spec/dspark/calibration.ts +2 -2
  214. package/src/spec/dspark/deepspec-module.ts +2 -2
  215. package/src/spec/dspark/generate-dflash.ts +2 -2
  216. package/src/spec/dspark/generate.ts +2 -2
  217. package/src/spec/dspark/loader.ts +1 -1
  218. package/src/spec/dspark/module-dflash.ts +4 -3
  219. package/src/spec/dspark/module.ts +3 -3
  220. package/src/spec/dspark/quantize-drafter.ts +1 -1
  221. package/src/spec/generate.ts +1 -1
  222. package/src/spec/glm52-mtp-source.ts +1 -1
  223. package/src/spec/ngram-source.ts +1 -1
  224. package/src/spec/prefix-state.ts +45 -0
  225. package/src/spec/qwen-mtp-source.ts +166 -69
  226. package/src/spec/serve-loop.ts +154 -156
  227. package/src/spec/source.ts +42 -10
  228. package/src/spec/two-model.ts +1 -1
  229. package/src/ssd-cache.ts +80 -12
  230. package/src/ssd-durability.ts +25 -13
  231. package/src/tool-call.ts +51 -14
  232. package/src/train/flash-cce.ts +2 -2
  233. package/src/train/forward.ts +6 -0
  234. package/src/train/loss.ts +1 -1
  235. package/src/train/prefix-shared.ts +5 -1
  236. package/src/train/segmented.ts +5 -3
  237. package/src/train/trainer.ts +6 -6
  238. package/src/vision/prompt.ts +1 -1
  239. package/src/vision/qwen3vl-tower.ts +52 -10
  240. package/src/web/app.html +1 -1
  241. package/src/web/app.js +5 -3
  242. package/src/web/src/adapters-panel.ts +3 -3
  243. package/src/web/src/assistant.ts +2 -36
  244. package/src/web/src/chat.ts +4 -4
  245. package/src/web/src/composer.ts +4 -4
  246. package/src/web/src/hub.ts +1 -1
  247. package/src/web/src/markdown.ts +0 -0
  248. package/src/web/src/memory-panel.ts +1 -1
  249. package/src/web/src/model-picker.ts +1 -1
  250. package/src/web/src/palette.ts +1 -1
  251. package/src/web/src/protocol.ts +3 -39
  252. package/src/web/src/sessions.ts +1 -1
  253. package/src/web/src/ui-catalog.ts +4 -18
  254. package/src/web-assets.ts +47 -0
  255. package/src/weights.ts +55 -9
  256. package/docs/archive/investigations/curve-runs/bisector-route.json +0 -98
  257. package/docs/archive/investigations/curve-runs/bisector-trace.json +0 -1393
  258. package/docs/archive/investigations/curve-runs/curve-terrain.json +0 -1179
  259. package/docs/archive/investigations/curve-runs/curve-terrain.svg +0 -42
  260. package/docs/archive/investigations/hlg-ab.html +0 -34
  261. package/docs/archive/investigations/hlg-explorer.html +0 -238
  262. package/docs/archive/investigations/hlg-figs/cliff.svg +0 -1
  263. package/docs/archive/investigations/hlg-figs/cross.svg +0 -1
  264. package/docs/archive/investigations/hlg-figs/edges.svg +0 -1
  265. package/docs/archive/investigations/hlg-figs/k20-grids.svg +0 -1
  266. package/docs/archive/investigations/hlg-figs/k20-peaks.svg +0 -1
  267. package/docs/archive/investigations/hlg-figs/lw-curve.svg +0 -1
  268. package/docs/archive/investigations/hlg-figs/terrain.svg +0 -1
  269. package/docs/archive/investigations/hlg-report.html +0 -262
  270. package/docs/archive/investigations/hlg-runs/hlg-ab.json +0 -149
  271. package/docs/archive/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
  272. package/docs/archive/investigations/hlg-runs/hlg-blind.json +0 -107
  273. package/docs/archive/investigations/hlg-runs/hlg-cliff.json +0 -197
  274. package/docs/archive/investigations/hlg-runs/hlg-cross.json +0 -505
  275. package/docs/archive/investigations/hlg-runs/hlg-edge.json +0 -388
  276. package/docs/archive/investigations/hlg-runs/hlg-eval1.json +0 -25
  277. package/docs/archive/investigations/hlg-runs/hlg-frontier.json +0 -86
  278. package/docs/archive/investigations/hlg-runs/hlg-grid.json +0 -242
  279. package/docs/archive/investigations/hlg-runs/hlg-grid2.json +0 -343
  280. package/docs/archive/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
  281. package/docs/archive/investigations/hlg-runs/hlg-map.json +0 -383
  282. package/docs/archive/investigations/hlg-runs/run0-original.log +0 -159
  283. package/docs/archive/investigations/hlg-runs/runA-recommended.log +0 -131
  284. package/docs/archive/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
  285. package/docs/archive/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
  286. package/docs/archive/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
  287. package/docs/archive/investigations/hlg-runs/runAD-ab-samples.log +0 -8
  288. package/docs/archive/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
  289. package/docs/archive/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
  290. package/docs/archive/investigations/hlg-runs/runAG-eval-config2.log +0 -16
  291. package/docs/archive/investigations/hlg-runs/runB-gain-variants.log +0 -192
  292. package/docs/archive/investigations/hlg-runs/runC-pivots.log +0 -162
  293. package/docs/archive/investigations/hlg-runs/runD-oetf.log +0 -132
  294. package/docs/archive/investigations/hlg-runs/runE-eotf.log +0 -162
  295. package/docs/archive/investigations/hlg-runs/runF-shaper-working.log +0 -86
  296. package/docs/archive/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
  297. package/docs/archive/investigations/hlg-runs/runH-wsweep.log +0 -176
  298. package/docs/archive/investigations/hlg-runs/runI-asweep.log +0 -176
  299. package/docs/archive/investigations/hlg-runs/runJ-knobmap.log +0 -49
  300. package/docs/archive/investigations/hlg-runs/runK-frontier.log +0 -19
  301. package/docs/archive/investigations/hlg-runs/runL-widemap.log +0 -65
  302. package/docs/archive/investigations/hlg-runs/runM-frontier-emb.log +0 -15
  303. package/docs/archive/investigations/hlg-runs/runN-verify.log +0 -116
  304. package/docs/archive/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
  305. package/docs/archive/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
  306. package/docs/archive/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
  307. package/docs/archive/investigations/hlg-runs/runR-blind-transcript.log +0 -79
  308. package/docs/archive/investigations/hlg-runs/runS-cross-coherence.log +0 -33
  309. package/docs/archive/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
  310. package/docs/archive/investigations/hlg-runs/runU-cross-12b.log +0 -33
  311. package/docs/archive/investigations/hlg-runs/runV-lw-e4b.log +0 -22
  312. package/docs/archive/investigations/hlg-runs/runW-lw-12b.log +0 -22
  313. package/docs/archive/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
  314. package/docs/archive/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
  315. package/docs/archive/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
  316. package/docs/archive/mlx-bun-lab-report.html +0 -275
  317. package/docs/archive/wwdc-mlx-bun.html +0 -848
  318. package/docs/design/adapters-end-to-end.md +0 -205
  319. package/docs/design/batching-perf-path.md +0 -255
  320. package/docs/design/batching-v2-plan.md +0 -529
  321. package/docs/design/bucketing-stage.md +0 -197
  322. package/docs/design/compat-cli-surface-design.md +0 -230
  323. package/docs/design/docs-reorg-plan.md +0 -165
  324. package/docs/design/dspark-serving-program.md +0 -371
  325. package/docs/design/dspark-speculative-decoding.md +0 -318
  326. package/docs/design/grammar-spec-batching-integration.md +0 -301
  327. package/docs/design/memory-inference-path.md +0 -422
  328. package/docs/design/memory-synthesis.md +0 -287
  329. package/docs/design/memory-system.md +0 -237
  330. package/docs/design/mlx-lm-serving-execution-seam.md +0 -81
  331. package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
  332. package/docs/design/omlx-adoption-map.md +0 -98
  333. package/docs/design/orpo-dynamic-lambda.md +0 -220
  334. package/docs/design/orpo-future-enhancements.md +0 -340
  335. package/docs/design/paged-kv-cache.md +0 -214
  336. package/docs/design/parallel-slots.md +0 -567
  337. package/docs/design/parity-tier-dag.md +0 -161
  338. package/docs/design/repo-cleanup-plan.md +0 -231
  339. package/docs/design/runtime-isolation.md +0 -263
  340. package/docs/design/segmented-backward-training.md +0 -543
  341. package/docs/design/spec-decode-larger-targets.md +0 -229
  342. package/docs/design/ssd-kv-cold-tier.md +0 -415
  343. package/docs/design/structured-output.md +0 -484
  344. package/docs/design/tauri-desktop-app.md +0 -127
  345. package/docs/design/the-dreaming-master-plan.md +0 -445
  346. package/docs/design/turboquant-kv.md +0 -235
  347. package/docs/design/turboquant-weights.md +0 -467
  348. package/docs/design/w4a16-compute-precision-spike.md +0 -232
  349. package/docs/design/web-chat-beat-matrix.md +0 -315
  350. package/docs/design/write-pipeline-entity.md +0 -468
  351. package/docs/planning/IDEAS.md +0 -69
  352. package/docs/reference/embedding.md +0 -197
  353. package/docs/reference/features-matrix.md +0 -101
  354. package/docs/reference/orpo-quickstart.md +0 -135
  355. package/src/flags.ts +0 -11
  356. /package/docs/{design → archive/investigations}/colibri-glm52-port.md +0 -0
  357. /package/docs/{planning → archive/planning}/chunk-task-roadmap.md +0 -0
  358. /package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +0 -0
  359. /package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +0 -0
  360. /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
  361. /package/docs/{planning → archive/planning}/release-notes-v0.2.0.md +0 -0
  362. /package/src/{memory-plan.ts → admission-plan.ts} +0 -0
  363. /package/{docs/archive/investigations → src/serve/assets}/curve-terrain.html +0 -0
  364. /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
@@ -0,0 +1,188 @@
1
+ // The echo source (K3c, Lab tier) — a growing per-request k-gram index over
2
+ // the session's own tokens, and the branch-point rule that decides how far a
3
+ // copied span may run.
4
+ //
5
+ // PORT. The index is the TS port of `GrowingMatcher` from the corpus study
6
+ // (reports/k3-replication/analyze.py): a sequence plus a k-gram → [start
7
+ // positions] map, appended incrementally so a lookup never rescans the prefix,
8
+ // with the bucket scan capped at the NEAREST candidates. The corpus tool ran
9
+ // it offline over recorded turns to measure how much of an agent's output is
10
+ // copy-determined; this runs the same structure online, over promptIds plus
11
+ // everything emitted (injected tokens included — the model cannot tell them
12
+ // apart, so neither does the index).
13
+ //
14
+ // THE INTERESTING PART is not the lookup, it is the STOPPING RULE. A match
15
+ // tells you where this exact k-gram occurred before; it does not tell you how
16
+ // far the future agrees with the past. So the span extends only while EVERY
17
+ // nearby occurrence continues the same way. The moment two occurrences
18
+ // disagree — the session history forks — the span stops. That is where
19
+ // old-query-vs-new-query divergence lives, and delimiters fall out of it for
20
+ // free: a closing quote is where histories fork.
21
+ //
22
+ // DOCTRINE. An echo span is a GUESS, unlike the schema rows. It ships under
23
+ // policy "verify" unless it is unambiguous all the way to a delimiter-class
24
+ // token — the engine then keeps only the prefix the model itself agrees with.
25
+ // Lab tier: default off, and a paired A/B decides whether it ever becomes a
26
+ // default (PLAN K3).
27
+ import type { Proposal, ProposalSource, TokenView } from "./proposal";
28
+
29
+ export interface EchoConfig {
30
+ /** Anchor length: the k-gram that has to match for a span to be proposed. */
31
+ k: number;
32
+ /** Longest span the source will propose (the session clamps again). */
33
+ maxSpan: number;
34
+ /** Bucket scan cap — the NEAREST occurrences win (recency is the signal;
35
+ * an unbounded scan is also the only quadratic risk here). */
36
+ maxCandidates: number;
37
+ /** Stop growing the index past this many tokens (memory cap). */
38
+ indexMax: number;
39
+ /** Token ids that legitimately END a copied span (a closing quote, the
40
+ * markup that follows an argument value). Derived from the template by the
41
+ * strict-row compiler; empty means "no span can be asserted". */
42
+ delimiters: ReadonlySet<number>;
43
+ }
44
+
45
+ /** Growing k-gram index over one request's token sequence. */
46
+ export class EchoIndex {
47
+ readonly #k: number;
48
+ readonly #indexMax: number;
49
+ readonly #seq: number[] = [];
50
+ /** hash(k-gram) → start positions, oldest first. Hash collisions are
51
+ * harmless: every candidate's gram is compared id-for-id before use. */
52
+ readonly #buckets = new Map<number, number[]>();
53
+ #frozen = false;
54
+
55
+ constructor(k: number, indexMax: number) {
56
+ this.#k = Math.max(1, Math.floor(k));
57
+ this.#indexMax = Math.max(this.#k, Math.floor(indexMax));
58
+ }
59
+
60
+ get length(): number { return this.#seq.length; }
61
+ /** True once the memory cap stopped the index from growing. */
62
+ get frozen(): boolean { return this.#frozen; }
63
+
64
+ append(ids: readonly number[]): void {
65
+ for (const id of ids) {
66
+ if (this.#seq.length >= this.#indexMax) { this.#frozen = true; return; }
67
+ this.#seq.push(id);
68
+ const start = this.#seq.length - this.#k;
69
+ if (start < 0) continue;
70
+ const h = this.#hash(start);
71
+ const bucket = this.#buckets.get(h);
72
+ if (bucket) bucket.push(start);
73
+ else this.#buckets.set(h, [start]);
74
+ }
75
+ }
76
+
77
+ /** Start positions whose k-gram equals `gram` AND which have at least one
78
+ * token after it, nearest (most recent) first. */
79
+ candidates(gram: readonly number[], cap: number): number[] {
80
+ if (gram.length !== this.#k) return [];
81
+ let h = 0x811c9dc5;
82
+ for (const id of gram) h = this.#mix(h, id);
83
+ const bucket = this.#buckets.get(h >>> 0);
84
+ if (!bucket) return [];
85
+ const out: number[] = [];
86
+ for (let i = bucket.length - 1; i >= 0 && out.length < cap; i--) {
87
+ const pos = bucket[i]!;
88
+ if (pos + this.#k >= this.#seq.length) continue; // no continuation yet
89
+ let ok = true;
90
+ for (let j = 0; j < this.#k; j++) {
91
+ if (this.#seq[pos + j] !== gram[j]) { ok = false; break; } // collision
92
+ }
93
+ if (ok) out.push(pos);
94
+ }
95
+ return out;
96
+ }
97
+
98
+ /** Extend forward from the given anchors while EVERY anchor agrees.
99
+ * Returns the agreed continuation, whether it stopped at a fork, and how
100
+ * many occurrences corroborated the WHOLE span (survivors at the last
101
+ * accepted token) — one occurrence is a copy, several are a pattern. */
102
+ extend(
103
+ anchors: readonly number[], maxSpan: number, delimiters: ReadonlySet<number>,
104
+ ): { ids: number[]; branchStop: boolean; agreed: number } {
105
+ const ids: number[] = [];
106
+ let agreed = 0;
107
+ let live = anchors.map((p) => p + this.#k);
108
+ while (ids.length < maxSpan) {
109
+ let next: number | undefined;
110
+ let forked = false;
111
+ const stillLive: number[] = [];
112
+ for (const p of live) {
113
+ const v = this.#seq[p];
114
+ // An occurrence that ran out of history simply drops out: a prefix
115
+ // that ends is not evidence of disagreement.
116
+ if (v === undefined) continue;
117
+ if (next === undefined) next = v;
118
+ else if (v !== next) { forked = true; break; }
119
+ stillLive.push(p + 1);
120
+ }
121
+ // A fork means the session's own history disagrees about what comes
122
+ // next. That is exactly the position the model must decide for itself.
123
+ if (forked) return { ids, branchStop: true, agreed };
124
+ if (next === undefined) break; // every occurrence exhausted
125
+ ids.push(next);
126
+ agreed = stillLive.length;
127
+ live = stillLive;
128
+ // A delimiter is a legitimate END: injecting the closing quote is the
129
+ // point, continuing past it is not.
130
+ if (delimiters.has(next)) break;
131
+ }
132
+ return { ids, branchStop: false, agreed };
133
+ }
134
+
135
+ #hash(start: number): number {
136
+ let h = 0x811c9dc5;
137
+ for (let j = 0; j < this.#k; j++) h = this.#mix(h, this.#seq[start + j]!);
138
+ return h >>> 0;
139
+ }
140
+
141
+ #mix(h: number, id: number): number {
142
+ let x = h ^ id;
143
+ x = Math.imul(x, 0x01000193);
144
+ return x >>> 0;
145
+ }
146
+ }
147
+
148
+ /** ProposalSource over the echo index. */
149
+ export class EchoSource implements ProposalSource {
150
+ readonly name = "echo";
151
+ readonly windowNeeded: number;
152
+ readonly index: EchoIndex;
153
+
154
+ constructor(private readonly config: EchoConfig, promptIds: readonly number[]) {
155
+ this.windowNeeded = config.k;
156
+ this.index = new EchoIndex(config.k, config.indexMax);
157
+ this.index.append(promptIds);
158
+ }
159
+
160
+ observe(ids: readonly number[]): void {
161
+ this.index.append(ids);
162
+ }
163
+
164
+ propose(view: TokenView): Proposal | null {
165
+ const { k, maxSpan, maxCandidates, delimiters } = this.config;
166
+ const gram = view.tail(k);
167
+ if (gram.length < k) return null;
168
+ const anchors = this.index.candidates(gram, maxCandidates);
169
+ if (anchors.length === 0) return null;
170
+ const { ids, branchStop, agreed } = this.index.extend(anchors, maxSpan, delimiters);
171
+ if (ids.length < 2) return null;
172
+ // POLICY. Assert only with CORROBORATION: several occurrences of this
173
+ // context all continued the same way, nothing forked, and the span stops
174
+ // at a delimiter-class token. A single occurrence is just a copy — and a
175
+ // copy will happily replay whatever followed it in the transcript,
176
+ // including another role's turn, so it has to be the model's call.
177
+ // Everything else rides policy "verify", where a wrong guess costs a
178
+ // rewound forward, never a wrong token.
179
+ const endsAtDelimiter = delimiters.has(ids[ids.length - 1]!);
180
+ const corroborated = agreed >= 2;
181
+ return {
182
+ ids,
183
+ policy: !branchStop && endsAtDelimiter && corroborated ? "assert" : "verify",
184
+ origin: "echo",
185
+ branchStop,
186
+ };
187
+ }
188
+ }
@@ -0,0 +1,452 @@
1
+ // Token fast-forwarding (K3) — the per-request proposal table.
2
+ //
3
+ // DOCTRINE. This is PURE CONTEXT EXTENSION, not speculation. A transformer is
4
+ // a next-token function over a token sequence; it has no memory of who wrote
5
+ // which token, so an APPENDED token is indistinguishable from a sampled one.
6
+ // When the engine already knows the next m tokens (a chat template's tool-call
7
+ // scaffold is a deterministic function of the request's `tools`), it appends
8
+ // them itself in one append and resumes sampling after them. The append can
9
+ // use several execution chunks while retaining the complete span. No
10
+ // draft, no verify, no rollback, and no comparison against what the model
11
+ // "would have" produced — nothing here routes through src/spec/.
12
+ //
13
+ // The echo tier (K3c) adds a weaker claim over the SAME mechanism: a span
14
+ // copied from earlier in the session, held under policy "verify" — the engine
15
+ // keeps the prefix the model agrees with (read from the free logits of the
16
+ // very same forward) and rewinds the rest. See ./proposal.ts for the two
17
+ // policies and ./echo-index.ts for the branch-point rule.
18
+ //
19
+ // This module is bookkeeping only: it owns the running history, runs the
20
+ // sources in priority order, clamps what they return, and counts. It never
21
+ // touches MLX, the tokenizer, or the template. Row COMPILATION is
22
+ // ./schema-rows.ts.
23
+ import { flagOn, runtimeNumber, runtimeValue } from "../runtime-config";
24
+ import { EchoSource, type EchoConfig } from "./echo-index";
25
+ import type {
26
+ Proposal, ProposalOrigin, ProposalPolicy, ProposalSource, TokenView,
27
+ } from "./proposal";
28
+
29
+ export type { Proposal, ProposalPolicy, ProposalSource, TokenView };
30
+
31
+ /** Which producer a strict row came from (telemetry + the mismatch policy). */
32
+ export type FillKind = "scaffold" | "name" | "key" | "close" | "turn-end";
33
+
34
+ export interface FillRow {
35
+ /** Token ids that must be the TAIL of the generated history for this row to
36
+ * fire (matched as an exact suffix ending at the token just pushed). */
37
+ trigger: number[];
38
+ /** Ids appended immediately after the trigger. Sliced out of a full
39
+ * template rendering — never `encode(fragment)` in isolation. */
40
+ emit: number[];
41
+ kind: FillKind;
42
+ /** Template-derived rows need a parser-owned context before asserting. */
43
+ requiresContext?: true;
44
+ }
45
+
46
+ export interface StrictFillContext {
47
+ observe(ids: readonly number[]): void;
48
+ allows(row: FillRow): boolean;
49
+ }
50
+
51
+ /** Echo-tier (K3c) configuration; null disables the source entirely. */
52
+ export interface FillEchoConfig {
53
+ k: number;
54
+ maxSpan: number;
55
+ maxCandidates: number;
56
+ indexMax: number;
57
+ }
58
+
59
+ export interface FillPlan {
60
+ rows: FillRow[];
61
+ /** Echo tier: null = strict rows only (MLX_BUN_FILL=strict). */
62
+ echo: FillEchoConfig | null;
63
+ /** Token ids that legitimately END an injected span — the closing quote /
64
+ * markup that follows an argument value, derived from the template by the
65
+ * strict-row compiler. A span is cut so it ends AT the first delimiter it
66
+ * contains, never past one. */
67
+ delimiters?: ReadonlySet<number>;
68
+ /** EOS ids. An injected span is cut BEFORE the first one: end-of-generation
69
+ * is the model's decision, never ours. (The grammar jump burst yields its
70
+ * ids with NO eos check — that bug is deliberately not inherited.) */
71
+ eos: number[];
72
+ }
73
+
74
+ export interface FillStats {
75
+ /** Fills that put at least one token into the stream. */
76
+ events: number;
77
+ /** Total injected tokens (billed as generated). */
78
+ injected: number;
79
+ /** …from strict schema/template rows (always policy "assert"). */
80
+ strict: number;
81
+ /** …from the echo index. */
82
+ echo: number;
83
+ /** Length of each injected span, in event order. */
84
+ spanLens: number[];
85
+ /** In-flight sampled tokens dropped unexamined at an ASSERT fill — a
86
+ * discarded pipeline dispatch, NOT a rejected draft. */
87
+ wastedSamples: number;
88
+ /** Tool-call parses that failed after strict rows had fired. */
89
+ parseFallback: number;
90
+ /** Spans shortened by a clamp (EOS / delimiter / max span / budget). */
91
+ indexTruncated: number;
92
+ /** Tokens the model actually sampled (the real decode steps). */
93
+ decodeSteps: number;
94
+ /** Verify-policy proposals that reached the forward. */
95
+ verifyEvents: number;
96
+ /** Tokens accepted under verify (the model's own argmax agreed). */
97
+ verifyAccepted: number;
98
+ /** Tokens proposed under verify and dropped (incl. rejected-at-position-0,
99
+ * which costs nothing — that check happens before the forward). */
100
+ verifyRejected: number;
101
+ /** Verify proposals dropped because this model's caches cannot rewind. */
102
+ verifyUnsupported: number;
103
+ /** Wall time in cache checkpoint / rollback / commit (verify only). */
104
+ checkpointMs: number;
105
+ /** Echo spans that stopped because the session history forked. */
106
+ branchStops: number;
107
+ }
108
+
109
+ const emptyStats = (): FillStats => ({
110
+ events: 0, injected: 0, strict: 0, echo: 0, spanLens: [],
111
+ wastedSamples: 0, parseFallback: 0, indexTruncated: 0, decodeSteps: 0,
112
+ verifyEvents: 0, verifyAccepted: 0, verifyRejected: 0, verifyUnsupported: 0,
113
+ checkpointMs: 0, branchStops: 0,
114
+ });
115
+
116
+ export type FillMode = "off" | "strict" | "echo";
117
+
118
+ /** MLX_BUN_FILL=off|strict|echo (default off). `echo` is additive: the strict
119
+ * rows still apply (and stay policy "assert"); the echo index joins as a
120
+ * second, weaker source. Lab tier — default off, A/B before any default. */
121
+ export function resolveFillMode(raw = runtimeValue("MLX_BUN_FILL")): FillMode {
122
+ if (raw === undefined || raw === "" || raw === "off" || raw === "0") return "off";
123
+ if (raw === "strict" || raw === "1") return "strict";
124
+ if (raw === "echo") return "echo";
125
+ throw new Error(`MLX_BUN_FILL=${raw}: expected off|strict|echo`);
126
+ }
127
+
128
+ /** Hard cap on one injected span (MLX_BUN_FILL_MAX_SPAN, default 32). */
129
+ export function fillMaxSpan(): number {
130
+ const n = Math.floor(runtimeNumber("MLX_BUN_FILL_MAX_SPAN", 32));
131
+ return n >= 2 ? n : 2;
132
+ }
133
+
134
+ /** Assert-append chunk cap; zero selects the model's qualified limit. */
135
+ export function fillAppendChunkSize(): number {
136
+ return Math.max(0, Math.floor(runtimeNumber("MLX_BUN_FILL_APPEND_CHUNK_SIZE", 0)));
137
+ }
138
+
139
+ /** Echo-tier knobs. k: anchor length (MLX_BUN_FILL_K, default 8 — the corpus
140
+ * study's token-level threshold). candidates: nearest-occurrence bucket cap
141
+ * (MLX_BUN_FILL_CANDIDATES, default 24). indexMax: token cap on the growing
142
+ * index (MLX_BUN_FILL_INDEX_MAX, default 131072). */
143
+ export function fillEchoConfig(): FillEchoConfig {
144
+ return {
145
+ k: Math.max(2, Math.floor(runtimeNumber("MLX_BUN_FILL_K", 8))),
146
+ maxSpan: fillMaxSpan(),
147
+ maxCandidates: Math.max(1, Math.floor(runtimeNumber("MLX_BUN_FILL_CANDIDATES", 24))),
148
+ indexMax: Math.max(1024, Math.floor(runtimeNumber("MLX_BUN_FILL_INDEX_MAX", 131072))),
149
+ };
150
+ }
151
+
152
+ /** MLX_BUN_FILL_TRACE=1 — cache-alignment assertions + per-event logging on
153
+ * the engine's fill path. Off by default (the invariant it checks is the
154
+ * easiest bug in the feature, and the check costs a JS compare). */
155
+ export function fillTraceEnabled(): boolean {
156
+ return flagOn("MLX_BUN_FILL_TRACE", false) || fillTracePath() !== null;
157
+ }
158
+
159
+ /** MLX_BUN_FILL_TRACE=<file.jsonl> — additionally append one record per
160
+ * proposal: the proposed ids/text next to what the model's OWN logits said
161
+ * at every span position (position 0 = the in-flight sample, j>0 = argmax
162
+ * after ids[j-1]), for BOTH policies. Under `assert` the readback is
163
+ * trace-only (one lm_head over the span) — the served path never pays it.
164
+ * This is the list that answers "would the model have produced this span?"
165
+ * without a verify pass in production. */
166
+ export function fillTracePath(): string | null {
167
+ const raw = runtimeValue("MLX_BUN_FILL_TRACE");
168
+ if (raw === undefined || raw === "" || raw === "0" || raw === "1" || raw === "true") return null;
169
+ return raw;
170
+ }
171
+
172
+ export interface FillTraceRecord {
173
+ ts: string;
174
+ origin: string;
175
+ policy: string;
176
+ generated: number;
177
+ proposedLen: number;
178
+ accepted: number;
179
+ /** Index of the first position where the model disagreed, or -1. */
180
+ firstMismatch: number;
181
+ proposed: number[];
182
+ actual: number[];
183
+ proposedText?: string;
184
+ actualText?: string;
185
+ }
186
+
187
+ /** Strict schema/template rows as a ProposalSource. Always policy "assert":
188
+ * the spans are sliced out of the model's own chat template, so they are
189
+ * determined by construction, not guessed. */
190
+ export class StrictRowSource implements ProposalSource {
191
+ readonly name = "strict";
192
+ readonly #byLastTrigger = new Map<number, FillRow[]>();
193
+ /** Longest trigger — the tail this source needs to match against. */
194
+ readonly maxTrigger: number = 1;
195
+ get windowNeeded(): number { return this.maxTrigger; }
196
+ #enabled = true;
197
+
198
+ constructor(rows: readonly FillRow[], readonly context?: StrictFillContext) {
199
+ for (const row of rows) {
200
+ if (row.trigger.length === 0 || row.emit.length === 0) continue;
201
+ this.maxTrigger = Math.max(this.maxTrigger, row.trigger.length);
202
+ const last = row.trigger[row.trigger.length - 1]!;
203
+ const bucket = this.#byLastTrigger.get(last);
204
+ if (bucket) bucket.push(row);
205
+ else this.#byLastTrigger.set(last, [row]);
206
+ }
207
+ // Longest trigger wins (most specific row); plan order breaks ties.
208
+ for (const bucket of this.#byLastTrigger.values())
209
+ bucket.sort((a, b) => b.trigger.length - a.trigger.length);
210
+ }
211
+
212
+ get enabled(): boolean { return this.#enabled; }
213
+ disable(): void { this.#enabled = false; }
214
+
215
+ observe(ids: readonly number[]): void { this.context?.observe(ids); }
216
+
217
+ propose(view: TokenView): Proposal | null {
218
+ if (!this.#enabled) return null;
219
+ const tail = view.tail(this.maxTrigger);
220
+ const last = tail[tail.length - 1];
221
+ if (last === undefined) return null;
222
+ const bucket = this.#byLastTrigger.get(last);
223
+ if (!bucket) return null;
224
+ for (const row of bucket) {
225
+ const t = row.trigger;
226
+ if (t.length > tail.length) continue;
227
+ const base = tail.length - t.length;
228
+ let ok = true;
229
+ for (let i = 0; i < t.length; i++) {
230
+ if (tail[base + i] !== t[i]) { ok = false; break; }
231
+ }
232
+ if (ok && (!row.requiresContext || this.context?.allows(row))) {
233
+ return {
234
+ ids: [...row.emit],
235
+ policy: "assert",
236
+ // The scaffold and the close come from the TEMPLATE; the name and
237
+ // key spans are what the request's SCHEMA determines.
238
+ origin: row.kind === "name" || row.kind === "key" ? "schema" : "template",
239
+ };
240
+ }
241
+ }
242
+ return null;
243
+ }
244
+ }
245
+
246
+ /** One request's proposal sources plus the running match state.
247
+ *
248
+ * CONTRACT. push() returns a proposal; the caller MUST then call commit()
249
+ * exactly once with the number of ids it actually emitted (all of them for an
250
+ * assert, the accepted prefix for a verify, 0 when it declined). Nothing
251
+ * enters the session's history until commit — so a partial accept needs no
252
+ * rollback here. */
253
+ export class FillSession {
254
+ readonly stats: FillStats = emptyStats();
255
+ /** Execution chunk size, independent of the number of tokens injected. */
256
+ readonly appendChunkSize: number;
257
+ readonly #sources: ProposalSource[];
258
+ readonly #strict: StrictRowSource;
259
+ readonly #eos: Set<number>;
260
+ readonly #delimiters: ReadonlySet<number>;
261
+ readonly #maxSpan: number;
262
+ readonly #window: number;
263
+ /** Bounded tail of the token history — all a source can see directly (the
264
+ * echo index keeps its own full copy via observe()). */
265
+ #tail: number[] = [];
266
+ #length = 0;
267
+ #budget = Number.POSITIVE_INFINITY;
268
+
269
+ /** Optional token→text decoder for the proposal trace (set by the serve
270
+ * layer, which owns the tokenizer). */
271
+ readonly decode: ((ids: readonly number[]) => string) | null;
272
+
273
+ constructor(
274
+ readonly plan: FillPlan,
275
+ promptIds: readonly number[],
276
+ options: {
277
+ maxSpan?: number;
278
+ appendChunkSize?: number;
279
+ sources?: ProposalSource[];
280
+ decode?: (ids: readonly number[]) => string;
281
+ strictContext?: StrictFillContext;
282
+ } = {},
283
+ ) {
284
+ this.decode = options.decode ?? null;
285
+ this.appendChunkSize = options.appendChunkSize ?? fillAppendChunkSize();
286
+ if (!Number.isSafeInteger(this.appendChunkSize) || this.appendChunkSize < 0)
287
+ throw new Error("fill appendChunkSize must be a nonnegative safe integer");
288
+ this.#eos = new Set(plan.eos);
289
+ this.#delimiters = plan.delimiters ?? new Set<number>();
290
+ this.#maxSpan = Math.max(2, Math.floor(options.maxSpan ?? fillMaxSpan()));
291
+ this.#strict = new StrictRowSource(plan.rows, options.strictContext);
292
+ const echo = plan.echo
293
+ ? new EchoSource(
294
+ { ...plan.echo, maxSpan: this.#maxSpan, delimiters: this.#delimiters } as EchoConfig,
295
+ promptIds,
296
+ )
297
+ : null;
298
+ // Priority: determined before guessed, then anything the caller injected
299
+ // (the seam the spec lane's DraftSources would adapt onto — see
300
+ // proposal.ts MIGRATION NOTE).
301
+ this.#sources = [
302
+ this.#strict,
303
+ ...(echo ? [echo] : []),
304
+ ...(options.sources ?? []),
305
+ ];
306
+ // Every source declares the tail it needs; the session retains the max.
307
+ this.#window = Math.max(
308
+ 1, ...this.#sources.map((s) => s.windowNeeded ?? 1),
309
+ );
310
+ // A row may be triggered by the prompt's own tail (a re-rendered turn that
311
+ // ends mid-scaffold), so the history starts seeded.
312
+ this.#tail = promptIds.slice(Math.max(0, promptIds.length - this.#window));
313
+ this.#length = promptIds.length;
314
+ }
315
+
316
+ /** Are strict rows still armed? (Cleared by noteParseFailure.) */
317
+ get strictEnabled(): boolean { return this.#strict.enabled; }
318
+
319
+ /** Feed one SAMPLED token, then ask the sources what follows. `budget` is
320
+ * the caller's remaining allowance (max_tokens − generated). */
321
+ push(tokenId: number, budget: number = Number.POSITIVE_INFINITY): Proposal | null {
322
+ this.stats.decodeSteps++;
323
+ this.#append([tokenId]);
324
+ this.#budget = budget;
325
+ for (const source of this.#sources) {
326
+ const proposal = source.propose(this);
327
+ if (!proposal) continue;
328
+ if (proposal.branchStop) this.stats.branchStops++;
329
+ const clamped = this.#clamp(proposal.ids, budget, proposal.origin === "echo");
330
+ if (!clamped) continue;
331
+ // An assert claim that a clamp cut short for any reason OTHER than
332
+ // ending at a delimiter is no longer the claim the source made. Strict
333
+ // rows are exempt: they are determined by construction, and a budget
334
+ // clamp shortens the span without weakening it.
335
+ const weakened = proposal.origin === "echo" &&
336
+ clamped.ids.length !== proposal.ids.length &&
337
+ !this.#delimiters.has(clamped.ids[clamped.ids.length - 1]!);
338
+ return {
339
+ ...proposal,
340
+ ids: clamped.ids,
341
+ policy: weakened ? "verify" : proposal.policy,
342
+ };
343
+ }
344
+ return null;
345
+ }
346
+
347
+ /** Record what the engine actually emitted from `proposal` (all of it for an
348
+ * assert; the accepted prefix for a verify; 0 when the engine declined).
349
+ * MUST be called exactly once per proposal returned by push(). */
350
+ commit(proposal: Proposal, accepted: number): void {
351
+ const n = Math.max(0, Math.min(accepted, proposal.ids.length));
352
+ const rejected = proposal.ids.length - n;
353
+ if (proposal.policy === "verify") {
354
+ this.stats.verifyAccepted += n;
355
+ this.stats.verifyRejected += rejected;
356
+ }
357
+ if (n === 0) return;
358
+ const ids = proposal.ids.slice(0, n);
359
+ this.#append(ids);
360
+ this.stats.events++;
361
+ this.stats.injected += n;
362
+ if (proposal.origin === "echo") this.stats.echo += n;
363
+ else this.stats.strict += n;
364
+ this.stats.spanLens.push(n);
365
+ }
366
+
367
+ /** A verify proposal reached the forward (the checkpoint was taken). */
368
+ noteVerifyEvent(checkpointMs: number): void {
369
+ this.stats.verifyEvents++;
370
+ this.stats.checkpointMs += checkpointMs;
371
+ }
372
+
373
+ /** A verify proposal was dropped: this model's caches cannot rewind. */
374
+ noteVerifyUnsupported(): void {
375
+ this.stats.verifyUnsupported++;
376
+ }
377
+
378
+ /** The engine dropped an in-flight sampled token to take an ASSERT fill. */
379
+ noteWastedSample(): void {
380
+ this.stats.wastedSamples++;
381
+ }
382
+
383
+ /** The tool-call parser rejected this request's markup. Strict rows are
384
+ * disarmed for the rest of the request — the template's own rendering
385
+ * disagreed with what the model actually emits, so every further row is
386
+ * suspect. (Echo rows are unaffected: they never claimed to be schema.)
387
+ *
388
+ * REALITY NOTE: the served parse (ToolAwareStream.toolCalls →
389
+ * parseGeneratedToolCalls) runs at sink FLUSH, i.e. after the generation
390
+ * has ended, so within one request this is telemetry and the disarm has no
391
+ * remaining tokens to act on. The seam is here so a future incremental
392
+ * parse (or a plan cached across requests) disarms for real. */
393
+ noteParseFailure(): void {
394
+ if (this.stats.strict === 0) return; // no strict row armed this request
395
+ this.stats.parseFallback++;
396
+ this.#strict.disable();
397
+ }
398
+
399
+ // --- TokenView ---------------------------------------------------------
400
+ get length(): number { return this.#length; }
401
+ get budget(): number { return this.#budget; }
402
+ tail(n: number): readonly number[] {
403
+ return n >= this.#tail.length ? this.#tail : this.#tail.slice(this.#tail.length - n);
404
+ }
405
+
406
+ #append(ids: readonly number[]): void {
407
+ this.#length += ids.length;
408
+ for (const source of this.#sources) source.observe?.(ids);
409
+ this.#tail.push(...ids);
410
+ if (this.#tail.length > this.#window)
411
+ this.#tail.splice(0, this.#tail.length - this.#window);
412
+ }
413
+
414
+ /** Clamp order: EOS → delimiter → max span / caller budget → reject <2.
415
+ * A one-token span saves no forward (the engine already had that token's
416
+ * successor in flight), so it is never worth the discarded dispatch.
417
+ *
418
+ * `applyDelimiters` is ECHO-ONLY. A delimiter marks where an argument VALUE
419
+ * ends, which is exactly where a copied span must stop — but a template
420
+ * scaffold legitimately contains the same tokens as structure (`{"name": "`
421
+ * is three quotes deep), and it is determined by construction. Clamping
422
+ * strict rows on delimiters would cut every scaffold at its first quote. */
423
+ #clamp(
424
+ emit: readonly number[], budget: number, applyDelimiters: boolean,
425
+ ): { ids: number[]; truncated: boolean } | null {
426
+ let ids = emit as number[];
427
+ let truncated = false;
428
+ for (let i = 0; i < ids.length; i++) {
429
+ if (this.#eos.has(ids[i]!)) { ids = ids.slice(0, i); truncated = true; break; }
430
+ }
431
+ if (applyDelimiters && this.#delimiters.size) {
432
+ for (let i = 0; i < ids.length; i++) {
433
+ // A span may END at a delimiter — injecting the closing quote is the
434
+ // point — but never continue past one.
435
+ if (this.#delimiters.has(ids[i]!)) {
436
+ if (i + 1 < ids.length) { ids = ids.slice(0, i + 1); truncated = true; }
437
+ break;
438
+ }
439
+ }
440
+ }
441
+ const cap = Math.min(
442
+ this.#maxSpan,
443
+ Number.isFinite(budget) ? Math.max(0, Math.floor(budget)) : this.#maxSpan,
444
+ );
445
+ if (ids.length > cap) { ids = ids.slice(0, cap); truncated = true; }
446
+ if (ids.length < 2) return null;
447
+ if (truncated) this.stats.indexTruncated++;
448
+ return { ids: ids === emit ? [...ids] : ids, truncated };
449
+ }
450
+ }
451
+
452
+ export type { ProposalOrigin };