mtplx 2.12.0__tar.gz → 2.12.2__tar.gz

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 (1181) hide show
  1. mtplx-2.12.2/CHANGELOG.md +4570 -0
  2. mtplx-2.12.2/PKG-INFO +369 -0
  3. mtplx-2.12.2/README.md +318 -0
  4. mtplx-2.12.2/mtplx/attention_context.py +262 -0
  5. mtplx-2.12.2/mtplx/attention_math.py +45 -0
  6. mtplx-2.12.2/mtplx/attention_split.py +919 -0
  7. mtplx-2.12.2/mtplx/backends/family_settings.py +541 -0
  8. mtplx-2.12.2/mtplx/backends/gemma4_assistant.py +4060 -0
  9. mtplx-2.12.2/mtplx/cache_bank/codec.py +948 -0
  10. mtplx-2.12.2/mtplx/cache_bank/cold_tier.py +3340 -0
  11. mtplx-2.12.2/mtplx/cache_bank/disk_budget.py +159 -0
  12. mtplx-2.12.2/mtplx/cache_state.py +5346 -0
  13. mtplx-2.12.2/mtplx/chat_encode_cache.py +196 -0
  14. mtplx-2.12.2/mtplx/chat_encoding.py +347 -0
  15. mtplx-2.12.2/mtplx/checkpoint_anchors.py +306 -0
  16. mtplx-2.12.2/mtplx/cli.py +5390 -0
  17. mtplx-2.12.2/mtplx/commands/forge_qwen4_exp.py +539 -0
  18. mtplx-2.12.2/mtplx/commands/public.py +16631 -0
  19. mtplx-2.12.2/mtplx/compile_state.py +121 -0
  20. mtplx-2.12.2/mtplx/compiled_forward.py +148 -0
  21. mtplx-2.12.2/mtplx/config.py +409 -0
  22. mtplx-2.12.2/mtplx/constrained.py +680 -0
  23. mtplx-2.12.2/mtplx/context_copy.py +397 -0
  24. mtplx-2.12.2/mtplx/dashboard/_static/assets/index-BTDFdIWu.css +1 -0
  25. mtplx-2.12.2/mtplx/dashboard/_static/assets/index-DIdoATFR.js +273 -0
  26. mtplx-2.12.2/mtplx/dashboard/_static/index.html +15 -0
  27. mtplx-2.12.2/mtplx/default_models.py +1089 -0
  28. mtplx-2.12.2/mtplx/demotions.py +252 -0
  29. mtplx-2.12.2/mtplx/engine_session.py +2651 -0
  30. mtplx-2.12.2/mtplx/errors.py +119 -0
  31. mtplx-2.12.2/mtplx/expert_layout.py +278 -0
  32. mtplx-2.12.2/mtplx/fast_sampling.py +907 -0
  33. mtplx-2.12.2/mtplx/float32_operand.py +37 -0
  34. mtplx-2.12.2/mtplx/generation.py +17105 -0
  35. mtplx-2.12.2/mtplx/graphbank.py +6288 -0
  36. mtplx-2.12.2/mtplx/jsonc.py +130 -0
  37. mtplx-2.12.2/mtplx/kernels/hc_verify_read.py +892 -0
  38. mtplx-2.12.2/mtplx/kernels/laguna_moe_gather_gemm.py +275 -0
  39. mtplx-2.12.2/mtplx/kernels/moe_sorted_gather_nax.py +668 -0
  40. mtplx-2.12.2/mtplx/kernels/qsa_pooled_row.py +515 -0
  41. mtplx-2.12.2/mtplx/kernels/qsa_verify_select.py +695 -0
  42. mtplx-2.12.2/mtplx/kernels/qwen4_m4_routed_down.py +645 -0
  43. mtplx-2.12.2/mtplx/kernels/sdpa_2pass.py +402 -0
  44. mtplx-2.12.2/mtplx/kernels/sdpa_2pass_paged.py +513 -0
  45. mtplx-2.12.2/mtplx/kernels/sdpa_2pass_paged_q8.py +297 -0
  46. mtplx-2.12.2/mtplx/kernels/sdpa_gqa_packed.py +788 -0
  47. mtplx-2.12.2/mtplx/kernels/sdpa_gqa_packed_quant.py +543 -0
  48. mtplx-2.12.2/mtplx/kernels/sdpa_nax_flash.py +463 -0
  49. mtplx-2.12.2/mtplx/kernels/sdpa_nax_flash_dsplit.py +390 -0
  50. mtplx-2.12.2/mtplx/kernels/sdpa_nax_tile.py +422 -0
  51. mtplx-2.12.2/mtplx/memory_plan.py +888 -0
  52. mtplx-2.12.2/mtplx/model_scheduler.py +1063 -0
  53. mtplx-2.12.2/mtplx/models/laguna_fused.py +1337 -0
  54. mtplx-2.12.2/mtplx/models/qwen4_exp.py +7396 -0
  55. mtplx-2.12.2/mtplx/moe_packed_projections.py +398 -0
  56. mtplx-2.12.2/mtplx/moe_sorted_gather.py +481 -0
  57. mtplx-2.12.2/mtplx/mtp_history_cache_only.py +55 -0
  58. mtplx-2.12.2/mtplx/mtp_patch.py +1164 -0
  59. mtplx-2.12.2/mtplx/one_copy.py +529 -0
  60. mtplx-2.12.2/mtplx/opencode.py +1058 -0
  61. mtplx-2.12.2/mtplx/pi.py +717 -0
  62. mtplx-2.12.2/mtplx/prefill_plan.py +360 -0
  63. mtplx-2.12.2/mtplx/profiles.py +1093 -0
  64. mtplx-2.12.2/mtplx/qwen3_5_mtp_patch.py +430 -0
  65. mtplx-2.12.2/mtplx/qwen4_fixed_verify.py +421 -0
  66. mtplx-2.12.2/mtplx/qwen4_m4_stage3.py +987 -0
  67. mtplx-2.12.2/mtplx/qwen4_prefill_profile.py +254 -0
  68. mtplx-2.12.2/mtplx/reasoning_codecs.py +697 -0
  69. mtplx-2.12.2/mtplx/runtime.py +1638 -0
  70. mtplx-2.12.2/mtplx/sampling.py +391 -0
  71. mtplx-2.12.2/mtplx/server/dashboard_state.py +691 -0
  72. mtplx-2.12.2/mtplx/server/flight_recorder.py +566 -0
  73. mtplx-2.12.2/mtplx/server/omlx_bridge/adapter.py +317 -0
  74. mtplx-2.12.2/mtplx/server/openai.py +44650 -0
  75. mtplx-2.12.2/mtplx/server/prefill_safety.py +278 -0
  76. mtplx-2.12.2/mtplx/server/request_policy.py +953 -0
  77. mtplx-2.12.2/mtplx/server/request_spans.py +401 -0
  78. mtplx-2.12.2/mtplx/server/retry_prompt.py +68 -0
  79. mtplx-2.12.2/mtplx/server/served_window.py +57 -0
  80. mtplx-2.12.2/mtplx/server/stream_rate.py +176 -0
  81. mtplx-2.12.2/mtplx/server/stream_recovery.py +107 -0
  82. mtplx-2.12.2/mtplx/session_bank.py +5192 -0
  83. mtplx-2.12.2/mtplx/system_memory.py +782 -0
  84. mtplx-2.12.2/mtplx/thermal_pressure.py +240 -0
  85. mtplx-2.12.2/mtplx/ui/onboarding.py +2202 -0
  86. mtplx-2.12.2/mtplx/vendored_arrays_cache.py +829 -0
  87. mtplx-2.12.2/mtplx/version.py +6 -0
  88. mtplx-2.12.2/mtplx/vision/splice.py +404 -0
  89. mtplx-2.12.2/mtplx.egg-info/PKG-INFO +369 -0
  90. mtplx-2.12.2/mtplx.egg-info/SOURCES.txt +1049 -0
  91. mtplx-2.12.2/mtplx.egg-info/requires.txt +29 -0
  92. mtplx-2.12.2/pyproject.toml +123 -0
  93. mtplx-2.12.2/scripts/check_prompt_scoring.py +128 -0
  94. mtplx-2.12.2/tests/test_admission_keeps_the_conversation.py +447 -0
  95. mtplx-2.12.2/tests/test_agent_rewrites_off_retries.py +177 -0
  96. mtplx-2.12.2/tests/test_answer_memory_stop.py +158 -0
  97. mtplx-2.12.2/tests/test_answer_room_request.py +116 -0
  98. mtplx-2.12.2/tests/test_api_benchmark_contracts.py +824 -0
  99. mtplx-2.12.2/tests/test_ar_batch_ragged_ssm_mask.py +129 -0
  100. mtplx-2.12.2/tests/test_arrays_cache_patch.py +605 -0
  101. mtplx-2.12.2/tests/test_attention_gate_exactness.py +102 -0
  102. mtplx-2.12.2/tests/test_blank_retry_greedy.py +98 -0
  103. mtplx-2.12.2/tests/test_cache_bank.py +1136 -0
  104. mtplx-2.12.2/tests/test_canon_policy_smalls.py +377 -0
  105. mtplx-2.12.2/tests/test_chat_encode_cache.py +213 -0
  106. mtplx-2.12.2/tests/test_chat_segment_memo.py +609 -0
  107. mtplx-2.12.2/tests/test_chat_turn_segments.py +566 -0
  108. mtplx-2.12.2/tests/test_checkpoint_anchors.py +476 -0
  109. mtplx-2.12.2/tests/test_checkpoint_pre_image.py +278 -0
  110. mtplx-2.12.2/tests/test_cold_tier_defaults.py +115 -0
  111. mtplx-2.12.2/tests/test_cold_tier_disk_budget.py +117 -0
  112. mtplx-2.12.2/tests/test_cold_tier_incremental_encode.py +451 -0
  113. mtplx-2.12.2/tests/test_cold_tier_recurrent_boundary_gate.py +121 -0
  114. mtplx-2.12.2/tests/test_cold_tier_transactional_replace.py +564 -0
  115. mtplx-2.12.2/tests/test_compiled_verify_evaluated_weights.py +526 -0
  116. mtplx-2.12.2/tests/test_compiled_verify_tf32_off.py +66 -0
  117. mtplx-2.12.2/tests/test_config.py +508 -0
  118. mtplx-2.12.2/tests/test_constrained.py +1684 -0
  119. mtplx-2.12.2/tests/test_copy_block_accept.py +549 -0
  120. mtplx-2.12.2/tests/test_death_signature_macos27.py +1028 -0
  121. mtplx-2.12.2/tests/test_decoded_state_lifetime.py +101 -0
  122. mtplx-2.12.2/tests/test_default_models.py +931 -0
  123. mtplx-2.12.2/tests/test_dense_mrope_compiled_route.py +573 -0
  124. mtplx-2.12.2/tests/test_draft_telemetry_truth.py +662 -0
  125. mtplx-2.12.2/tests/test_env_flag_parsing.py +716 -0
  126. mtplx-2.12.2/tests/test_finished_answers_are_never_regenerated.py +232 -0
  127. mtplx-2.12.2/tests/test_first_token_logprobs_api.py +547 -0
  128. mtplx-2.12.2/tests/test_first_token_logprobs_gemma4.py +211 -0
  129. mtplx-2.12.2/tests/test_fixed_m4_dispatch_guard.py +285 -0
  130. mtplx-2.12.2/tests/test_float32_operand.py +58 -0
  131. mtplx-2.12.2/tests/test_gdn_boundary_retention.py +219 -0
  132. mtplx-2.12.2/tests/test_gemma4_chunked_prefill.py +885 -0
  133. mtplx-2.12.2/tests/test_gemma4_prompt_scoring.py +218 -0
  134. mtplx-2.12.2/tests/test_gemma4_scoring_cache.py +90 -0
  135. mtplx-2.12.2/tests/test_gemma4_scoring_policy.py +153 -0
  136. mtplx-2.12.2/tests/test_gemma4_session_cache.py +321 -0
  137. mtplx-2.12.2/tests/test_gemma4_tokenizer_probe.py +81 -0
  138. mtplx-2.12.2/tests/test_generation_sustained.py +2017 -0
  139. mtplx-2.12.2/tests/test_graphbank_compiled_verify.py +2714 -0
  140. mtplx-2.12.2/tests/test_graphbank_program_retirement.py +206 -0
  141. mtplx-2.12.2/tests/test_graphbank_trace_lifetime.py +76 -0
  142. mtplx-2.12.2/tests/test_hc_verify_read.py +591 -0
  143. mtplx-2.12.2/tests/test_hidden_tool_guard_opt_in.py +276 -0
  144. mtplx-2.12.2/tests/test_host_memory_allowance.py +332 -0
  145. mtplx-2.12.2/tests/test_idle_session_release.py +607 -0
  146. mtplx-2.12.2/tests/test_jsonc.py +82 -0
  147. mtplx-2.12.2/tests/test_kv_attention_trace_526.py +344 -0
  148. mtplx-2.12.2/tests/test_kv_quant_meta_state_rewind_526.py +90 -0
  149. mtplx-2.12.2/tests/test_laguna_model.py +1127 -0
  150. mtplx-2.12.2/tests/test_lease_survives_aborted_prefill.py +146 -0
  151. mtplx-2.12.2/tests/test_memguard_admission.py +2017 -0
  152. mtplx-2.12.2/tests/test_memguard_backend_contract.py +933 -0
  153. mtplx-2.12.2/tests/test_memguard_degraded.py +247 -0
  154. mtplx-2.12.2/tests/test_memguard_dense_qsa.py +157 -0
  155. mtplx-2.12.2/tests/test_memguard_lease_growth.py +232 -0
  156. mtplx-2.12.2/tests/test_memguard_limit_allowances.py +92 -0
  157. mtplx-2.12.2/tests/test_memguard_moe_rows.py +56 -0
  158. mtplx-2.12.2/tests/test_memguard_ownership.py +200 -0
  159. mtplx-2.12.2/tests/test_memguard_pressure_trim.py +166 -0
  160. mtplx-2.12.2/tests/test_memguard_queued_persistence.py +414 -0
  161. mtplx-2.12.2/tests/test_memguard_release_protection.py +187 -0
  162. mtplx-2.12.2/tests/test_memguard_snapshot_width.py +346 -0
  163. mtplx-2.12.2/tests/test_memory_limit_g5.py +240 -0
  164. mtplx-2.12.2/tests/test_memory_plan.py +453 -0
  165. mtplx-2.12.2/tests/test_memory_pressure_guard.py +410 -0
  166. mtplx-2.12.2/tests/test_metal_memory_caps.py +419 -0
  167. mtplx-2.12.2/tests/test_model_scheduler_persistence.py +909 -0
  168. mtplx-2.12.2/tests/test_moe_sorted_gather.py +366 -0
  169. mtplx-2.12.2/tests/test_moe_sorted_gather_nax.py +714 -0
  170. mtplx-2.12.2/tests/test_mtp_fused_experts.py +150 -0
  171. mtplx-2.12.2/tests/test_mtp_history_cache_only.py +337 -0
  172. mtplx-2.12.2/tests/test_mtp_history_settled.py +451 -0
  173. mtplx-2.12.2/tests/test_mtp_patch.py +516 -0
  174. mtplx-2.12.2/tests/test_one_copy_admission.py +356 -0
  175. mtplx-2.12.2/tests/test_one_copy_cancelled_answer.py +186 -0
  176. mtplx-2.12.2/tests/test_one_copy_conversation.py +524 -0
  177. mtplx-2.12.2/tests/test_one_copy_postcommit.py +145 -0
  178. mtplx-2.12.2/tests/test_one_copy_reserve_resize.py +1298 -0
  179. mtplx-2.12.2/tests/test_one_copy_restarted_draft_history.py +351 -0
  180. mtplx-2.12.2/tests/test_openai_bridge.py +2949 -0
  181. mtplx-2.12.2/tests/test_opencode.py +884 -0
  182. mtplx-2.12.2/tests/test_owner_idle_pool.py +164 -0
  183. mtplx-2.12.2/tests/test_parallel_tool_prompt_policy.py +150 -0
  184. mtplx-2.12.2/tests/test_phys_footprint_guard.py +211 -0
  185. mtplx-2.12.2/tests/test_postcommit_prefix_reuse.py +330 -0
  186. mtplx-2.12.2/tests/test_prefill_admission_shed.py +877 -0
  187. mtplx-2.12.2/tests/test_prefill_geometric_tail.py +211 -0
  188. mtplx-2.12.2/tests/test_prefill_inforward_boundary_capture.py +724 -0
  189. mtplx-2.12.2/tests/test_prefill_reread.py +732 -0
  190. mtplx-2.12.2/tests/test_pressure_abort_393.py +153 -0
  191. mtplx-2.12.2/tests/test_promoted_paged_capacity_526.py +1225 -0
  192. mtplx-2.12.2/tests/test_prompt_publication_is_request_local.py +240 -0
  193. mtplx-2.12.2/tests/test_prompt_scoring_admission.py +154 -0
  194. mtplx-2.12.2/tests/test_prompt_scoring_parent.py +79 -0
  195. mtplx-2.12.2/tests/test_prompt_scoring_probe.py +58 -0
  196. mtplx-2.12.2/tests/test_prompt_scoring_topk.py +145 -0
  197. mtplx-2.12.2/tests/test_public_cli.py +10334 -0
  198. mtplx-2.12.2/tests/test_px3_planner_seats.py +270 -0
  199. mtplx-2.12.2/tests/test_qsa_fixed_bank_score_scale.py +181 -0
  200. mtplx-2.12.2/tests/test_qsa_index_block_writes_evaluated.py +558 -0
  201. mtplx-2.12.2/tests/test_qsa_indexer_select_metal.py +953 -0
  202. mtplx-2.12.2/tests/test_qsa_memory_plan_393.py +141 -0
  203. mtplx-2.12.2/tests/test_qsa_pooled_row.py +259 -0
  204. mtplx-2.12.2/tests/test_qsa_verify_select.py +330 -0
  205. mtplx-2.12.2/tests/test_quant_partials_fp16_526.py +251 -0
  206. mtplx-2.12.2/tests/test_qwen4_bucket_publication.py +196 -0
  207. mtplx-2.12.2/tests/test_qwen4_capacity_consumers.py +729 -0
  208. mtplx-2.12.2/tests/test_qwen4_fixed_bank_capacity.py +159 -0
  209. mtplx-2.12.2/tests/test_qwen4_fixed_m4_capacity_bucket.py +578 -0
  210. mtplx-2.12.2/tests/test_qwen4_fixed_m4_copy_windows.py +749 -0
  211. mtplx-2.12.2/tests/test_qwen4_fixed_m4_float32_admission.py +42 -0
  212. mtplx-2.12.2/tests/test_qwen4_fixed_m4_gate_cache.py +114 -0
  213. mtplx-2.12.2/tests/test_qwen4_fixed_m4_growth_layerwise.py +410 -0
  214. mtplx-2.12.2/tests/test_qwen4_fixed_m4_verify_exactness.py +382 -0
  215. mtplx-2.12.2/tests/test_qwen4_growth_recovery.py +225 -0
  216. mtplx-2.12.2/tests/test_qwen4_m4_stage3_residual_tail.py +659 -0
  217. mtplx-2.12.2/tests/test_qwen4_moe_sorted_rows.py +155 -0
  218. mtplx-2.12.2/tests/test_qwen4_opdiet.py +944 -0
  219. mtplx-2.12.2/tests/test_qwen4_ple_gate_exactness.py +122 -0
  220. mtplx-2.12.2/tests/test_qwen4_prefill_profile_hc.py +57 -0
  221. mtplx-2.12.2/tests/test_qwen4_rows_gather_scale.py +47 -0
  222. mtplx-2.12.2/tests/test_qwen4_verify_gate_sites.py +157 -0
  223. mtplx-2.12.2/tests/test_qwen4_yarn_amplitude.py +36 -0
  224. mtplx-2.12.2/tests/test_ram_miss_reason.py +250 -0
  225. mtplx-2.12.2/tests/test_reasoning_stream_split.py +182 -0
  226. mtplx-2.12.2/tests/test_recovery_metrics_attribution.py +82 -0
  227. mtplx-2.12.2/tests/test_release_2114_catalog.py +205 -0
  228. mtplx-2.12.2/tests/test_release_pins.py +162 -0
  229. mtplx-2.12.2/tests/test_release_qa_fixwave.py +310 -0
  230. mtplx-2.12.2/tests/test_retry_prompt_reuses_served_ids.py +280 -0
  231. mtplx-2.12.2/tests/test_served_window.py +129 -0
  232. mtplx-2.12.2/tests/test_server_obs_stream_termination.py +483 -0
  233. mtplx-2.12.2/tests/test_server_openai.py +15017 -0
  234. mtplx-2.12.2/tests/test_session_bank.py +720 -0
  235. mtplx-2.12.2/tests/test_small_mac_prefill.py +283 -0
  236. mtplx-2.12.2/tests/test_split_partials_fp16_526.py +230 -0
  237. mtplx-2.12.2/tests/test_stable_prefix_boundary.py +352 -0
  238. mtplx-2.12.2/tests/test_start_reuses_chosen_model.py +491 -0
  239. mtplx-2.12.2/tests/test_stop_resend_session_continuity.py +736 -0
  240. mtplx-2.12.2/tests/test_stream_phase_rate.py +193 -0
  241. mtplx-2.12.2/tests/test_stream_recovery_paths.py +293 -0
  242. mtplx-2.12.2/tests/test_stream_response_tail.py +533 -0
  243. mtplx-2.12.2/tests/test_system_memory_guard.py +766 -0
  244. mtplx-2.12.2/tests/test_thermal_pressure.py +217 -0
  245. mtplx-2.12.2/tests/test_tool_choice_prompt_modes.py +96 -0
  246. mtplx-2.12.2/tests/test_tool_fed_retry_parsed_call.py +282 -0
  247. mtplx-2.12.2/tests/test_ttft_spans.py +614 -0
  248. mtplx-2.12.2/tests/test_verify_kernels_rehearsal.py +189 -0
  249. mtplx-2.12.2/tests/test_vision_keyed_partial_restore.py +257 -0
  250. mtplx-2.12.2/tests/test_vision_keyed_restore_rules.py +172 -0
  251. mtplx-2.12.2/tests/test_vision_restore_span_guard.py +130 -0
  252. mtplx-2.12.2/tests/test_vision_session_frontier_gate.py +463 -0
  253. mtplx-2.12.2/tests/test_vision_session_restore.py +928 -0
  254. mtplx-2.12.2/tests/test_wide_prefill_chunk_after_reclaim.py +213 -0
  255. mtplx-2.12.2/tests/test_working_set.py +114 -0
  256. mtplx-2.12.0/CHANGELOG.md +0 -4431
  257. mtplx-2.12.0/PKG-INFO +0 -369
  258. mtplx-2.12.0/README.md +0 -318
  259. mtplx-2.12.0/mtplx/attention_context.py +0 -139
  260. mtplx-2.12.0/mtplx/attention_math.py +0 -22
  261. mtplx-2.12.0/mtplx/attention_split.py +0 -744
  262. mtplx-2.12.0/mtplx/backends/family_settings.py +0 -540
  263. mtplx-2.12.0/mtplx/backends/gemma4_assistant.py +0 -3515
  264. mtplx-2.12.0/mtplx/cache_bank/codec.py +0 -807
  265. mtplx-2.12.0/mtplx/cache_bank/cold_tier.py +0 -2871
  266. mtplx-2.12.0/mtplx/cache_state.py +0 -4789
  267. mtplx-2.12.0/mtplx/chat_encode_cache.py +0 -96
  268. mtplx-2.12.0/mtplx/chat_encoding.py +0 -314
  269. mtplx-2.12.0/mtplx/cli.py +0 -5375
  270. mtplx-2.12.0/mtplx/commands/forge_qwen4_exp.py +0 -553
  271. mtplx-2.12.0/mtplx/commands/public.py +0 -16415
  272. mtplx-2.12.0/mtplx/compile_state.py +0 -39
  273. mtplx-2.12.0/mtplx/compiled_forward.py +0 -144
  274. mtplx-2.12.0/mtplx/config.py +0 -410
  275. mtplx-2.12.0/mtplx/constrained.py +0 -544
  276. mtplx-2.12.0/mtplx/context_copy.py +0 -382
  277. mtplx-2.12.0/mtplx/dashboard/_static/assets/index-CZI5_teW.js +0 -273
  278. mtplx-2.12.0/mtplx/dashboard/_static/assets/index-Cmxkc0GH.css +0 -1
  279. mtplx-2.12.0/mtplx/dashboard/_static/index.html +0 -15
  280. mtplx-2.12.0/mtplx/default_models.py +0 -1124
  281. mtplx-2.12.0/mtplx/demotions.py +0 -247
  282. mtplx-2.12.0/mtplx/engine_session.py +0 -2428
  283. mtplx-2.12.0/mtplx/errors.py +0 -86
  284. mtplx-2.12.0/mtplx/expert_layout.py +0 -190
  285. mtplx-2.12.0/mtplx/fast_sampling.py +0 -738
  286. mtplx-2.12.0/mtplx/generation.py +0 -15868
  287. mtplx-2.12.0/mtplx/graphbank.py +0 -5199
  288. mtplx-2.12.0/mtplx/jsonc.py +0 -127
  289. mtplx-2.12.0/mtplx/kernels/laguna_moe_gather_gemm.py +0 -273
  290. mtplx-2.12.0/mtplx/kernels/qwen4_m4_routed_down.py +0 -442
  291. mtplx-2.12.0/mtplx/kernels/sdpa_2pass.py +0 -373
  292. mtplx-2.12.0/mtplx/kernels/sdpa_2pass_paged.py +0 -509
  293. mtplx-2.12.0/mtplx/kernels/sdpa_2pass_paged_q8.py +0 -288
  294. mtplx-2.12.0/mtplx/kernels/sdpa_gqa_packed.py +0 -784
  295. mtplx-2.12.0/mtplx/kernels/sdpa_gqa_packed_quant.py +0 -506
  296. mtplx-2.12.0/mtplx/kernels/sdpa_nax_flash.py +0 -460
  297. mtplx-2.12.0/mtplx/kernels/sdpa_nax_flash_dsplit.py +0 -388
  298. mtplx-2.12.0/mtplx/kernels/sdpa_nax_tile.py +0 -419
  299. mtplx-2.12.0/mtplx/memory_plan.py +0 -809
  300. mtplx-2.12.0/mtplx/model_scheduler.py +0 -823
  301. mtplx-2.12.0/mtplx/models/laguna_fused.py +0 -1336
  302. mtplx-2.12.0/mtplx/models/qwen4_exp.py +0 -6967
  303. mtplx-2.12.0/mtplx/moe_packed_projections.py +0 -396
  304. mtplx-2.12.0/mtplx/mtp_patch.py +0 -1157
  305. mtplx-2.12.0/mtplx/opencode.py +0 -886
  306. mtplx-2.12.0/mtplx/pi.py +0 -587
  307. mtplx-2.12.0/mtplx/profiles.py +0 -1089
  308. mtplx-2.12.0/mtplx/qwen3_5_mtp_patch.py +0 -426
  309. mtplx-2.12.0/mtplx/qwen4_fixed_verify.py +0 -398
  310. mtplx-2.12.0/mtplx/qwen4_m4_stage3.py +0 -974
  311. mtplx-2.12.0/mtplx/qwen4_prefill_profile.py +0 -243
  312. mtplx-2.12.0/mtplx/reasoning_codecs.py +0 -687
  313. mtplx-2.12.0/mtplx/runtime.py +0 -1631
  314. mtplx-2.12.0/mtplx/sampling.py +0 -384
  315. mtplx-2.12.0/mtplx/server/dashboard_state.py +0 -655
  316. mtplx-2.12.0/mtplx/server/flight_recorder.py +0 -557
  317. mtplx-2.12.0/mtplx/server/omlx_bridge/adapter.py +0 -307
  318. mtplx-2.12.0/mtplx/server/openai.py +0 -39117
  319. mtplx-2.12.0/mtplx/server/request_policy.py +0 -947
  320. mtplx-2.12.0/mtplx/session_bank.py +0 -3413
  321. mtplx-2.12.0/mtplx/system_memory.py +0 -187
  322. mtplx-2.12.0/mtplx/ui/onboarding.py +0 -2116
  323. mtplx-2.12.0/mtplx/vendored_arrays_cache.py +0 -822
  324. mtplx-2.12.0/mtplx/version.py +0 -6
  325. mtplx-2.12.0/mtplx/vision/splice.py +0 -319
  326. mtplx-2.12.0/mtplx.egg-info/PKG-INFO +0 -369
  327. mtplx-2.12.0/mtplx.egg-info/SOURCES.txt +0 -924
  328. mtplx-2.12.0/mtplx.egg-info/requires.txt +0 -29
  329. mtplx-2.12.0/pyproject.toml +0 -115
  330. mtplx-2.12.0/tests/test_api_benchmark_contracts.py +0 -824
  331. mtplx-2.12.0/tests/test_arrays_cache_patch.py +0 -588
  332. mtplx-2.12.0/tests/test_attention_gate_exactness.py +0 -43
  333. mtplx-2.12.0/tests/test_cache_bank.py +0 -1122
  334. mtplx-2.12.0/tests/test_canon_policy_smalls.py +0 -349
  335. mtplx-2.12.0/tests/test_chat_encode_cache.py +0 -144
  336. mtplx-2.12.0/tests/test_cold_tier_defaults.py +0 -114
  337. mtplx-2.12.0/tests/test_config.py +0 -492
  338. mtplx-2.12.0/tests/test_constrained.py +0 -739
  339. mtplx-2.12.0/tests/test_default_models.py +0 -927
  340. mtplx-2.12.0/tests/test_dense_mrope_compiled_route.py +0 -558
  341. mtplx-2.12.0/tests/test_draft_telemetry_truth.py +0 -662
  342. mtplx-2.12.0/tests/test_env_flag_parsing.py +0 -657
  343. mtplx-2.12.0/tests/test_fixed_m4_dispatch_guard.py +0 -284
  344. mtplx-2.12.0/tests/test_gdn_boundary_retention.py +0 -150
  345. mtplx-2.12.0/tests/test_gemma4_session_cache.py +0 -320
  346. mtplx-2.12.0/tests/test_generation_sustained.py +0 -1786
  347. mtplx-2.12.0/tests/test_graphbank_compiled_verify.py +0 -2612
  348. mtplx-2.12.0/tests/test_graphbank_trace_lifetime.py +0 -75
  349. mtplx-2.12.0/tests/test_host_memory_allowance.py +0 -210
  350. mtplx-2.12.0/tests/test_jsonc.py +0 -76
  351. mtplx-2.12.0/tests/test_laguna_model.py +0 -1122
  352. mtplx-2.12.0/tests/test_memory_plan.py +0 -444
  353. mtplx-2.12.0/tests/test_memory_pressure_guard.py +0 -407
  354. mtplx-2.12.0/tests/test_metal_memory_caps.py +0 -416
  355. mtplx-2.12.0/tests/test_model_scheduler_persistence.py +0 -582
  356. mtplx-2.12.0/tests/test_mtp_patch.py +0 -454
  357. mtplx-2.12.0/tests/test_openai_bridge.py +0 -2938
  358. mtplx-2.12.0/tests/test_opencode.py +0 -656
  359. mtplx-2.12.0/tests/test_phys_footprint_guard.py +0 -203
  360. mtplx-2.12.0/tests/test_postcommit_prefix_reuse.py +0 -273
  361. mtplx-2.12.0/tests/test_prefill_admission_shed.py +0 -740
  362. mtplx-2.12.0/tests/test_prefill_geometric_tail.py +0 -211
  363. mtplx-2.12.0/tests/test_prefill_inforward_boundary_capture.py +0 -725
  364. mtplx-2.12.0/tests/test_pressure_abort_393.py +0 -145
  365. mtplx-2.12.0/tests/test_public_cli.py +0 -9933
  366. mtplx-2.12.0/tests/test_px3_planner_seats.py +0 -263
  367. mtplx-2.12.0/tests/test_qsa_indexer_select_metal.py +0 -949
  368. mtplx-2.12.0/tests/test_qsa_memory_plan_393.py +0 -137
  369. mtplx-2.12.0/tests/test_qwen4_fixed_bank_capacity.py +0 -157
  370. mtplx-2.12.0/tests/test_qwen4_fixed_m4_gate_cache.py +0 -113
  371. mtplx-2.12.0/tests/test_qwen4_m4_stage3_residual_tail.py +0 -527
  372. mtplx-2.12.0/tests/test_qwen4_opdiet.py +0 -932
  373. mtplx-2.12.0/tests/test_reasoning_stream_split.py +0 -129
  374. mtplx-2.12.0/tests/test_release_2114_catalog.py +0 -203
  375. mtplx-2.12.0/tests/test_release_pins.py +0 -138
  376. mtplx-2.12.0/tests/test_release_qa_fixwave.py +0 -307
  377. mtplx-2.12.0/tests/test_server_obs_stream_termination.py +0 -382
  378. mtplx-2.12.0/tests/test_server_openai.py +0 -14645
  379. mtplx-2.12.0/tests/test_session_bank.py +0 -644
  380. mtplx-2.12.0/tests/test_stable_prefix_boundary.py +0 -350
  381. mtplx-2.12.0/tests/test_stop_resend_session_continuity.py +0 -732
  382. mtplx-2.12.0/tests/test_system_memory_guard.py +0 -341
  383. mtplx-2.12.0/tests/test_vision_restore_span_guard.py +0 -124
  384. mtplx-2.12.0/tests/test_vision_session_frontier_gate.py +0 -323
  385. mtplx-2.12.0/tests/test_vision_session_restore.py +0 -764
  386. {mtplx-2.12.0 → mtplx-2.12.2}/CITATION.cff +0 -0
  387. {mtplx-2.12.0 → mtplx-2.12.2}/LICENSE +0 -0
  388. {mtplx-2.12.0 → mtplx-2.12.2}/MANIFEST.in +0 -0
  389. {mtplx-2.12.0 → mtplx-2.12.2}/NOTICE +0 -0
  390. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/__init__.py +0 -0
  391. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/a3b_compiled_target_prefix.py +0 -0
  392. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/a3b_mtp_batch.py +0 -0
  393. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/a3b_whole_moe.py +0 -0
  394. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/adaptive.py +0 -0
  395. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/adaptive_dtemp.py +0 -0
  396. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/app_settings.py +0 -0
  397. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/aria2_downloader.py +0 -0
  398. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/arrays_cache_patch.py +0 -0
  399. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/artifacts.py +0 -0
  400. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/backends/__init__.py +0 -0
  401. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/backends/deepseek_mtp.py +0 -0
  402. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/backends/descriptors.py +0 -0
  403. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/backends/glm_mtp.py +0 -0
  404. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/backends/hy_v3_mtp.py +0 -0
  405. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/backends/mimo_mtp.py +0 -0
  406. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/backends/nemotron_h_mtp.py +0 -0
  407. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/backends/qwen3_next.py +0 -0
  408. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/backends/registry.py +0 -0
  409. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/backends/step3p5_mtp.py +0 -0
  410. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/batched_decode.py +0 -0
  411. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/batching/__init__.py +0 -0
  412. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/batching/admission.py +0 -0
  413. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/batching/buckets.py +0 -0
  414. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/batching/scheduler.py +0 -0
  415. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/batching/state.py +0 -0
  416. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/__init__.py +0 -0
  417. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/code_eval.py +0 -0
  418. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/programming_prompts.py +0 -0
  419. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/prompts/aime_2026.jsonl +0 -0
  420. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/prompts/calibration_coding.jsonl +0 -0
  421. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/prompts/default.jsonl +0 -0
  422. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/prompts/flappy.jsonl +0 -0
  423. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/prompts/long_code.jsonl +0 -0
  424. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/prompts/long_code_uncapped.jsonl +0 -0
  425. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/prompts/python_modules_long.jsonl +0 -0
  426. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/__init__.py +0 -0
  427. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/aime.py +0 -0
  428. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/batch_equivalence.py +0 -0
  429. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/capture_commit_equivalence.py +0 -0
  430. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/competitor_baselines.py +0 -0
  431. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/contract_probe.py +0 -0
  432. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/harness.py +0 -0
  433. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/mtp1_gate.py +0 -0
  434. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/mtp1_sampler_smoke.py +0 -0
  435. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/mtp_adaptive.py +0 -0
  436. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/mtp_chain_probe.py +0 -0
  437. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/mtp_depth_grid.py +0 -0
  438. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/mtp_depth_sweep.py +0 -0
  439. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/mtp_tree_probe.py +0 -0
  440. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/multi_qmv_probe.py +0 -0
  441. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/preflight.py +0 -0
  442. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/runtime_smoke.py +0 -0
  443. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/session_bank.py +0 -0
  444. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/truth.py +0 -0
  445. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/verify_profile.py +0 -0
  446. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/verify_qmm_probe.py +0 -0
  447. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/runners/verify_ratio.py +0 -0
  448. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/schema.py +0 -0
  449. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/validators/__init__.py +0 -0
  450. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/validators/aime.py +0 -0
  451. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/benchmarks/validators/basic.py +0 -0
  452. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/block_attention.py +0 -0
  453. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/cache_bank/__init__.py +0 -0
  454. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/cache_bank/reconcile.py +0 -0
  455. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/commands/__init__.py +0 -0
  456. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/commands/forge.py +0 -0
  457. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/commands/forge_mixed_convert.py +0 -0
  458. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/commands/forge_qwen4_audit.py +0 -0
  459. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/commands/trace.py +0 -0
  460. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/commands/trace_analysis.py +0 -0
  461. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/commands/trace_clients.py +0 -0
  462. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/commands/trace_metrics.py +0 -0
  463. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/commands/trace_record.py +0 -0
  464. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/commands/trace_report.py +0 -0
  465. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/compressed_tensors.py +0 -0
  466. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/constants.py +0 -0
  467. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/correctors/__init__.py +0 -0
  468. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/correctors/diagonal_affine.py +0 -0
  469. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/correctors/low_rank.py +0 -0
  470. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/daemon_client.py +0 -0
  471. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/dashboard/__init__.py +0 -0
  472. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/data/qwen38_code_ranked_64k.npy +0 -0
  473. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/deepseek_mtp_patch.py +0 -0
  474. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/deepseek_v4_adaptive_width.py +0 -0
  475. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/deepseek_v4_attention_island.py +0 -0
  476. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/deepseek_v4_attn_proj_wide_m3.py +0 -0
  477. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/dense_mrope.py +0 -0
  478. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/diagnostics.py +0 -0
  479. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/draft_lm_head.py +0 -0
  480. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/draft_sampling.py +0 -0
  481. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/env.py +0 -0
  482. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/fan_mode.py +0 -0
  483. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/forkev_telemetry.py +0 -0
  484. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/frspec_draft.py +0 -0
  485. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/gdn_capture.py +0 -0
  486. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/gemma4_pair.py +0 -0
  487. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/glm_mtp_patch.py +0 -0
  488. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/hardware.py +0 -0
  489. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/hf_loader.py +0 -0
  490. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/hy_v3_mtp_patch.py +0 -0
  491. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernel_selfcheck.py +0 -0
  492. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/__init__.py +0 -0
  493. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/a3b_whole_moe.py +0 -0
  494. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/copy_leaf.py +0 -0
  495. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/fused_norm.py +0 -0
  496. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/gdn_blocked_prefill.py +0 -0
  497. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/gdn_conv_norm.py +0 -0
  498. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/gdn_gated_norm.py +0 -0
  499. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/gdn_out_fused.py +0 -0
  500. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/gdn_prefill_prework.py +0 -0
  501. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/gdn_step_fused.py +0 -0
  502. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/gdn_tape_headquarter.py +0 -0
  503. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/hadamard_rotate.py +0 -0
  504. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/hc_prefill.py +0 -0
  505. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/hyper_connection.py +0 -0
  506. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/hyper_connection_v3.py +0 -0
  507. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_decode.py +0 -0
  508. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_dense_mlp.py +0 -0
  509. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_gated_oproj.py +0 -0
  510. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_moe_combine.py +0 -0
  511. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_moe_merged.py +0 -0
  512. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_moe_shared.py +0 -0
  513. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_moe_swiglu.py +0 -0
  514. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_prefill_moe_combine.py +0 -0
  515. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_prefill_qk_rope.py +0 -0
  516. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_prefill_router.py +0 -0
  517. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_qk_rope_sliding.py +0 -0
  518. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_qk_yarn_full.py +0 -0
  519. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_qkvg_fused.py +0 -0
  520. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_residual_router.py +0 -0
  521. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_route_csort.py +0 -0
  522. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_router_topk.py +0 -0
  523. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_sdpa_2pass.py +0 -0
  524. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_sdpa_pair.py +0 -0
  525. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/laguna_steel_attn.py +0 -0
  526. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/lm_head_topk.py +0 -0
  527. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/logits_topk.py +0 -0
  528. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/moe_glu_decode.py +0 -0
  529. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/native_gdn_tail.py +0 -0
  530. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qmv4_matmul.py +0 -0
  531. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qsa_dense_band_sdpa.py +0 -0
  532. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qsa_flash_skip.py +0 -0
  533. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qsa_indexer_compile.py +0 -0
  534. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qsa_indexer_prefill.py +0 -0
  535. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qsa_indexer_prepare.py +0 -0
  536. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qsa_indexer_select.py +0 -0
  537. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qsa_prefill_direct.py +0 -0
  538. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qsa_prefill_flash.py +0 -0
  539. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qsa_prefill_probe.py +0 -0
  540. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qwen4_m4_rope.py +0 -0
  541. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qwen4_m4_route.py +0 -0
  542. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qwen4_m4_routed_glu.py +0 -0
  543. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qwen4_m4_stage3.py +0 -0
  544. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qwen4_moe_prefill_combine.py +0 -0
  545. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/qwen4_qsa_m4_fused_kv_gather.py +0 -0
  546. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/ternary_qmv.py +0 -0
  547. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/verify_mlp_fused.py +0 -0
  548. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kernels/vqmm_crossrow.py +0 -0
  549. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kpi/__init__.py +0 -0
  550. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kpi/reference_vllm.py +0 -0
  551. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kpi/runtime_kpis.py +0 -0
  552. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/kv_quant.py +0 -0
  553. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/laguna_alt_step.py +0 -0
  554. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/laguna_compiled_step.py +0 -0
  555. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/lane_explain.py +0 -0
  556. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/launch_lane.py +0 -0
  557. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/lfm2_fast.py +0 -0
  558. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/loop_guard.py +0 -0
  559. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/metadata_scrub.py +0 -0
  560. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/mimo_mtp_patch.py +0 -0
  561. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/mlx_process_env.py +0 -0
  562. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/model_catalog.py +0 -0
  563. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/model_updates.py +0 -0
  564. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/models/__init__.py +0 -0
  565. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/models/deepseek_v4.py +0 -0
  566. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/models/laguna.py +0 -0
  567. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/models/laguna_config.py +0 -0
  568. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/models/prism_hadamard_qwen35.py +0 -0
  569. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/mtp_activation_stats.py +0 -0
  570. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/mtp_adapters.py +0 -0
  571. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/mtp_batch_numerics.py +0 -0
  572. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/native_mlp.py +0 -0
  573. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/nax_detect.py +0 -0
  574. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/nax_verify.py +0 -0
  575. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/nemotron_h_mtp_patch.py +0 -0
  576. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/ngram_row_layout.py +0 -0
  577. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/optimization_profiles.py +0 -0
  578. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/os_memory.py +0 -0
  579. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/packed_concats.py +0 -0
  580. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/ple_prefill_lookahead.py +0 -0
  581. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/ple_row_gather.py +0 -0
  582. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/prefill_bench.py +0 -0
  583. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/prefill_rungs.py +0 -0
  584. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/progress_heartbeat.py +0 -0
  585. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/proj_fusion.py +0 -0
  586. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/proj_quant.py +0 -0
  587. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/proposal_reranker.py +0 -0
  588. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/qsa_mtp_precompute.py +0 -0
  589. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/qsa_restore_staging.py +0 -0
  590. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/qwen4_block_verify.py +0 -0
  591. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/qwen4_claim_contract.py +0 -0
  592. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/qwen4_draft_device_chain.py +0 -0
  593. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/qwen4_draft_k20_prescatter.py +0 -0
  594. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/qwen4_verify_glue.py +0 -0
  595. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/qwen_row_owned_router.py +0 -0
  596. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/ragged_attention.py +0 -0
  597. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/ragged_kv_cache.py +0 -0
  598. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/reasoning_effort.py +0 -0
  599. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/request_capture.py +0 -0
  600. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/retrieval.py +0 -0
  601. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/roofline_profile.py +0 -0
  602. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/rope_origin.py +0 -0
  603. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/route_tape.py +0 -0
  604. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/runtime_options.py +0 -0
  605. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/server/__init__.py +0 -0
  606. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/server/hyper.py +0 -0
  607. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/server/mtp_batch.py +0 -0
  608. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/server/omlx_bridge/__init__.py +0 -0
  609. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/server/omlx_bridge/thinking.py +0 -0
  610. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/server/omlx_bridge/tool_calling.py +0 -0
  611. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/server/response_envelope.py +0 -0
  612. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/server/responses.py +0 -0
  613. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/server_urls.py +0 -0
  614. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/speculative.py +0 -0
  615. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/step3p5_mtp_patch.py +0 -0
  616. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/swival.py +0 -0
  617. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/templates/qwen36_froggeric_v19/chat_template.jinja +0 -0
  618. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/templates/qwen36_froggeric_v21_3/chat_template.jinja +0 -0
  619. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/thermal.py +0 -0
  620. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/thermal_sidecar.py +0 -0
  621. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/thinking_guard.py +0 -0
  622. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/trace_parity.py +0 -0
  623. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/turboquant.py +0 -0
  624. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/ui/__init__.py +0 -0
  625. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/ui/banner.py +0 -0
  626. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/ui/chat_printer.py +0 -0
  627. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/ui/download_progress.py +0 -0
  628. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/ui/panels.py +0 -0
  629. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/ui/progress.py +0 -0
  630. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/vendored_hy_v3.py +0 -0
  631. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/verify_crossrow.py +0 -0
  632. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/verify_kernels.py +0 -0
  633. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/verify_qmv.py +0 -0
  634. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/vision/__init__.py +0 -0
  635. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/vision/mrope.py +0 -0
  636. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/vision/processing.py +0 -0
  637. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/vision/qwen3_vl_tower.py +0 -0
  638. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx/vision_graft.py +0 -0
  639. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx.egg-info/dependency_links.txt +0 -0
  640. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx.egg-info/entry_points.txt +0 -0
  641. {mtplx-2.12.0 → mtplx-2.12.2}/mtplx.egg-info/top_level.txt +0 -0
  642. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/agent_session_gate.py +0 -0
  643. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/agent_user_path_qa.py +0 -0
  644. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/aime_serve_gate.py +0 -0
  645. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/aime_shape_memory_bench.py +0 -0
  646. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/analyze_head_sweeps.py +0 -0
  647. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/audit_agent_prompt_fidelity.py +0 -0
  648. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/audit_catalog_sizes.py +0 -0
  649. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/bonsai_memory_table.py +0 -0
  650. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/build_bonsai_mtplx_pack.py +0 -0
  651. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/build_flash_next_quality_pack.py +0 -0
  652. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/build_flash_next_quality_pack.sh +0 -0
  653. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/bundle_native_runtime_wheel.py +0 -0
  654. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/check_ai_attribution.py +0 -0
  655. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/code_eval_gate.py +0 -0
  656. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/collect_mtp_activation_stats.py +0 -0
  657. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/collect_mtp_hidden_calib.py +0 -0
  658. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/combine_mtp_adapters.py +0 -0
  659. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/compiled_verify_exactness.py +0 -0
  660. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/convert_qwen4_exp_fp8.py +0 -0
  661. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/convert_step37_to_mtplx_step3p5.py +0 -0
  662. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/decodecliff_probe_20260830.py +0 -0
  663. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/decodecliff_pulse_cell_20260830.py +0 -0
  664. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_adaptive_width_arms.sh +0 -0
  665. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_adaptive_width_guarded.py +0 -0
  666. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_attention_island_arms.sh +0 -0
  667. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_attention_island_guarded.py +0 -0
  668. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_attn_proj_wide_m3_arms.sh +0 -0
  669. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_attn_proj_wide_m3_guarded.py +0 -0
  670. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_build_mtp_model.py +0 -0
  671. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_decode_verify.py +0 -0
  672. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_dispatch_census.py +0 -0
  673. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_guard_window.py +0 -0
  674. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_logits_gate.py +0 -0
  675. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_moe_tail_arms.sh +0 -0
  676. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_moe_tail_gate.py +0 -0
  677. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_moe_tail_guarded_bracket.py +0 -0
  678. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_mtp_bind_check.py +0 -0
  679. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_mtpk_bench.py +0 -0
  680. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_op_census.py +0 -0
  681. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_smoke_generate.py +0 -0
  682. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_sparse_gate_probe.py +0 -0
  683. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/deepseek_v4_validate_moe_tail_k3_bracket.py +0 -0
  684. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/dense_mrope_parity_ours.py +0 -0
  685. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/dense_mrope_reference_check.py +0 -0
  686. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/dense_vision_compiled_parity.py +0 -0
  687. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/eval_mtp_corrector.py +0 -0
  688. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/filter_mtp_adapter.py +0 -0
  689. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/fp16_turbo_exactness_20260707.py +0 -0
  690. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/fresh_venv_smoke.sh +0 -0
  691. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/gauntlet_scoreboard.py +0 -0
  692. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/gen_models_manifest.py +0 -0
  693. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/graft_vision_tower.py +0 -0
  694. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/hygiene_scan.sh +0 -0
  695. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/install_macos.sh +0 -0
  696. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/install_preview_global.sh +0 -0
  697. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/kvcache_exactness_audit_20260703.py +0 -0
  698. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/kvcache_soak_20260703.py +0 -0
  699. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/kvcache_warm_probe_20260703.py +0 -0
  700. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/make_fp16_precision_sibling.py +0 -0
  701. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/measure_request_capture.py +0 -0
  702. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/midform_gate.py +0 -0
  703. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/model_release_policy.py +0 -0
  704. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/mtp_cost_probe.py +0 -0
  705. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/oc_tap.py +0 -0
  706. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/oc_tap_diff.py +0 -0
  707. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/opencode_concurrency_qa.py +0 -0
  708. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/phase0h_paged_verifier_exactness.py +0 -0
  709. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/pillar_gate_qa.py +0 -0
  710. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/probe_mlx_pr3026_qsdpa.py +0 -0
  711. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/probe_mx_compile_buckets.py +0 -0
  712. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/probe_paged_gqa_sdpa_routes.py +0 -0
  713. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/qsa_indexer_prefill_numeric_check.py +0 -0
  714. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/qsa_prefill_battery2_20260830.py +0 -0
  715. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/qsa_prefill_battery_20260830.py +0 -0
  716. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/qsa_prefill_flash_numeric_check.py +0 -0
  717. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/quality_fp16_parent_logitdiff_20260707.py +0 -0
  718. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/qwen35b_mtp_batch_evalplus_guarded.py +0 -0
  719. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/qwen35b_mtp_batch_numerics_attribution.py +0 -0
  720. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/qwen35b_mtp_batch_numerics_guarded.py +0 -0
  721. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/qwen4_quality_component_parity.py +0 -0
  722. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/qwen4_vision_compiled_parity.py +0 -0
  723. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/qwen4exp_mtp_tiny_smoke.py +0 -0
  724. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/qwen4exp_numeric_check.py +0 -0
  725. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/r1_chisquare_verifier_correctness.py +0 -0
  726. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/release_macos_v1.sh +0 -0
  727. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/render_release_notes.py +0 -0
  728. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/replay_chat_request.py +0 -0
  729. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/restamp_head_quant_packs.py +0 -0
  730. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/run_context_degradation_diagnostics.py +0 -0
  731. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/run_harness_check.py +0 -0
  732. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/select_runtime_wheel.py +0 -0
  733. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/serve_openai_mtplx.py +0 -0
  734. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/session_cache_followup_qa.py +0 -0
  735. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/session_forensics.py +0 -0
  736. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/sparkle_rehearsal_kit.sh +0 -0
  737. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/step_acceptance.py +0 -0
  738. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/step_smoke.py +0 -0
  739. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/stream_qa_gate.sh +0 -0
  740. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/train_mtp_adapter_c4.py +0 -0
  741. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/upload_head_quant_packs.py +0 -0
  742. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/validate_step_mtp_injector.py +0 -0
  743. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/walltime-lab/run_project.py +0 -0
  744. {mtplx-2.12.0 → mtplx-2.12.2}/scripts/write_launch_lane_matrix.py +0 -0
  745. {mtplx-2.12.0 → mtplx-2.12.2}/setup.cfg +0 -0
  746. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_a3b_compiled_target_prefix.py +0 -0
  747. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_a3b_mtp_batch.py +0 -0
  748. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_a3b_mtp_batch_driver.py +0 -0
  749. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_a3b_whole_moe.py +0 -0
  750. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_adaptive.py +0 -0
  751. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_adaptive_dtemp.py +0 -0
  752. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_agent_prompt_fidelity.py +0 -0
  753. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_aime_serve_gate.py +0 -0
  754. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_allow_swap_427.py +0 -0
  755. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_app_cli_env_parity.py +0 -0
  756. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_ar_batch_cache_family_guard_420.py +0 -0
  757. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_ar_batch_penalties.py +0 -0
  758. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_aria2_downloader.py +0 -0
  759. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_artifacts.py +0 -0
  760. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_async_decode.py +0 -0
  761. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_attention_split.py +0 -0
  762. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_background_warmup.py +0 -0
  763. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_bank_history_policy.py +0 -0
  764. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_batch_pump_heartbeat.py +0 -0
  765. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_batched_decode.py +0 -0
  766. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_batched_greedy_argmax_tiebreak.py +0 -0
  767. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_batching_foundation.py +0 -0
  768. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_bench_depth_sweep_flags.py +0 -0
  769. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_block_verify_exact_law.py +0 -0
  770. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_bonsai_memory_table.py +0 -0
  771. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_bonsai_pack_metadata.py +0 -0
  772. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_bracket_tool_rescue.py +0 -0
  773. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_build_bonsai_mtplx_pack.py +0 -0
  774. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_bundle_native_runtime_wheel.py +0 -0
  775. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cache_state.py +0 -0
  776. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_canon_gate_hardening.py +0 -0
  777. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_canon_postcommit_extension.py +0 -0
  778. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_canonicalize_multimodal.py +0 -0
  779. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_ccopy_bank_route.py +0 -0
  780. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cli_greedy_draft_coupling.py +0 -0
  781. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cli_parity_tools.py +0 -0
  782. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_client_controls_default.py +0 -0
  783. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_code_eval.py +0 -0
  784. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_code_eval_gate.py +0 -0
  785. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cold_encode_bounded_units.py +0 -0
  786. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cold_prefix_ram_shadow.py +0 -0
  787. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cold_tier_backlog_admission.py +0 -0
  788. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cold_tier_disk_usage_scan.py +0 -0
  789. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cold_tier_eviction_yield.py +0 -0
  790. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cold_tier_foreground_yield.py +0 -0
  791. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cold_tier_min_useful_matched.py +0 -0
  792. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cold_tier_reconcile.py +0 -0
  793. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cold_tier_stats_cache.py +0 -0
  794. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cold_tier_write_budget.py +0 -0
  795. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_committed_reasoning_canonicalization.py +0 -0
  796. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_committed_token_splice.py +0 -0
  797. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_compile_state.py +0 -0
  798. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_compiled_ar_wiring.py +0 -0
  799. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_compiled_forward.py +0 -0
  800. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_compressed_tensors.py +0 -0
  801. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_config_profile_precedence.py +0 -0
  802. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_context_copy_joined_tokens.py +0 -0
  803. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_context_copy_ramp.py +0 -0
  804. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_context_copy_stats.py +0 -0
  805. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_context_degradation_profiles.py +0 -0
  806. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_convert_qwen4_exp_fp8_scales.py +0 -0
  807. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_correctors.py +0 -0
  808. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_cost_depth_policy.py +0 -0
  809. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_daemon_client.py +0 -0
  810. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_dashboard_endpoints.py +0 -0
  811. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_decode_growth_guards.py +0 -0
  812. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_adaptive_width_bracket.py +0 -0
  813. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_adaptive_width_policy.py +0 -0
  814. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_attention_island.py +0 -0
  815. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_attention_island_bracket.py +0 -0
  816. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_attn_proj_wide_m3.py +0 -0
  817. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_attn_proj_wide_m3_bracket.py +0 -0
  818. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_decode.py +0 -0
  819. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_dtypes.py +0 -0
  820. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_indexer.py +0 -0
  821. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_kernel_paths.py +0 -0
  822. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_loader.py +0 -0
  823. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_moe_tail.py +0 -0
  824. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_moe_tail_bracket.py +0 -0
  825. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_mtp.py +0 -0
  826. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_new_math.py +0 -0
  827. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_o_lora.py +0 -0
  828. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_parity.py +0 -0
  829. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_sinkhorn_kernel.py +0 -0
  830. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_spec.py +0 -0
  831. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_deepseek_v4_swiglu_clamp.py +0 -0
  832. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_demotions.py +0 -0
  833. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_demotions_generation_sites.py +0 -0
  834. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_demotions_graphbank_sites.py +0 -0
  835. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_demotions_qsa_prefill_sites.py +0 -0
  836. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_dense_decode_max_context.py +0 -0
  837. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_dense_mrope.py +0 -0
  838. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_dense_mrope_generation.py +0 -0
  839. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_dense_vision_compiled_parity_script.py +0 -0
  840. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_descriptor_family_collisions.py +0 -0
  841. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_descriptors_mimo_tune.py +0 -0
  842. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_descriptors_qwen4_exp_tune.py +0 -0
  843. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_device_draft_core.py +0 -0
  844. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_diagnostics.py +0 -0
  845. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_doctor_failed_start.py +0 -0
  846. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_download_progress.py +0 -0
  847. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_draft_confidence_trace.py +0 -0
  848. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_draft_launch_provenance.py +0 -0
  849. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_draft_lm_head.py +0 -0
  850. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_draft_lm_head_chunked_requantization.py +0 -0
  851. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_draft_temp_policy.py +0 -0
  852. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_encode_seam_unification.py +0 -0
  853. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_endpoint_probe_hygiene.py +0 -0
  854. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_engine_session_concurrency.py +0 -0
  855. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_engine_session_env.py +0 -0
  856. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_exact_verify_route.py +0 -0
  857. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_exactness_320_319.py +0 -0
  858. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_expected_value_greedy.py +0 -0
  859. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_family_settings.py +0 -0
  860. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_fast_sampling.py +0 -0
  861. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_final_committed_frontier_byte_skip.py +0 -0
  862. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_final_tool_preamble_stream_parity.py +0 -0
  863. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_finish_stop_origin.py +0 -0
  864. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_flash_next_quality_m5_ultra.py +0 -0
  865. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_flight_recorder.py +0 -0
  866. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_forge_cli.py +0 -0
  867. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_forge_discover_filter.py +0 -0
  868. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_forge_family_serve_depths.py +0 -0
  869. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_forge_mixed_convert.py +0 -0
  870. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_forge_mtp_key_forms.py +0 -0
  871. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_forge_mtp_norm_convention.py +0 -0
  872. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_forge_quality_build_script.py +0 -0
  873. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_forge_qwen4_exp_lane.py +0 -0
  874. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_forge_qwen4_exp_roundtrip.py +0 -0
  875. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_forge_verify_depths.py +0 -0
  876. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_forkev_telemetry.py +0 -0
  877. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_fused_add_rmsnorm_exactness.py +0 -0
  878. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_gdn_blocked_prefill.py +0 -0
  879. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_gdn_capture_rows.py +0 -0
  880. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_gdn_conv_norm_fused.py +0 -0
  881. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_gdn_conv_norm_rows.py +0 -0
  882. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_gdn_gated_norm.py +0 -0
  883. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_gdn_out_fused.py +0 -0
  884. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_gdn_postconv_fusion.py +0 -0
  885. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_gdn_postconv_impl_gate.py +0 -0
  886. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_gdn_prefill_prework.py +0 -0
  887. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_gdn_step_fused.py +0 -0
  888. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_gdn_tape_headquarter.py +0 -0
  889. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_gemma4_rotating_cache_trim.py +0 -0
  890. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_generation_rate_fields.py +0 -0
  891. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_generation_store_on_prefill.py +0 -0
  892. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_greedy_confidence_runner_telemetry.py +0 -0
  893. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_greedy_draft_coupling.py +0 -0
  894. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_greedy_trio_ports.py +0 -0
  895. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_hc_prefill.py +0 -0
  896. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_health_qwen4_install_reports.py +0 -0
  897. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_hermes_native_contract.py +0 -0
  898. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_hf_loader.py +0 -0
  899. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_hy_v3_mtp_backend.py +0 -0
  900. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_hy_v3_mtp_graft.py +0 -0
  901. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_hygiene_scan.py +0 -0
  902. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_hyper_scheduler.py +0 -0
  903. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_hyper_v3.py +0 -0
  904. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_idle_persistence_pump.py +0 -0
  905. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_idle_postcommit_subagent.py +0 -0
  906. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_install_macos_script.py +0 -0
  907. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_issue_404_macos27_mpp_compat.py +0 -0
  908. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_kernel_selfcheck.py +0 -0
  909. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_kernel_selfcheck_bonsai.py +0 -0
  910. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_kernel_selfcheck_qwen4_prefill.py +0 -0
  911. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_kv_quant_memory.py +0 -0
  912. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_kv_quant_q4_kernel_route.py +0 -0
  913. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_laguna_alt_step.py +0 -0
  914. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_laguna_compiled_step.py +0 -0
  915. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_laguna_fused.py +0 -0
  916. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_laguna_steel_attn.py +0 -0
  917. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_launch_lane.py +0 -0
  918. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_lazy_snapshot_cow.py +0 -0
  919. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_lease_keeps_draft_history.py +0 -0
  920. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_lfm2_descriptor_resolution.py +0 -0
  921. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_lfm2_fast.py +0 -0
  922. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_loop_guard.py +0 -0
  923. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_max_idle_watchdog.py +0 -0
  924. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_max_lifecycle.py +0 -0
  925. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_memory_plan_bonsai.py +0 -0
  926. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_metadata_scrub.py +0 -0
  927. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_midform_gate.py +0 -0
  928. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_mimo_catalog.py +0 -0
  929. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_mlx_process_env.py +0 -0
  930. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_model_catalog.py +0 -0
  931. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_model_scheduler.py +0 -0
  932. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_model_updates.py +0 -0
  933. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_moe_force_unsorted.py +0 -0
  934. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_moe_glu_decode.py +0 -0
  935. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_moe_glu_verify.py +0 -0
  936. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_moe_packed_projections.py +0 -0
  937. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_mtp_activation_stats.py +0 -0
  938. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_mtp_adapters.py +0 -0
  939. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_mtp_alias_load_path.py +0 -0
  940. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_mtp_batch_numerics.py +0 -0
  941. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_mtp_batch_serving.py +0 -0
  942. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_mtp_depth_n.py +0 -0
  943. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_mtp_depth_sweep.py +0 -0
  944. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_mtp_final_state_covers_committed_tokens.py +0 -0
  945. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_mtp_norm_convention.py +0 -0
  946. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_mtp_payload_guards.py +0 -0
  947. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_mtp_weightless_degrade.py +0 -0
  948. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_nax_attention_hardware.py +0 -0
  949. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_nax_detect.py +0 -0
  950. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_nax_verify.py +0 -0
  951. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_ngram_prewarm_option.py +0 -0
  952. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_ngram_prewarm_reservation.py +0 -0
  953. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_ngram_row_hotness.py +0 -0
  954. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_ngram_row_layout.py +0 -0
  955. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_ngram_table_memory.py +0 -0
  956. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_no_mlx_imports.py +0 -0
  957. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_non_finite_logits_and_lazy_sampler_law.py +0 -0
  958. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_omlx_bridge.py +0 -0
  959. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_omlx_bridge_late_system_messages.py +0 -0
  960. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_onboarding.py +0 -0
  961. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_one_shot_family_contract.py +0 -0
  962. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_optimization_profiles.py +0 -0
  963. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_orphan_stream_guard_first_line.py +0 -0
  964. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_orphan_tool_markup.py +0 -0
  965. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_pack_contract_states.py +0 -0
  966. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_paged_tailmask_elide.py +0 -0
  967. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_passive_probe_telemetry.py +0 -0
  968. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_penalties.py +0 -0
  969. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_penalty_request_wiring.py +0 -0
  970. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_persistent_replay.py +0 -0
  971. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_phase0h_paged_verifier_exactness.py +0 -0
  972. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_policy_fingerprint_stability.py +0 -0
  973. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_postcommit_arrival_wait.py +0 -0
  974. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_postcommit_cross_session_yield.py +0 -0
  975. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_postcommit_resolve_for_request.py +0 -0
  976. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_postcommit_tools_plumbing.py +0 -0
  977. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_postcommit_wait.py +0 -0
  978. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_postcommit_wait_integration.py +0 -0
  979. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_postcommit_wait_progress.py +0 -0
  980. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_pre_first_token_telemetry.py +0 -0
  981. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_prefill_bench.py +0 -0
  982. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_prefill_chunk_defaults.py +0 -0
  983. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_prefill_tps_regression.py +0 -0
  984. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_prism_bonsai_kernels.py +0 -0
  985. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_prism_hadamard_qwen35.py +0 -0
  986. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_profile_default_resolution.py +0 -0
  987. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_profiles.py +0 -0
  988. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_proj_fusion.py +0 -0
  989. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_proj_quant.py +0 -0
  990. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_prompt_encoding.py +0 -0
  991. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_pull_tls_trust.py +0 -0
  992. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qmv4_matmul.py +0 -0
  993. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_dense_band_sdpa.py +0 -0
  994. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_flash_skip.py +0 -0
  995. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_gate_safety.py +0 -0
  996. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_gather.py +0 -0
  997. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_gather_rows.py +0 -0
  998. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_indexer_compile.py +0 -0
  999. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_indexer_fused_integration.py +0 -0
  1000. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_indexer_prefill_mpp_runtime.py +0 -0
  1001. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_indexer_prefill_runtime_blocks.py +0 -0
  1002. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_indexer_prefill_simd_topk.py +0 -0
  1003. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_indexer_prefill_static.py +0 -0
  1004. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_indexer_prepare_metal.py +0 -0
  1005. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_mtp_precompute.py +0 -0
  1006. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_mtp_precompute_wiring.py +0 -0
  1007. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_portable_producer_gate.py +0 -0
  1008. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_prefill_direct.py +0 -0
  1009. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_prefill_direct_greedy.py +0 -0
  1010. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_prefill_direct_numeric.py +0 -0
  1011. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_prefill_direct_routing.py +0 -0
  1012. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_prefill_direct_static.py +0 -0
  1013. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_prefill_flash_contract.py +0 -0
  1014. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_prefill_flash_direct_parity.py +0 -0
  1015. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_prefill_flash_static.py +0 -0
  1016. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_prefill_gather_tier.py +0 -0
  1017. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_prefill_runtime_gates_static.py +0 -0
  1018. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_prefill_wiring.py +0 -0
  1019. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_restore_staging.py +0 -0
  1020. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_selector_prefix_contract.py +0 -0
  1021. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qsa_tiled_scores.py +0 -0
  1022. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen35b_mtp_batch_evalplus_guarded.py +0 -0
  1023. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen35b_mtp_batch_numerics_attribution.py +0 -0
  1024. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen35b_mtp_batch_numerics_guarded.py +0 -0
  1025. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen38_family.py +0 -0
  1026. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen3_5_mtp_backend.py +0 -0
  1027. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen3_5_mtp_object_level.py +0 -0
  1028. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen3_5_mtp_sidecar_norms.py +0 -0
  1029. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_block_verify.py +0 -0
  1030. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_claim_contract.py +0 -0
  1031. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_compiled_mtp_prepare.py +0 -0
  1032. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_draft_chain_pipeline.py +0 -0
  1033. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_draft_device_chain.py +0 -0
  1034. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_draft_k20_prescatter.py +0 -0
  1035. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_exp_capture_commit.py +0 -0
  1036. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_exp_compile.py +0 -0
  1037. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_exp_gdn_inproj_fusion.py +0 -0
  1038. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_exp_inforward_boundary_capture.py +0 -0
  1039. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_exp_qsa_cache.py +0 -0
  1040. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_exp_qsa_qkv_fusion.py +0 -0
  1041. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_exp_rope.py +0 -0
  1042. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_fixed_bank_bf16_parity.py +0 -0
  1043. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_fixed_host_tokens_static.py +0 -0
  1044. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_fixed_m4_vision_admission.py +0 -0
  1045. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_frspec_native.py +0 -0
  1046. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_hc_prefill_compile.py +0 -0
  1047. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_hc_prefill_read_switch.py +0 -0
  1048. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_m4_routed_glu.py +0 -0
  1049. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_m4_stage3.py +0 -0
  1050. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_moe_prefill_combine.py +0 -0
  1051. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_mtp_history_cache_only.py +0 -0
  1052. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_ple_first_gather_early.py +0 -0
  1053. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_ple_lookahead_restored_suffix.py +0 -0
  1054. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_ple_prefill_lookahead.py +0 -0
  1055. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_ple_serving_verdicts.py +0 -0
  1056. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_prefill_dq_gemm.py +0 -0
  1057. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_prefill_midloop_eval.py +0 -0
  1058. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_qsa_m4_fused_kv_gather.py +0 -0
  1059. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_route_kernel.py +0 -0
  1060. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_verify_glue.py +0 -0
  1061. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_verify_sdpa_head_chunk.py +0 -0
  1062. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_vision_compiled_parity_cpu.py +0 -0
  1063. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_vision_compiled_parity_script.py +0 -0
  1064. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_vision_compiled_route_e2e.py +0 -0
  1065. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_vision_mrope.py +0 -0
  1066. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen4_wide_prefill_chunk.py +0 -0
  1067. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_qwen_row_owned_router.py +0 -0
  1068. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_ragged_kv_cache.py +0 -0
  1069. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_release_2114_manifest.py +0 -0
  1070. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_release_notes_template.py +0 -0
  1071. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_repetition_stop_legitimate_code.py +0 -0
  1072. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_repetition_stop_long_cycle.py +0 -0
  1073. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_request_capture.py +0 -0
  1074. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_request_log_privacy.py +0 -0
  1075. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_request_observability_golden.py +0 -0
  1076. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_response_envelope_parity.py +0 -0
  1077. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_retrieval.py +0 -0
  1078. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_route_tape.py +0 -0
  1079. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_runtime_ar_final_state.py +0 -0
  1080. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_runtime_deepseek_v4_o_lora.py +0 -0
  1081. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_runtime_env_flags_refresh.py +0 -0
  1082. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_runtime_env_latebind.py +0 -0
  1083. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_runtime_kpis.py +0 -0
  1084. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_runtime_model_alias.py +0 -0
  1085. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_runtime_obs_graphbank.py +0 -0
  1086. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_runtime_obs_kernel_bails.py +0 -0
  1087. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_runtime_obs_profiles.py +0 -0
  1088. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_runtime_obs_stream_holdback.py +0 -0
  1089. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_runtime_scheduler_histogram.py +0 -0
  1090. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_runtime_session_sizing.py +0 -0
  1091. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_sampling.py +0 -0
  1092. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_scoped_reasoning_history.py +0 -0
  1093. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_sdpa_gqa_packed.py +0 -0
  1094. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_sdpa_gqa_packed_quant.py +0 -0
  1095. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_sdpa_nax_flash.py +0 -0
  1096. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_sdpa_nax_tile.py +0 -0
  1097. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_select_runtime_wheel.py +0 -0
  1098. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_serve_generation_mode_default.py +0 -0
  1099. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_serve_network_share.py +0 -0
  1100. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_server_obs_caps_and_health.py +0 -0
  1101. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_server_obs_postcommit_health.py +0 -0
  1102. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_server_obs_reasoning_rows.py +0 -0
  1103. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_server_origin_policy.py +0 -0
  1104. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_server_responses.py +0 -0
  1105. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_session_bank_boundary_shed.py +0 -0
  1106. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_session_bank_chain_shed.py +0 -0
  1107. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_session_bank_env_caps.py +0 -0
  1108. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_session_bank_gc.py +0 -0
  1109. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_session_bank_lease_accounting.py +0 -0
  1110. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_session_bank_protected_terminal.py +0 -0
  1111. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_session_bank_recurrent_partial_restore.py +0 -0
  1112. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_session_bank_restore_aliasing.py +0 -0
  1113. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_session_bank_snapshot_settle.py +0 -0
  1114. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_session_identity_turn_boundary.py +0 -0
  1115. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_smart_fan_stall_probe.py +0 -0
  1116. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_smart_fan_wedged_request.py +0 -0
  1117. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_snapshot_free_rejection_repair.py +0 -0
  1118. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_source_wrapper.py +0 -0
  1119. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_ssd_boundary_repersist.py +0 -0
  1120. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_ssd_spill.py +0 -0
  1121. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_stats_footer_scope.py +0 -0
  1122. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_step3p5_mtp_patch.py +0 -0
  1123. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_stream_error_receipt.py +0 -0
  1124. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_stream_guard_env.py +0 -0
  1125. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_stream_release_pacer.py +0 -0
  1126. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_stream_splitter_contract.py +0 -0
  1127. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_stream_stall_watchdog.py +0 -0
  1128. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_stream_transcript_golden.py +0 -0
  1129. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_stream_visible_cadence.py +0 -0
  1130. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_sustained_long_context_qa.py +0 -0
  1131. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_tail_ar_warm_restore_identity.py +0 -0
  1132. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_tail_config_sampler_presence.py +0 -0
  1133. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_tail_gemma4_stream_holdback.py +0 -0
  1134. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_tail_profile_truth.py +0 -0
  1135. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_tensoroffset_kv_inplace.py +0 -0
  1136. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_thermal.py +0 -0
  1137. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_thermal_sidecar.py +0 -0
  1138. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_thinking_guard.py +0 -0
  1139. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_thinking_splitter_marker_quotes.py +0 -0
  1140. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_tiny_max_tokens_warning_436.py +0 -0
  1141. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_tokenizer_pretokenizer_repair.py +0 -0
  1142. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_tool_aware_stream_translator.py +0 -0
  1143. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_tool_call_hidden_guard_and_key_repair.py +0 -0
  1144. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_tool_nested_args_streaming.py +0 -0
  1145. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_trace_diagnostics.py +0 -0
  1146. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_trace_incidents.py +0 -0
  1147. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_trace_parity.py +0 -0
  1148. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_tune_qwen4_exp_verify_strategy.py +0 -0
  1149. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_tune_record_wizard_parity.py +0 -0
  1150. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_turboquant.py +0 -0
  1151. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_turboquant_fallback.py +0 -0
  1152. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_ui_progress.py +0 -0
  1153. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_validators.py +0 -0
  1154. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_verify_crossrow.py +0 -0
  1155. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_vision_control_literals.py +0 -0
  1156. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_vision_session_cache.py +0 -0
  1157. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_vision_tower.py +0 -0
  1158. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_vision_trunk_forward_positions.py +0 -0
  1159. {mtplx-2.12.0 → mtplx-2.12.2}/tests/test_vllm_reference.py +0 -0
  1160. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/__init__.py +0 -0
  1161. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/README.md +0 -0
  1162. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/__init__.py +0 -0
  1163. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/build.py +0 -0
  1164. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/constants.py +0 -0
  1165. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v1/copy_blocks.metal +0 -0
  1166. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v1/float8.metal +0 -0
  1167. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v1/gather_kv_cache.metal +0 -0
  1168. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v1/kv_scale_update.metal +0 -0
  1169. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v1/pagedattention.metal +0 -0
  1170. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v1/reshape_and_cache.metal +0 -0
  1171. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v1/utils.metal +0 -0
  1172. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v2/copy_blocks.metal +0 -0
  1173. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v2/float8.metal +0 -0
  1174. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v2/gather_kv_cache.metal +0 -0
  1175. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v2/gdn_linear_attention.metal +0 -0
  1176. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v2/kv_scale_update.metal +0 -0
  1177. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v2/pagedattention.metal +0 -0
  1178. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v2/reshape_and_cache.metal +0 -0
  1179. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v2/turboquant.metal +0 -0
  1180. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/kernels_v2/utils.metal +0 -0
  1181. {mtplx-2.12.0 → mtplx-2.12.2}/vllm_metal/metal/paged_ops.cpp +0 -0
@@ -0,0 +1,4570 @@
1
+ # Changelog
2
+
3
+ All notable user-facing changes to MTPLX. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [2.12.2] - 2026-10-03
8
+
9
+ ### Fixed
10
+
11
+ - **Macs with 8, 16 and 32 GB serve ordinary prompts again.** 2.12.1 refused prompts that 2.12.0 served on these Macs with the model the app picks for them. On an M5 Max set up as each Mac (its engine limit, its free-memory floors and a fixed free-memory reading), 2.12.1 refused an 18-token prompt to Qwen 3.5 4B on 8 GB with 3 GiB free, 2,600 to 7,800-token prompts and an agent's first turn to Ternary Bonsai 2 27B on 16 GB, and 6,800 and 7,800-token prompts and an agent's third turn to Qwen 3.8 27B Optimized Speed on 32 GB; GitHub's 7 GB M1 test machine refused a one-line request with a 507. 2.12.2 serves all of them (October 3), and the M1 test machine passes. Three changes:
12
+ - A dense model's prompt is priced at what it was measured to use through `mtplx serve`: the 4B 0.19 GiB for a 59-token prompt and 1.55 to 1.65 GiB per 2,048-token chunk, Bonsai 2 27B 0.45 GiB for a 98-token prompt, Bonsai and the 27B 2.5 to 2.7 GiB per 2,048-token chunk, plus a margin of 0.09 to 0.25 GiB. 2.12.1 charged every model the 27B's 3 GiB per 2,048-token chunk and about 2 GiB for a one-line prompt. Models with routed experts, Flash-Next and Gemma 4 keep their 2.12.1 prices.
13
+ - A prompt that does not fit in 2,048-token chunks runs in 1,024-token chunks, or 512-token chunks for prompts up to about 8,000 tokens, before it is refused (not Flash-Next or Gemma 4, which keep their own chunk widths). A narrower chunk is used only where it saves at least 128 MiB. A 1,024-token chunk uses about 1 GiB less on the 27B models; a 7,000-token Bonsai prompt took 9.5 s at 2,048, 9.6 s at 1,024 and 9.7 s at 512 tokens a chunk (single runs).
14
+ - Memory outside the GPU allocator is charged only past 4 GiB on Macs under 64 GB. 2.12.1 charged it past a sixteenth of RAM (1 GiB on 16 GB, 2 GiB on 32 GB), while a healthy engine holds 2.1 to 2.9 GiB there with Bonsai and 2.3 to 4.0 GiB with the 27B. Macs with 64 GB or more are unchanged.
15
+ - **#499's 99,355-token 27B turn on a 48 GB Mac runs in 1,024-token chunks**, priced at 34.87 GiB against the 36 GiB limit, where 2.12.1 refused it (tests with the reporter's numbers).
16
+
17
+ ### Changed
18
+
19
+ - **Dependencies.** urllib3 2.8.0 (#582 by @dependabot).
20
+
21
+ ## [2.12.1] - 2026-10-02
22
+
23
+ ### Added
24
+
25
+ - **Log-probabilities for the first generated token** (#530 and #543 by @jvmenen). `/v1/completions` accepts `logprobs: K` with `max_tokens: 1` and returns OpenAI-shaped `choices[0].logprobs`; `/v1/chat/completions` accepts `logprobs: true` with `top_logprobs: K` and returns `choices[0].logprobs.content`. The values are the raw distribution of the row that produced the first token, before temperature, penalties, grammar masks and steering, and a full-prompt session cache hit uses the restored logits. Streaming, a non-empty `stop`, `max_tokens` other than 1 and K above `MTPLX_PROMPT_LOGPROBS_MAX` (default 128) return 400; `/v1/messages` does not expose logprobs. Chat entries report each token's own bytes as the tokenizer's decoder defines them, or null. The Gemma 4 pair serves it too. Measured by @jvmenen on an M5 Pro 64 GB with Qwen 3.6 35B-A3B, 240 prompts: the same top label as echo prompt scoring on 228 of 240, p50 486 ms against 562 ms.
26
+
27
+ - **Prompt scoring on Gemma 4** (#551 by @jvmenen). `/v1/completions` with `echo`, `logprobs` and `max_tokens: 0` answered HTTP 500 on the Gemma 4 pair. Scoring now runs the target's own prompt pass in spans no wider than the admitted prompt width, produces logits in bounded chunks, follows the serving memory policy and answers 507 under memory pressure.
28
+
29
+ - **Every response says where its time to the first token went.** `mtplx_stats.ttft_spans.exclusive_s` splits the time from the request's arrival to the first visible delta (content, reasoning or tool call) into named, non-overlapping spans: body parse, encode, the previous turn's save, the model-thread queue and what it waited behind, the memory admission and the engine. `attempts`, `discarded_attempt_wall_s` and `retry_path` name any generation a repair path threw away. Chat, Anthropic, `/v1/completions` and batched responses carry the spans, and `/health` shows the median and p90 of each span over recent requests.
30
+
31
+ - **Thermal pressure in `/health` and every receipt.** A cached sampler reads macOS's thermal pressure every two seconds; every flight sample carries it, every request receipt carries `thermal_pressure` and `thermal_pressure_max`, and decoding makes no extra system call.
32
+
33
+ - **`execution_window` in `/health`, and Pi and OpenCode set up from it.** The engine publishes the context window it can serve on this Mac. The app syncs Pi's and OpenCode's settings to it once the engine answers, and `mtplx start pi`, `mtplx start opencode` and `mtplx connect opencode` do the same, including against a server that is already running and a server that runs another model, which the client is now set up for. Pi's `maxTokens` stays equal to the window, as in 2.12.0, and a window pair the user set is never changed.
34
+
35
+ - **`--memory-limit SIZE|max` for `mtplx serve`** (#548), and `MTPLX_MEMORY_LIMIT_BYTES=max` for launchers. `max` uses everything outside macOS's own reserve, 112 GiB on a 128 GB Mac, for a Mac that runs nothing but the server; on Macs of 32 GB and under it equals the default. The whole-Mac checks apply at every limit.
36
+
37
+ - **The app explains a re-read and warns about disk.** While a prompt is read again, the Live gauge and the Activity and Requests rows say why, where the state resumes, what the SSD restored and the server's estimate, which is shown only after a prompt speed has been measured. Every cache miss code the server sends has a plain sentence in all 13 languages. A banner on the Live tab shows when the SSD cache is low on disk (two copies of the largest conversation do not fit above the 10 GiB floor) or full (saves stopped).
38
+
39
+ - **The live gauge shows the current phase's decode rate** (reasoning, answer or tool call) instead of the whole answer's average, which a slow reasoning phase dragged down; the finished request keeps its overall rate.
40
+
41
+ - **Diagnostics.** `memory_guard.guard_degraded` on `/health` and the dashboard stream when a guard step raises; `session_bank.last_ram_miss_reason` for a shared prompt start the RAM cache passed over (#534 by @jvmenen); `prefill_system_memory` in each request's metrics row; `MTPLX_KV_ATTENTION_TRACE=1` or `nonfinite` and `MTPLX_PREFILL_GUARD_TRACE=1`. A request that fails with non-finite logits logs the cache, route, offset and capacity of the first layer involved.
42
+
43
+ - **Experimental, off by default.**
44
+ - `MTPLX_SSD_INCREMENTAL_ENCODE=1` (#556 by @jvmenen): the SSD cache saves only the KV blocks that changed. Off because its block fingerprint is not a content identity: flipping the signs of any two float32 values leaves it unchanged, so a changed block could reuse its predecessor's data and restore the wrong state.
45
+ - `MTPLX_FIXED_M4_COPY_WINDOWS=1`: Flash-Next's context-copy rounds replay on the compiled verifier, bit-identical to the eager window in tests. Off because it has not been checked on the full model; no speed is claimed.
46
+ - `MTPLX_THINKING_GUARD_SCOPE=all` (#545 by @bpmforge): the opt-in reasoning budget guard (`--agent-thinking-budget` or `MTPLX_THINKING_BUDGET`) also covers plain chat with thinking on. Off because it can end reasoning early.
47
+
48
+ ### Changed
49
+
50
+ - **The memory guard prices every request and releases idle state before it refuses** (#567, #525, #499). 2.12.0 checked only requests with 4,096 or more uncached tokens and charged them once plus a flat 3 GiB. Every request is now priced at its largest moment (the end of the prompt including any restore copy, the move to paged memory, the start of the answer), with full-width quantized snapshots, routed MoE rows, the draft head's history (4,096 bytes a token on the 27B) and each backend's own prompt pass. Before a refusal the engine gives back, in measured steps, its buffer pool, the request's own prompt copy, superseded entries, and idle conversations whole, oldest and SSD-saved first, never a conversation that is generating; a request that arrives during a release waits instead of getting a 409. A refusal is a 507 that names what holds the memory and when a retry can work. On a 48 GB Mac with the 27B, #499's turn is priced at 39.47 GiB and refused before prefill, where 2.12.0 admitted it and reached 38.5 GiB against a 36 GiB limit. Pressure trims now halve what the session cache holds: on #525's 64 GB Mac, 421 trims in a row had evicted nothing. A guard step that raises fails open and shows as `guard_degraded`. `MTPLX_PREFILL_ADMISSION_MIN_MISS_TOKENS` is removed.
51
+
52
+ - **The guard reads what macOS can hand out, before every prompt chunk.** 2.12.0 read `kern.memorystatus_level`, which counts memory macOS can free only by compressing other apps as available. The supply is now free, purgeable and file-backed pages from `host_statistics64`, read before every prompt chunk. The stop floor is the largest of 1 GiB, 2.5% of RAM and a sixteenth of wired memory, and the shed floor is twice that (6.2 to 6.4 GB and 12.4 to 12.8 GB in the October 2 session). A prompt also stops when macOS compresses another eighth of RAM across a run of requests (16 GiB on 128 GB), or holds a quarter of RAM compressed while still losing ground; macOS 27's early compression on a healthy Mac does not count. When only the Mac is short, the engine clears its buffer pool, then narrows Flash-Next's prompt chunk from 4,096 to 2,048 rows, then releases idle state, and picks the chunk width again afterwards. On September 29, 2.12.0's 130,048-token cold Flash-Next prompt drove 27 GB into compressed memory until the test's watchdog stopped it; build 0e0bb014 served it with no swap and 15 MB of compressor growth (not re-run on the final build).
53
+
54
+ - **Memory outside the GPU allocator counts once it passes a sixteenth of RAM.** 2.12.0's allowance was the larger of 8 GiB and RAM minus the reserve minus the limit, so the ceiling stayed at 112 GiB on a 128 GB Mac at any limit. The allowance is now RAM/16 between 1 and 8 GiB, an explicit limit caps it and the MLX cache limit at a twelfth of the limit (8 GiB at 96, 7.5 at 90, 4 at 48), and memory past it is charged. On October 2 the process ceiling was 98 GiB at the 90 GiB default.
55
+
56
+ - **The default memory limit on 128 GB Macs is 90 GiB (was 96 GiB).** From 128 GB up the engine leaves at least 38 GiB outside its allocator; Macs under 128 GB and from 192 GB up keep the 75% rule. On September 27, with 16 GB of other apps (build aa8ec218), a 74,000-token compaction-style request was refused with a 507 at 96 GiB to keep the Mac safe and served at 90 GiB. A model whose resident floor is between 90 and 96 GiB keeps 96, and one above 96 keeps everything outside the reserve. What it costs: on 128 GB M1 to M4 Macs without the native prompt kernel (pip and Homebrew), Flash-Next Optimized Speed plans 65,536 tokens instead of 114,688; from the app, with the kernel, they plan 262,144 once the kernel passes its startup check, which has not run on M1 to M4 hardware. Qwen 3.8 27B Optimized Quality's session cache under a full 262K window is 42.5 GiB instead of 48 GiB. `--memory-limit 96G` restores the old limit.
57
+
58
+ - **The MLX buffer pool goes back to macOS when a request ends and when the model goes idle.** It held 0.53 GiB at the median and 3.5 GiB at p90 between 4,104 logged requests. It keeps its full size while work runs, because a 2 GiB bound cost 6 to 10% of the 16K and 64K prompt rate. `MTPLX_CLEAR_CACHE_AFTER_REQUEST=off` keeps the pool.
59
+
60
+ - **Flash-Next verifies without copying the conversation** (`MTPLX_ONE_COPY`, on by default for Flash-Next). The compiled verifier adopts the buffers the prompt was read into instead of copying them into a padded buffer, the session cache keeps a reference to the live cache with its recurrent restore points instead of views, and buffers that must grow are resized one layer at a time (0.36 to 0.57 GB at the 147K edge in the October 2 session, against 4.2 GB for a padded copy). A turn that cannot afford the growth verifies eagerly for that turn only. The memory saved has not been measured on its own. `MTPLX_ONE_COPY=0` restores the copying store.
61
+
62
+ - **An answer whose cache cannot grow stops between rounds.** Growth happens at the top of a round; a refused growth ends the answer with every streamed token kept, `finish_reason` `length`, `memory_stop` and a plain message, and the next turn reuses the conversation. A context-copy round reserves before it stages its window, and a cancel or error during growth hands the cache back whole. Before its prompt, a request's longest answer is priced (`_answer_room`); when only a shorter answer fits, the request runs with that limit and reports `answer_capped_for_memory` if it reaches it. The stop has run only in tests on small models.
63
+
64
+ - **Screenshots keep the conversation's cache.** Image conversations are matched and committed in a view keyed by each image's content, a restore never lands inside an image, and recurrent restore points are kept before, between and after images. A turn that changes text after a screenshot resumes at the change; a request with different pixels in the same place never reuses the old image's state.
65
+
66
+ - **Recurrent restore points are kept within a byte budget** on a fixed 8,192-token grid instead of being thinned after every capture; a 16,371-token match that restored at 4,096 now restores at 8,192.
67
+
68
+ - **The SSD cache replaces an entry only after its successor is installed, and holds two copies of the largest conversation.** Both writers reserve, write beside the existing entries, install, and only then retire old entries; a save that does not fit is skipped with a recorded reason. The budget is normally a quarter of the usable space, raised to two copies of the largest conversation above a 10 GiB free-disk floor and never above the configured cap; `stats()` reports the disk state.
69
+
70
+ - **A streamed turn ends at its last token** (adapted from #557 by @jvmenen). For a session the client names, the final chunk and `[DONE]` no longer wait for the session's save (18 ms at the median and 160 ms at p90 in the flight logs, up to 30 s behind another client's prompt, #425). The next request of the same session waits for that save before it reads the session, other sessions never wait, and a stalled model thread answers that request with 503 `session_commit_stalled` after `MTPLX_STREAM_STALL_DEADLINE_S` (300 s). `MTPLX_STREAM_TERMINAL_FRAME_BEFORE_COMMIT=0` restores the old order.
71
+
72
+ - **The hidden-tool stream guard is opt-in** (`MTPLX_STREAM_HIDDEN_TOOL_GUARD=on`). It cancelled any tool call that buffered 2,048 tokens and 30 s outside a known parameter, which cut long valid tool calls.
73
+
74
+ - **Gemma 4 processes its prompt in chunks.** The whole-prompt pass grew with the square of the prompt: on the 31B pack, cold prompts of 6,026, 12,026 and 24,026 tokens peaked at 25.16, 43.02 and 92.08 GiB with 12.9, 41.4 and 173.5 s to the first token. Chunked, they peaked at 21.54, 25.48 and 31.1 GiB with 11.7, 20.3 and 45.1 s, and 33,026 tokens fit in 37.75 GiB (September 27). The output changes by rounding (at 4K, from the eleventh word); `MTPLX_GEMMA4_PREFILL_CHUNK_TOKENS=whole` restores the single pass, whose first turn matches 2.12.0. A saved Gemma prompt restores after an edit only within its last 2,047 tokens.
75
+
76
+ - **Faster kernels, each bit-identical to the code it replaces and checked on first use** (end-to-end speed against 2.12.0 was not measured). On M5 Macs, Flash-Next prompt processing reads each routed token row in place and fuses SiLU into the gate and up step (10.0 to 7.65 ms per layer at a 4,096-token chunk, kernel alone, `MTPLX_MOE_SORTED_GATHER_KERNEL=0` turns it off), and the same kernel serves Qwen 3.5 and 3.6 MoE models such as the 35B-A3B (1.19 to 1.24 times as fast for the expert module). Flash-Next's compiled verifier reads hyper-connections in three kernels instead of about eleven on single-die M3, M4 and M5 GPUs (`MTPLX_QWEN4_HC_VERIFY_READ=0`), selects attention blocks in two kernels (0.843 to 0.590 ms per verify step at 4K, 12 layers chained), builds the pooled index key row in one (1.380 to 0.759 ms at 4K), and on Optimized Speed reduces the expert slots in parallel (275.4 to 257.0 µs per layer). The draft head's history append during prompt processing evaluates only its cache on Qwen 3.5, 3.6 and 3.8 packs (#555 by @jvmenen, `MTPLX_MTP_HISTORY_CACHE_ONLY=0` turns it off). Sampled copy rounds read the GPU once per 8 rows (3 reads instead of 24 for a fully accepted 24-token block). Long Flash-Next conversations size the verifier in 8,192-token steps (`MTPLX_QWEN4_FIXED_M4_CAPACITY_BUCKET`), 95% fewer re-traces at 16K to 64K.
77
+
78
+ - **Agent turns tokenize only what is new** (#533, #541 and #542 by @jvmenen). Segments are memoized by tokenizer identity, its added tokens with their flags, and the text; plain chat is cut into turns only when the cut is proven on the tokenizer. Measured by @jvmenen on an M5 Pro with Qwen 3.6 35B-A3B: an 82K-token tool transcript encodes in 57 ms instead of 107 ms, a short follow-up to an 80K chat in 1.3 ms instead of 66 ms, and the Gemma 4 check takes about 15 µs instead of about 47 ms. `MTPLX_CHAT_SEGMENT_MEMO_TOKENS` sets the memo's token budget, and `MTPLX_CHAT_SEGMENT_MEMO=off` and `MTPLX_CHAT_TURN_SEGMENTS=off` turn them off.
79
+
80
+ - **Prompt scoring picks its top-K without a full-vocabulary log-softmax** (#532 by @jvmenen): bitwise the same values, 3.4 to 5.2 ms per chunk on the GPU instead of 47 ms (synthetic, M5 Pro).
81
+
82
+ - **Parallel tool calls use the tokenizer's native tool template** (#270 by @shiftedx) when a Pi or Hermes request sets `parallel_tool_calls: true`; omitted or false keeps the hybrid format. Native tool prompts keep the client's system instructions and honor a forced `tool_choice`.
83
+
84
+ - **Dependencies.** pip and Homebrew install exactly MLX 0.32.2, the version the app bundles (0.32.3's Flash-Next output differs and was not checked). llguidance 1.8 or later is required. transformers is allowed up to 5.16 (#538 by @dependabot), the releases checked against the packs' tokenizers.
85
+
86
+ - **`ttft_s` starts at the request's arrival**, so it includes the body read and JSON parse, and `usage.completion_tokens` describes the returned attempt.
87
+
88
+ ### Removed
89
+
90
+ - **The stalled-agent retry.** It re-ran a whole turn when an answer after tool results ended with text its pattern read as a promise to act, such as `Say "A" or "B" and I'll run it.`, and threw away finished answers (82 s of them in one session). The read-only force-answer retry keeps the pattern, opt-in.
91
+
92
+ - **`MTPLX_PREFILL_ADMISSION_MIN_MISS_TOKENS`**, since every request is priced.
93
+
94
+ ### Fixed
95
+
96
+ - **Flash-Next prompts on M5 Macs no longer read leftover memory in the expert step.** MLX 0.32.2's tensor-unit kernel for expert-sorted quantized matrix multiplies skipped rows when a call routed more than 32,767 rows and the count was not a multiple of its tile. Flash-Next sends each token to ten experts, so in 2.12.0, 794 of every 4,096 cold prompt lengths above 3,277 tokens (19.4%) took part of their expert outputs from uninitialized memory, and warm suffixes over 512 tokens by the same rule. Such calls are padded and sliced back, bit-identical to a correct call, for every MoE model on M5 GPUs with MLX before 0.32.3.
97
+
98
+ - **Structured output no longer loops** (#547). The schema was serialised with sorted keys before compiling, which forced alphabetical key order, and JSON whitespace was unbounded with carriage returns allowed; the reported case ended in two spaces and 640 carriage returns. The grammar keeps the client's key order and allows one space, or one or two newlines plus at most 20 spaces or tabs, never a carriage return. A client `x-guidance` that sets any whitespace option compiles exactly as sent. A replay of the reporter's schema on September 27: 16 of 16 valid and ordered, longest whitespace run 3.
99
+
100
+ - **Screenshots after tool calls work in streaming clients** (#581). A streaming request whose last user message carried an image after a tool call, the shape OpenCode sends after a screenshot tool, could fail with "vision requests must not use the session bank without content-keyed ids". With the reporter's script on Qwen 3.8 27B Optimized Speed, 3 of 5 streaming runs failed on 2.12.0 and 5 of 5 passed on 2.12.1 (October 2).
101
+
102
+ - **Constrained drafting follows the plain decoding distribution.** Every drafted verify row is masked at its own grammar position, so with top-k or top-p a constrained request no longer over-weights legal tokens, and greedy constrained output commits the same stream with more drafts accepted. The session cache keeps unmasked logits, so a later request on the same prefix is not bound by an earlier grammar.
103
+
104
+ - **4-bit and 8-bit KV cache no longer produce NaN** (#526). The compiled verifier's promoted paged cache was sized once (prompt plus 16,384 tokens) and never grew, so longer answers wrote across heads and past the buffers. Every forward now reserves its window where its mask is built, the dispatcher reserves before any replay, growth is checked against the Mac's memory and refused with a 507 before a row moves, and capacity is read from the live buffers. In our reproduction on the 27B, an answer counting to 4,000 failed on 2.12.0 at 8-bit KV and now finishes at 8-bit and 4-bit (no M3 run). Cost: 8-bit decode below the old capacity is about 3% slower (57.42 against 59.25 tok/s on the fix's own build, alternating boots, September 27).
105
+
106
+ - **FP16 queries keep attention's unnormalized partials in float32** in every split attention kernel, quantized or not, so a block of large scores can no longer overflow and turn the output into NaN. BF16 outputs are bit-identical to 2.12.0 (108 of 108 across 11 kernel families); FP16 partial buffers double (25 to 50 MB per 27B layer call at 32K). No M1 or M2 run.
107
+
108
+ - **"Failed to create Metal shared event" after long non-streaming runs** (#544). Unread lazy writes of Flash-Next's index blocks and draft-head history each kept a Metal event alive, and the non-stream path saved them in the session cache. The writes are evaluated with the forward that makes them, and the draft history is scheduled at every commit and at the end of a generation. Live events stayed at 6 to 13 against about 2,900 on 2.12.0 (September 27 A/B) and read 1 to 9 through three 4,096-token non-stream answers on a build of this release (October 2). A run past 140K generated tokens has not been repeated.
109
+
110
+ - **Host memory growth in long sessions is slower** (#546, partly fixed). A shared compiled verify program is replaced after `MTPLX_COMPILED_VERIFY_TRACES_PER_PROGRAM` traces (default 16), and every call binds to its own bank. Part of each replaced trace stays inside MLX, so the growth is slower and capped per program, not gone; a restart returns it.
111
+
112
+ - **Queued SSD saves can no longer hold memory the session cache already gave back** (#536 by @vahako). Under many short sessions memory climbed from 83.4 to 104.1 GB in four minutes on a 128 GB M5 Max until 507s (the author's measurement on 2.12.0). Saves whose entry the cache dropped count against `MTPLX_PERSISTENCE_MAX_PENDING_BYTES` (default 1/32 of RAM, 4 GiB on 128 GB, at least 512 MiB) and the oldest are cancelled; a save whose entry is still in RAM is never cancelled. Every memory-pressure eviction also cancels its own queued saves.
113
+
114
+ - **The turn after a compaction gets its memory.** A refused request frees its memory at once instead of keeping its frames alive (4.95 GB in one case); the SSD cache refuses a row that has no usable restore point from its description before reading its tensors (4.17 GB in one case) and tries the next candidates; a conversation's own unusable entries leave RAM before a refusal, and its restore source is protected, moved to SSD first, and released last when nothing can save it.
115
+
116
+ - **Cancel and resend keep the conversation.** A cancelled answer hands its cache back in one step; a prompt that stops part-way returns the conversation at its restore point; and a repeated prompt that restored a whole saved conversation no longer resumes one slot short, where its first answer token overwrote the last prompt token (also in 2.12.0).
117
+
118
+ - **Flash-Next's compiled verifier keeps running as a conversation outgrows its buffers.** The buffers are resized layer by layer and billed at the rows they gain; a resize that memory refuses answers on the eager verifier for that turn, and the next turn resizes the rest.
119
+
120
+ - **OpenCode answers are no longer capped on windows under 64K tokens.** OpenCode sends `min(limit.output, 32000)` with every request and 2.12.0's plugin stripped only 32,000, so the reply reserve became every answer's cap (16,384 on a 32,768 window, 10,240 on 20,480). Both plugins strip the reserve at every window, and `--max-response-tokens N` now reaches a running server whole, also above 32,000.
121
+
122
+ - **Pi's and OpenCode's configs are read as the clients read them** (comments, trailing commas, a byte order mark). The app moved a file it could not parse as strict JSON aside, which lost every other provider; a file that still does not parse is now left untouched and named.
123
+
124
+ - **`mtplx start` keeps the model you picked** (#573). A local folder picked in setup was swapped for this Mac's default repository on the next start, which started a 20.7 GB download of a model already on disk. Setup records where a pick came from; only the verified default follows when the default moves, now also Flash-Next on 256 GB Macs; a `config.toml` model folder loads as configured whatever its name; a missing folder is named instead of replaced.
125
+
126
+ - **The app's badge no longer sticks on Degraded** (#528). A second start against the app's own engine reconnects instead; Refresh and the live stats share one check; the badge separates the engine from the live stats ("Running · reconnecting live stats"). Closing the window during a load no longer kills it; Start or Restart after Stop launches; Stop during a preparing start launches nothing; requests go to the port the engine listens on; a failed configuration change leaves the engine running with its own notice; Stop signals only processes carrying this app's launch id; another server on the port is named and never stopped; the benchmark runs only on the app's own engine; requests use the API key the engine started with.
127
+
128
+ - **Retries.** A well-formed bare tool call is no longer retried as orphan markup unless its markup is incomplete (#539 by @jvmenen; on his Qwen 3.6 35B-A3B agent loop, long tool turns reached their first token in 10 to 20 s instead of 25 to 87 s; `MTPLX_TOOL_FED_RETRY_PARSED_CALL_GUARD=0` restores the old behaviour). A blank greedy answer is not regenerated, because `--blank-retry-attempts` now applies only when the sampler samples (#540 by @jvmenen; a one-token completion 1,061 to 265 ms). The reasoning repair continues the retry prompt and the stream stats count every pass (#552 by @jvmenen). `MTPLX_AGENT_REWRITES=off` also turns off the two steering retries (#553 by @jvmenen). The remaining re-generations build their prompt from the served token ids and stand down when no exact prompt exists.
129
+
130
+ - **Draft heads with fused MoE experts load their experts** (#574 by @jvmenen). Qwen 3.5 and 3.6 MoE heads stored as fused `gate_up_proj` and `down_proj` tensors were dropped at load and kept random experts; on his Forge pack of Qwen 3.6 35B-A3B, depth-2 acceptance went from 0.69 and 0.37 to 0.95 and 0.85, the same as numbered experts.
131
+
132
+ - **`--scheduler-mode ar_batch` keeps right-pad tokens out of shorter rows' recurrent state** on the Qwen 3.x hybrids (from #522 by @freddyhaddad). The default serial scheduler was not affected.
133
+
134
+ - **Streamed replies no longer start with a blank line** when the separator after the close tag arrives in a later chunk, which showed in Gemma 4 streams (#531 by @pierre427).
135
+
136
+ - **Gemma 4 renders its tool history in the format it is asked to use**, and history turns render the bytes the model generated (#571 by @jvmenen).
137
+
138
+ - **Saving a turn no longer refreshes the recency of the entry it restores from** (#554 by @jvmenen), so it cannot evict the entry the next request needs.
139
+
140
+ - **Flash-Next's compiled and eager verifiers agree at near ties.** The block-score divisor is read from memory instead of a 7-digit constant inside compiled code, and the eager verifier rounds four gates the way the compiled verifier does, on every Mac.
141
+
142
+ - **`/health` and `--kv-quant` help describe the KV quantization routes that actually run**, and the dashboard's value axes fit their labels (#572 by @jvmenen).
143
+
144
+ - **Tests no longer read the user's `~/.mtplx/config.toml` or depend on the Mac's memory size** (#535 by @jvmenen), and every client config the suite can write points at a scratch file.
145
+
146
+ ## [2.12.0] - 2026-09-23
147
+
148
+ ### Added
149
+
150
+ - **Dense 27B image requests take the compiled verify route too.** The route carries the image position offset as a graph input and leaves the text trace unchanged. Eighteen complete requests, nine with thinking on and nine with thinking off, all finished with `stop`, and 762 compiled image rounds and 511 text rounds compared against the eager verifier from the same cache showed zero differences in logits, hidden states, cache state or captures. A seeded 27B image reply run separately on each route agreed for its first 61 tokens, then parted at a rounding difference between the eager route's live-length attention buffers and the compiled route's padded buffers, with identical prefill logits, hidden states, draft inputs and rotary positions. Compiled text verification has always carried the same rounding difference; it is not a position error. `MTPLX_DENSE_VISION_COMPILED_VERIFY=0` keeps dense image requests on the eager verifier; the shared image switch `MTPLX_QWEN4_VISION_COMPILED_VERIFY=0` and the state-rebase refusal still take precedence.
151
+
152
+ - **Ternary Bonsai 2 27B** (issue #515). MTPLX loads Prism ML's 2-bit
153
+ ternary Bonsai 2 27B natively (`prism_hadamard_qwen35`: the Hadamard
154
+ rotation is applied at run time; a pack without its rotation metadata or
155
+ its vision tower is refused), with image input. The MTPLX pack
156
+ `Ternary-Bonsai-2-27B-MTPLX-Optimized-Speed` carries Prism ML's weights
157
+ byte for byte and adds the Qwen3.8-27B draft head, 8.85 GB in total;
158
+ served id `mtplx-bonsai-2-27b-optimized-speed`, minimum engine 2.12.0.
159
+ The draft head is on by default at depth 1. Two kernels written for the
160
+ pack make every round cheaper: the Hadamard rotation as one kernel (same
161
+ bits as the chain it replaces) and a ternary kernel for the verify and
162
+ decode rows (mean KL to Prism ML's float32 reference 2.92e-6 against
163
+ stock's 3.02e-6 over 1,630 positions; greedy output identical with the
164
+ kernels on and off). Verify runs compiled on Bonsai too (0 divergent
165
+ rounds in 613). On an M5 Max, alternating boots before and after the
166
+ kernels, 512 tokens: 50.4 -> 64.4 tok/s at 4K (+28%) and 45.2 -> 57.1 at
167
+ 16K (+26%); the dense 27B measured 52.6 and 51.0 in the same session, so
168
+ Bonsai is now faster than the 4-bit 27B, in about half the memory.
169
+ `MTPLX_PRISM_FUSED_ROTATION=0` and `MTPLX_BONSAI_TERNARY_QMV=0` turn the
170
+ kernels off. Reasoning effort is `medium` by default in the app, the
171
+ CLI and every client; `xhigh` is available, and `low` is not offered
172
+ because Prism ML does not support it. On a stopwatch coding task `xhigh`
173
+ spent 577 s and 21,848 reasoning tokens without an answer, while
174
+ `medium` finished the task and a correction turn.
175
+ `scripts/build_bonsai_mtplx_pack.py` builds, stamps and restamps the pack
176
+ from measured evidence; `scripts/bonsai_memory_table.py` measures the
177
+ peak memory per RAM class.
178
+
179
+ - **MiMo V2.6 Qwen 9B Optimized Speed.** Xiaomi's MiMo-V2.6-Distill-Qwen-9B
180
+ (a Qwen3.5-9B fine-tune for coding and agents, MIT) joins the catalog as
181
+ `mimo-v26-qwen-9b-optimized-speed`: pack
182
+ `Youssofal/MiMo-V2.6-Qwen-9B-MTPLX-Optimized-Speed`, 8,695,116,595 bytes,
183
+ 6-bit g64 body, Xiaomi's BF16 vision tower and the Qwen3.5-9B draft head
184
+ (the checkpoint ships none); served id
185
+ `mtplx-mimo-v26-qwen-9b-optimized-speed`, the Qwen 3.5 contract (sampler
186
+ 0.6 / 0.95 / 20, depth 2 by default). KL 0.0054 and 97.3 percent top-1
187
+ agreement against Xiaomi's BF16 checkpoint over 19,265 tokens. The app and
188
+ the CLI list it right before Qwen 3.5 9B on M3, M4 and M5 Macs from 16 GB
189
+ (second after Bonsai 2 on 16 to 31 GB); name-only lookups resolve it to
190
+ `qwen3_5`, and the onboarding list has its own row.
191
+
192
+ - **Flash-Next Optimized-Quality recipe.** `flash-next-optimized-quality`
193
+ in Forge: the main model and the draft head at 8 bits, group size 64, the
194
+ structural weights in BF16, the n-gram table at 4 bits, group size 32 (the
195
+ fixed-width verifier rejects an 8-bit table). The streaming audit stamps
196
+ its own result into the pack and
197
+ `scripts/build_flash_next_quality_pack.sh` builds, verifies with a full
198
+ load and tests chat, a tool call and an image in one command. The
199
+ published pack is 169,958,537,520 bytes in 57 files, and its weights need
200
+ about 128.5 GiB with the n-gram table on SSD. It has not yet been run on a
201
+ 256 GB Mac, so on 256 GB and 512 GB Macs it is listed second, after
202
+ Flash-Next Optimized Speed.
203
+
204
+ - **Compiled verify route for image requests on Flash-Next.** An image
205
+ request used to fall back to eager verification for the whole
206
+ conversation. The compiled verifier now owns a rotary origin per request:
207
+ the image position delta is an int32 graph input, the text trace is
208
+ unchanged, and requests with different images share one compiled
209
+ program. Six prompt shapes the route cannot represent exactly are refused
210
+ by name and stay eager; `MTPLX_QWEN4_VISION_COMPILED_VERIFY=0` turns the
211
+ route off; `MTPLX_COMPILED_VERIFY=parity2` compares every compiled round
212
+ against the eager forward and records per-leaf differences;
213
+ `scripts/qwen4_vision_compiled_parity.py` proves the route on a real pack
214
+ and refuses to say `exact` without a compiled dispatch and a warm restore
215
+ into generated tokens. On 440 image rounds and 429 text rounds of the
216
+ same requests, the compiled and eager routes differ only by rounding, with
217
+ a mean KL per round of 0.0015 for images and 0.0013 for text.
218
+
219
+ - **Packs can declare their generation mode.** `recommended_generation_mode`
220
+ (`mtp` or `ar`), with a reason and the measurement behind it, in the
221
+ runtime contract. `mtplx serve` applies it when the mode flag is omitted or
222
+ `auto`; an explicit `--generation-mode` wins; the head stays loaded so the
223
+ daemon can switch live. No shipped pack declares `ar`.
224
+
225
+ - **Use MTPLX settings for connected apps.** A switch in the inference
226
+ settings (`control_client_settings`, env `MTPLX_MANAGED_CLIENT_CONTROLS`)
227
+ that says whether MTPLX or the connected client controls the request
228
+ settings; on by default. Pi mirrors the live reasoning setting and
229
+ restores its own choice when the switch is off. Translated into all 13
230
+ app languages.
231
+
232
+ - **One catalog for the app and the CLI.** 25 entries with the same
233
+ identities, sizes and recommendation rule, a shared recommendation
234
+ fixture both sides are tested against, and model descriptions that follow
235
+ the app language without a restart. On M3, M4 and M5: the 4B below 16 GB,
236
+ Bonsai 2 from 16 GB (`BONSAI_RECOMMENDATION_MIN_GIB`), the Qwen 3.8 27B
237
+ Optimized Speed from 32 GB, and Flash-Next Optimized Speed first from
238
+ 256 GB with Optimized Quality second. On a 96 GB Mac the Flash-Next
239
+ option is Bare Speed (peak 78 GiB), because Optimized Speed peaks at
240
+ 87 GiB.
241
+
242
+ - **`forge build` converts Qwen3.8-Flash-Next sources** (PR #508, Bradford
243
+ Matthews, issue #390). Forge handed every Flash-Next fine-tune to the
244
+ pinned mlx-lm, which does not know the `qwen4_exp` architecture, and
245
+ stopped with `Model type qwen4_exp not supported`. A BF16 source of that
246
+ family now takes its own path. The 51B n-gram table is quantized one
247
+ shard at a time straight into `ngram-table.safetensors` and is never held
248
+ in memory. The trunk is converted with MTPLX's own model code under the
249
+ Optimized Speed recipe, one tensor at a time, because saving a whole
250
+ shard in one step ran long enough for macOS to stop the GPU work.
251
+ `module_overrides` still apply on top. The draft head is written in the
252
+ layout the runtime loads. New recipe keys: `ngram.bits` and
253
+ `ngram.group_size` (default 4 and 32, the layout the runtime expects),
254
+ `qwen4_mtp_bits` (default: the same as the trunk) and `qwen4_qsa_8bit`.
255
+ A build of this family is checked on the two rows its verify step
256
+ measures, plain decoding and depth 3, instead of failing on `required
257
+ depths: D1, D2`. The contributor built
258
+ `orcarouter/Qwen3.8-Flash-Next-Uncensored` end to end on an M5 Max with
259
+ 128 GB.
260
+ - **Remove a downloaded model from the app** (PR #377, Philip John
261
+ Basile). The model picker has a Remove action with a confirmation. It
262
+ runs `mtplx remove` for exactly the entry shown, only inside the primary
263
+ model folder from Settings (additional model folders stay read-only), and
264
+ it refuses the model the server is currently serving. When the command
265
+ refuses, the app shows its reason instead of an exit code. The new
266
+ strings are in all thirteen languages.
267
+
268
+ - **`MTPLX_MLX_COMMAND_BUFFER_MB` sets MLX's command buffer limit.** MLX
269
+ closes a GPU command buffer once it has touched 50 MiB of buffers on a Max.
270
+ At 128K tokens on Flash-Next, lifting that limit took decode from 51 to 54
271
+ tok/s to 65.9 tok/s, but peak memory during prompt processing rose from
272
+ 92.0 to 103.7 GB at 16K, and close to the memory limit the gain shrank to
273
+ 45.6 against 49.4 tok/s. MTPLX keeps MLX's default. A value given to MLX
274
+ directly still wins, and `/health` shows the value the engine was given.
275
+ - **Measuring tools for the Flash-Next speed work, all off by default.**
276
+ `MTPLX_QWEN4_PREFILL_PROFILE=1` times each part of a prompt pass,
277
+ `MTPLX_QWEN4_EXPERT_OVERLAP_PROBE=1` counts the distinct experts of the
278
+ verify rows, `MTPLX_FIXED_M4_DONATION_PROBE=1` reports a verify step that
279
+ copies the attention cache instead of reusing it and can name every piece
280
+ of state that moved, and `MTPLX_EXPERIMENT_DEPTH_CEILING` raises the draft
281
+ depth limit for acceptance tests only. The compiled verifier can also
282
+ report how its host time splits between gathering rows, preparing inputs,
283
+ replaying the graph and encoding.
284
+ - **An experimental dense-band attention route for Flash-Next prompts.**
285
+ `MTPLX_QSA_DENSE_BAND_SDPA=1` applies the attention mask inside the score
286
+ matrix multiply below the sparse switch point, with bit-identical output
287
+ in tests and the stock call's peak memory plus one mask plane. It stays
288
+ off until a full-model check on real prompts confirms it.
289
+
290
+ ### Changed
291
+
292
+ - **Flash-Next prefill runs five more steps as single GPU kernels.** The
293
+ hyper-connection read, the expert-output combine, the gated-delta prework
294
+ and the gated-delta norm gate run as one kernel each on every Mac; on M5
295
+ chips the wide projections also run as one dequantize plus a dense matrix
296
+ multiply (M1 to M4 keep the quantized multiply, where MLX's two paths are
297
+ not known to agree). Output is bit-identical to 2.12.0 without them: every
298
+ position's logits and 256 greedy tokens on code and prose at 16K and code
299
+ at 64K. On an M5 Max, alternating boots without and with the kernels:
300
+ 1,459 -> 1,695 tok/s at 16K (+16%) and 1,415 -> 1,548 at 64K (+9%), same
301
+ peak memory. `MTPLX_QWEN4_HC_PREFILL_READ=0`,
302
+ `MTPLX_QWEN4_MOE_PREFILL_COMBINE=0`, `MTPLX_QWEN4_GDN_PREFILL_PREWORK=0`,
303
+ `MTPLX_QWEN4_GDN_GATED_NORM=0` and `MTPLX_QWEN4_PREFILL_DQ_GEMM=0` turn one
304
+ step off; `MTPLX_QWEN4_PREFILL_DQ_GEMM=1` forces the projection step on any
305
+ Mac. Every Flash-Next load checks the four kernels that run on every Mac
306
+ on its own GPU first (bit-exact against the stock chain at the family's
307
+ shapes and thread counts, about 58 ms); a kernel that fails is turned off
308
+ for the process, the stock path serves, and `/health` shows the lane.
309
+
310
+ - **Every Bonsai load checks its two kernels on the GPU it runs on.** The
311
+ fused Hadamard rotation must return the stock operations' exact bits and
312
+ the ternary kernel must match stock; a kernel that fails is turned off for
313
+ the process, the stock path serves, and `/health` shows the lane. Both are
314
+ plain SIMD code and run on M1 to M5.
315
+
316
+ - **`mtplx tune` pins the fans only when asked.** It pinned them at
317
+ maximum on every run, so Forge's verification did too, even without
318
+ `--max`. Tune now pins them only with its new `--max` flag or
319
+ `--require-max-fans`, and Forge passes them only when it is given
320
+ `--max`. The app's tuning and `mtplx start` still ask for maximum fans;
321
+ a bare `mtplx tune` leaves them on automatic.
322
+
323
+ - **Speed: Flash-Next prompt processing and long-context decode.** 2.11.3
324
+ against 2.12.0 on the same M5 Max and Python runtime, alternating boots,
325
+ fans at maximum, the GPU memory limit raised to 120 GiB
326
+ (`iogpu.wired_limit_mb=122880`; the default is about 96 GiB), seed 1731,
327
+ thinking off, 512-token answers: at 65,502
328
+ tokens, first token 85.5 to 60.1 s, prompt processing 768 to 1,094 tok/s
329
+ (+42%), decode 56.1 to 63.5 tok/s (+13%); at 4,061 tokens, first token
330
+ 5.26 to 2.88 s, prompt processing 786 to 1,453 tok/s (+85%), decode 74.4
331
+ to 74.1 (tie). Saved attention buffers are sized on the cache's growth step and
332
+ written in place (at some prompt lengths all 24 key and value buffers
333
+ were copied every verify round, about 135 MB each at 128K; the 24 writes
334
+ went from 7.8 to 0.6 ms). Flash-Next builds all sampled draft depths with
335
+ one device sync and sends each depth to the GPU as it is built. Prompt
336
+ processing uses 4,096-token chunks on M5 chips and begins sparse
337
+ attention at 16K for those chunks; the sparse attention kernel reads the
338
+ cache in place (19.8 to 7.1 µs per row at 64K, bit-identical); block
339
+ selection runs on one simdgroup per row (2.68 to 0.58 µs per row at 18K);
340
+ the hyper-connection write is one fused kernel; block selection no
341
+ longer compiles a new GPU program per context size (40 to 56 ms each);
342
+ the last chunk of a cold prompt is no longer cut into 256-row forwards;
343
+ the n-gram pre-read fills the small scale and bias files first, and the
344
+ warm pass reads only the files that are not in memory. A warm agent turn
345
+ reprocesses at most 64 tokens at the end of its prompt instead of up to
346
+ 256. Restore points are recorded inside the last wide forward instead of
347
+ ending extra forwards at each one: on the same build a cold 4,061-token
348
+ prompt prefills in 2.79 to 2.81 s instead of 3.06 to 3.10 s
349
+ (`MTPLX_GDN_BOUNDARY_INFORWARD=0` restores the old layout).
350
+
351
+ - **The Qwen 3.8 27B is unchanged.** 2.11.3 against 2.12.0 on the same Mac,
352
+ measured like the Flash-Next pairs: at 4,061 tokens, first token 6.07 s on
353
+ both, prompt processing 680 and 679 tok/s, decode 46.7 and 44.2 tok/s; at
354
+ 16,350 tokens, first token 26.7 and 27.2 s, prompt processing 617 and 604
355
+ tok/s, decode 41.4 and 43.2 tok/s. Prompt processing and the first token
356
+ are within 2.1 percent, and one decode round costs 3.6 percent more at 4K
357
+ and 0.2 percent more at 16K. Decode moved 5.4 percent down at 4K and 4.3
358
+ percent up at 16K because the two builds accept different draft tokens
359
+ from the same seed.
360
+
361
+ - **The n-gram table streams from SSD on every Mac.** On Macs with 160 GB
362
+ or more earlier releases also loaded the 29.8 GiB table into GPU memory,
363
+ and nothing in this release reads that copy. It now streams from SSD on
364
+ every Mac and is never wired. On 256 GB and 512 GB Macs this raises
365
+ Flash-Next Optimized Quality's memory for context and the session cache
366
+ from 29.7 to 59.5 GiB and lowers its wired memory from 164.3 to
367
+ 134.5 GiB.
368
+
369
+ - **A model that does not fit gets the floor window.** A plan whose verdict
370
+ was "does not fit" became "no limit", so the 27B picked by hand on a 16
371
+ or 24 GB Mac, and Flash-Next on 96 GB, were served the full 262,144-token
372
+ maximum under a MODEL DOES NOT FIT banner. They now get the 4,096-token
373
+ floor and one startup line that says so; an explicit `--context-window`
374
+ still wins.
375
+
376
+ - **The decode context ceiling follows the model's own KV size.** Every
377
+ model was budgeted with the 27B's 65,536 bytes per token (Flash-Next uses
378
+ 24,576) and every Flash-Next start printed a warning about it. The value
379
+ now comes from the model's config; on 16 and 24 GB Macs the 27B ceiling
380
+ is 65,536 and 98,304 tokens instead of a flat 131,072.
381
+
382
+ - **One launch setup for the app, `mtplx start` and `mtplx serve`.** Pi runs
383
+ on the serial scheduler from every entry point (the OpenCode preset
384
+ measured it at 51.4 against 36.8 decode tok/s at 8K), the SSD cache size
385
+ is automatic everywhere (it was a fixed 100 GB on `mtplx start hermes`
386
+ and 32 GB on `mtplx start opencode`), the minimum saved prefix is 512
387
+ tokens everywhere, and the app's presets no longer fix the prompt chunk at
388
+ 2,048 tokens for every model. A test builds the app's launch command for
389
+ 294 combinations of client, model family and Mac size and compares it
390
+ with the engine's table.
391
+
392
+ - **The memory planner admits a model on a tight Mac when only the
393
+ session-bank floor is unfunded.** When the engine budget cannot fund the
394
+ weights, the 3 GiB runtime transient, the 1 GiB bank floor and one KV
395
+ block together, but can fund the weights, the transient plus a 256 MiB
396
+ margin and one block at the dense KV width, the plan admits the model
397
+ with the bank floor at zero (restores come from the SSD tier). The rule
398
+ applies to a pack whose weights are no larger than the Bonsai pack's
399
+ 8,834,412,216 bytes, the pack the margin was measured on (Qwen 3.5 9B
400
+ Optimized Speed gets 20,480 tokens on 16 GiB), and to a larger pack only
401
+ when it stamps its own measured memory table for those weights
402
+ (`memory_evidence` in its runtime contract); any other pack is refused as
403
+ before. Measured on Bonsai 2 27B: the 16 GiB class admits 8,192 tokens
404
+ (peak 11.78 to 11.80 GiB under the 12 GiB budget). Every plan that funds
405
+ the floor is unchanged.
406
+
407
+ - **The SSD conversation-cache size follows RAM from the terminal too.**
408
+ `mtplx serve` defaulted to a flat 100 GB, so a 16 GB Mac got a 100 GB
409
+ store while the app scaled it. The default is now `auto` everywhere:
410
+ 16 GiB on Macs with 16 GB of RAM or less, 24 GiB up to 32 GB, 32 GiB up
411
+ to 64 GB (100 GiB when the disk has at least 150 GiB free), and 100 GiB
412
+ above 64 GB. An explicit size still wins.
413
+
414
+ - **Flash-Next on a 96 GB Mac** has one 84 GiB engine budget, shared by
415
+ the catalog, the memory planner and the verify memory check: a planned
416
+ 86,016-token window with sparse prompt processing, 20,480 without.
417
+
418
+ - **Request captures hold no content by default** (PR #356, Philip John
419
+ Basile). Capture is still off unless `MTPLX_REQUEST_CAPTURE_DIR` is set.
420
+ When it is on, a record now holds the sampler settings, the seed, token
421
+ counts and SHA-256 digests, and no prompt text, answer text, messages,
422
+ exception text or token IDs; keys that look like credentials are redacted
423
+ at any depth. Each kind of content has its own opt-in
424
+ (`MTPLX_REQUEST_CAPTURE_INCLUDE_PROMPT_TOKENS`,
425
+ `..._COMPLETION_TOKENS`, `..._PROMPT_TEXT`, `..._RESPONSE_TEXT`,
426
+ `..._MESSAGES`, `..._EXCEPTION_TEXT`). An opt-in to token IDs keeps the
427
+ whole sequence, because an exact replay needs every ID;
428
+ `MTPLX_REQUEST_CAPTURE_PROMPT_TOKEN_LIMIT` and
429
+ `MTPLX_REQUEST_CAPTURE_COMPLETION_TOKEN_LIMIT` bound them when wanted.
430
+ `mtplx trace` reads the request log and the flight recorder, not these
431
+ files, so it is unaffected. Records are now `capture_version` 2.
432
+
433
+ - **The draft head writes only its cache during Flash-Next prompt
434
+ processing.** Its pass over each chunk ran the whole layer although only
435
+ the cache it writes is used. It now writes the cache and returns, with the
436
+ same cache bits. The full pass cost 0.09 to 0.12 s per 4,096-token chunk,
437
+ 3.3 s of a 104 s prompt at 128K. `MTPLX_QWEN4_MTP_HISTORY_CACHE_ONLY=0`
438
+ restores the full pass.
439
+ - **Less memory between Flash-Next prompt chunks.** A three-row copy of each
440
+ recurrent layer's state kept that layer's whole input stream alive until
441
+ the next chunk. The chunk's evaluation now includes those states, which
442
+ frees about 3.0 GB between 4,096-token chunks (85.84 GB instead of 88.86).
443
+ Prompt forwards of 1,024 rows or more also hand finished layers to the GPU
444
+ every four layers, so each layer's temporary data is freed with the layer.
445
+ `MTPLX_QWEN4_PREFILL_MIDLOOP_EVAL` sets the number of layers, and `0` turns
446
+ it off.
447
+ - **The wide prompt chunk's memory check counts what a chunk holds.** It
448
+ charged a flat 8 GiB per 4,096 rows on top of the prompt's KV, which
449
+ refused the wide chunk at 64K and 128K on a 128 GB Mac. It now charges the
450
+ larger of the last dense forward and the last forward of the prompt, and
451
+ those two refusals became grants. A refused wide chunk now shows up on
452
+ `/health`, in the request log and in `mtplx doctor --explain`, and the
453
+ request uses 2,048-token chunks. `MTPLX_QWEN4_PREFILL_WIDE_CHUNK=0` and
454
+ `MTPLX_QSA_PREFILL_WIDE_MIN_CONTEXT=0` turn the wide chunk off.
455
+ - **Model-tuned settings live in one block per model family,** each value
456
+ with its source and its measurement. This changed no behavior. One check
457
+ now decides whether a Mac's GPU has tensor units for every fast path, and
458
+ `MTPLX_FORCE_GPU_FAMILY_FALLBACK=1` now also covers Flash-Next prompt
459
+ processing, so an M5 can run MTPLX's M1 to M4 paths.
460
+ - **The server guide covers Open WebUI's Controls** (issue #513): leave them
461
+ on Default, so the settings MTPLX tuned for the model apply.
462
+ - **Release checks.** The release script refuses to publish while the notes
463
+ or README still carry draft wording, runs the model checks after the unit
464
+ suites finish, and fails when a catalog repository cannot be found. The
465
+ native package tests run in CI instead of being skipped. New scripts
466
+ compare the prompt the server builds for an agent session with the
467
+ model's own chat template token by token, and prove the compiled image
468
+ routes on the real Flash-Next and 27B packs with one command each.
469
+
470
+ ### Fixed
471
+
472
+ - **Model list icons.** Every trailing icon in the model list draws in one
473
+ 28-point slot on the trash can's axis (the checks sat 5 to 6 points to the
474
+ right), and the trash of a downloaded model appears only while the pointer
475
+ is at the right end of the row or the button has keyboard focus; the row's
476
+ context menu also offers Remove downloaded files.
477
+
478
+ - **The repetition stops are off by default.** In 2.11.3 and earlier the
479
+ server ended a reply when a block of up to 96 tokens repeated four times
480
+ in a row, deleting every copy. Code repeats exactly, so it cut a Tetris
481
+ board literal at its ninth identical row, a correct patch that renamed a
482
+ type at 15 identical call sites (the tool call never closed), an empty
483
+ sudoku grid, a 256-entry table of zeros and 64 identical board `<div>`
484
+ lines. A replay over 301,194 saved replies (104.6 million tokens from the
485
+ MTPLX app, OpenCode, Hermes, Pi and Codex) found two real patches it
486
+ would have cut. `MTPLX_REPETITION_STOP=1` turns it back on, together with
487
+ a new check for exact loops with a period of 97 to 8,192 tokens that ends
488
+ a loop at three whole copies, never before token 768, at 0.4 to 1.5
489
+ microseconds per step (a 4-bit 9B test build's 129-token loop stops at
490
+ 5,202 tokens instead of 16,916, reason `long_cycle`). The new check also
491
+ cuts a platformer map with 8 identical empty rows, which is why it ships
492
+ off too. Evidence: `tests/test_repetition_stop_legitimate_code.py`.
493
+
494
+ - **A server started without `mtplx serve` uses each model's own
495
+ sampler.** `python -m mtplx.server.openai` sampled Qwen 3.8, Bonsai 2
496
+ and Flash-Next at temperature 0.6 while `/health` reported 1.0. With no
497
+ sampler flags it now uses 1.0, top-p 0.95 and top-k 20, as `mtplx serve`
498
+ and the app already did. Explicit flags win, and Qwen 3.5 and 3.6 stay
499
+ at 0.6.
500
+
501
+ - **Forge keeps a draft head's declared settings unless the evidence is
502
+ clear.** Calibration switched a grafted Qwen 3.5 9B head to `pre_norm`
503
+ on one prompt and six tokens in which every option accepted nothing. A
504
+ switch now needs at least 4 prompts, 48 draft rounds per option and a
505
+ lead of 0.25 accepted tokens per round, and an inconclusive calibration
506
+ is recorded in `mtplx_runtime.json`. The probe samples 8 prompts and 4
507
+ windows, 49 s on the Qwen 3.5 9B pack including the model load.
508
+
509
+ - **The memory planner never gives a lighter model less context.** On a 16
510
+ GB Mac a 7.53 GiB pack planned 12,288 tokens while an 8.08 GiB pack
511
+ planned 20,480. A lighter pack now drops the cache floor when that gives
512
+ it more context, up to what the tight-machine rule can grant. No catalog
513
+ model's window changes.
514
+
515
+ - **Eager BF16 verification uses the attention-gate rounding already used by compiled verification.** This affects the shared gated full-attention path in BF16 Qwen3-Next, Qwen 3.5, 3.6 and 3.8 models, including their MoE variants. It applies when verification runs eagerly: dense image requests, contexts beyond the compiled route's 32,768-token limit, buffer-growth fallbacks, warm-restore rounds before compilation, and eager copy, repair or final-save forwards within an otherwise compiled request. The compiled text verify graph, prompt processing and plain decoding keep their existing numerics. Float16 and float32 gates are unchanged, including Bonsai's float16 path. Gemma 4 and Flash-Next use different attention implementations and are unchanged by this fix. Five exact gate tests cover the BF16 contract and the unchanged dtypes and phases.
516
+
517
+ - **A quoted tool tag no longer ends the thinking.** A pasted traceback
518
+ containing `</parameter>` made the rest of the model's thinking appear in
519
+ the visible chat. A thinking block ends only at `</think>` or at the
520
+ opener of a real tool call (tools declared, line start, outside a code
521
+ fence, call-shaped; a bare `<function=NAME>` must name a declared tool).
522
+ The same rule decides whether a reasoning-only answer is retried, and a retry fed a tool
523
+ result starts its stream clean. 117 contract cases; 10 streamed requests
524
+ on the release build quoting up to 43 tags in one thinking block, none
525
+ leaked.
526
+
527
+ - **Gemma 4 streamed replies finish** (issue #517). A reply without tools
528
+ failed after generating with `'Gemma4ThinkingContentStreamSplitter'
529
+ object has no attribute 'suppressed_tool_markup_chars'`. The splitter
530
+ contract is stated once on the base class; 37 contract tests pass (12
531
+ failed before).
532
+
533
+ - **Stop, then resend, no longer processes the whole prompt again.** The
534
+ app rotated the conversation's server session id after every Stop, so
535
+ the resend was a new conversation to the daemon (26,294 tokens processed
536
+ again, 24.5 s to the first token in the report). The app keeps the
537
+ session across a Stop, and the daemon lets the follow-up wait, at most
538
+ `MTPLX_SESSION_CANCEL_HANDOFF_WAIT_S` (30 s), while the stopped generation
539
+ lets go of the session instead of answering 409; the request log records
540
+ `request_session_cancel_handoff`.
541
+
542
+ - **Cancelling an app start no longer corrupts the app's memory.** The
543
+ daemon supervisor's health probe and restart delay were asynchronous
544
+ closure literals in default arguments of a public initializer. Copies
545
+ emitted into different modules disagreed on the asynchronous frame size,
546
+ and the closure wrote past the end of its frame. Debug builds crashed
547
+ with "freed pointer was not the last allocation" in 3 of 3 runs; the
548
+ release build did not crash in a manual check, but the fault was in every
549
+ build. The defaults are named static functions. A start cancelled with
550
+ Stop writes no failed-start report.
551
+
552
+ - **Flash-Next image positions are consistent through the whole reply**
553
+ (since 2.10.1 only the main verification forward ran inside the image
554
+ position scope; the copy-round verification, the repairs and the final
555
+ save used plain text positions, about 990 positions off after a
556
+ 1,024-token image). One helper carries the positions to all eight trunk
557
+ forward sites. Saved image conversations carry a new cache key
558
+ (`qwen4_mrope_v2`), so an existing image conversation is processed again
559
+ from its first image once; text before it is still reused.
560
+
561
+ - **Dense Qwen 3.5 and 3.8 27B models, and Bonsai, rope image tokens at
562
+ their grid positions**, and an image turn restores the session's text
563
+ history instead of processing it again (a Pi conversation reprocessed
564
+ 154,899 tokens, 184 s, after an image was attached).
565
+
566
+ - **The in-flight memory guard reads what the kernel can still hand out**
567
+ (issue #516: a kernel watchdog restart on a 128 GB M5 Max with the worker
568
+ at 118 GiB resident and `allow_swap` on). `kern.memorystatus_level` and
569
+ the process footprint feed the guard; abort floor 2.5 percent of RAM
570
+ (3.2 GiB on 128 GB, never under 1 GiB), shed floor twice that, checks
571
+ every 2 s under the shed floor so the sustained-pressure abort fires
572
+ about 6 s after the floor is crossed. `allow_swap` keeps admitting past
573
+ the fit; it does not switch the guard off. Kill switch
574
+ `MTPLX_SYSTEM_MEMORY_GUARD=0`.
575
+
576
+ - **A Swift parity test read a 16 KB pipe before waiting on its child**,
577
+ so the suite could hang for five minutes on a long CLI dry run.
578
+
579
+ - **Saving a long session to SSD no longer holds up the next request**
580
+ (issue #505, reported by peterloron). On a 64 GB Mac with the 27B at about
581
+ 104K tokens, the background job that writes a session to the SSD cache
582
+ held the model for more than 500 s while a request waited behind it, and
583
+ the stream watchdog ended that request at 300 s. The job was meant to give
584
+ way to a waiting request, but it only looked between pieces of work whose
585
+ size nothing limited. Each piece is now at most 32 MiB and the job looks
586
+ after every one, including while it reads a restored session's boundary
587
+ records. The files on disk are byte for byte the same. `/health` reports
588
+ `encode_units`, `encode_slow_units` and `encode_longest_unit_s` for the SSD
589
+ cache, and any piece slower than 1 s is logged with what it was.
590
+ - **`mtplx remove` acts on the entry you named.** A folder in the model
591
+ cache whose name spells one of the public aliases resolved to the
592
+ first-party model's folder instead, so `mtplx models` could print a
593
+ delete command for one entry that would have removed another. An exact
594
+ folder name now wins. When two copies of a model sit in two model
595
+ folders, the refusal says to choose one with `--cache-dir`; a typed
596
+ `--cache-dir` now does that even when the other folder comes from
597
+ `MTPLX_MODEL_DIRS` or the config file.
598
+ - **A stuck request no longer holds the fans at maximum** (PR #295,
599
+ El-Patronum). In Smart fan mode a request holds the fans up until it
600
+ ends, and a safety net restores them when the server has been idle for
601
+ 120 s with a request still holding them. A request that was stuck still
602
+ counted as activity, so the safety net never ran: one report had both fans
603
+ at maximum for about fifteen hours behind a request whose client had
604
+ already gone. A request now counts as activity only while the model is
605
+ making progress. After 180 s without any (`MTPLX_FOREGROUND_STALL_DEADLINE_S`,
606
+ `0` switches the check off) it stops counting, and the fans return to
607
+ automatic 120 s later. Nothing is cancelled. The batched serving paths now
608
+ report progress during prefill as well, which also stops the stream
609
+ watchdog from failing a healthy batched prefill that runs longer than its
610
+ 300 s deadline.
611
+ - **Forge builds a model that ships as one weights file** (issue #492). A
612
+ model small enough for a single `model.safetensors` has no
613
+ `model.safetensors.index.json`. `mtplx inspect` read the file's header
614
+ and reported the draft head as present, but Forge looked for the head
615
+ through the index only, so it converted the model, wrote no
616
+ `mtp.safetensors`, and stopped at calibration with `return_hidden
617
+ requires an MTP-patched runtime`. Forge now reads the same headers
618
+ `inspect` does.
619
+ - **`mtplx doctor` shows why the app could not start** (issue #504). When
620
+ the app's server stops before it is ready, the app showed one line and
621
+ cut it off, and nothing was written to disk. The app now writes the
622
+ server's last output to `~/.mtplx/logs/last-failed-start.log` (the launch
623
+ line has its secrets masked), and `mtplx doctor` includes the last 60
624
+ lines as `app.last_failed_start` when the file is less than 14 days old.
625
+ - **A download blocked by a company proxy now says what to do** (issue
626
+ #495). Behind a proxy that inspects HTTPS, model downloads stopped at
627
+ setup step 6 of 7 with `CERTIFICATE_VERIFY_FAILED` while `curl` worked,
628
+ because the proxy's root certificate is in the macOS keychain and Python
629
+ verifies against its own bundle. The error now explains that and names
630
+ both fixes (`SSL_CERT_FILE`, or the `truststore` package). When
631
+ `truststore` is installed, downloads verify through the keychain
632
+ automatically; MTPLX does not depend on it, and `MTPLX_SYSTEM_TRUST=0`
633
+ switches that off.
634
+ - **The memory guards read the process's real footprint from macOS**
635
+ (PR #500, Maikel Vos). Every guard compared MLX's own account of its
636
+ allocations with the Metal limit. `mtplx/os_memory.py` now reads
637
+ `phys_footprint`, the counter the system's own memory-pressure logic
638
+ uses, and the guard before a long prompt, its refusal check and the
639
+ background pressure loop add the part of it MLX's account does not
640
+ explain. Only the part beyond what a daemon normally holds outside Metal
641
+ is added (the larger of 8 GiB and what the machine leaves after the
642
+ system reserve and the Metal limit), because the Metal limit is not the
643
+ process's budget: comparing the whole footprint with it reads a full
644
+ session on a 48 GB Mac as critical. `MTPLX_HOST_MEMORY_ALLOWANCE_BYTES`
645
+ sets that allowance, and `0` is the strict comparison. The guard's log
646
+ line and the `mem` block on `/health` now carry `phys_footprint_bytes`
647
+ and `host_overhang_bytes`, so a memory report names its holder.
648
+ - **A session over its cache budget continues instead of being read again**
649
+ (issue #499). On a 48 GB Mac one conversation may hold about 7.4 GB of
650
+ warm cache, and a 142,000-token session needs 11 GB, so the cache keeps a
651
+ reference to the live state instead of a copy. With speculative decoding
652
+ on, that entry dropped the draft head's history it was handed, so the
653
+ next turn's restore failed (`no_snapshot_coverage`), the copy on disk had
654
+ been written without the history and was refused
655
+ (`ssd_missing_mtp_history`), and the whole prompt was read again: 570 s
656
+ to the first token at 150,000 tokens in the report, on every turn. The
657
+ entry now keeps the history (about 4 KB per token, next to 64 KB per
658
+ token of main state) in memory and in its SSD copy, so the next turn
659
+ continues and a restart restores from disk. Copies written by older
660
+ versions lack the history; each long session reads its prompt once more
661
+ after the update.
662
+ - **Long sessions no longer leak one whole KV cache per turn** (issue #456,
663
+ diagnosed by peterloron; also the memory growth in #499 and #438). When a
664
+ session's snapshot is over the per-session limit, the session cache keeps
665
+ a reference to the live cache instead of a copy. That entry recorded 0
666
+ bytes, so every memory check missed it: the check before a long prompt
667
+ logged `bank_bytes_before=0` and answered 507 with memory it could have
668
+ freed, the pressure routine never started, and the automatic cache limit
669
+ mistook the reference for the running request and evicted useful
670
+ snapshots instead. The entries were also skipped by the rules that retire
671
+ an older copy of the same conversation, so a turn that could not reuse
672
+ the reference built a new cache and left the old one allocated (measured
673
+ by peterloron on a 64 GB M4 Max: +3.7 GiB per turn). These entries now
674
+ report what they hold, a session keeps one at most, eviction and
675
+ `/admin/cache/clear` release the cache they point at, and the check
676
+ before a long prompt can release one that belongs to another
677
+ conversation. `/health` shows `lease_entries`, `lease_nbytes` and a
678
+ per-entry `held_nbytes`. A very long session on a small Mac can now push
679
+ its own older snapshots out of memory (they stay on the SSD cache),
680
+ because the cache finally stays inside its limit.
681
+ - **Dashboard draft totals fall back to the per-depth counts** (PR #490,
682
+ Wu Shuwen; issue #401). The "accepted of drafted" line and the drafted
683
+ per verify call tile total `accepted_by_depth` and `drafted_by_depth`
684
+ when a payload carries no flat totals. The server has sent the flat
685
+ totals since 2.11.3, and an emitted total still wins.
686
+ - **Plain decoding has one path (PR #507, David Tai).** David Tai found
687
+ that the pipelined AR path drew the first output token from the
688
+ request's NumPy generator and every later token from a separate
689
+ `mx.random` stream, so one seed gave a different continuation depending
690
+ on whether the path engaged, and his change made the two agree. After
691
+ this release's n-gram change no model offers the pipelined mode, so the
692
+ path and its `MTPLX_AR_PIPELINE` switch are removed.
693
+ - **A draft head taken from a raw checkpoint no longer drafts backwards**
694
+ (PR #511, Stuart Rowlands). Hugging Face checkpoints store the MTP head's
695
+ RMSNorm gains zero-centred. mlx-lm restores the +1.0 convention on the
696
+ trunk but drops every `mtp.*` key first, and only Forge restored it on the
697
+ head. A sidecar that reached the Qwen 3.5 / 3.8 loader any other way
698
+ (copied in by hand, or extracted from the base checkpoint) bound without
699
+ an error and then ran at about 0% acceptance, slower than plain decoding:
700
+ the negative gain inverts the head's output, so the right token lands
701
+ near the bottom of the distribution (measured median rank 247,513 of
702
+ 248,320). The loader now applies the same detector-gated restoration
703
+ Forge uses. A head already in the absolute convention passes through
704
+ byte for byte, so nothing is shifted twice.
705
+ - **The GPU wired limit is clamped to what the Mac allows.** When the
706
+ memory the engine wanted wired was above the limit macOS allows, MLX
707
+ refused the request, the error was dropped, and the model ran with none
708
+ of its memory wired and no GPU keepalive: extra time after every idle
709
+ pause, and memory macOS could evict under pressure. The wired limit is
710
+ now set to the largest value the Mac accepts, and the startup log says
711
+ how much macOS allowed and gives the `sudo sysctl iogpu.wired_limit_mb`
712
+ command that wires all of it. On a stock 96 GB Mac that limit is about
713
+ 72 GiB.
714
+ - **Image placeholder text stays text** (PR #519, François-David Collin).
715
+ A client that sent the image placeholder string back as plain text made
716
+ the chat template render it as a real image slot, and the request was
717
+ refused with `prompt contains more image placeholders than images`. Only
718
+ real image parts create image slots now; placeholder text is kept as
719
+ readable, escaped text in message content, in reasoning and in tool-call
720
+ arguments, so an agent that edits code containing a placeholder keeps
721
+ its image turns.
722
+ - **The two new official packs no longer read "needs contract repair".**
723
+ `mtplx inspect` and `mtplx serve` told users to rebuild Bonsai 2 and
724
+ Flash-Next Optimized Quality with Forge, because each pack's exactness
725
+ record is still open, and `mtplx inspect` exited with code 3. They now show
726
+ "Official MTPLX pack, qualification pending" while the exactness
727
+ measurement is still to be published, the pack runs, and `mtplx inspect`
728
+ exits with 0. Bonsai 2's measurement is published (mean KL 4.0e-6 against
729
+ Prism ML's runtime in float16, 1.1e-7 with float32 auxiliary tensors), so
730
+ it shows as verified.
731
+ - **A pack that needs a newer MTPLX is refused before any weight is read.**
732
+ When a pack's `min_engine_version` is newer than the running engine,
733
+ `mtplx serve` and `mtplx inspect` stop with "This model needs MTPLX X or
734
+ later (you have 2.12.0). Update MTPLX, then try again." No unsafe flag
735
+ overrides it.
736
+ - **The free-disk check before a download matches the download.** The app
737
+ asked for 2.5 times a model's download size, about 396 GiB for Flash-Next
738
+ Optimized Quality. The app and the CLI catalog now ask for the bytes still
739
+ to download plus 5 GiB, the rule `mtplx pull` already used.
740
+ - **A made-up tool call in a reply without tools is hidden completely.**
741
+ The stream filter closed its hidden span at the first `</function>`, so
742
+ the block's own `</tool_call>` reached the user as visible text. It now
743
+ waits for the closing tag that matches the opening one, as the
744
+ non-streamed path does.
745
+ - **Thinking-off tool sessions keep the template's own tokens.** The prompt
746
+ cut the empty thinking block in two, so from the second request of a
747
+ tool session the history no longer matched the saved state. Across 2,020
748
+ audited conversation turns the unexplained differences from the chat
749
+ template fell from 848 to 0, and the predicted reusable history matched
750
+ the next request in 834 of 834 cases, up from 498. For OpenCode with
751
+ thinking on, turns that extend the saved stream went from 28 of 36 to 36
752
+ of 36.
753
+ - **The open inference menu no longer loads the CPU.** Live metrics redrew
754
+ the whole app scene. With the menu open during inference the app's CPU
755
+ use went from 47.9 and 27.4 percent to 2.2 and 3.3 percent.
756
+ - **Activity and Cache show every saved prefix.** Several prefixes saved
757
+ from one conversation shared one row identity, which hid entries the
758
+ engine still held. Rows are now keyed by the session and the token hash.
759
+ - **Each model folder keeps its own name and row in the model list.** The
760
+ list named a local folder after the first of `public_model_id`,
761
+ `served_model_id`, `model_id` and `forge_provenance.source_repo`, and
762
+ every build of a source records that source. Two builds of one source,
763
+ such as a 4-bit and a 6-bit, shared one row named after the source,
764
+ which launched whichever build was found first, and a downloaded
765
+ official pack that records its base model, such as Qwen 3.8 27B
766
+ Optimized Speed, was listed again under that base model. A folder is
767
+ now named by `public_model_id` or `served_model_id`, else by its folder
768
+ name, and keeps its own row; the same folder name under two model
769
+ folders still shares one row, first folder first.
770
+ - **The inference panel no longer pins a 2,048-token prompt chunk.** The
771
+ panel showed 2,048 when nothing was stored and sent it with every live
772
+ settings change, which the app saved, so later launches passed
773
+ `--prefill-chunk-tokens 2048` and skipped Flash-Next's 4,096-token chunk
774
+ on M5. `GET /v1/mtplx/settings` now reports an unpinned chunk as null with
775
+ a read-only `prefill_chunk_tokens_default`, `POST` accepts 0 or `auto` to
776
+ unpin, the panel has an Auto option, and a saved 2,048 yields to the
777
+ engine once.
778
+ - **Setup uses the runtime and model defaults it selected.** An app runtime
779
+ started from another MTPLX folder could import that folder's package
780
+ first and report the wrong version; the runtime wrapper now runs Python
781
+ in safe-path mode. When tuning is skipped, the installed pack's own depth
782
+ applies, so Bonsai starts at depth 1 instead of 2.
783
+ - **The bundled native kernels load on a fresh Mac.** The app's native
784
+ wheel finds its libraries inside the installed package instead of the
785
+ build machine's paths, and it is built against MLX's macOS 15 release so
786
+ it loads on macOS 15 and later. The source distribution no longer
787
+ includes local profiling scripts.
788
+ - **If Flash-Next's compiled verifier cannot run on a GPU, the request
789
+ continues.** Its first dispatch runs under a guard; on a GPU that refuses
790
+ the kernel, the path is retired with one log line and the same round runs
791
+ eagerly, with unchanged output.
792
+ - **Flash-Next's sparse prefill kernel for M1 to M4 checks its numbers
793
+ before it is used.** The app builds the kernel against MLX's macOS 15
794
+ release and runs MLX's macOS 26 release, which the version check cannot
795
+ tell apart. Before its first use in a process the kernel runs one small
796
+ call on random inputs against the pure MLX path and is used only when the
797
+ largest difference is within 0.005; otherwise prefill runs on the pure MLX
798
+ path and `/health` gives the reason.
799
+ - **The app's setup error on macOS 26.0 and 26.1 names the cause.** MLX
800
+ 0.32.2 for macOS 26 needs macOS 26.2 or later, so the engine's import
801
+ check fails on 26.0 and 26.1 whatever the network does. Setup asked users
802
+ to check their network access to PyPI; it now says the engine needs macOS
803
+ 26.2 or later and asks them to update macOS.
804
+ - **Performance reports explain a slow response.** `/health`, the request
805
+ log and `mtplx doctor --explain` count every fallback from the fast path
806
+ with its reason, and Pi traces include compaction requests and the image
807
+ context.
808
+ - **A Qwen model with MiMo in its name keeps the Qwen settings.** The MiMo
809
+ family was matched on the folder name, so Xiaomi's Qwen-architecture
810
+ distills named MiMo (MiMo-V2.6-Distill-Qwen-9B) were served with the MiMo
811
+ settings, draft depth 1 only and reasoning off. The family now comes from
812
+ the checkpoint's own model type and architecture, so such a distill keeps
813
+ the Qwen 3.5 settings.
814
+ - **The packed-GQA verify counter counts accepted windows too** (issue
815
+ #506). It counted only declined windows, and only while the compiled
816
+ verifier was being traced, so a healthy daemon showed 96 declines and no
817
+ accepts.
818
+ - **Quality packs built with Forge carry the Flash-Next sampler settings.**
819
+ A pack built with the `flash-next-optimized-quality` recipe now records
820
+ temperature 1.0, top-p 0.95 and top-k 20 in its `mtplx_runtime.json`, as
821
+ the two Flash-Next Speed packs do.
822
+ - **The `--reasoning-effort` help lists each model's levels and default.**
823
+ Qwen 3.8 27B offers `xhigh`, `medium` and `low`, and Bonsai 2 offers
824
+ `xhigh` and `medium`; both default to `medium`. Flash-Next defaults to
825
+ `xhigh` in chat and to `medium` in coding-agent configurations.
826
+ - **`mtplx inspect` reports whether a pack takes images,** and a pack that
827
+ keeps its vision tower in one weight file with no index, like Prism ML's,
828
+ is served with vision.
829
+ - **`/health` no longer swallows a cancellation or an interrupt** while it
830
+ reads its optional counters.
831
+
832
+ ## [2.11.3] - 2026-09-17
833
+
834
+ ### Added
835
+
836
+ - **Ordered model library directories** (PR #387, Philip John Basile;
837
+ issue #388). One primary model folder (downloads and Forge output go
838
+ there) plus an ordered list of additional read-only folders that the app
839
+ and the CLI both search: the first complete copy wins, duplicates are
840
+ collapsed by real path, and an unplugged drive is skipped instead of
841
+ crashing. The app has a Settings card for the folders (localized in all
842
+ thirteen languages); the CLI takes `--model-search-dir` (repeatable) or
843
+ `model_dirs` in `~/.mtplx/config.toml`, and `MTPLX_MODEL_DIRS`;
844
+ `mtplx doctor` lists the roots. A download or Forge build into a folder
845
+ whose volume is not mounted now says so instead of `Permission denied`.
846
+
847
+ - **`mtplx gc` reclaims orphaned SSD session-cache files** (PR #502,
848
+ ArctifoxNL; issue #493). `mtplx gc` reports the live entries, the bytes on
849
+ disk and everything in `~/.mtplx/session-bank/` that the manifest no
850
+ longer reaches; `--apply` deletes it, `--force` allows that beside a
851
+ running daemon, `--json` is machine-readable and `--dir` points it at
852
+ another store. It needs neither MLX nor a model. The walk it runs is the
853
+ same one the daemon uses (`mtplx/cache_bank/reconcile.py`), so the command
854
+ and the engine cannot disagree about what is garbage.
855
+ - **Idle limit for the warm session cache** (issue #481). Nothing in the
856
+ daemon dropped warm state on a five- or ten-minute clock (the only idle
857
+ timer is one hour; the ten-minute window orders eviction victims under
858
+ pressure), but the limit was not adjustable and none of the cache
859
+ variables were documented. `MTPLX_SESSION_BANK_IDLE_TTL_S` now sets the
860
+ idle limit (`0` keeps entries until memory needs them), and the five
861
+ `MTPLX_SESSION_BANK_*` variables are documented together in
862
+ `docs/server.md`. `/health` and the dashboard stream report
863
+ `idle_ttl_s` as `null` when the sweep is off; the stream used to write
864
+ a bare `Infinity`, which a browser's JSON parser rejects, so every live
865
+ dashboard snapshot was unparseable with the knob at `0`.
866
+ - **`mtplx doctor` names the runtime that answers** (issue #479). A new
867
+ `runtime.identity` check reports the MTPLX version and path the doctor
868
+ imported, the `mtplx` first on PATH and whether it runs the same
869
+ interpreter (an installer shim or Homebrew venv ahead of a source
870
+ checkout is a warning with `which -a mtplx` as the fix), and the GPU
871
+ architecture with whether the M5 tensor-unit route is available. The
872
+ issue's kernel-build failure on macOS 15 is unreachable on 2.11.2 code;
873
+ the check answers which MTPLX actually ran.
874
+ - **Responses API** (PR #219, Philip John Basile). `POST /v1/responses` is served as
875
+ a stateless, text-only adapter over the chat runtime, covering
876
+ client-executed function, custom and namespace tools, with SDK-backed
877
+ tests. `docs/api.md` carries the Codex CLI settings that make its first
878
+ request work (hosted web search off, image generation off, a dedicated
879
+ `CODEX_HOME` so installed Codex apps do not add their connector schemas).
880
+ - **Models in the shared Hugging Face cache are found** (issue #445). The
881
+ model resolver consults `HF_HOME` / `HF_HUB_CACHE` after MTPLX's own
882
+ cache, honours `HF_HUB_OFFLINE`, and holds the copy to the same
883
+ completeness check, so a pack already downloaded with the Hugging Face
884
+ CLI is served without a second download; Forge builds from the local
885
+ copy first and stamps the revision it built from.
886
+ - **aria2 download backend as an opt-in** (PR #452, zeeshanhaque21).
887
+ `mtplx pull --download-backend aria2` uses aria2c for multi-connection
888
+ pulls and fails with an install hint when aria2c is missing;
889
+ `--download-backend auto` picks aria2c when it is installed. The built-in
890
+ downloader stays the default.
891
+ - **Appearance row localized.** The Settings row for Appearance (its title,
892
+ caption and the System, Dark and Light segments) was English in every
893
+ language; it now follows the app language in all thirteen tables.
894
+ - **Chat sidebar and Forge timestamps follow the app language.** The
895
+ relative times under each chat row and in the Forge tab were formatted
896
+ in the macOS locale, so a Turkish app still read "in 0 sec"; both now
897
+ use the active app language.
898
+ - **Launch chooser, Hermes overlay and tune-step captions localized.** Five
899
+ captions (the launch chooser's "Pick how you want to use it." and custom
900
+ client note, the Hermes auto-approve, install-status and Terminal notes,
901
+ and the tune step's ready line) were English in every language; the ten
902
+ strings now resolve through the app language in all thirteen tables.
903
+ - **Forge buttons localized.** The Retry verify, Discard and Use it now
904
+ buttons in the Forge verify and registered stages were English in every
905
+ language; they now follow the app language.
906
+ - **Turkish** (issue #470). The app ships a thirteenth language table
907
+ (Türkçe, 1,562 strings) next to the twelve from 2.11.
908
+ - **Remove a stale custom model from the picker** (PR #471, nRanzo). A
909
+ persisted custom model whose files are gone can be removed from the
910
+ picker with a right-click and a confirmation; official, installed and
911
+ currently selected models cannot. Nothing on disk is touched.
912
+ - **README model table** (issues #238, #408). Every shipped pack with its
913
+ Hugging Face repo, the smallest Mac it is offered on next to its measured
914
+ peak serving memory, what it is for, and the profile and depth MTPLX
915
+ resolves on its own; two tests keep the table equal to the catalog.
916
+
917
+ - **A bounded request replay tool.** `scripts/replay_chat_request.py`
918
+ replays a captured chat request against a daemon with an explicit output
919
+ budget, so a 100k-token agent turn can be re-measured without re-running
920
+ the agent's tools.
921
+
922
+ ### Fixed
923
+
924
+ - **A conversation with tools stays warm from turn to turn.** The app's
925
+ JSON encoder wrote the keys of its tool declarations in a different order
926
+ on every request and the native template rendered that order verbatim,
927
+ so an 18,776-token web-search conversation prefilled from the start on
928
+ every turn (14.7 s and 15.2 s to the first token, 0 cached tokens). Tool
929
+ schemas are put into one fixed key order once, at the server boundary, with
930
+ every field, value and list order preserved; a `tool_choice: none` turn keeps the
931
+ declarations in the prompt and on the same cache identity while calls
932
+ stay disabled; and the server's request-only closing instruction is no
933
+ longer banked as client history (that alone replayed a 2,820-token
934
+ answer, 2.8 s). Replayed 19k-token turns against the same build without
935
+ the fix: 15.26 s to 0.57 s and 13.93 s to 0.66 s to the first token;
936
+ installed-app follow-ups at 0.29 s and 0.28 s with 11,421 and 12,510
937
+ cached tokens. Seven regression tests across the app, OpenCode, Pi and
938
+ Hermes. A conversation older than this release prefills once more on
939
+ its first turn.
940
+ - **Agent launches no longer override the model's verify path, and the
941
+ adaptive depth policy no longer learns a startup spike as its recurring
942
+ cost.** The app's OpenCode, Pi and Hermes presets and the CLI launchers
943
+ exported `MTPLX_LAZY_TARGET_DISTRIBUTIONS=1` and
944
+ `MTPLX_LAZY_BONUS_VERIFY=1` from an older tuning, which on Flash-Next
945
+ switched off the batched compiled verifier the chat launch already used;
946
+ both exports are gone and the model's defaults apply (explicit operator
947
+ exports still win, other families keep their profile defaults). The
948
+ expected-value depth policy seeded its cost estimate with a one-off
949
+ 125 ms first call (the next three were 30 to 31 ms) and chose the slower
950
+ eager path for 453 of 464 cycles; it now calibrates on the minimum of its
951
+ four warm-up samples before the weighted average starts, and a sustained
952
+ rise still lowers the depth. A 108,919-token OpenCode turn, four
953
+ alternating runs against the same build without the change: 48.83 to
954
+ 61.77 tok/s (+26.5 percent), compiled verify calls 11 to 376 per 1,024
955
+ output tokens, memory flat; 200,073 tokens hold at about 50 tok/s.
956
+ - **OpenCode 2 loads the MTPLX session-header plugin** (issue #498). The
957
+ plugin is a small package with the version 1 entrypoint and a version 2
958
+ `setup` hook scoped to the MTPLX provider; 1.18.29 and 2.0.5 clients both
959
+ send the headers, the managed registration migrates in place and other
960
+ plugins are preserved.
961
+ - A conversation whose generation-final snapshot was refused for size (over the per-session cap, issue #499's 48 GB shape) reports that refusal as the next turn's `cache_miss_reason` (`oversized_snapshot_skipped`) and in the bank's `last_oversized_skip`; it used to surface as the cold tier's `ssd_prefix_miss`.
962
+
963
+ - **A busy daemon is no longer killed as dead** (issue #487, HenriGrimm).
964
+ The app's watchdog killed a live daemon whose generation-final prefix
965
+ commit took 19-25 s on a 110-150k vision agent session: two missed health
966
+ probes were read as death while the commit succeeded. The watchdog now
967
+ reaps only when the daemon's process is gone, its port stops accepting
968
+ connections, or it has been silent for 90 s; a refresh or a chat stream
969
+ that fails once goes through the same check. The slow commit itself is
970
+ fixed too: a prompt whose screenshots exceeded the vision embed cache's
971
+ row budget evicted its own images and re-ran the tower for every one on
972
+ the model-owner thread; the prompt's images are pinned for the pass, and
973
+ the commit records the wall of each phase in the flight event so a slow
974
+ commit names its phase.
975
+ - **Gemma 4 warm turns restore across rewritten conversation turns**
976
+ (PR #283, Craig Tollifson). The Gemma 4 prompt path now uses the session
977
+ bank's near-prefix restore, so an edited or re-rendered turn no longer
978
+ re-prefills the whole conversation. Two exactness holes in the sliding
979
+ window trim it relies on were closed on the way in: a restored entry's
980
+ trim outside the last-update rollback left stale rows in the buffer that
981
+ the next step counted as history, and the bank's identity for a Gemma 4
982
+ entry now comes from the runtime's declared history policy, never a
983
+ literal, so AR-only sessions cannot hit a policy mismatch.
984
+
985
+ - **A wedged daemon of the app's own no longer moves the configured port,
986
+ and a port fallback is never saved** (issue #503). After a hard freeze
987
+ the daemon's listener could survive while `/health` stopped answering;
988
+ the app's port preflight read that as "another app", moved to the next
989
+ free port, saved the new port to settings, and every client pinned to
990
+ the configured port (an agent connector on 8001) was stranded for good,
991
+ with the engine showing Running. The preflight now asks the OS who holds
992
+ the port: a listener carrying the app's own launch marker in its
993
+ environment is stopped in place and the configured port is kept. For a
994
+ genuinely foreign occupant the launch still moves to a free port, but
995
+ only for that launch: settings keep the configured port, a save made
996
+ meanwhile writes the configured port back (changing the port on purpose
997
+ still wins), the banner stays until the next start, and the next start
998
+ tries the configured port again. A listener without the marker (a
999
+ stranger's app, a CLI-started `mtplx serve`) is never signalled.
1000
+ - **Deep-context sessions persist on the machines that can restore them**
1001
+ (PR #496, Dizzler7). A 12 GiB warm snapshot (Qwen3.8-27B, Q8 KV, more
1002
+ than 100k tokens) was refused by the flat 8 GiB per-session cap on Macs
1003
+ under 96 GB, so those sessions never reached the SSD tier and came back
1004
+ cold after a restart. The per-session cap is now sized by the memory
1005
+ plan: two thirds of the bank budget, held under half of what the engine
1006
+ budget leaves after the weights and the runtime transients (a restore
1007
+ holds the snapshot next to its banked copy): 13.2 GiB on a 64 GB Mac with
1008
+ the 27B, 32 GiB on a 128 GB Mac with the 27B, 10.5 GiB with Flash-Next.
1009
+ The PR's flat 32 GiB would have pushed a 64 GB Mac into swap; the
1010
+ flat fallbacks for a machine without a plan are unchanged. From the same
1011
+ PR: 64 GB Macs with 150 GiB of free disk default the SSD cap to 100 GiB
1012
+ instead of 32, and the hourly SSD write budget default is 128 GiB (was
1013
+ 64).
1014
+ - **Remote Qwen checkpoints without MTP weights inspect as autoregressive,
1015
+ and Forge refuses to build speculative artifacts from them** (PR #489,
1016
+ Philip John Basile). A Hugging Face checkpoint whose listing carried every
1017
+ trunk shard but no draft head (`nex-agi/Nex-N2.5-mini`) was reported as
1018
+ `missing-model-weights` because the trunk check only looked at a local
1019
+ directory; inspection now uses the same local-or-remote trunk check and
1020
+ reports the model as runnable with MTP off. Forge's runtime probe treated
1021
+ `can_run` as MTP evidence and admitted AR-only trunks into a speculative
1022
+ conversion that cannot create the missing trained head; it now requires
1023
+ MTP weight evidence and says so.
1024
+ - **SSD cache eviction yields to active requests.** A populated cache could
1025
+ keep scanning and deleting old snapshots during the next reply, competing
1026
+ with generation. Eviction now releases the store lock before waiting,
1027
+ reclaims retired snapshots in bounded batches, and reads surviving blob
1028
+ references once per pass instead of once per evicted entry. Concurrent
1029
+ writes and shared blobs remain protected. The shared server fix applies
1030
+ to the native app, OpenCode, Hermes, and Pi.
1031
+ - **The SSD session cache reconciles itself and its cap means the whole
1032
+ directory** (issue #493; three reports: 394,155 orphaned blobs, 44.1 GB,
1033
+ against 17 entries after three weeks; 471,541 blobs, 67 GB, on a 60 GB cap
1034
+ after three days of uptime). The reconciliation that deletes files the
1035
+ manifest no longer reaches only ran when a write found the store near its
1036
+ cap, which a generous cap never reached; and after each cleanup the cap
1037
+ check assumed every unaccounted byte was gone, although blobs still shared
1038
+ by a later snapshot of the same conversation stay on disk when the entry
1039
+ that paid for them is evicted, so the directory could sit above
1040
+ `--ssd-session-cache-max-size` for good. The daemon now reconciles the
1041
+ store in the background every time it opens the cache (yielding to
1042
+ requests, never blocking the boot or the first request; one
1043
+ `mtplx_ssd_session_cache_reconcile` line in the daemon log says what it
1044
+ reclaimed), the cap check counts the directory as it is on disk, reclaims
1045
+ garbage on the writer thread before it would evict live entries for the
1046
+ room, and never walks the store on the request thread. Blobs of a write in
1047
+ flight, and blobs a row committed during a pass names, are never deleted.
1048
+ `docs/server.md` documents the cap, the pass and `mtplx gc`.
1049
+ - **A reply's own whitespace no longer breaks its warm restore.** The
1050
+ visible content of a reply is served with its leading and trailing
1051
+ whitespace removed, so a client's echo could never carry the model's
1052
+ own whitespace tokens; a turn cut by the token budget after a newline
1053
+ came back with the end-of-turn marker where the newline was, the
1054
+ exactness rule refused the restore and the next request re-prefilled
1055
+ the whole turn. The committed-id splice now puts the model's own
1056
+ whitespace back, and it also runs when thinking is off.
1057
+ - **Copied blocks at the verify cache's growth edge kept their rows.** The
1058
+ context-copy path verified a copied block through the fixed-capacity
1059
+ buffers of the compiled verify; a block that straddled the buffer's end (a
1060
+ 512-token growth grant on the 27B, the rounding slack of a freshly
1061
+ restored Flash-Next entry) was written with a functional slice update that
1062
+ silently drops the rows past the end, so the rest of the turn attended
1063
+ over missing rows (an answer cut off mid-statement, or a file written
1064
+ twice, was the visible symptom). Both copy-block routes reserve their rows
1065
+ before the forward and the write grows the buffers as a last guard.
1066
+ - **Flight recorder counts non-streaming requests.** A non-streaming
1067
+ chat completion fed the recorder no token events, so `mtplx trace`
1068
+ showed a 24k-token generation as a prefill with zero tokens for its
1069
+ whole life. Non-streaming requests now report their tokens as they
1070
+ are produced, the same as streamed ones.
1071
+ - **OpenCode compacted after every reply on small context windows** (issue
1072
+ #480). MTPLX registered the model with OpenCode with the reply limit equal
1073
+ to the context window; OpenCode reserves the reply limit out of the window
1074
+ before deciding whether the conversation still fits, so on an 8K window
1075
+ the usable conversation was zero tokens and OpenCode wrote its
1076
+ `## Objective / Important Details / Work State` summary after every turn
1077
+ (48 summaries in 98 turns in the report). `mtplx connect opencode`, the
1078
+ quickstart and the app now advertise a reply budget of half the window,
1079
+ capped at the 32,000 OpenCode uses on large windows, so compaction happens
1080
+ only when the conversation is near the limit.
1081
+ - **261,120-token prompts decode on Flash-Next** (PR #482, davidtai). At
1082
+ the pack's full context the first speculative verify step ran out of GPU
1083
+ memory after the whole prompt had been read: the verify's KV-cache update
1084
+ reallocated the entire cache buffer on every step (about 6.4 GB across the
1085
+ twelve full-attention layers at that context), and its multi-row
1086
+ attention built a score table that grows with the context. The KV update
1087
+ now writes its rows into the existing buffer (exact; the compiled verify
1088
+ path and the rollback are unchanged), and the verify attention is split
1089
+ across query-head groups so each call stays on the fused kernel in the
1090
+ three-to-eight-row band the verify uses (exact per group;
1091
+ `MTPLX_QWEN4_VERIFY_SDPA_HEAD_CHUNK=0` turns it off; `/health` reports
1092
+ `verify_sdpa_head_chunk` when it engages). Speed and peak memory at 16k
1093
+ are unchanged; the 27B models are not affected.
1094
+ - **The Flash-Next converter accepts the official FP8 checkpoint** (PR
1095
+ #474, Graham Jenkins). Qwen's Flash-Next-FP8 revision stores one shared
1096
+ scalar scale for its 128 n-gram embedding shards instead of per-block
1097
+ scales; the converter now falls back to that scalar for n-gram shards and
1098
+ accounts for it, with regression tests for both layouts.
1099
+ - **Flash-Next block verification is exact at every depth.** Three
1100
+ independent audits enumerated the block-verify acceptance rule on tiny
1101
+ vocabularies and found it exact at depths 1 and 2 but off by up to 4e-2
1102
+ total variation at depth 3, in the cases where the last depth's acceptance probability had
1103
+ been clipped to 1. The ladder now caps the reach budget by the realised
1104
+ reach, propagates that feasible budget, and corrects a rejection from the
1105
+ shortfall the acceptance probabilities leave; an enumeration test compares the emitted joint
1106
+ distribution with the target for both rules at depths 1 to 4 (exact to 1e-12).
1107
+ - **Warm agent turns on hybrid models restore the recurrent state exactly.**
1108
+ A near-prefix restore whose gap to the banked turn was 1 to 8 tokens (the
1109
+ shape of a re-rendered agent turn) trimmed the attention KV but kept the
1110
+ GDN state from the longer sequence, then re-ran one token on it; the whole
1111
+ turn decoded on state the new prompt never produced and later turns
1112
+ inherited it, on the 8-bit Quality pack exactly like the 4-bit ones. Every
1113
+ partial restore of a recurrent entry now lands on a stored recurrent
1114
+ boundary at or below the match point.
1115
+ - **The banked state after an MTP turn covers every committed token.** The
1116
+ final token that ended a response (a deferred greedy correction, a fresh
1117
+ sample, or any max_tokens exit) was committed but never forwarded, so the
1118
+ banked state was one token short of the key it was filed under and the
1119
+ next warm turn decoded as if the terminator never existed (temperature-0
1120
+ clients such as Cline hit it every turn). The final token is now
1121
+ forwarded before the state is banked.
1122
+ - **Non-finite logits fail loudly** instead of becoming token 0, which is
1123
+ `!` in the Qwen vocabulary (the thousands of exclamation marks of issue
1124
+ #311). The request ends with `finish_reason: error`, code
1125
+ `non_finite_logits`, a message with the NaN/inf counts, the session's
1126
+ cached state is dropped, and the daemon stays up. A row that constrained
1127
+ decoding masks to -inf with a finite winner is legitimate and passes the
1128
+ guard.
1129
+ - **The Flash-Next AR sampler follows the reference nucleus rule.** The
1130
+ pipelined AR sampler measured top-p on the top-k slice alone, keeping one
1131
+ to five fewer of the top-20 tokens than the other sampling paths at 1.0/0.95/20;
1132
+ it now measures the nucleus on the full-vocabulary softmax like the CPU
1133
+ reference (test: identical supports, total variation below 1e-5).
1134
+ - **Hindi, Thai and vowelled Arabic tokenize as the model was trained.**
1135
+ transformers' Qwen2Tokenizer replaced the packs' pre-tokenizer regex with
1136
+ its Qwen2-era one, splitting combining marks off their letters (Hindi 32
1137
+ tokens instead of 20, Thai 17 instead of 9, Arabic 45 instead of 31 on the
1138
+ same sentences; Latin, code and Chinese unchanged), and the six 27B packs
1139
+ shipped that regex baked into tokenizer.json. The loader restores the
1140
+ Qwen3 regex on every Qwen3-family pack.
1141
+ - **32 GB Macs get a working draft head** (issue #483, HeyCocoa). The
1142
+ fast draft LM head was built through a 2.4 GiB dense intermediate; on an
1143
+ M1 Pro 32 GB the requantize hit a silent Metal out-of-memory and the head
1144
+ read back as zeros, collapsing MTP acceptance to under 1%. The head is
1145
+ now built in row chunks (bit-identical result, no dense intermediate), a
1146
+ zeroed head is refused, and the install falls back to the resident target
1147
+ head as the drafter.
1148
+ - **A long agent turn stays warm across the model's own token seams.** A
1149
+ model's sampled tokens are not always the standard encoding of their own
1150
+ text: in a Hermes session at effort xhigh the 27B wrote `"Nothing` as one
1151
+ token 8,498 tokens into a write_file call where the tokenizer encodes `"`
1152
+ then `Nothing`. The client's re-tokenized history then diverged from the
1153
+ committed stream on identical bytes, the generation-final snapshot was
1154
+ refused, the fallback re-prefill was preempted by the next request, and
1155
+ that request re-prefilled the whole 29,842-token turn (30,038 tokens,
1156
+ 41 s to first token). Wherever the resent prompt and the session's
1157
+ committed ids decode to the same text, the committed ids are now served
1158
+ (a bounded window per seam, never across a real edit), on both the
1159
+ request and the snapshot side; the request log carries `token_splice`
1160
+ under `committed_reasoning_canonicalization`.
1161
+ `MTPLX_COMMITTED_TOKEN_SPLICE=0` restores the old behaviour.
1162
+ - **A mid-conversation system message no longer rewrites message 0**
1163
+ (issue #477). Claude Code's per-turn `<system-reminder>` notes arrive as
1164
+ late system messages; hoisting them into the leading system message
1165
+ changed the first message every turn and cost a cold re-prefill of the
1166
+ whole history. A late system message now becomes a user turn in place.
1167
+ - **The Flash-Next serving optimizations reach a served daemon.** Four
1168
+ hot-path settings (`MTPLX_QWEN4_OPDIET`, `MTPLX_QWEN4_VERIFY_GLUE`,
1169
+ `MTPLX_QWEN4_DRAFT_K20_PRESCATTER`, `MTPLX_QWEN4_BLOCK_VERIFY`) were read
1170
+ once at import, before the server applied the Flash-Next defaults,
1171
+ so `/health` reported them configured while the daemon ran with all four
1172
+ off (davidtai's PR #475 found the same problem). The settings are
1173
+ re-read when the model's runtime env is applied, before the load. Three of
1174
+ them are on by default; the exact block verify
1175
+ (`MTPLX_QWEN4_BLOCK_VERIFY`) is an opt-in export, because on a
1176
+ 45,000-token reasoning turn at effort xhigh it accepted 3.5 percent fewer
1177
+ draft tokens per round than the standard verify across alternating
1178
+ boots (both are distribution-exact; the faster one is the default).
1179
+ - **Reasoning substitution checks the whole tool call.** The
1180
+ committed-reasoning canonicalizer compared tool calls by their loop key
1181
+ (command or path only), so a `write` to the same file with new content
1182
+ passed the check and the prompt was served with the old body. The check now
1183
+ compares the complete argument set in both markup dialects.
1184
+ - **Desktop web views can be allowlisted for CORS** (issue #473). The
1185
+ origin validator accepted only `http` and `https`, so
1186
+ `--cors-origin tauri://localhost` (Jan.app) and `MTPLX_CORS_ORIGINS`
1187
+ entries such as `app://obsidian.md` were refused as invalid and the
1188
+ requests got a 403. Any scheme is an origin scheme now; a wildcard, a
1189
+ bare host, a path, a query or credentials are still refused, and the
1190
+ allowlist still never opens `/admin` or the browser sign-in routes.
1191
+ - **Request-capture registry no longer grows for the life of the daemon**
1192
+ (noticed in PR #356, PhilipJohnBasile). With `MTPLX_REQUEST_CAPTURE_DIR` set, the in-memory map from request id
1193
+ to capture file kept an entry for every request ever captured, including
1194
+ the ones the ring had already moved to `pruned/`; it now forgets an id
1195
+ as soon as its file leaves the ring.
1196
+ - **Streams held for minutes when the first line looked like tool-control
1197
+ markup** (issue #468). An answer whose first line was a bare name such as
1198
+ `value` or `value=abc` stayed in the orphan-marker hold until the stream
1199
+ finished. A bare tool-control line closes on its own line, so the hold is
1200
+ released as soon as the first line closes.
1201
+ - **Retrieval models are validated at startup** (issue #445 audit). A
1202
+ missing `--embedding-model` or `--reranker-model` used to boot a daemon
1203
+ that answered the first `/v1/embeddings` request with a 500 and a
1204
+ traceback; the CLI and the server module now refuse to launch with the
1205
+ `mtplx pull` hint, a checkpoint that disappears later surfaces as a
1206
+ structured 404, and `/v1/models` lists only retrieval models that
1207
+ resolved. Retrieval models are also found in the shared Hugging Face
1208
+ cache.
1209
+ - **Dashboard request log** (issue #401). The per-depth acceptance card
1210
+ prints real accepted and drafted totals, and the when column shows the
1211
+ completion time of every recorded request, cancelled and disconnected
1212
+ rows included.
1213
+ - **`mtplx connect opencode` follows the live daemon** (issue #472). The
1214
+ command names a host and a port, not a pack, so it now takes the model id
1215
+ and the image-input flag from the daemon that answers there; an explicit
1216
+ `--model-id` still wins, and without a daemon the pack metadata for the id
1217
+ in hand answers as before. 2.11.2 already advertised image input from the
1218
+ pack metadata; the port-only form assumed the catalog default id.
1219
+ - **`/health` fast-path checks against the server's own overrides.** The
1220
+ Flash-Next path keeps the verify snapshot and pins the batched target
1221
+ distributions on purpose, and `/health` reported those three keys as
1222
+ `ok: false` against the profile block. The expectation is now the
1223
+ runtime override the server resolved, with the entry naming its source
1224
+ and the profile value it replaced.
1225
+ - **Hermes profile warning on every launch.** Hermes v0.21 deprecates the
1226
+ `TERMINAL_CWD` line in `.env`; the CLI profile writer and the app no
1227
+ longer write it. The working directory reaches Hermes through
1228
+ `terminal.cwd` in its config, as before.
1229
+ - **Flash-Next agent launches draft to the chosen depth.** With the
1230
+ Adaptive depth switch unset, the app's Pi and Hermes launches no longer
1231
+ turn on the expected-value depth policy for Flash-Next: measured on the
1232
+ shipped 2.11.2 settings, the policy decodes 7 to 8 percent slower than a
1233
+ fixed depth 3 at 2k and at 19k tokens of context, and the 27B pair is a
1234
+ tie, so the 27B presets are unchanged. The switch still turns the policy
1235
+ on explicitly.
1236
+
1237
+ ### Changed
1238
+
1239
+ - **transformers floor raised to 5.10.0** (Dependabot alert 24, the
1240
+ `save_pretrained` path-traversal range); the lock moves to 5.14.1.
1241
+ - **The dead M=8 K-split verify kernel is removed** (issue #322). Nothing
1242
+ dispatched `nax_qmm_m8` since it landed (0.51-0.87x of stock on every
1243
+ live shape); the evidence stays as a comment beside the M=16 eligibility
1244
+ helper so the closed branch is not re-litigated.
1245
+ - **Docs.** The adaptive depth policy flag (`docs/server.md`), the request-capture
1246
+ ring size (`MTPLX_REQUEST_CAPTURE_KEEP`), the reasoning effort surfaces
1247
+ (`docs/api.md`, issue #484) and the OpenCode CLI's per-turn system-prompt
1248
+ rebuild with its measured cost (`docs/perf`) are documented.
1249
+
1250
+ ## [2.11.2] - 2026-09-06
1251
+
1252
+ A correctness release for every Mac that is not an M5, seven session-bank
1253
+ and memory fixes for long agent sessions, native vision for Flash-Next in
1254
+ Hermes, OpenCode and Pi, a verifier-depth fix for Flash-Next agent turns,
1255
+ and app and CLI repairs.
1256
+
1257
+ ### Fixed
1258
+
1259
+ - **Flash-Next multimodal requests keep QSA sparse attention.** Image-bearing
1260
+ requests ran dense causal attention on the belief that the reference does;
1261
+ Qwen's reference applies the sparse indexer to multimodal input with
1262
+ image-aware M-RoPE positions on the indexer's queries and pooled
1263
+ block-start keys. The engine follows it, checked against an independent
1264
+ position oracle. A 54k image conversation went from about 19 tok/s and a
1265
+ 128 GiB allocator peak to a 92 GiB peak with turns completing at 43k-74k.
1266
+ Dense-path vision cache entries are not reused. Image-bearing turns still
1267
+ decode on the eager verifier (33-50 tok/s here versus 54-61 text-only);
1268
+ `MTPLX_QWEN4_VISION_QSA=0` is a diagnostic rollback.
1269
+ - **Hermes, OpenCode and Pi advertise image input from the pack's metadata.**
1270
+ All three registered the model as text-only, so Hermes routed screenshots
1271
+ through its auxiliary Analyze Image tool as separate requests and OpenCode
1272
+ and Pi could not attach an image. The app and `mtplx` probe `config.json`
1273
+ and the weight index (`vision_config` plus a vision-tower weight), never
1274
+ the model name; existing text-only entries are upgraded; `/v1/models`
1275
+ carries `supports_vision` and `modalities`.
1276
+ - **A warm vision turn is admitted as warm.** The pre-prefill guard compared
1277
+ raw image-pad tokens against content-keyed snapshots, projected a warm turn
1278
+ as a full miss, shed the snapshot it needed and could refuse with a 507.
1279
+ Admission uses the content-keyed identity restore uses; an anonymous image
1280
+ turn rejoins the session owning the matching snapshot
1281
+ (`session_source: vision_bank_prefix`); different pixels never adopt an
1282
+ older image's state.
1283
+ - **A longer bank entry no longer erases a shorter exact prefix it cannot
1284
+ restore.** Flash-Next recurrent checkpoints sit on 2,048-token boundaries
1285
+ and the exact prompt end is not one, so the generation-final entry
1286
+ superseded the exact prompt entry and the next tool turn restored 2,048
1287
+ tokens and re-prefilled 31k. A longer entry replaces a shorter one only
1288
+ when it carries every restore point the shorter one supplies.
1289
+ - **Hermes history matches the committed stream.** The profile enables
1290
+ `reasoning_echo`, so Hermes sends its reasoning back (the next tool turn
1291
+ reused 15,754 of 16,005 tokens); the cache producer, canonicalization and
1292
+ next-turn comparison strip the tool-call preamble the way Hermes' wire
1293
+ does, an intentionally empty visible answer stays empty, and a response's
1294
+ own reasoning survives an older interrupted turn. The Hermes profile also
1295
+ requests `compression.tool_image_retention: until_compaction`, which needs
1296
+ a Hermes change that is not upstream yet; current Hermes versions ignore
1297
+ the key.
1298
+ - **The Flash-Next expected-value depth policy measures draft and verify
1299
+ cost per depth** when the compiled fixed-M4 verifier is engaged, instead of
1300
+ assuming a shorter eager draft is cheaper than compiled depth 3; it leaves
1301
+ one-time compilation out of the estimate and re-probes both depths. The
1302
+ rehearsal's 15k coding turn went from about 11 % to 90-96 % of cycles on
1303
+ the compiled route and from 41-47 to 56-59 tok/s (machine in use, not a
1304
+ quiet A/B). `MTPLX_ADAPTIVE_VERIFY_COST_FEEDBACK=0` restores the prior.
1305
+ - **The prefill gauge and the Avg Prefill card measure the same chunk work.**
1306
+ The card averaged completed-request rates (a cached follow-up counted like
1307
+ a full prompt; setup counted as prefill: 422 shown for a 1,040 tok/s
1308
+ prompt) and the gauge preferred a setup-diluted cumulative rate (296 for a
1309
+ 1,613 tok/s chunk). Both read measured chunks now; the card averages the
1310
+ last 100 and shows their peak, recorded once per chunk server-side.
1311
+ - **The prefill gauge no longer flickers through an agent's tool loop.**
1312
+ It morphs to the prefill face only for at least 1,024 new tokens, judged
1313
+ from the resolved session's known prefix on the first frame and from
1314
+ the measured cache split after; short suffix prefills keep the decode
1315
+ face. No timer and no hold.
1316
+ - **Latency receipts are complete**: `ttft_s` includes admission and
1317
+ pending-history waits (a 39 s history-rebuild wait had vanished), decode
1318
+ tok/s uses the generator's decode time instead of charging prompt setup to
1319
+ decode twice, the context tile grows with the answer, and the verify
1320
+ waterfall shows the first live request.
1321
+ - **App and Python test suites leave the user's files alone**: the
1322
+ daemon-supervisor tests overwrote the saved model and onboarding choices
1323
+ through the real settings file; synthetic Python requests entered the real
1324
+ request log and flight recorder.
1325
+ - **27B on M1-M4: the flash-decoding verify route is gated by hardware
1326
+ (#459, #464, #467, #461).** 2.11 turned the route on in turbo without
1327
+ a GPU-family gate. It engages once the KV buffer reaches 8,192 tokens and
1328
+ uses the M5 GPU's tensor units; on M1-M4 GPUs it returned wrong attention
1329
+ (unrelated reasoning, imaginary tasks, mixed languages, no tool calls) and
1330
+ on macOS 15 the kernel failed to build (`MetalPerformancePrimitives.h`
1331
+ not found). The route now runs only on an M5-class GPU on macOS 26.2 or
1332
+ newer; everywhere else the packed-GQA verify kernel 2.10.2 shipped serves,
1333
+ validated at startup. `/health degradation.nax` carries `available`, the
1334
+ `gpu_family_or_os` bail counts and, new, `flash_dispatch_counters` for an
1335
+ engaged route. Rehearsed on an M5 Max with the reporters' 14k and 32k
1336
+ diff-summary prompts: the forced M1-M4 path and the native route both
1337
+ answer correctly; the route counts 66 dispatches with no bails. Item 3
1338
+ of #455 (incoherent output with MTP on, through Pi on an M3 Ultra)
1339
+ matches the route's engagement point and is not yet confirmed from that
1340
+ machine; #455's AR slowdown is not explained by this change.
1341
+ - **AR-only sessions restore again (#465).** A target-only AR runtime
1342
+ (`--no-load-mtp`) banked its postcommit prefix under the `cycle` history
1343
+ policy while every lookup, the prefill store and the cache fingerprint
1344
+ said `committed`, so the bank refused the longest entry with
1345
+ `policy_mismatch` and Hermes or Pi re-prefilled the whole prompt on every
1346
+ top-level turn (14.5k tokens, about two minutes on an M1 Max). One policy
1347
+ per runtime is now derived in one place. The 90-116 s first turn after
1348
+ idle in #455 runs the same configuration on an M3 Ultra: the same
1349
+ mechanism on paper, unconfirmed from that machine.
1350
+ - **The pre-prefill memory guard refuses a prompt that still projects over
1351
+ the memory limit after reclamation (#450).** It had admitted a 136k
1352
+ prompt at 105.4 GB against a 103.1 GB limit after clearing the allocator
1353
+ cache; the reporter's 128 GB Mac kernel-panicked four times. The guard
1354
+ re-projects after every reclamation step and answers with a structured
1355
+ 507 before prefill, naming the projection, the limit and the uncached
1356
+ tokens; the engine keeps its sessions. `--allow-swap` keeps the operator's
1357
+ explicit choice; between the warning line and the limit nothing changes.
1358
+ - **Deep conversations retain their session at recognized turn boundaries (#446).** A live
1359
+ session's committed stream carries the reasoning it streamed and clients
1360
+ resend the history without it, so from turn 2 on every request's raw
1361
+ shared prefix with its session ends where turn 1 started generating. The
1362
+ resolver accepted that match only as a fraction of the new prompt: 22,437
1363
+ shared tokens passed at 89k (25.06 %) and failed at 112k (20.1 %), the
1364
+ request minted a new anonymous session and block-restored 2,048 tokens of
1365
+ turn 1's snapshot, 105 s of prefill for a turn that had been 24 s (the
1366
+ reporter's 14,336 and 18,432-token remnants at turn 4 or 5, five chains of
1367
+ five; the same line crossed one chain in three on 2.10.x). Sessions now
1368
+ record the prompt length of every turn they generated from, and a shared
1369
+ prefix on one of those boundaries keeps the session whatever fraction of
1370
+ the prompt it is; edited histories keep the fraction rule, and unrelated
1371
+ conversations that share only a long system prompt still diverge before
1372
+ any boundary. The request log's prefix diagnostic names the rule
1373
+ (`reuse_rule`, `turn_boundary`).
1374
+ - **The sparse-prefill native wheel is signed for notarization.** Its QSA
1375
+ kernel library and extension module are signed with the Developer ID,
1376
+ hardened runtime and a secure timestamp before they enter the runtime
1377
+ wheel (the app's signing pass cannot reach inside the archive), and the
1378
+ release script verifies every Mach-O in that wheel before submitting the
1379
+ app. The first 2.11.2 submission was rejected on exactly those two files.
1380
+ - **Later conversations are admitted to the session bank again (#454).**
1381
+ The background-task heuristic (short answer + different system prompt)
1382
+ classified every conversation-continuing short turn as a title job and
1383
+ served it sessionless, so only the first conversation after a restart was
1384
+ banked and every later one re-prefilled at 0 % cache. Only the task shape
1385
+ (system prompt + one user turn) infers a background task now. Reproduced
1386
+ with the reporter's script: sessions two to four went from 0 % to 100 %
1387
+ cached on their third turn.
1388
+ - **`mtplx run`, `mtplx ask` and one-shot `mtplx chat` work on Flash-Next
1389
+ (#463).** The one-shot path applied only the profile defaults, never the family lanes serve
1390
+ stamps, and crashed in the legacy capture walker
1391
+ (`AttributeError: 'DecoderLayer' object has no attribute
1392
+ 'input_layernorm'`, then `KeyError: 'conv_states'`). It resolves the same
1393
+ runtime contract as serve and tune, and the legacy capture commit declines
1394
+ family-native captures instead of raising. Receipt: 52 tok/s, MTP depth 3.
1395
+ - **The interactive terminal chat resolves the same contract.** The REPL
1396
+ (`mtplx chat` with no prompt, `mtplx start cli`) still applied only the
1397
+ profile defaults; it now runs the serve environment before the model loads.
1398
+ - **The in-process generators use the family's verifier.** `mtplx run` and
1399
+ the terminal chat hardcoded the legacy capture-commit verifier over the
1400
+ batched verifier the Flash-Next contract selects; a two-turn terminal
1401
+ session on Flash-Next degenerated into repetition and a later run ended
1402
+ in a Metal GPU address fault. The resolved strategy and core now reach
1403
+ generation, as they always did in serve.
1404
+ - **The terminal chat keeps reasoning in its own channel.** It stored
1405
+ `thought</think>answer` as the assistant's content, so Qwen 3.8's
1406
+ template nested that after an empty thinking block and the next turn's
1407
+ history was malformed. Reasoning and content are stored separately.
1408
+ - **`--expect-python` validates the final answer.** It compiled the
1409
+ reasoning and the Markdown fence as Python and failed a valid program;
1410
+ it now splits off the reasoning with the model's codec, unwraps one
1411
+ enclosing fence, and still rejects malformed or missing programs.
1412
+ - **The app's Hermes profile `.env` keeps a configured reasoning effort on
1413
+ its own line.** With Performance › Reasoning effort set, the app wrote
1414
+ `TERMINAL_CWD="…"HERMES_MTPLX_REASONING_EFFORT="xhigh"` as one statement;
1415
+ Hermes' dotenv parser rejected it ("could not parse statement") on every
1416
+ launch and silently lost both the working directory and the effort. Found
1417
+ by the release harness run through Hermes v0.21.0.
1418
+ - **The Hermes profile no longer stamps `terminal.backend: local` (#460).**
1419
+ MTPLX wrote it into `~/.hermes/profiles/mtplx/config.yaml` on every
1420
+ launch, overriding a Docker sandbox the user had configured; the merge
1421
+ keeps what the user set.
1422
+ - **A root Hermes sandbox choice reaches the MTPLX profile.** Hermes
1423
+ profiles do not inherit `~/.hermes/config.yaml`, so a profile with no
1424
+ `terminal.backend` of its own now receives the root `terminal` section;
1425
+ an explicit backend in the profile always wins and nothing else from the
1426
+ root config is copied. Both the app and `mtplx` write it, idempotently.
1427
+ - **The setup wizard measures free space on the model store's volume
1428
+ (#466).** With `~/.mtplx/models` on an external drive (a symlink or
1429
+ `MTPLX_MODEL_DIR`) it read the home volume and refused every catalog
1430
+ model as "insufficient space". The download step and Forge share the fix.
1431
+ - **A foreign-looking occupant of the daemon port is re-probed for five
1432
+ seconds before the app moves ports (#409).**
1433
+ - **Installation survives a failing native-wheel selector.** When the
1434
+ optional step that picks the native sparse-prefill wheel fails, the app
1435
+ installs the bundled pure wheel and records that choice instead of
1436
+ stopping; installation health and the wheel fingerprint checks are
1437
+ unchanged.
1438
+ - **`build_and_run.sh --no-launch` no longer terminates a running app.**
1439
+ It refuses to overwrite the exact running bundle and leaves every other
1440
+ MTPLX instance alone; the launch path is unchanged.
1441
+ - **SSD prefix restore reads only the needed prefix and slices the committed
1442
+ MTP history to it (PR #444 by @softpudding).** Fixed verifier capacity is
1443
+ renewed for adaptive depths and copy windows; shared verifier programs are
1444
+ released when a model unloads; completed request banks are released while
1445
+ the shared programs stay.
1446
+ - **The memory guard asks the live sessions and the bank for the reusable
1447
+ prefix before projecting, walks chain snapshots before giving up, and
1448
+ reclaims allocator storage before evicting useful snapshots (#447,
1449
+ reported and measured by @nomishbhardwaj).**
1450
+ - The expected-value depth policy measures conditional acceptance
1451
+ correctly; prefill pipeline resolution stays out of decode and
1452
+ ineligible chunks; a stale A3B target-prefix test literal.
1453
+
1454
+ ### Added
1455
+
1456
+ - **`--stream-stall-deadline-s` on `mtplx serve` and Performance › Advanced ›
1457
+ Stall watchdog in the app (#448)**, 0 disables; the Flash-Next
1458
+ sparse-prefill loops tick the owner heartbeat so a long page-in is not
1459
+ mistaken for a stall.
1460
+ - **An MTP on/off verdict in the trace economics** for any depth policy:
1461
+ `mtp_pays` compares the tokens a run delivered per second with the
1462
+ matched AR rate the caller supplies, and `break_even_acceptance` is the
1463
+ acceptance at which the run would only have matched AR, under its
1464
+ observed cycle cost, non-draft output and depth mix. Proposal cycles are
1465
+ counted from the first draft position, not from verifier calls; `mtplx
1466
+ trace` reports `proposal_cycles`, `cycle_cost_ar_steps`,
1467
+ `drafts_per_cycle`, `acceptance_margin`, `break_even_basis` and `mtp_pays`.
1468
+ - **Opt-in interleaved n-gram rows (#449, David Tai's layout).**
1469
+ `python -m mtplx.ngram_row_layout <table> --out <cache>` writes a derived
1470
+ cache with one 100-byte record per row, checked bit-for-bit, and
1471
+ `MTPLX_NGRAM_ROW_FILE` serves from it. Off by default; no speed claim
1472
+ until the cold-row measurement exists. `docs/diagnostics/ngram-row-cache.md`.
1473
+ - **Adaptive depth switch under Draft depth in the app.** It shows the
1474
+ daemon's live policy: the Pi and Hermes launches have always named the
1475
+ expected-value policy through their presets, chat and the other targets
1476
+ pass none. A flip applies live and is remembered across launches for
1477
+ every target (the launch then passes an explicit `--adaptive-policy`);
1478
+ the switch is hidden for a family that owns its own draft policy.
1479
+ `/v1/mtplx/settings` accepts `adaptive_policy` and reports
1480
+ `adaptive_depth_supported`.
1481
+ - **`mtplx trace --hermes-db <state.db> [--hermes-log <agent.log>]`** joins a
1482
+ Hermes session to engine receipts by token counts and completion clock;
1483
+ ambiguous joins stay unmatched. Trace charts leave missing samples and
1484
+ observation gaps out of the curve.
1485
+ - **`scripts/run_harness_check.py`** runs an agent CLI with the real exit
1486
+ code recorded and a timeout counted as a failure.
1487
+ - **Traces inspectable across harnesses**: exact Pi joins, retained tool
1488
+ results, prefix diagnostics kept with their request, verifier route costs,
1489
+ new tool content distinguished from reduced prefix reuse.
1490
+ - **Forge extracts MTP heads stored outside the `mtp.` prefix (PR #442 by
1491
+ @stooit)**: GLM-4 MoE, GLM-5.3-Flash, DeepSeek-V3.2 and MiMo layouts;
1492
+ MiMo's output head bound instead of random; MiMo reaches tune; forge takes
1493
+ verification depths from the tune policy.
1494
+ - **Flash-Next tuning uses the serving family contract (PR #457 by
1495
+ @stooit)** with real draft-cycle means and explicit budgets.
1496
+ - **Sparse prefill packaged for the bundled Python with a compatible fallback
1497
+ (#423 by @humanrouter)**, validated through the real app installer.
1498
+ - **`--compare-static` fixed-depth baselines for `mtp-adaptive` (PR #276 by
1499
+ @rinaldofesta)**; composer view lookup isolated to the main actor in the
1500
+ app tests (PR #372 by @PhilipJohnBasile).
1501
+
1502
+ ## [2.11.1] - 2026-09-03
1503
+
1504
+ MTPLX 2.11. The artifact number is 2.11.1 because 2.11.0 was consumed by a
1505
+ mis-stamped upload to PyPI on 2026-09-01 that was retracted; it contained
1506
+ nothing beyond 2.10.2.
1507
+
1508
+ ### Added
1509
+
1510
+ - **Agent-session release gate (`scripts/agent_session_gate.py`).** One
1511
+ command drives the coding-agent turn loop every harness ends up sending
1512
+ (OpenCode, Pi, Hermes, Claude Code, Cline share the OpenAI-compatible
1513
+ shape): a long real-code prompt, short same-session turns, an auto tool
1514
+ round and a forced-choice round with their tool results, judged from the
1515
+ engine's own per-request receipts. It fails on warm-turn dead time over
1516
+ 1 s, a warm TTFT over 1.5 s on a small suffix, a bank restore under 90% of
1517
+ the prompt, a postcommit wait over 2 s, a tool-call turn whose
1518
+ generation-final snapshot was not banked in O(1), a decode drop past 20%
1519
+ of the cold turn, or any stream error — every symptom of the 2026-09-03
1520
+ OpenCode regressions, none of which a unit test or a single-request
1521
+ benchmark could see. `release_macos_v1.sh` runs it after the pillar gate
1522
+ against the same daemon (`MTPLX_RELEASE_AGENT_GATE_CONTEXT_TOKENS`,
1523
+ default 40000). The daemon's final stream chunk already carries every
1524
+ field it reads, so it works against any install with no log access.
1525
+ - **Cache-miss receipts.** The flight recorder records the outcome of every
1526
+ generation-final snapshot attempt (mode, reason, both stream lengths, the
1527
+ divergence token); the committed-reasoning canonicalizer records why it
1528
+ stood aside; `MTPLX_DEBUG_POSTCOMMIT_MISMATCH_DIR=<dir>` dumps the
1529
+ generated and re-rendered token windows around a refused snapshot.
1530
+ - **Light appearance for the app (#428).** A curated cream palette
1531
+ (cream ground, warm ink type, graphite chrome, matching code colors),
1532
+ not an inversion; every pair is gated on WCAG AA and the dark palette
1533
+ is byte-identical to 2.10. Settings > Behavior > Appearance: System,
1534
+ Dark, Light. The default stays Dark.
1535
+ - **The app in twelve languages.** Onboarding opens with a searchable
1536
+ language step with flags (English, Simplified Chinese, Spanish, Hindi,
1537
+ Arabic, Brazilian Portuguese, French, Russian, Japanese, German, Korean,
1538
+ Indonesian); the same picker in Settings switches the app live, and
1539
+ Arabic lays the app out right-to-left. Every string goes through the
1540
+ localization layer with English as the fallback. Installs that finished
1541
+ onboarding on an earlier version are asked once, on the first launch
1542
+ after updating, so they learn the app speaks their language and where
1543
+ to change it; the pick applies live and the prompt never returns.
1544
+ - **Paste images and files into chat.** ⌘V in the composer attaches
1545
+ what is on the clipboard: a screenshot or an image copied from a
1546
+ browser or Preview becomes an image attachment, a file copied in
1547
+ Finder becomes a document attachment (PDF, docx, md, txt, or an image
1548
+ file), exactly like the paperclip and drag-and-drop. Text still pastes
1549
+ as text. On a model without vision, a pasted or dropped image shows a
1550
+ card that says the model can't see images instead of riding along
1551
+ silently.
1552
+ - **Choose a model folder.** The model picker's add row takes a local
1553
+ folder as well as a Hugging Face `org/repo`: a native folder chooser
1554
+ (or a typed path) is checked for a complete MTPLX install, remembered
1555
+ as a row in the picker, and selected in one step, so switching between
1556
+ models never means typing the directory again. The onboarding
1557
+ local-folder step gets the same chooser, and a folder that carries a
1558
+ catalog model's name is recognised as that model at a different
1559
+ location.
1560
+ - **`--allow-swap` serves past the machine fit again (#427).** 2.10
1561
+ capped the default window at the memory plan's fit and refused prompts
1562
+ past it with a 507; operators who ran 2.9.x past the fit on purpose
1563
+ (32 GB Macs, swap accepted) lost that option. With the flag the
1564
+ default window is the model's own maximum, prompts past the fit are
1565
+ admitted, the banner and `/health` say so, and the plan still reports
1566
+ the overcommit. `MTPLX_ALLOW_SWAP=1` is the env form for the app and
1567
+ `mtplx start`.
1568
+ - **Flash-Next speed lane on by default (PR #391 by @davidtai).** The
1569
+ compiled fixed-M4 verifier, batched target distributions, compiled MTP
1570
+ prepare, relaxed draft ties and the fused K/V gather are the family
1571
+ defaults for the Flash-Next geometry, with a 32-row rows-gather fence
1572
+ and FR-Spec drafting on packs whose lm_head has the Q8 group-64 layout
1573
+ it needs. Same-hour pairs against 2.10.2 with the copy lane on: round
1574
+ time -12% at 16k, -18.5% at 100k, -14% at 206k. A per-request memory
1575
+ gate hands prompts whose bank promotion would not fit back to the
1576
+ plain verify (`MTPLX_QWEN4_FIXED_M4_MAX_CONTEXT` is the operator belt).
1577
+ - **Flash-Next: David's decode and prefill stack, on by default (PR #391 by
1578
+ @davidtai).** The second half of the PR, ported commit by commit under his
1579
+ name and measured on the coding cells: the two-kernel MoE routing head, the
1580
+ exact op diet, block verification in the accept loop, the fused QSA rope
1581
+ glue inside the compiled verify body, the M4 kernel trio, the PLE prefill
1582
+ lookahead (chunk k+1's n-gram rows gathered under chunk k, also on the
1583
+ restored-suffix prefill of warm agent turns), the first chunk's gather at
1584
+ request arrival, the n-gram table pre-read at model load
1585
+ (`--ngram-prewarm auto|all|off|<GiB>`), and the session bank's boundary
1586
+ shedding and protected terminal. The pre-scatter draft read serves greedy
1587
+ requests. Every item is exact (token-identical at temperature 0) and every
1588
+ key yields to an explicit export, `=0` included. 16k: 41.8 to 39.2 ms per
1589
+ round (-6.2%, -18.6% against 2.10.2), 63.2 to 68.4 tok/s; 100k: 46.7 to
1590
+ 43.4 ms (-7.2%, -20.4% against 2.10.2), 54.0 to 60.9 tok/s, TTFT 117.0 to
1591
+ 113.2 s; 206k: not re-measured after the release-night harness crash
1592
+ (2.11 as built: 56.2 ms, 44.8 tok/s); peak memory within 2.3 GB of 2.11 as
1593
+ built at 100k, flat at 16k.
1594
+ - **27B flash-decoding verify route on in turbo** (`MTPLX_NAX_FLASH_ROUTE=1`,
1595
+ dim-split block defaults from the 72.7k and 128k sweeps).
1596
+ - **Opt-in Steel sparse-GQA prefill consumer for M3** (PR #423 by
1597
+ @humanrouter), shipped as a native extension, not yet in the app bundle.
1598
+ - **StreamScope two-turn copy-lane arm** so the streaming gate covers
1599
+ block-sized emits.
1600
+ - **The n-gram pre-read reserves the engine's growth to its budget.** The
1601
+ automatic pre-read at model load subtracts max(KV estimate, engine budget
1602
+ minus the weights on disk) from free memory instead of the KV estimate
1603
+ alone; on a 128 GB Mac it drops from 23.4 GiB to 14.2 GiB, the amount the
1604
+ page cache can keep once a long prefill has grown the engine to its
1605
+ envelope. `tests/test_ngram_prewarm_reservation.py`.
1606
+ - **Settings > Memory card (#431 @Journey0723, #427 @localbylocal).** A memory
1607
+ limit in GB and an allow-swap switch, carried into the daemon as
1608
+ `MTPLX_MEMORY_LIMIT_BYTES` / `MTPLX_ALLOW_SWAP`; the card shows the plan the
1609
+ engine computed for this Mac.
1610
+ - **`finish_stop_origin` in the public stats and the request log (#414, PR
1611
+ #426 by @atirna).** A stop's commit path is diagnosable from the log alone.
1612
+ - **`/health` carries `qwen4_install_reports`.** The stage-3 kernel report,
1613
+ the rope glue's per-item verdicts and the n-gram pre-read plan, so a
1614
+ default's engagement is readable without the serve log.
1615
+ - **`--cors-origin` / `MTPLX_CORS_ORIGINS`.** Browser pages MTPLX serves
1616
+ itself are always allowed; a browser front-end on another origin now
1617
+ needs its origin listed here (repeatable flag, or a comma-separated
1618
+ env var the daemon inherits from `mtplx start`). `/admin` and the
1619
+ sign-in routes stay same-origin only regardless. See the CORS entry
1620
+ under Fixed.
1621
+ - **`mtplx doctor` reports the Hugging Face token `mtplx pull` will use**
1622
+ (`hugging face token: from HF_TOKEN` / `` from `hf auth login` `` /
1623
+ `none`), and the JSON report carries `token_source`, `token_used_by_pull`
1624
+ and `token_policy`.
1625
+
1626
+ ### Fixed
1627
+
1628
+ - **The pre-prefill memory guard no longer evicts a session's own
1629
+ restorable entry.** The 2.10.2 guard (#415) estimated a prompt's
1630
+ reusable prefix by exact match only. A follow-up turn the session bank
1631
+ serves by block prefix, such as the turn after a forced tool call, read
1632
+ as a full miss near the memory line and the guard cleared the session as
1633
+ superseded before the restore ran: on a 128 GB Mac serving Flash-Next, a
1634
+ 41,901-token turn re-prefilled cold in 54 s with a 41,391-token restore
1635
+ available. The guard now asks the bank the same question the restore
1636
+ does (`SessionBank.longest_shared_prefix_tokens`, block-aligned) and pins
1637
+ such entries; its receipt carries `reusable_prefix_mode`. Found by the
1638
+ release script's agent-session gate.
1639
+
1640
+ - **Agent sessions on Flash-Next slowed to ~20 tok/s and stalled 5-8 s before
1641
+ every tool turn.** A 43k-token OpenCode session measured on the live daemon
1642
+ lost 146 s of a 14-minute task to three engine defects, none of them decode
1643
+ (the decode rounds ran at 65-70 tok/s throughout; the "21 tok/s" was dead
1644
+ time divided into 200-token tool turns):
1645
+ - Every request started the n-gram first-chunk gather at arrival
1646
+ (`MTPLX_QWEN4_PLE_FIRST_GATHER_EARLY`, on by default since the #391 port)
1647
+ and chained a page-warm of the *rest of the prompt's* rows — on warm
1648
+ bank turns whose prefill is 20-400 tokens, all of it waste (650k rows at
1649
+ 43k), and the owner thread then blocked on the gather at scope exit:
1650
+ 5-7.5 s per turn once memory pressure had evicted the table, charged to
1651
+ nothing in the receipt. The gather is declined when a RAM bank entry can
1652
+ already serve past the first chunk, and an unconsumed gather is never
1653
+ waited for. Warm-turn dead time 5-7.5 s → 0.01 s; the memory-pressure
1654
+ notices went with the page-warm storm.
1655
+ - The bank hydrated its own SSD twin on every warm turn (0.6 s each): the
1656
+ exact-prefix entry was excluded from the "RAM already serves this" bar,
1657
+ so the bar read 0 and the cold row was decoded unread.
1658
+ - The generation-final snapshot of a tool-call turn was refused on
1659
+ every OpenCode turn in the daemon's life (83/83 went to the retokenizing
1660
+ GPU re-prefill): tool arguments are not byte-stable through parse →
1661
+ client → re-render (a file ending in `\n` came back one token short),
1662
+ and a turn ending in `tool_calls` never advanced the session's committed
1663
+ stream, so the committed-think substitution could not apply to the
1664
+ turns that needed it. The committed post-think body (text + tool-call
1665
+ markup as generated) is now substituted into the re-render when the
1666
+ turn's calls match, and `tool_calls` finishes commit like `stop`. Tool
1667
+ rounds bank in O(1) and the next turn restores the whole turn (measured:
1668
+ write-turn follow-up 3,535 tokens re-prefilled / 3.7 s → 20 tokens /
1669
+ 0.12 s).
1670
+ - A pending postcommit that was still prefilling when the 30 s bound
1671
+ expired was aborted and the request re-prefilled the same tokens from
1672
+ scratch (30 s waited + 38 s re-prefill after a 25k-token turn). The
1673
+ wait now extends while the job heartbeats at chunk boundaries and
1674
+ abandons only a silent job (`MTPLX_POSTCOMMIT_WAIT_STALL_S`, 15 s;
1675
+ ceiling `MTPLX_POSTCOMMIT_WAIT_CEILING_S`, 600 s).
1676
+ - A forced `tool_choice` rewrote the system contract and the bank
1677
+ identity, so one forced round re-prefilled a 41k session cold twice
1678
+ (41 s + 44 s). The clause now rides a transient trailing user turn like
1679
+ every other per-request steering text, and the bank fingerprint no
1680
+ longer carries tail-only contracts (forced choice, post-tool answer,
1681
+ read-only force-answer, Pi convergence): a transition round keeps the
1682
+ session's prefix. Forced round at 41k: 0.58 s.
1683
+ - A cold prompt of ~30k+ tokens could end with `finish_reason: "error"`
1684
+ after its whole prefill: the PLE lookahead's engagement verdict, a
1685
+ benchmark-arm assertion, raised inside a user request when the sidecar
1686
+ declined a low-entropy first chunk. Serving records the verdict
1687
+ (counter, `last_scope_status`, one warning); `MTPLX_QWEN4_PLE_PREFILL_
1688
+ LOOKAHEAD_STRICT=1` keeps the raise for measurement arms.
1689
+ Receipts: `scripts/agent_session_gate.py` (new release gate, below) at 40k
1690
+ passes end to end — warm turns 0.15 s TTFT / 0.01 s dead time / 66-84
1691
+ tok/s, auto tool round banked in O(1), forced round warm. OpenCode CLI,
1692
+ same task, bank on vs off: aggregate decode 87.1 vs 83.8 tok/s (the bank
1693
+ does not touch decode), total TTFT 4.5 s vs 14.0 s over three turns.
1694
+ - **A second of dead air before every reply after a short pause.** macOS
1695
+ drops an idle process's GPU residency about 2.5 s after its last Metal
1696
+ command and rebuilds it on the next one at ~9 ms per GiB, so on
1697
+ Flash-Next (77 GiB of weights) the first prefill after any pause of a
1698
+ few seconds — every chat turn, every agent tool-call round trip — paid
1699
+ ~0.75-1.0 s before the first token (measured: a "hi" 1.17 s from the
1700
+ app, 0.08 s back-to-back). The engine now keeps its working set
1701
+ resident with a sub-millisecond kernel on the model queue once per
1702
+ second while it is attentive (a request completed in the last 10
1703
+ minutes, `MTPLX_GPU_KEEPALIVE_ATTENTIVE_S`), then parks. Same "hi"
1704
+ after 3-90 s of quiet: server time-to-first-token 0.083 s, first text
1705
+ on screen in the app 0.26 s (was 0.87 s). `/health` reports
1706
+ `gpu_keepalive`; every request record carries whether it started warm.
1707
+ `MTPLX_GPU_KEEPALIVE=0` disables it.
1708
+ - **Replies typed in lurches on Flash-Next follow-ups.** The engine writes
1709
+ one stream frame per committed token, so a decode round lands as two
1710
+ frames a few milliseconds apart every ~40 ms and a context-copy block
1711
+ as a burst. The app's typewriter estimated the round gap from every
1712
+ frame, the 3 ms intra-round gaps pulled it to ~19 ms, and each round
1713
+ was then revealed on the next display tick and followed by idle frames
1714
+ — text updated on 42% of frames, in chunks, at the raw round cadence.
1715
+ Frames within one display frame now count as one arrival, the estimate
1716
+ tracks the real round gap, and a round types across it (a 100-character
1717
+ copy block over four frames instead of one).
1718
+ - **Any web page could drive the local model.** The server ran CORS with a
1719
+ wildcard origin and credentials, so on the default keyless localhost bind
1720
+ a page in any tab could POST to `/v1/*`, read the answers, list and clear
1721
+ sessions under `/admin/*`, and keep the GPU busy. Browser requests are
1722
+ now same-origin by default (the page's `Origin` must match the `Host` it
1723
+ used), allowlisted origins reach the API but never `/admin` or sign-in,
1724
+ every other origin gets a 403 with no CORS headers, and requests without
1725
+ an `Origin` header (the app, OpenCode, Pi, Claude Code, Cline, curl) are
1726
+ untouched. The browser-auth cookie is `Secure` over https.
1727
+ - **The API key stays out of URLs, argv and the Logs pane.** Opening the
1728
+ browser dashboard from the app used `/mtplx/browser-auth?mtplx_api_key=…`,
1729
+ so the key landed in browser history; the app now asks the daemon for a
1730
+ single-use 60 s ticket (`POST /mtplx/browser-auth/ticket`) and opens the
1731
+ URL it returns, falling back to the old form only when a daemon predates
1732
+ the route. The dashboard's own sign-in posts the key in a same-origin
1733
+ body (`POST /mtplx/browser-auth`) and a 401 shows a sign-in prompt instead
1734
+ of "Connection to MTPLX lost". The daemon reads its key from a user-only
1735
+ (0600) file under Application Support instead of `--api-key <key>` on
1736
+ argv, and the launched command line the app logs masks every `*-key`,
1737
+ `*-token`, `*-secret` and `*-password` value.
1738
+ - **Play, Restart and first launch no longer wait on mtplx.com.** Every
1739
+ daemon launch awaited the release manifest on a 60 s timeout; offline,
1740
+ firewalled and captive-portal Macs sat in "Launching" for a minute per
1741
+ Play. The runtime decision is local now, the manifest fetch is bounded to
1742
+ 3 s and refreshes the About card in the background, and first-run
1743
+ onboarding is decided from the saved settings alone.
1744
+ - **The model-pack Update button no longer freezes the window** while it
1745
+ resolves the runtime, walks the pack and spawns the CLI; that work runs
1746
+ off the main actor.
1747
+ - **A settings file with one bad value keeps every other setting.** One
1748
+ wrong-typed field in `settings.json` (a hand edit, a downgrade) used to
1749
+ make the whole file undecodable, re-run onboarding, and overwrite custom
1750
+ models, the API key and tuned records with defaults. Bad fields now fall
1751
+ back individually (logged with their path), a malformed custom model or
1752
+ tune record is skipped while its siblings load, and a file that cannot
1753
+ be read at all is kept beside itself as `settings.json.unreadable-<stamp>`
1754
+ with a banner and Reveal in Finder.
1755
+ - **An unopenable chat store is kept, not silently swapped for memory.**
1756
+ `chats.store` and its `-wal`/`-shm` sidecars are renamed beside
1757
+ themselves, a fresh store starts, and the sidebar says so with Reveal in
1758
+ Finder; only if no store can be created does the session run in memory,
1759
+ and then the sidebar says that too. The chat models carry a versioned
1760
+ schema so the next model change has a migration path.
1761
+ - **Unsaved Settings edits survive switching tabs.** The draft lives above
1762
+ the tab now; an "Unsaved changes" row offers Save / Apply + Restart and
1763
+ Revert. Mode still saves on pick.
1764
+ - **A failed reply shows the server's error and offers Retry.** The
1765
+ daemon's `finish_reason: "error"` frame (memory guard, context overflow,
1766
+ tool-loop exception) was decoded as an ordinary finish, so the turn read
1767
+ "Interrupted reply" or "No visible answer generated." with no message and
1768
+ no Retry. The message is shown, persisted with the turn, and labels the
1769
+ settled bubble "Failed: <message>".
1770
+ - **A reply the daemon never finished is filed as incomplete.** A stream
1771
+ that ended without its terminal chunk (process death, dropped
1772
+ connection) was persisted as a complete answer with `finish_reason:
1773
+ "stop"`. It is now `"incomplete"`, shown as interrupted, with Retry.
1774
+ - **Chats are auto-titled in every language.** The title guard compared
1775
+ against the English literal "New Chat", so non-English users kept the
1776
+ placeholder forever; rows the old guard left untitled are named at
1777
+ launch from their first message.
1778
+ - **A failed web search or fetch is recorded as a failure.** Offline or
1779
+ blocked providers came back as an empty result set marked success, the
1780
+ model was told "No results", and a failed fetch still added a source.
1781
+ Failures are marked on the live strip and the persisted trace, the model
1782
+ is told the tool failed, and no phantom source is added.
1783
+ - **Esc on the chat surface does one thing.** It stops a streaming reply,
1784
+ otherwise closes the chat; Stop Generating in the menu moves to ⌘⇧. so
1785
+ two Esc bindings no longer race.
1786
+ - **Attaching a file no longer freezes the app.** Extraction (PDF page
1787
+ walk, docx unzip, image decode) runs off the main actor with a per-card
1788
+ spinner; a file that cannot be read stays on the strip with the reason
1789
+ instead of vanishing. New caps: 500 PDF pages and 200,000 characters per
1790
+ attachment, noted on the card and in the text the model sees.
1791
+ - **A badly typed value in `~/.mtplx/config.toml` no longer bricks every
1792
+ command.** `context_window = "64k"` made `status`, `list`, `config show`
1793
+ and everything else exit with a traceback; that one key now falls back
1794
+ with a one-line warning naming it, and `mtplx config set` refuses a bad
1795
+ value plainly without writing.
1796
+ - **Ctrl-C at a prompt exits quietly** with status 130 instead of a
1797
+ traceback.
1798
+ - **`mtplx bench` works from any directory.** Prompt suites resolve inside
1799
+ the installed package; a bare `mtplx bench` lists its actions.
1800
+ - **Prose that quotes `[Calling tool:` no longer swallows the answer.** The
1801
+ streaming filter held everything after the marker until a `]` arrived and
1802
+ then dropped it at finish; the hold is now bounded by the call's own
1803
+ grammar, so real calls (including multi-line JSON arguments) are still
1804
+ hidden and ordinary text streams through.
1805
+ - **The attached terminal chat gives up on a daemon that stops
1806
+ responding** (5 s to connect, 120 s with nothing on the wire) with a
1807
+ plain error and exit 1 instead of hanging forever.
1808
+ - **One Hugging Face token policy.** `mtplx pull`, update checks, `inspect`
1809
+ and `doctor` agree: `HF_TOKEN`/`HUGGING_FACE_HUB_TOKEN`, then the token
1810
+ stored by `hf auth login`, else anonymous; a stored token the Hub refuses
1811
+ is retried anonymously so a stale login never breaks a public pull. Pulls
1812
+ previously ignored the login token that `doctor` reported as present.
1813
+ - **Passwordless-sudo setup validates the rule before installing it.** The
1814
+ rule is character-checked (an unescaped space silently turned the rest of
1815
+ a path into an argument, and visudo accepted it), parsed by `visudo -c -f`
1816
+ on a temp file, and installed by one privileged script that names the
1817
+ `thermalforge` binary fan control actually runs (`~/.mtplx/bin`), not a
1818
+ PATH copy; a missing binary is reported plainly; every sudo call is
1819
+ bounded and a no-tty session says to run `mtplx max --grant-sudo` in a
1820
+ terminal.
1821
+ - **The fan-restore sidecar clears its marker only after the fans verifiably
1822
+ return to auto**, after the socket path and the sudo fallback alike, and
1823
+ writes what it did to `~/.mtplx/logs/thermal-sidecar.log`.
1824
+ - **`mtplx models --update` never removes a live file before the new pack
1825
+ is complete.** Changed files are set aside as `.stale`, removed only after
1826
+ a complete download, and restored on any failure or interruption; a kill
1827
+ mid-update is recovered on the next run.
1828
+ - **The curl installer no longer writes into Homebrew's bin.** It used to
1829
+ `cp` over the `mtplx` symlink there, replacing the Cellar program. The
1830
+ global launcher is opt-in (`MTPLX_GLOBAL_BIN`), a launcher this installer
1831
+ did not create is never replaced, and the same rule covers the legacy
1832
+ preview installer.
1833
+ - **An `opencode.json` or Pi `models.json` MTPLX cannot read is left
1834
+ alone.** Both apps accept JSONC (comments, trailing commas), and MTPLX
1835
+ now reads them the same way and merges; a file that still does not parse
1836
+ is left untouched with a message naming the file and position instead of
1837
+ being moved aside and replaced. A rewrite keeps the previous file as
1838
+ `<name>.before-mtplx-<stamp>.bak` and `connect` prints where it wrote.
1839
+ - **First-run routing refuses what cannot run.** An Intel Mac was told
1840
+ "selected because this is not Apple Silicon" and handed a 27B download;
1841
+ a Mac whose memory could not be read got the 27B; every Mac under 32 GiB
1842
+ got the 9B. Intel and too-small Macs now get one plain sentence and exit
1843
+ 1 before any download, unreadable memory selects the smallest pack and
1844
+ says why, and under 32 GiB the CLI names the same pack the app's picker
1845
+ lists first (the 9B from 16 GiB, the 4B below that).
1846
+ - **The browser dashboard:** the Speculative tab's hard-coded vLLM
1847
+ comparison is gone; settings sliders send only the keys you moved and
1848
+ follow the server otherwise; the live TPS gauge goes idle when the server
1849
+ does instead of pinning the last request's speed.
1850
+ - **A resumed download can no longer splice a new commit's tail onto a
1851
+ stale partial.** `mtplx pull`, and the app's downloader behind it,
1852
+ range-resumed any `*.incomplete` partial next to the target whichever
1853
+ commit had written it and accepted the result on size alone, so a pack
1854
+ repaired in place on the Hub could come back as a corrupt file that
1855
+ loaded. A transfer marker now records the blob every file is fetched
1856
+ from: a partial whose blob changed, or one nothing vouches for, is
1857
+ discarded; a landed file whose recorded blob changed is refetched; and
1858
+ every LFS file is hashed as it lands against the sha256 the Hub
1859
+ publishes, so a mismatch discards the partial with a plain message
1860
+ instead of installing it. The progress figures are unchanged.
1861
+ - **Published packs no longer name the machine that forged them.**
1862
+ `mtplx forge` stamped the local trunk directory it was pointed at into
1863
+ `mtplx_runtime.json` (`base_trunk`, `forge_provenance.source_repo`, the
1864
+ forge inputs), and the scrubber written to prevent that had no caller,
1865
+ so the flagship 27B packs carried a home directory. The stamper names a
1866
+ local trunk by its Hub identity (the pull marker's repo id and commit,
1867
+ or the `owner--name` cache layout), `forge publish` uploads scrubbed
1868
+ copies of every top-level JSON document that carries a local path, and
1869
+ `mtplx model publish-check` gains a `no_local_paths` gate with
1870
+ `--scrub` to rewrite staged documents in place. The scrubber itself no
1871
+ longer mistakes every string that starts with a slash for a path.
1872
+ - **The browser dashboard's Thermal tab no longer shows an internal
1873
+ benchmarking rule.** On a default install (thermal polling off) every
1874
+ generation raised a banner saying that per the project's Universal
1875
+ Thermal Rule model work should run under verified max-fan mode. The
1876
+ banner is gone; the fan panel keeps its note about
1877
+ `--enable-thermal-poll`.
1878
+ - **macOS 27 no longer answers long prompts with a 500 (#404, #405, #407).**
1879
+ The macOS 27 Metal Performance Primitives header rejects the
1880
+ address-space-qualified cooperative-tensor operands the QSA sparse
1881
+ prefill kernels and the 27B flash-decoding route used, so 2.10.x on the
1882
+ 27 betas failed every prompt past the ~32K sparse-prefill crossover
1883
+ mid-request ("Unable to build metal library from source"). The seven
1884
+ kernel sites use the address-space-neutral operand types, and a startup
1885
+ probe dispatches the real sparse-prefill pipeline once, degrading to
1886
+ dense prefill with a diagnostic on an SDK that refuses it. Credit
1887
+ @mrmurphy (first patch), @sunnybluesea (root cause, three-site sweep,
1888
+ macOS 27 receipts), reporters @DigiJoe79 and @rameshn007.
1889
+ - **An explicit `MTPLX_MEMORY_LIMIT_BYTES` is the engine budget both ways
1890
+ (#443 @yermakovm).** The planner clamped the configured Metal limit under
1891
+ its own 75% envelope, so a 96 GB Mac serving the 69.2 GiB Flash-Next
1892
+ Bare-Speed pack was refused as "does not fit" (72 GiB budget against a
1893
+ 73.3 GiB minimum) even under a limit of 80G that runs it. An operator-set
1894
+ limit now plans the engine budget up to the machine's RAM, the banner
1895
+ says "(Metal limit)", and the "does not fit" note names the lever. A
1896
+ `--memory-budget` below the machine still wins, so a simulated smaller
1897
+ seat stays small.
1898
+ - **Flash-Next no longer 500s under concurrency in the `ar_batch` lane (#420).**
1899
+ mlx-lm's batch generator merges every batched prompt's caches and the
1900
+ family's QSA cache has no merge, so two concurrent requests under
1901
+ `--scheduler-mode ar_batch` (the app's lane) raised an unhandled
1902
+ `ValueError` while sequential requests were fine. The lane now probes
1903
+ the model's cache family at startup, says so in the banner and in
1904
+ `/health` (`scheduler.ar_batch_unavailable_reason`), and serves
1905
+ concurrent requests one at a time on the solo lane instead. The 27B
1906
+ keeps batching. `MTPLX_AR_BATCH_CACHE_PROBE=0` skips the probe.
1907
+ - **A client-capped one-token answer is diagnosable from the serve log
1908
+ (#436).** Pi caps `max_tokens` at its model `contextWindow` minus a
1909
+ chars/4 estimate of the transcript, and sends `max_tokens=1` when the
1910
+ estimate overflows; the answer then stops after one token, often inside
1911
+ a tool call, and Pi reports a truncated response. The server now logs
1912
+ one WARNING naming the client's cap, and the `mtplx_openai_generation`
1913
+ trace line carries `max_tokens`, `effective_max_tokens` and
1914
+ `finish_reason`.
1915
+ - **Anthropic `/v1/messages` image blocks reach the vision tower (#441).**
1916
+ `image` blocks (base64 or URL, including ones nested inside a
1917
+ `tool_result`) become image parts instead of base64 prose, so Claude
1918
+ Code's pasted screenshots and its Read tool on image files are seen
1919
+ instead of hallucinated at 13x to 52x the token cost. Text-only
1920
+ requests render byte-identically; `count_tokens` counts the vision
1921
+ placeholder the way the chat path does.
1922
+ - **Copy-lane streaming no longer pastes and freezes.** The server
1923
+ releases block-sized token batches piece by piece across the round
1924
+ (`MTPLX_STREAM_PACER`, default on; StreamScope copy-lane arm burst p95
1925
+ 58 to 9 characters), and the app's typewriter rates arrivals over a
1926
+ wall-clock window and types each block across its round
1927
+ (95th-percentile flush 65 to 25 characters, longest on-screen pause
1928
+ 767 to 233 ms on the same flow).
1929
+ - **Fully accepted copy blocks skip the recurrent-state replay** in the
1930
+ Flash-Next family commit; the Route Tape records every copy-lane round.
1931
+ - **`reasoning_effort` accepts the off-ish values some clients send**
1932
+ (PR #433 by @sypsyp97).
1933
+ - **App CPU:** the decode chip publishes only on change, metrics
1934
+ snapshots slow to 500 ms while a turn streams, and the redundant
1935
+ auto-scroll task is gone.
1936
+ - **The chat composer scrolls past ten lines instead of jumping to the top
1937
+ (#424, PR #437 by @MohammedThowfiq).**
1938
+ - **The Performance mode survives a model restart and the launch logs the
1939
+ effective scheduling (#398 @variablefate).** The picker is a saved global;
1940
+ Settings shows "Running now: ..." and the log pane carries the
1941
+ `--scheduler-mode` / `--batching-preset` the daemon launched with.
1942
+ - **A small request from another session no longer kills a marathon
1943
+ postcommit (#432 @nomishbhardwaj).** `MTPLX_POSTCOMMIT_MARATHON_PROTECT_TOKENS`
1944
+ reaches the cross-session abort with a bounded grace (one 30 s window per
1945
+ landed commit, keyed to the session), and the pending record is seeded
1946
+ from the committed frontier at arm time.
1947
+ - **A draining MTPLX is not mistaken for a foreign process on its port, and a
1948
+ configured port never moves to +1 on a transient occupant (#409 @kmei3560;
1949
+ CLI lane; the app's own port handling is unchanged in this release).**
1950
+ - **A streamed response closes within 30 s of its last token when another
1951
+ request's prefill is queued ahead of its session snapshot (#425
1952
+ @66duke66).** `MTPLX_STREAM_COMMIT_WAIT_MAX_S` bounds the post-generation
1953
+ commit wait; the snapshot lands in the background and the next turn waits
1954
+ for it through the pending-postcommit path.
1955
+ - **The compiled fixed-M4 lane no longer skips long prompts because the
1956
+ allocator cache looked like live memory.** After a 100k prefill on a 128 GB
1957
+ Mac the gate read the freed prefill scratch held by the allocator as live
1958
+ and fell back to the plain verify; it now releases that cache when it
1959
+ stands between the request and the lane and re-reads.
1960
+ - **The download panel counts only the files the repo ships.** Progress
1961
+ was the byte count of the whole model folder, so shards from a
1962
+ superseded revision or staging leftovers from an interrupted Hugging
1963
+ Face transfer counted as downloaded: the panel read 47.38 GB of
1964
+ 18.52 GB at 100 percent while still downloading. The daemon's progress
1965
+ events, the resume decision and the disk headroom check now use the
1966
+ repo's manifest, and the app never prints past the repo size. The
1967
+ leftovers are reported instead (`stale_bytes`, `stale_files` on the
1968
+ events and in `mtplx pull`), and a stray `*.incomplete` next to landed
1969
+ weights no longer keeps a byte-complete folder "partial" through every
1970
+ Retry: only a needed file's partial with no final copy is a transfer.
1971
+
1972
+ ## [2.10.2] - 2026-09-01
1973
+
1974
+ ### Fixed
1975
+
1976
+ - **Memory refusals are now honest and proactive (#415).** Before a large
1977
+ prefill is admitted, the server projects its footprint, proactively
1978
+ clears superseded SessionBank entries, and, when the request genuinely
1979
+ cannot fit, answers a structured HTTP 507 upfront instead of letting
1980
+ the stream die mid-flight. Streamed requests that fail now emit an
1981
+ honest error receipt (`stream_error`, `error_kind`) instead of being
1982
+ logged as client cancellations.
1983
+ - **Anthropic `/v1/messages` usage no longer double-counts cached
1984
+ prefixes.** `input_tokens` now excludes `cache_read_input_tokens`
1985
+ (Anthropic's fields are disjoint; OpenAI's are cumulative), so Claude
1986
+ Code and Pi context/auto-compaction math sees true totals on session
1987
+ cache hits. Adopted from PR #417 by @amichaelblock-lgtm.
1988
+ - **Claude Code no longer times out on long prefills.** Claude Code's
1989
+ stream watchdog resets only on real message events and ignores SSE
1990
+ comments and protocol pings, so a first turn past its 300 s idle
1991
+ window (large MCP toolsets reach 137k to 165k tokens) died with
1992
+ "Stream idle timeout". The Anthropic bridge now emits prefill
1993
+ keep-alives as empty `thinking_delta` events; a measured 165k-token
1994
+ first turn on the 27B now survives a multi-minute prefill and
1995
+ completes.
1996
+ - **Compile kill-switch precedence (from PR #395 by @maceip).** An
1997
+ explicit `MTPLX_COMPILED_GDN=0` / `MTPLX_QWEN4EXP_COMPILE=0` now wins
1998
+ over profile auto-arming everywhere, including `set_ar_pipeline_mode`
1999
+ re-arming; the two flags are aliases with explicit-off precedence.
2000
+ - **Stop-cause telemetry (#414).** Generation stats now record
2001
+ `finish_stop_origin` distinguishing model EOS, stop sequences, length
2002
+ caps, and repetition stops, so early-stop reports are diagnosable from
2003
+ request logs.
2004
+ - **Source-checkout onboarding leaves the global install alone.** Running
2005
+ the app from a source checkout no longer tries to upgrade or repoint
2006
+ the user's terminal `mtplx` installation, and QA builds prefer their
2007
+ explicitly allowed source wrapper over stale app-managed or Homebrew
2008
+ runtimes (production bundle precedence unchanged). The `--kv-quant`
2009
+ help text now describes the shipped packed-quant q4 kernel.
2010
+
2011
+ ### Added
2012
+
2013
+ - **Dark lanes for measured techniques** (off by default, receipts in-tree):
2014
+ double-buffered AR decode via `mx.async_eval` (`MTPLX_ASYNC_AR=1`,
2015
+ ported from PR #396 by @maceip; measured flat at product cells) and
2016
+ M-batched fused MoE GLU verify kernels (`MTPLX_FUSED_MOE_VERIFY=1`;
2017
+ bit-identical per token, measured slower at verify widths, kept as
2018
+ wiring platform).
2019
+
2020
+ ## [2.10.1] - 2026-08-30
2021
+
2022
+ ### Added
2023
+
2024
+ - **Sparse prefill lane for Flash-Next.** Adapts the fused Metal kernels
2025
+ from PR #397 by @maceip: the model's own block scores drive a
2026
+ block-sparse FlashAttention prefill instead of full attention masks.
2027
+ On an M5 Max against 2.10.0: a 98k-token prompt processes in 114.5 s
2028
+ instead of 175.7 s with peak memory down from 91.4 to 83.0 GB, a
2029
+ 131k-token prompt measures 810 tok/s, and a 262,144-token cold prompt
2030
+ completes in 355 s at 87.4 GB peak where 2.10.0 climbed to 119 GB and
2031
+ produced nothing (#393, reported by @blackjose007-stack). Auto-enabled
2032
+ on M4/M5 generation GPUs for prompts past 32k tokens; other machines
2033
+ keep the dense path; `MTPLX_QSA_PREFILL=0` disables. The kernels also
2034
+ handle YaRN rope scaling.
2035
+ - **Image input for Flash-Next.** The packs always shipped their vision
2036
+ weights; the runtime now serves them. Native multimodal position
2037
+ encoding (M-RoPE) lands for the family, so images work in app chat,
2038
+ over the API, and in Pi (#328, reported by @nmqanh). Pi model configs
2039
+ written by an older MTPLX upgrade in place and user edits survive.
2040
+
2041
+ ### Fixed
2042
+
2043
+ - **Flash-Next loads on 96 GB Macs.** The preload memory check used
2044
+ reserve constants tuned on 128 GB machines and refused packs that
2045
+ actually fit; the reserve now scales with total RAM (#400, reported by
2046
+ @JordiPosthumus). 128 GB and larger machines are unchanged.
2047
+ - **M2/M3 GPU crash at launch.** Three Metal kernels requested
2048
+ 1,024-thread threadgroups; M2/M3 generation GPUs cap these kernels at
2049
+ 896 threads and refused them. A one-shot startup probe now routes
2050
+ those machines to the standard kernels instead of crashing (#400).
2051
+ - **Honest Flash-Next memory admission.** The plan now prices the
2052
+ family's QSA caches and prefill transients, over-window requests
2053
+ answer a structured HTTP 507 instead of wedging the machine under
2054
+ memory pressure, and the QSA cache grows geometrically instead of by
2055
+ quadratic reallocation (#393).
2056
+ - **Empty first answers with tools declared.** A first turn that ended
2057
+ entirely inside the reasoning channel returned an empty message when
2058
+ tools were present (agent clients, chat with web search). The server
2059
+ now continues the turn to a visible answer, and a failed continuation
2060
+ returns the first pass instead of an error.
2061
+ - **Greedy Turbo exactness on the 27B.** Temperature-0 verification
2062
+ routes to stock kernels, restoring token-exact agreement between
2063
+ Turbo and plain decode.
2064
+ - **App runtime self-heal.** The app proves its Python runtime imports
2065
+ before trusting it and rebuilds it from the bundled wheel on failure,
2066
+ ending the crash loop that survived app reinstalls because the broken
2067
+ runtime folder was kept.
2068
+ - **Decode speed receipts.** Session-restore time was attributed to
2069
+ decode, understating decode speed on warm long-context turns.
2070
+ Attribution is corrected; generation behavior is unchanged.
2071
+
2072
+ ## [2.10.0] - 2026-08-28
2073
+
2074
+ ### Added
2075
+
2076
+ - **Long generations hold their decode speed.** A 34k-token uncapped chat
2077
+ answer decayed from 86 to 25 tok/s inside one request because every
2078
+ long-generation guard keyed off prompt length: the draft head's committed
2079
+ history cache grew unbounded during decode, and the allocator clear-cache
2080
+ cadence never armed for short prompts (8.6 GB of MLX allocator cache in a
2081
+ single request). The history cache now resets and regrows after 16,384
2082
+ live-appended tokens (`MTPLX_MTP_HISTORY_LIVE_RESET_THRESHOLD`, keyed on
2083
+ appends so restored session seeds are never dropped; output stream
2084
+ unchanged by the verify contract), and the clear-cache cadence arms
2085
+ mid-generation when live context crosses the threshold (0.6 GB on the
2086
+ same workload with it armed).
2087
+ - **Context-copy block rounds on the batched verify lane.** Flash-Next's
2088
+ lane never had the copy mechanic, so file rewrites decoded at plain MTP
2089
+ depth even with every output token already in the prompt. A prompt n-gram
2090
+ match now proposes up to a 24-token block through the lane's normal
2091
+ verify forward with the identical probability-ratio acceptance, so
2092
+ sampling behavior is unchanged. Two-turn rewrite receipt: rewrite turn
2093
+ 87.6 tok/s vs 73.8 for the fresh build, 2,400 of 3,963 tokens from 177
2094
+ copy rounds, zero copy rounds on novel text.
2095
+ `MTPLX_CONTEXT_COPY_BATCHED=0` disables.
2096
+ - **Decay observability.** Receipts carry the context-copy counters and the
2097
+ live-reset fields; `MTPLX_DROP_EVENTS=0` launches also get a per-round
2098
+ `round_timing_ms` series. The growth-lever envs (clear-cache cadence,
2099
+ history window and live reset, verify snapshot, family capture-commit,
2100
+ drop-events) join the operator-beats-profile list.
2101
+ - **Reasoning effort works on Flash-Next, everywhere.** The `qwen4_exp`
2102
+ family now declares the same reasoning codec as the dense 27B (Qwen
2103
+ think-tag parser; effort levels `xhigh` / `medium` / `low`; modes
2104
+ auto/on/off), so `reasoning_effort` on a request is honored instead of
2105
+ silently dropped — previously the family resolved "no levels", the
2106
+ field was discarded before it was read, and the chat template's own
2107
+ `xhigh` fallback burned thousands of thinking tokens with no way to
2108
+ turn it down. The family default is `xhigh`; the 27B keeps its
2109
+ measured `medium` coding default. Full surface parity in the same
2110
+ pass: the app's effort picker renders and persists for the family,
2111
+ `mtplx run` and `mtplx chat` gain `--reasoning-effort`, and
2112
+ `/v1/messages` (Anthropic-bridge) requests now forward the flat
2113
+ `reasoning_effort` field for every family instead of dropping it.
2114
+ - **The n-gram sidecar stops costing 30 GB of RAM on paper.** The 32 GB
2115
+ Flash-Next n-gram table streams from SSD by default, but the memory
2116
+ plan, session-bank budget, Metal floor, and the app's memory card all
2117
+ still counted it as wired weights — a 128 GB Mac printed a false
2118
+ "MODEL DOES NOT FIT", resolved a 30 GB-pessimistic context window, and
2119
+ auto-budgeted the session bank 30 GB too small. One policy now drives
2120
+ gather behavior and every accounting surface; the serve banner and the
2121
+ app's Memory Detail card say `n-gram table 29.8G streamed from SSD
2122
+ (not wired)`. A new hot-row LRU (`MTPLX_NGRAM_HOT_MB`, default 1024)
2123
+ keeps decode-sized gathers in RAM, byte-identical by construction and
2124
+ by test — measured against the previously shipping streamed default:
2125
+ +5-10% AR and +7.5-16% MTP decode, at identical memory. In the
2126
+ product default config the MTP register now meets or beats the 30
2127
+ GB-wired resident pin (+2.4%), so the wired mode remains only a
2128
+ bench pin for 160 GB+ machines (`MTPLX_NGRAM_RESIDENT=1`).
2129
+ - **Both Flash-Next packs are public on Hugging Face** —
2130
+ `Youssofal/Qwen3.8-Flash-Next-MTPLX-Bare-Speed` and
2131
+ `Youssofal/Qwen3.8-Flash-Next-MTPLX-Optimized-Speed` — and first-run
2132
+ onboarding now offers them on Macs that fit them, right behind the
2133
+ 27B trio, resolving straight to the published repos.
2134
+
2135
+ - **Flash-Next turbo, first-class.** The two Qwen 3.8 Flash-Next serve
2136
+ packs now resolve the **turbo** launch profile by default across
2137
+ `mtplx start` / `serve` / `quickstart` / the app — the same measured
2138
+ launch rule the quantized 27B/9B flagships follow. On this family turbo
2139
+ rides the qwen4_exp fast lane (pipelined AR decode, compiled GDN,
2140
+ layer-owned capture-commit, and the fused hyper-read/GDN kernels below)
2141
+ rather than the 27B NAX verify patch, which stays off for the family
2142
+ until it earns its own measured win. Both packs' canonical ids
2143
+ (`mtplx-flash-next-bare-speed`, `mtplx-flash-next-optimized-speed`)
2144
+ now resolve from HF ids, folder names, and `mtplx pull` aliases;
2145
+ derivative or renamed packs deliberately do not inherit the ids.
2146
+ - **Verify-width GDN conv kernel.** The conv + SiLU + L2-norm chain that
2147
+ speculative verify blocks previously ran as an eager op sequence now runs
2148
+ as one Metal dispatch for blocks of up to six rows, with a sliding
2149
+ in-block conv window, and stays fully compatible with the family's
2150
+ capture-commit rollback (it emits the exact rows the stash retains).
2151
+ Default-on for the family after two boot-triple A/B batteries in both
2152
+ orders (+3.1% and +2.3% MTP decode; the fused arm posted the campaign's
2153
+ best arm mean).
2154
+ - **One-dispatch GDN decode step.** The whole GatedDeltaNet decode
2155
+ step — causal conv + SiLU + L2 norm, decay/beta gates, the fp32 delta
2156
+ recurrence, and the gated-RMS output epilogue — now runs as a single
2157
+ Metal dispatch between the in/out projections, cutting a GDN layer
2158
+ from ~6 GPU sends to 3. Default-on for the family after two
2159
+ boot-triple A/B batteries in both arm orders (+1.7% and +2.2% decode,
2160
+ the confirm triple's fused arm holding the six fastest rows).
2161
+ - **Qwen 3.8 Flash-Next, day-0 native.** A new first-class model family
2162
+ (`qwen4_exp`): the 125B-A6B Qwen4-generation preview with GDN hybrid
2163
+ MoE, Qwen Sparse Attention, and the 32 GB n-gram memory sidecar —
2164
+ served by an in-tree MLX backend that is parity-exact against the
2165
+ reference implementation, with the native MTP draft head running
2166
+ through MTPLX's standard speculative lane (measured 1.6-1.7× over AR
2167
+ through the real server). Two packs: **Bare Speed** (flat 4-bit,
2168
+ fastest) and **Optimized Speed** (dynamic quant with 8-bit attention,
2169
+ higher quality). The n-gram table streams from SSD by default, so the
2170
+ packs fit 96 GB+ Macs with headroom; both appear in the app and CLI
2171
+ model pickers on machines that fit them, with the family's own serving
2172
+ contract (temperature 1.0, adaptive draft depth) applied end to end.
2173
+ - `mtplx forge verify --stamp`: records a first-load smoke baseline and
2174
+ writes the pack's `mtplx_runtime.json` in place — the step that turns a
2175
+ "family-compatible-unverified" model into a verified one — without
2176
+ rebuilding or copying the artifact. Families the tune instrument cannot
2177
+ measure (Flash-Next today) take their rows through a locally booted
2178
+ `mtplx serve`, the lane that actually applies their family contract.
2179
+ - **The CLI's first 90 seconds behave like 2026.** The terminal chat
2180
+ gets readline line editing with a persistent history
2181
+ (`~/.mtplx/history`, 1000 entries; arrow keys used to print raw
2182
+ escape sequences into the prompt); prompts pipe in
2183
+ (`echo "..." | mtplx run`, and a piped prompt into the chat entry
2184
+ answers through the same path `--prompt` uses, while an empty pipe
2185
+ keeps the non-tty refusal); `serve`/`run`/`chat` join the main help
2186
+ and `list`/`remove`/`config`/`env`/`dashboard`/`integrate` get a
2187
+ "Server and scripting" help group, with a one-line difflib
2188
+ "Did you mean" on any typo across all registered subcommands (exit
2189
+ code still 2); `mtplx ask` joins `run`/`chat` on
2190
+ `--reasoning-effort` and the chat REPL gains `/effort
2191
+ <level|status>`, live per turn like `/reasoning`; `mtplx --version`
2192
+ drops the redundant parenthetical when display and package versions
2193
+ match; and `mtplx hardware` stops printing "hardware acceleration
2194
+ confirmed: false" at humans for a field that means "not profiled"
2195
+ (JSON output unchanged everywhere).
2196
+ - Streaming endpoints (`/v1/chat/completions`, `/v1/completions`,
2197
+ `/v1/messages`) emit a `: keep-alive` SSE comment every 5 seconds
2198
+ while a stream is still silent before its first token (#358). Long
2199
+ prefills — minutes at 32k+ prompts — previously put zero liveness
2200
+ bytes on the wire, so strict client/proxy read-timeouts (Claude
2201
+ Code, Cursor, Open WebUI, nginx, cloudflared) dropped the
2202
+ connection mid-compute. SSE comments are ignored by every compliant
2203
+ parser; once tokens flow the comments stop. Disable with
2204
+ `MTPLX_SSE_HEARTBEAT=0`; tune the cadence with
2205
+ `MTPLX_SSE_HEARTBEAT_INTERVAL_S` (minimum 1s).
2206
+ - **Machine memory governor** (issue #305). MTPLX now plans its memory
2207
+ against the Mac it is actually on instead of assuming a 128 GB studio
2208
+ machine. At startup the serve banner prints the machine plan (engine
2209
+ budget, weights, resolved context window, session-bank budget); the
2210
+ default context window is the largest one whose full-window KV really
2211
+ fits (a 48 GB Mac serving the Speed model defaults to 196,608 tokens
2212
+ instead of a physically impossible 262,144 — 128 GB machines are
2213
+ unchanged), and an explicit `--context-window` above the fit still wins
2214
+ but is flagged loudly. The session bank stays idle-aggressive and
2215
+ yields dynamically as a long-context request's KV materializes, ahead
2216
+ of any swap; macOS pressure and an earlier allocator-relative signal
2217
+ drive the existing shedding guard. `/health`, the dashboard snapshot
2218
+ and the app carry `memory_plan`, guard events, and a pressure banner.
2219
+ `MTPLX_MEMORY_BUDGET=48G` reproduces a real 48 GB seat exactly (test-pinned).
2220
+ - **Streaming SSD spill for large sessions** (issues #305, #323). Sessions
2221
+ above the per-session RAM cap — exactly the 100k+-token coding-agent
2222
+ sessions whose re-prefill costs minutes — now persist to the SSD tier
2223
+ through a tensor-by-tensor streaming writer (bounded memory, same
2224
+ on-disk format), instead of silently losing durability. Live-ref-only
2225
+ sessions reach the SSD tier for the first time; every remaining skip is
2226
+ recorded, never silent — including the disk-headroom size cap
2227
+ (`min(configured cap, free_disk/4)`), which now prints one console line
2228
+ per session naming the entry size, the effective cap, and free disk
2229
+ when it refuses a spill (found live on a 4 TB disk at 28 GiB free,
2230
+ which caps the lane at ~7 GiB and mutely excluded every 100k+-token
2231
+ session — the same silence class as #278, in a brand-new lane). A
2232
+ request arriving mid-write makes the streaming encode abort cleanly
2233
+ and re-dispatch for the next idle window (counted in
2234
+ `encode_yields_foreground`), so a spill in progress can never make a
2235
+ request wait; the dedicated writer thread keeps its own 600 s
2236
+ foreground pause, where waiting is free because nothing queues behind
2237
+ that thread.
2238
+
2239
+ - **RAMP: opt-in long-block and fuzzy re-anchor policy for context copy**
2240
+ (adapting community PR #375 by @johninthewinter). A fixed 48-token copy
2241
+ block replaces the confidence ladder and an exact n-gram miss falls
2242
+ through to a mismatch-tolerant short-anchor re-match, for edit-shaped
2243
+ temperature-0 agent workloads where the author measured +45.9 to +53.9
2244
+ percent decode with byte-identical output. Our own paired temperature-1
2245
+ chat-rewrite arms measured it a net loss (copy supply fell 504 to 123
2246
+ tokens on seed-identical streams), so it ships off by default
2247
+ (`MTPLX_RAMP_ENABLED`); off is byte-for-byte the prior proposer.
2248
+ - **One-sync greedy draft read on confidence lanes** (adapting community
2249
+ PR #288 by @ArthurOstapenko). Margin-gate and adaptive-depth lanes now
2250
+ read the greedy draft token and its confidence metrics in one GPU
2251
+ synchronization instead of two; the author's paired receipt is +0.559
2252
+ percent geomean on the ExpectedValue depth-3 lane. The default greedy
2253
+ chain and every sampled-draft lane are untouched, proven by an
2254
+ engagement counter that reads zero there.
2255
+ - **QSA rows-gather lane for verify widths** (adapting the per-query
2256
+ gather and GQA head-group broadcast from community PR #380 by @maceip).
2257
+ Multi-row QSA forwards previously staged a dense [rows, context] mask
2258
+ and read the full KV in all 12 QSA layers every verify round, a cost
2259
+ that grows with the generation. The opt-in lane gathers each row's
2260
+ selected blocks plus its visible tail at a constant width instead, with
2261
+ context-length and row-count routing fences so short contexts keep the
2262
+ fused dense path. Family default ON for Flash-Next (self-fenced to
2263
+ 2..8 rows at 16384+ tokens of context, so shorter contexts are
2264
+ bit-identical dense; `MTPLX_QSA_GATHER=0` is the kill switch). Paired
2265
+ 16,384-token receipt: the dense path's verify cost grew 37.0 to 46.9 ms
2266
+ per round across the run while the gather arm held flat 45.3 to 45.9,
2267
+ finishing its last window at 64.5 tok/s against 36.0 dense.
2268
+ Parity-tested against the dense path on the real layer.
2269
+
2270
+ ### Fixed
2271
+
2272
+ - **The context-copy lane earns its block size before spending it.** Copy
2273
+ rounds verify 16-24-token candidate blocks — a ~4x-cost forward versus a
2274
+ normal round — and the acceptance gate needed four sampled rounds per
2275
+ generation before it could suspend, so short coding-agent turns re-paid
2276
+ the full misfire cost every turn (measured: whole-turn verify 60-88
2277
+ ms/round at 8k context with 21/96 copy tokens accepted, decode down to
2278
+ ~27 tok/s). Blocks now stay at 8 tokens
2279
+ (`MTPLX_CONTEXT_COPY_PROBATION_K`) until the turn's acceptance EMA
2280
+ proves the content pays, and the suspension arms one round earlier.
2281
+ Long-context re-emission — where the lane is a measured +16.7% — opens
2282
+ to full blocks by its third round and keeps its win. The batched verify
2283
+ lane (Flash-Next's copy mechanic, above) carries the identical
2284
+ probation contract.
2285
+ - **Greedy draft coupling engages on launch-default-greedy servers.** The
2286
+ draft-sampler resolver received the raw request `temperature` — `None`
2287
+ when a client omits the field — so a server launched with
2288
+ `--temperature 0` serving such a client decoded greedily while drafts
2289
+ stayed at the family default (1.0): the silent sampled-draft acceptance
2290
+ collapse ([79/65/42]% by depth vs [96/87/76]% coupled), with no
2291
+ `draft_sampler_greedy_coupled` stamp to show for it. Both serve lanes
2292
+ now hand the resolver the effective sampler temperature, matching the
2293
+ resolver's documented contract; explicit-temperature requests were
2294
+ never affected.
2295
+ - **SSD spills no longer fire mid-turn: the writer's foreground pause now
2296
+ outlasts a long coding turn.** The writer already stood down while a
2297
+ request was in flight, but its liveness bound was 60 s — shorter than a
2298
+ typical agent turn (60–620 s) — so every multi-GB session spill fired
2299
+ under the live decode: a ~1 GB/min unified-memory drumbeat that tripped
2300
+ macOS memory pressure (the Live-tab banner) and stole decode (measured
2301
+ −30% when a write overlapped a turn). The bound is now 600 s
2302
+ (`MTPLX_SSD_WRITER_FOREGROUND_PAUSE_MAX_S`); writes drain in the gaps
2303
+ between turns, and a bound that still expires into live traffic is
2304
+ counted in `/admin/cache/ssd` as `writer_pause_expired_busy`. The cold
2305
+ tier is a cache — waiting out a turn costs delayed durability, never
2306
+ correctness.
2307
+ - **The session bank yields ahead of the prefill spike, so deep turns stop
2308
+ tripping the memory banner.** The dynamic ceiling reserved a static 3 GiB
2309
+ for generation transients, but a deep chunked prefill measures up to
2310
+ 12.4 GiB of peak-over-active — so on long coding sessions the bank kept
2311
+ entries while the allocator peak kissed 99%+ of the Metal limit, firing
2312
+ the warning banner on every deep turn. The ceiling now reserves the
2313
+ spike this process has actually observed (clamped between 3 GiB and
2314
+ half the post-weights memory, at most 16 GiB), demoting idle entries
2315
+ to SSD before the next spike can slam the ceiling.
2316
+ - **The memory banner now names the culprit.** The daemon reports which
2317
+ signal produced the pressure level (`memory_pressure_source`: system-wide
2318
+ macOS pressure vs this engine's allocator near its Metal limit, plus the
2319
+ live `allocator_fraction`), and the app's warning banner distinguishes
2320
+ "System memory pressure" (another process allocating; decode can dip
2321
+ until it passes — nothing was evicted) from the engine's own
2322
+ "Memory running high". Receipt for the split: an external 26 GB
2323
+ allocation storm dropped decode 65→22 tok/s with the engine's guard
2324
+ correctly doing nothing at all — the old copy blamed the engine for
2325
+ weather it didn't make. Attribution only claims what it can prove:
2326
+ when macOS and the allocator report pressure in the same tick the
2327
+ banner names the allocator (the "another process" copy asserts the
2328
+ engine's footprint is steady, which is false at a tie), and when the
2329
+ allocator probe cannot read at all the source reports `unknown` and
2330
+ the banner stays neutral.
2331
+ - **Long sessions persist to SSD: the writer's backlog budget no longer
2332
+ rejects a snapshot bigger than itself** (#384, thanks @sapiens77 for a
2333
+ forensic-grade report). The SSD writer bounds queued bytes at 4 GiB by
2334
+ default, but a single snapshot larger than the whole budget failed
2335
+ admission on every attempt even with an empty queue — at roughly 84
2336
+ KB/token of 27B KV that turned the SSD tier silently off past ~50k
2337
+ tokens, with only a counter as the trace. An empty queue is not backlog
2338
+ pressure: a lone oversized entry is now admitted, logged by name, and
2339
+ counted in `/admin/cache/ssd` as `admitted_oversized_alone`; a genuinely
2340
+ backlogged writer still rejects. The reporter measured 41.7x TTFT across
2341
+ a restart (282 s to 6.8 s) once writes could land. Also from the same
2342
+ thread: `request_session_source` now reports the header source instead
2343
+ of null for header-identified sessions.
2344
+ - **The memory governor's ceiling never evicts the live session's cache.**
2345
+ The dynamic bank ceiling subtracts an instantaneous working-set reading,
2346
+ so a deep prefill's transient allocator spike read as a standing
2347
+ commitment: on a 93k-token coding session the ceiling walked the session
2348
+ bank to zero bytes mid-request, evicting the in-flight session's own
2349
+ prefix entries, and every following agent turn re-prefilled from scratch
2350
+ (TTFT 54 to 57 s, prefill 909 down to 175 tok/s). The ceiling now
2351
+ squeezes idle sessions only; the active session's prefix chain survives
2352
+ even when the bank stays above target. Real macOS or allocator pressure
2353
+ keeps its take-anything eviction semantics, so the 48 GB swap-death
2354
+ protection is unchanged.
2355
+ - **Flash-Next serves coding agents at its official sampler.** The app's
2356
+ OpenCode and Hermes launch presets pre-fill the Qwen3.6-era coding
2357
+ sampler (temperature 0.6), and the Flash-Next model defaults left those
2358
+ slots untouched, so the daemon booted with an explicit `--temperature
2359
+ 0.6` that suppressed the pack-stamp injection — OpenCode requests were
2360
+ normalized to a 0.6 target against the family's official 1.0, with the
2361
+ draft still at the stamp's 1.0 (a mismatched verify pair). Flash-Next
2362
+ now clears the target-preset sampler slots on every launch target, so
2363
+ the zero-flag boot path injects the artifact's stamped 1.0/0.95/20 for
2364
+ target and draft alike, identical to a bare `mtplx serve`. The dense
2365
+ 27B was already correct (its preset pins the official triple); 3.6-era
2366
+ models keep their measured 0.6 lane.
2367
+ - **OpenCode's effort picker shows the whole Flash-Next dial.** The
2368
+ generated config only declared effort variants for tiers outside
2369
+ OpenCode's built-in list, trusting the client to surface the rest — but
2370
+ OpenCode Desktop 1.18.21 does not offer its full built-in list for
2371
+ custom openai-compatible providers, so xhigh (the Flash-Next chat
2372
+ default) was missing from the picker entirely. Every family tier is now
2373
+ written as an explicit variant, which renders on every OpenCode version
2374
+ and still merges over same-named built-ins; out-of-family tiers stay
2375
+ disabled.
2376
+ - **The KV-quantization control explains itself on Flash-Next.** The
2377
+ setting showed the anonymous "not supported for this model" line because
2378
+ the family had no entry in the KV-quant policy table. The policy is
2379
+ unchanged — the validated q8/q4 paged lane is wired to the dense-27B
2380
+ attention call sites, not to Flash-Next's QSA layers — but the app now
2381
+ states the architecture truth: the hybrid design keeps KV on 12 of 48
2382
+ layers (~24 KB/token), and a quantized QSA lane has no validation
2383
+ receipts yet. Qwen 3.5/3.6/3.8 keep the full q8/q4 control.
2384
+ - **The warm ladder yields to live traffic and no longer stamps the
2385
+ traffic clock** (adapting community PR #300 by @Blakeolson21). A request
2386
+ that had arrived but not yet completed read as an idle daemon, so the
2387
+ first request of a serve could share the GPU with a warm rung; and warm
2388
+ rungs stamped `last_request_at`, making /health report user traffic on
2389
+ an untouched daemon while the ladder deferred against its own output.
2390
+ Rung admission now checks live and queued foreground work as its own
2391
+ branch (safe at zero idle grace), and warm generations no longer move
2392
+ the request clock or counters anywhere.
2393
+ - **The Live tab's acceptance panel no longer goes blank after a finished
2394
+ request.** Two stacked causes: the daemon's idle warmup ladder published
2395
+ its rungs into the dashboard's `latest` slot, replacing the finished
2396
+ request's receipt (a warmup row with a null request id sat where the
2397
+ user's acceptance counters belonged), and the app's Live-tab gates only
2398
+ ever ticked on `.completed` stream frames, which have no replay, so one
2399
+ frame missed during a reconnect kept the panel on its placeholder
2400
+ forever. Warmup rows now stay out of the dashboard ring server side, the
2401
+ app refuses to merge a warmup row over a real receipt, and a finished
2402
+ request observed through the snapshot poller counts as completion
2403
+ evidence (deduplicated per request), so the panel lights up within one
2404
+ poll of a request finishing regardless of stream health.
2405
+ - **The compact tool contract no longer drops trailing tools** (#376,
2406
+ adapting community PR #379 by @ArctifoxNL). When the "Declared tools
2407
+ and schemas" line exceeded its 1200-character budget it was raw
2408
+ byte-cut, deleting whole tool names at the tail (`task` first, in
2409
+ Claude Code-shaped toolsets) — and the contract's own "never invent an
2410
+ undeclared tool" clause then made the model treat every dropped tool
2411
+ as nonexistent, killing subagents. Over budget, every declared tool
2412
+ name is now kept and only per-tool signature detail is shed.
2413
+ - **Cancellation errors name their real cause** (#381). One shared
2414
+ per-request cancel flag is tripped by several unrelated paths — the
2415
+ `POST /v1/mtplx/cancel` endpoint, client disconnect, stop-sequence
2416
+ completion, tool-call finalization, the stall watchdog, stream
2417
+ teardown — and the terminal frame blamed every non-disconnect trip on
2418
+ the POST endpoint, framing an endpoint nobody called. The first
2419
+ origin is now recorded when the flag trips, terminal frames and the
2420
+ cancellation metric report it, and an unattributed trip says so
2421
+ instead of inventing a caller.
2422
+ - `mtplx connect opencode` now actually writes `~/.config/opencode/opencode.json`
2423
+ (merge-preserving: other providers and plugins survive). It previously built
2424
+ the config, printed the config path, and wrote nothing — so a `--model-id`
2425
+ for a newly served model never reached OpenCode's provider models map and
2426
+ runs failed with `ProviderModelNotFoundError` surfaced as "Unexpected
2427
+ server error". Found wiring Flash-Next day-0.
2428
+ - The serve daemon and tune/bench children now start Python with `-P`,
2429
+ so the directory you launch from can never shadow the installed
2430
+ runtime. Previously, running `mtplx serve` from any folder containing
2431
+ an `mtplx/` package (a source checkout, a vendored copy) silently
2432
+ served that folder's code instead of the installed release.
2433
+ - **Unexecuted tool calls are no longer silently swallowed** (#349). A
2434
+ fresh-install user asking the built-in chat about their files saw the
2435
+ model "invoke" tools (`ls`, `find`, `search_files`, `read_file`) and
2436
+ get nothing back — no output, no error — because the server deleted
2437
+ dead tool-call markup from no-tools responses (#160) and from
2438
+ undeclared-tool fallbacks without telling anyone. Suppressed calls
2439
+ now leave a short, visible notice naming the tool and stating that
2440
+ nothing ran, this chat has no file/terminal access, and a coding
2441
+ agent (Claude Code, OpenCode, Hermes) connected to MTPLX provides
2442
+ it. The model reads the same notice in its history and stops
2443
+ claiming it ran tools; the reply is never blank. Code-fenced tool
2444
+ syntax examples are untouched, and `mtplx_stats` gains
2445
+ `unexecuted_tool_call_notice` for triage. The macOS app also
2446
+ persists a truthful `tool_not_executed` result for any tool call
2447
+ that finishes past the chat's tool-round budget, so replayed
2448
+ transcripts never show the model an unanswered tool call.
2449
+ - **SSD session-cache writes no longer starve on an idle server, and
2450
+ shutdown flushes them** (issue #290). The scheduler's durability lane
2451
+ was only reachable while the idle band was completely empty, so any
2452
+ self-chaining background occupant (the background warm ladder) could
2453
+ hold SSD writes off forever — entries sat in RAM with every cold-tier
2454
+ counter at zero and vanished on restart, costing a full re-prefill.
2455
+ The server now pumps the durability lane within seconds of going
2456
+ request-idle (foreground work still always wins, and the pump disarms
2457
+ the moment a request arrives), and a plain SIGTERM/Ctrl-C gives
2458
+ pending writes a bounded best-effort flush (default 10 s,
2459
+ `MTPLX_SHUTDOWN_SSD_FLUSH_S` overrides, `0` disables) with one honest
2460
+ console line — including the write the SSD writer thread already has
2461
+ in flight, which the old shutdown killed mid-file.
2462
+ - Metal allocation failures are answered as structured
2463
+ `insufficient_memory` (HTTP 507) errors with actionable advice, after
2464
+ the engine sheds its caches — instead of anonymous `internal_error`
2465
+ 500s that left the next request to hit the same wall (issue #348 class).
2466
+ - `--memory-budget 48G` (the bare-suffix spelling MTPLX's own messages
2467
+ advertise) crashed serve startup with a raw ValueError; single-letter
2468
+ and terabyte size suffixes parse now.
2469
+ - The dashboard "RAM session cache" settings no longer invent `8G/4G`
2470
+ when nothing is configured — they report the budgets the engine
2471
+ actually resolved.
2472
+ - The update dialog's release notes are readable in dark mode (#367).
2473
+ The generated notes page now declares `color-scheme: light dark` and
2474
+ pairs each appearance with readable text colors instead of shipping a
2475
+ light-only stylesheet that Sparkle's dark update window rendered as
2476
+ near-black-on-dark. The release script and the Sparkle rehearsal kit
2477
+ render through one shared template
2478
+ (`scripts/render_release_notes.py`), so the rehearsal now shows the
2479
+ exact page users get and the two can no longer drift apart.
2480
+ Notes pages already published under mtplx.com/releases/notes/ need a
2481
+ one-time re-render and re-upload to pick this up.
2482
+ - **Reasoning history preserves by default on Flash-Next.** The
2483
+ `qwen4_exp` family ships the byte-identical Qwen 3.8 chat template and
2484
+ the same preserve-by-default trained contract, but the auto policy
2485
+ only recognized `qwen3_8` and dropped the family onto the scoped
2486
+ fallback. Every agent round's session-cache postcommit then aborted
2487
+ with `reasoning_history_scoping_mismatch` and re-prefilled the whole
2488
+ assistant turn. With preserve on, mid-session agent turns cost about
2489
+ 20 new prefill tokens at 0.11 s first token (scoped paid 300 to 1500
2490
+ tokens at 1.8 to 2.2 s, measured on the release wall-clock rig).
2491
+ - **Coding-agent lanes default to medium reasoning effort on
2492
+ Flash-Next.** OpenCode and Pi config writers resolve the family's new
2493
+ agent-lane default (codec `default_agent_effort`) instead of the chat
2494
+ default. On the identical multi-file coding task, xhigh measured
2495
+ 150.2 s wall clock against 44.2 s at medium with the same correct
2496
+ output. Chat surfaces keep xhigh; both clients' effort pickers still
2497
+ offer every level per request.
2498
+ - **The Pi provider merge owns the transport contract.** The
2499
+ user-preserving config merge kept an older MTPLX's
2500
+ `supportsReasoningEffort: false` alive across every re-sync, which
2501
+ silently killed Pi's effort dial after an upgrade. MTPLX's own
2502
+ compatibility keys now update on sync; user-added keys still survive.
2503
+ - **The app's offline settings fallback shows family truth.** With the
2504
+ engine stopped, the inference settings panel fell back to a generic
2505
+ temperature 0.6 and the label "Custom model" for Flash-Next (and could
2506
+ persist that 0.6 over the engine's 1.0). The fallback table now
2507
+ carries the family's native 1.0 / 0.95 / 20 and the proper family
2508
+ name; live-daemon state was always correct.
2509
+ - **Onboarding verifies the terminal command through the login shell.**
2510
+ The setup step graded whatever executable the app's own process PATH
2511
+ found, and a Finder-launched app never inherits the shell rc's
2512
+ `/opt/homebrew/bin` ordering, so setup could certify "up to date"
2513
+ while the user's actual terminal still ran an older Homebrew install.
2514
+ The check now asks the user's login shell which executable wins and
2515
+ grades that one.
2516
+ - **Each conversation streams on its own turn stream** (#324). Switching
2517
+ conversations mid-generation no longer cross-wires or blanks either
2518
+ turn.
2519
+ - **Explicit performance settings are honored over client-target
2520
+ defaults** (#325), and the native Chat launch target stops silently
2521
+ ignoring "Handle multiple at once".
2522
+ - **The app accepts custom Hugging Face models the engine reports as
2523
+ runnable** (#359). Install completeness is judged by the source repo's
2524
+ own manifest instead of requiring an `mtplx_runtime.json`.
2525
+ - **Forge routes official NVIDIA Nemotron-H configs** by deriving the
2526
+ MTP pattern from `mtp_layers_block_type` (#341); load no longer
2527
+ crashes with an AttributeError.
2528
+ - **`mtplx remove` is fenced to the models cache and asks first.** The
2529
+ removal path ran `rmtree` on whatever the ref resolved to: a bare
2530
+ `.` or `/` resolved to the models cache directory itself and `..` to
2531
+ the whole `~/.mtplx` home (bin, config, session bank, every model),
2532
+ deleted without a word and exit 0. A ref must now resolve to a direct
2533
+ child of the models cache or the command refuses, a tty gets a
2534
+ confirmation naming the resolved path and size, `--yes` skips it for
2535
+ scripts, and a non-tty run without `--yes` refuses with the hint.
2536
+ - **A malformed `config.toml` no longer bricks every command.** A
2537
+ truncated or hand-edited config raised a raw TOMLDecodeError through
2538
+ `status`, `doctor`, even `stop`, so the one file meant to hold
2539
+ preferences could lock the user out of the CLI entirely. The loader
2540
+ now degrades to defaults with one stderr line naming the file, the
2541
+ parse error, and `mtplx config show`; a bad value for a single key
2542
+ degrades that key only.
2543
+ - **The daemonless CLI generate lanes couple the draft greedy under a
2544
+ greedy target.** One-shot `run`, the terminal chat, and `tune` call
2545
+ the engine directly and never pass the server's draft-sampler
2546
+ resolver, so `--temperature 0` kept the pack's stamped sampled draft
2547
+ (temperature 1.0) and paid the sampled-draft acceptance collapse
2548
+ ([79/65/42]% by depth vs [96/87/76]% coupled) on exactly the lane
2549
+ outside benchmarks run. The lanes now share one coupling helper; a
2550
+ user-typed `--draft-temperature` still wins, and a spec-less lane
2551
+ already mirrored the target and is unchanged.
2552
+
2553
+ ## [2.9.3] - 2026-08-26 (internal build — never published; ships as part of 2.10.0)
2554
+
2555
+ ### Fixed
2556
+
2557
+ - **Agent turns died with a fabricated
2558
+ `request cancelled via POST /v1/mtplx/cancel`** (issues #332, #343). After
2559
+ a complete streamed tool call the server cancels its own generation to end
2560
+ the turn; when the worker's acknowledgement outlived the stream loop's
2561
+ 250 ms poll, the loop misread its own cancel as a foreign
2562
+ `POST /v1/mtplx/cancel` and killed the healthy turn with that error — no
2563
+ one ever called the endpoint. Tool-calling streams now drain until the
2564
+ worker acknowledges and end with the `tool_calls` terminal frame; a real
2565
+ cancel and a real disconnect behave as before. Deterministic regression
2566
+ test included. Only tool-calling turns were affected, which is why plain
2567
+ streaming never reproduced it.
2568
+ - **Long-context decode past 128k, improved.** Past 131,072 prompt tokens dense decode
2569
+ silently repaged into a cache layout that structurally excluded the packed
2570
+ verify kernel, collapsing speculative decode to plain-AR speed (12.0 tok/s
2571
+ at 147k on an M5 Max 128 GB). The dense-decode ceiling is now memory-aware
2572
+ (`MTPLX_SUSTAINED_DENSE_DECODE_MAX_CONTEXT=auto`, floored at the old
2573
+ 131,072 literal so smaller machines cannot regress), keeping the packed
2574
+ lane engaged (16.3 tok/s at 147k; 18.4 with copy speculation off). The
2575
+ resolver announces itself in the serve log, and an exported ceiling env
2576
+ beats the profile instead of being silently stomped back.
2577
+ - **KV-cache quantization was effectively broken and is now usable.**
2578
+ Enabling q8/q4 crashed serving at warmup; q4 additionally re-dequantized
2579
+ the entire prefix every round, and quantized caches were refused by the
2580
+ compiled verify bank. All three fixed: the crash is gone, q4 routes
2581
+ through the exact packed-quant kernel via a persistent quantized bank,
2582
+ and the verify bank promotes quantized paged caches. Measured cost at 16k
2583
+ on an M5 Max vs off: q8 ≈ −4% decode, q4 ≈ −19% (down from crash / −50%).
2584
+ Still opt-in — the remaining gap to zero-loss, and the slow quantized
2585
+ lane past the dense ceiling, are known and being worked. A pre-existing
2586
+ q4 numerics defect at head_dim 128 is fenced fail-closed (the shipped
2587
+ head_dim 256 family is exact).
2588
+ - The dynamic-offset paged verify kernel had never compiled since its
2589
+ introduction (a pointer-cast Metal bug hidden behind its own mask gate)
2590
+ and crashed at q_len > 5 once compiled; both fixed. Still opt-in
2591
+ (`MTPLX_PAGED_TAILMASK_ELIDE`) pending its serve verdict, but the crash
2592
+ class is removed.
2593
+ - Warmup failures log their full traceback instead of a one-line skip.
2594
+
2595
+ ### Changed
2596
+
2597
+ - **mlx floor raised to 0.32.2.** A clean same-wheel A/B on an M5 Max
2598
+ measured 0.32.0 → 0.32.2 at +29% decode / +41% prefill at 88k context
2599
+ (+31% at 16k); the floor converges existing installs onto that stack.
2600
+ - Every request row now records packed-route bail counters and
2601
+ paged-adapter engagement, and `MTPLX_ROUTE_DEBUG=1` prints one line per
2602
+ layer naming the attention branch taken and every gate input — the
2603
+ decode cliff hid for months because fast lanes declined silently.
2604
+
2605
+ ### Added
2606
+
2607
+ - Release-pin regression tests: the shipped profiles' fast-lane values
2608
+ (compiled-verify ceiling, packed verify kernel, dense-decode ceiling),
2609
+ operator-env-beats-profile precedence, and the mlx floor are now pinned
2610
+ by tests so a silent lane loss fails CI instead of surfacing in a
2611
+ benchmark weeks later.
2612
+ - Experimental, off by default: `--scheduler-mode hyper` (single-user
2613
+ manufactured-concurrency chassis, trajectory-sha-exact vs serial),
2614
+ `MTPLX_NAX_TILE_ROUTE` (first M5 tensor-unit attention kernel at decode
2615
+ shapes), `MTPLX_ADAPTIVE_DTEMP`, `MTPLX_CCOPY_BANK_ROUTE`,
2616
+ `MTPLX_FORKEV_TELEMETRY`.
2617
+
2618
+ ## [2.9.2] - 2026-08-25
2619
+
2620
+ ### Changed
2621
+
2622
+ - **The serving endpoints are passthrough by default** (#282). No
2623
+ tool-result compaction, no read trimming, no injected steering text
2624
+ unless a rewrite feature is explicitly enabled.
2625
+ `MTPLX_AGENT_REWRITES` is the master switch; each feature arms only
2626
+ via its own environment variable. The macOS app stopped exporting
2627
+ the legacy compaction settings when launching coding agents, and the
2628
+ request log records what was and was not rewritten per request.
2629
+ - Managed client configs respect user edits: `mtplx start` and the app
2630
+ only update files they wrote themselves (#282).
2631
+ - **Chained greedy drafting is on by default for temperature 0
2632
+ requests below 12,288 prompt tokens** (#313, #315, #318). Gated A/B
2633
+ runs on an M5 Max measured +2.5 to +9.8 percent decode on 0.5k to 8k
2634
+ prompts and -2.9/-2.7 percent at 16k/32k, so a context fence keeps
2635
+ it off at and above 12,288. Tune with
2636
+ `MTPLX_GREEDY_TRIO_MAX_CONTEXT`, disable with
2637
+ `MTPLX_GREEDY_DRAFT_CHAIN=off`. Sampled requests are untouched.
2638
+
2639
+ ### Fixed
2640
+
2641
+ - The forge decides the MTP norm convention once per tensor set
2642
+ instead of blind-shifting three norm tensors by +1.0 (#301); packs
2643
+ from absolute-encoded sources no longer ship with draft acceptance
2644
+ collapsed to 0 to 2 percent. The runtime refuses to load a
2645
+ double-shifted trunk with a clear error (#306).
2646
+ - Images survive user-message canonicalization on consecutive or
2647
+ retried turns (#327); vision rows survive near-prefix cache
2648
+ restores (#296).
2649
+ - The literal-repetition stop covers width 2+ batched MTP cohorts
2650
+ (#311).
2651
+ - The default request JSONL is content-free as documented (#326).
2652
+ - The installer and app write the PATH line through a symlinked
2653
+ `~/.zshrc` instead of replacing the symlink (#292).
2654
+ - The dashboard Hardware card reports the real chip (#329).
2655
+ - `bench --harness depth-sweep` honors `--depths`, `--seed`, and
2656
+ `--generation-mode`, and refuses `--stock-ar` loudly (#285).
2657
+ - The fp16 fused add+rmsnorm kernel uses the exact 1024-lane
2658
+ dispatch (#319); the packed-concats exactness gate tests its claim
2659
+ honestly (#320).
2660
+ - The NAX turbo verify path stops using padded M=5 lanes that measured
2661
+ slower than stock.
2662
+ - The flight recorder samples non-streaming requests.
2663
+ - The app renders `<br>` variants inside markdown table cells
2664
+ (PR #273 by @El-Patronum).
2665
+ - `quantize: false` module overrides are honored (PR #281 by
2666
+ @shiftedx).
2667
+ - Capture tooling persists exact completion token ids on all three
2668
+ lanes (PR #330 by @CharliePetch).
2669
+
2670
+ ### Added
2671
+
2672
+ - Experimental, off by default: `MTPLX_FUSE_PROJ` load-time projection
2673
+ fusion (port of PR #316 by @grzracz), `MTPLX_VK_CROSSROW` crossrow
2674
+ wide-verify kernel, draft-confidence tracing with confidence-gated
2675
+ draft width, and marathon postcommit protection.
2676
+ - `HISTORY.md`: the dated record of putting native MTP on Apple
2677
+ Silicon.
2678
+
2679
+ ## [2.9.1] - 2026-08-22
2680
+
2681
+ ### Fixed
2682
+
2683
+ - **Agent sessions could silently truncate and then crash near 19,000
2684
+ tokens** (#310). Paged-KV capacity now derives from the pages actually
2685
+ allocated; long coding sessions run to the model's full advertised
2686
+ context.
2687
+ - **Shutdown segfault** (#303). The daemon parks its model-owner thread
2688
+ and clears MLX streams at exit; quit and restart are clean.
2689
+ - **Turbo applied its full configuration.** One turbo fast-path flag
2690
+ shipped runtime-dead in 2.9.0. The fast-path env is now a single
2691
+ shared block, `/health` reports exactly what the profile set, and a
2692
+ per-lane kernel selfcheck runs at startup.
2693
+ - **Multi-turn cache reuse on agent lanes.** One tokenization policy
2694
+ across all encode paths (no more cache walls at reasoning
2695
+ boundaries), tool-call turns bank their generated output directly
2696
+ from live KV, and interrupted background commits retry.
2697
+ - **Long sessions stop re-deriving prior reasoning.** Client-echoed
2698
+ reasoning is rendered for turns the committed cache has not covered,
2699
+ ending marathon re-thinks of already-derived plans.
2700
+ - Stamped pack draft-sampler settings win over stale client-side pins.
2701
+ - Model pack updates resolve the exact installed pack directory, so a
2702
+ stale legacy bare-name cache no longer shadows the canonical copy
2703
+ into a zero-byte no-op, and update progress reports cumulative bytes
2704
+ (first pip/brew release with the fixes app users received in the
2705
+ 2.9.0 updater hotfix, build 2009001).
2706
+
2707
+ ### Changed
2708
+
2709
+ - **OpenCode** runs uncapped by default (the managed plugin strips
2710
+ exactly the injected 32,000 ceiling; explicit caps pass through),
2711
+ with reasoning, effort selection, reasoning round-trip, and session
2712
+ cache identity honored end to end.
2713
+ - **Pi** gets a working reasoning-effort picker, a real advertised
2714
+ output ceiling (instead of Pi's silent 16,384 default), and a managed
2715
+ extension for cap hygiene and session identity — written identically
2716
+ by the app and `mtplx start pi`.
2717
+ - **Hermes** requests carry client identity and configured reasoning
2718
+ effort, and the server strips Hermes's injected 65,536 default cap.
2719
+ - `mtplx doctor` reports advertised output ceilings and the actual
2720
+ configured port for agent lanes.
2721
+
2722
+ ### Added
2723
+
2724
+ - **Flight recorder**: per-second per-request telemetry (tok/s, context,
2725
+ speculative acceptance by depth, verify/draft time split, outcome —
2726
+ cancelled and disconnected requests included) as local JSONL under
2727
+ `~/.mtplx/metrics`, rotation-capped at 256 MB. Disable with
2728
+ `MTPLX_FLIGHT_RECORDER=off`.
2729
+ - `GET /v1/mtplx/flight`: live phase, tok/s, acceptance, stall age, and
2730
+ generated-text tail for the request in flight.
2731
+ - `mtplx trace`: session timelines joined to OpenCode history,
2732
+ cache-reuse analysis, automatic pathology flags, repetition
2733
+ autopsies, and per-session HTML reports.
2734
+
2735
+ ## [2.9.0] - 2026-08-20
2736
+
2737
+ See the release notes: <https://mtplx.com/releases/notes/v2.9.0.html>.
2738
+
2739
+ ## [2.8.3] - 2026-08-18
2740
+
2741
+ ### Fixed
2742
+
2743
+ - **Streaming stays smooth while you actually touch the app.** The
2744
+ freeze-then-burst stutter that only appeared when a human was
2745
+ scrolling or moving the mouse — and never under hands-off testing —
2746
+ is fixed. The window-measurement guard ran in a run-loop phase that
2747
+ macOS skips while input events keep arriving, so precisely when you
2748
+ interacted, every layout pass re-measured the whole conversation and
2749
+ screen updates coalesced into bursts. The guard now runs on every
2750
+ run-loop turn, input storms included. Measured on the same machine,
2751
+ build, and prompt with synthesized human input: 40 s of continuous
2752
+ wheel-scrolling went from 70 UI stalls (18.7 s frozen, worst 1.3 s)
2753
+ to one 197 ms stall; 30 s of mouse movement over the transcript went
2754
+ from 91 stalls (26.7 s frozen) to zero.
2755
+ - **Scrolling up mid-generation no longer fights you.** The
2756
+ auto-follow used to yank the view back to the bottom against your
2757
+ fingers: its user-scroll signal was set asynchronously (the
2758
+ synchronous bottom-pin raced it), trackpad momentum ran unguarded,
2759
+ and classic wheel mice never registered as scrolling at all. User
2760
+ scrolling now wins immediately and in every form — pin attempts are
2761
+ refused while you scroll, momentum is covered, and scrolling back to
2762
+ the bottom re-engages following, matching how it already behaved for
2763
+ slow trackpad drags.
2764
+ - **Cancelled generations now leave full telemetry.** Stopping a reply
2765
+ mid-stream previously logged a stub record with no stream-smoothness
2766
+ census — the exact runs users complain about were the ones with no
2767
+ data. Cancelled requests now record the producer gap census, sliding
2768
+ throughput windows, and true decode rate for the streamed portion.
2769
+ - **Fixed a quadratic decode-cost path on whitespace-free content.**
2770
+ The incremental detokenizer's force-flush escape (tables, URLs,
2771
+ minified code) could never trim its token cache, so every new token
2772
+ re-decoded a growing buffer. The cache now stays bounded on
2773
+ arbitrarily long runs.
2774
+
2775
+ - **The transcript can no longer go blank mid-generation.** The first
2776
+ 2.8.3 candidate swapped the chat transcript to a lazy stack for a
2777
+ layout-cost fix; under fast streaming with the app's own scroll
2778
+ driver, the lazy container intermittently culled every visible row —
2779
+ flicker escalating to an entirely empty chat while the engine kept
2780
+ streaming. The transcript and both streaming-card stacks are eager
2781
+ again (row count stays bounded by the earlier-history slicer), and
2782
+ live-streamed markdown tables — including wrapping cells — render
2783
+ correctly while they arrive.
2784
+ - **Thinking is plain text now.** The reasoning well no longer runs
2785
+ the model's thoughts through the markdown renderer, and the live
2786
+ three-line ticker anchors its window at line breaks — rendered
2787
+ thought lines never re-wrap or visibly rewrite themselves as new
2788
+ tokens land.
2789
+ - **The stream no longer freezes and catches up in bursts.** Three
2790
+ server-side delivery fixes: the incremental decoder force-flushes
2791
+ whitespace-free runs (table separator rows, URLs, minified code)
2792
+ instead of holding them for their full length; freshly committed
2793
+ tokens are emitted before cache housekeeping barriers instead of
2794
+ after; and the auth gate was rewritten as pure ASGI so it no longer
2795
+ relays every stream frame through a buffered middleware channel.
2796
+ Measured on the same prompt and settings as the field report:
2797
+ sub-second delivery silences per answer dropped from ~30 to single
2798
+ digits, and generator-side gaps over 200 ms dropped to zero. Every
2799
+ request record now logs a producer gap census
2800
+ (`producer_gap_ms_p95`/`_max`, `producer_gaps_over_200ms`) so
2801
+ stream smoothness is auditable, not vibes.
2802
+ - **Finished replies no longer make the app progressively hotter.**
2803
+ Each settled turn leaked an invisible repeat-forever pulse animation
2804
+ (the thinking-indicator dots), and every leaked pulse drove display
2805
+ cycles that re-measured the window against the whole transcript —
2806
+ CPU at rest climbed with every completed reply, up to ~half a core.
2807
+ The dots now stop by construction when they leave the screen, and
2808
+ the window-measurement guard re-asserts itself after each render
2809
+ phase. With no engine running the app sits at 0.0% CPU regardless
2810
+ of transcript size; the remaining background cost while a daemon is
2811
+ running is the dashboard feed, tracked for the next release.
2812
+
2813
+ - **The desktop app no longer renders the whole transcript per frame —
2814
+ long chats stay smooth on screen, not just on the wire.** Founder
2815
+ testing on a heavy multi-turn conversation at temperature 1.0 caught
2816
+ the other half of the freeze-and-burst reports: the app's window
2817
+ re-measured every realized message on every layout invalidation (a
2818
+ third of the main thread at idle, up to 62×/s while streaming — the
2819
+ guard meant to prevent this had silently never applied), markdown
2820
+ fence classification re-walked every character of every block per
2821
+ frame, per-delta paths copied the entire answer to test emptiness,
2822
+ and scroll pacing state invalidated the full view tree per tick. All
2823
+ of it is now O(new content): fence counts and syntax-lex state are
2824
+ computed once per block, the min-size walk is dead, and the 10 Hz
2825
+ metrics chip no longer re-evaluates every bubble or parses its stream
2826
+ byte-by-byte. Catch-up after any hiccup is rate-limited (max 256
2827
+ chars/frame) so it reads as fast typing, never a paste.
2828
+ - **Markdown tables no longer draw rows on top of each other.** Table
2829
+ cells measured one line tall (the horizontal scroller proposes no
2830
+ width) but drew wrapped, so long cells bled over the rows below.
2831
+ Cells now measure at their placement width.
2832
+ - **Chat streaming no longer freezes mid-response and then dumps the
2833
+ backlog in one burst.** 2.8.0 added a wire safeguard for the uncapped
2834
+ repetition stop that held a fixed ~448-token tail off the stream on
2835
+ every uncapped request — which is every desktop, web, and agent chat.
2836
+ At chat speeds that silenced the stream from roughly token 320 to
2837
+ token 768: the visible symptom was reasoning freezing for 6-11
2838
+ seconds while the speed readout collapsed, then a flood of text at
2839
+ once, plus a final end-of-response burst. Capped requests (every
2840
+ benchmark and QA row) never arm the safeguard, which is how three
2841
+ releases shipped it unnoticed. The holdback is now engaged only while
2842
+ the output actually shows a forming loop — healthy responses stream
2843
+ live, byte for byte, exactly like 2.7.1 — and a real runaway loop
2844
+ still gets trimmed before most of it reaches the wire.
2845
+ (`MTPLX_REPETITION_STREAM_HOLDBACK=candidate|strict|off` selects the
2846
+ new default, the 2.8.0-2.8.2 behavior, or the pre-2.8 wire.)
2847
+ - **Fresh daemons no longer burn 30-60+ seconds of full-throttle GPU
2848
+ "warming" contexts chats never reach.** The turbo profile's 2.8.0
2849
+ background warm ladder walked prefills up to 32,768 tokens after
2850
+ every boot so deep-context *benchmark rows* would start warm — at the
2851
+ cost of every real user's Mac spinning up after launch (the "idle GPU
2852
+ burn" field reports), warm rungs re-firing between chat turns, and a
2853
+ chat sent mid-rung seeing multi-second time-to-first-token. The
2854
+ product ladder is back to the two rungs interactive chat actually
2855
+ touches (boot cost ~4.5 s on the 27B, matching 2.7.1); benchmark
2856
+ harnesses opt into the deep ladder with `MTPLX_WARMUP_LADDER`.
2857
+ Background warm steps now also wait for 90 seconds of request quiet
2858
+ (`MTPLX_WARMUP_IDLE_GRACE_S`) before touching the model, so warming
2859
+ never competes with an active conversation.
2860
+ - **The release pillar gate now fails on streaming freezes.** Every
2861
+ existing gate capped `max_tokens`, so the entire uncapped code path —
2862
+ the one every chat client uses — was invisible to release QA. A new
2863
+ `uncapped_stream_cadence` gate sends the real uncapped streamed chat
2864
+ and fails the release on any delivered-content gap over 2 seconds.
2865
+
2866
+ ## [2.8.2] - 2026-08-17
2867
+
2868
+ ### Fixed
2869
+
2870
+ - **`mtplx start` no longer re-runs tuning on every launch.** The tune
2871
+ record is keyed by a hash of the exact tune settings, and 2.8.0's
2872
+ per-model profile work updated the settings the tuner *saves* under
2873
+ without updating the hand-built dict the start wizard *looks up* with —
2874
+ the two hashes could never match again, so every start re-offered
2875
+ "Run tuning [recommended]" and accepting it meant minutes of maxed-out
2876
+ GPU with the API port still closed. That combination is the "loads the
2877
+ model, machine heats up, requests never arrive" experience reported
2878
+ within hours of 2.8.1 (#280). Save and lookup now derive the key
2879
+ through one shared constructor, with a regression test that fails if
2880
+ they ever drift apart again.
2881
+ - **The wizard tunes the model you actually picked.** A wizard pick was
2882
+ not marked as an explicit model selection, so the tuner re-resolved
2883
+ the hardware default: picking the 4B on a machine whose default is the
2884
+ 27B tuned the 27B — minutes of the wrong benchmark — and then saved a
2885
+ record the launched model could never use. Wizard picks now carry the
2886
+ same explicitness markers as CLI flags.
2887
+ - **A picked local model can no longer be silently swapped for the
2888
+ canonical default.** A local folder whose name matches the verified
2889
+ default (an LM Studio-style `Youssofal/Qwen…` layout, #279) was
2890
+ re-routed through default-model selection, replacing the on-disk path
2891
+ with the Hugging Face repo id — and start then demanded a download for
2892
+ a model that was already installed.
2893
+ - **Idle daemons no longer hammer `session-bank/manifest.sqlite`.**
2894
+ Every `/health` and dashboard poll opened the SSD session-cache
2895
+ manifest and ran a full-table aggregate (about eight sqlite opens per
2896
+ health check, continuously visible in Activity Monitor, #280). The
2897
+ aggregate is now cached against the store's mutation generation with a
2898
+ 5-second staleness bound: steady-state polling costs at most one
2899
+ manifest read per 5 s instead of dozens per second.
2900
+ - **`mtplx --version` reports the right version again.** The 2.8.1
2901
+ hotfix bumped the package version but missed the CLI banner constant,
2902
+ so the published 2.8.1 wheel identified itself as `2.8.0 (2.8.1)` —
2903
+ exactly the line users check to confirm they escaped the 2.8.0 vision
2904
+ defect.
2905
+
2906
+ ## [2.8.1] - 2026-08-17
2907
+
2908
+ ### Fixed
2909
+
2910
+ - **A cached vision conversation can no longer serve another image's
2911
+ context.** Session frontiers are keyed by token ids, and every image
2912
+ placeholder shares one id, so after 2.8.0 taught vision histories to
2913
+ commit (the long-session cache fix), a request that repeated a
2914
+ transcript with a different image could restore the previous image's
2915
+ KV wholesale. Our own release gate's correctness sentinel caught it
2916
+ before the DMG went out. Vision turns now keep their full warm-cache
2917
+ reuse through the content-keyed store (identical pixels restore
2918
+ everything, different pixels never read past the image) and simply
2919
+ stop advancing the raw-id session frontier. PyPI 2.8.0 shipped with
2920
+ the defect for about an hour; 2.8.1 is the same release plus this
2921
+ fix, and it is what the desktop build carries.
2922
+
2923
+ ## [2.8.0] - 2026-08-17
2924
+
2925
+ ### Added
2926
+
2927
+ - **Sharing the API over your network is now a one-liner.** `mtplx serve
2928
+ --host 0.0.0.0 --api-key-file ~/.mtplx/api-key` creates the key file with
2929
+ a fresh key when it doesn't exist (0600, printed once) instead of dying on
2930
+ the exact recovery command our own non-localhost refusal suggests. Wildcard
2931
+ binds print a dialable `Network OpenAI API Base URL` (your Mac's LAN
2932
+ address — what Parallels/VM guests and other devices should use), `--host`
2933
+ finally has help text, and docs/server.md gained a sharing section.
2934
+ Keyless non-localhost binds still refuse: that guard is the product.
2935
+ - **`/health` tells you when the engine is degraded.** A new additive
2936
+ `degradation` block reports compiled-verify state with the reason for any
2937
+ permanent-eager fallback, profile env keys an operator override beat, and
2938
+ NAX/GQA kernel bail counters. "Looks like turbo, runs slow" is no longer
2939
+ possible to miss from a harness.
2940
+ - **`mtplx doctor` prints the compiled-verify fence** — mode, threshold, and
2941
+ which profile or env supplied it (#255).
2942
+ - **Richer per-response `mtplx_stats`:** `finish_reason`,
2943
+ `draft_sampler_policy`/ownership/`greedy_coupled`,
2944
+ `repetition_stop_triggered` (+reason), `content_empty_reason` on capped
2945
+ thinking rows, and visible clamp stats (`context_cap_applied`,
2946
+ `effective_max_tokens`). All quiet-envelope: stamped only when they apply.
2947
+ - **Streaming golden coverage and measurement guidance.** The request
2948
+ matrix gains SSE arms, and the benchmarking guide now documents response
2949
+ caps, `enable_thinking:false` for capped harnesses, streaming measurement
2950
+ rules (TTFT at first *content* delta; progress frames are spec-valid
2951
+ empty deltas), and the exact prompt-scoring array contract.
2952
+ - **Warmup knobs:** the turbo profile ships `MTPLX_WARMUP_LADDER` crossing
2953
+ every compiled-verify bucket class up to its fence, and
2954
+ `MTPLX_WARMUP_PREFILL_CHUNK` is now env-tunable (default unchanged).
2955
+ - **AR mode joins the session bank (#246).** `--generation-mode ar` (and
2956
+ `--no-mtp`) now restores warm prefixes through the same path MTP uses and
2957
+ reports real cache stats (`cached_tokens`, `cache_source`,
2958
+ `cache_miss_reason`, true `prompt_tps`), so an AR control arm measures
2959
+ decode alone instead of paying a full re-prefill every turn.
2960
+ - **Prompt scoring on `/v1/completions`.** `echo: true` with `logprobs` and
2961
+ `max_tokens: 0` scores an entire prompt in one call: per-token logprobs,
2962
+ top-K alternatives, byte-exact `text_offset`, and a `token_ids` identity
2963
+ array. This is the lane KL-divergence quality harnesses need.
2964
+ - **Per-request draft-sampler resolver.** The draft temperature resolves per
2965
+ request from the artifact stamp, the request's own sampling, and greedy
2966
+ coupling at temperature 0, with the resolved value stamped in
2967
+ `mtplx_stats` so telemetry always equals engine reality.
2968
+ - **KV quantization is a real decode feature.** Paged q8 KV runs through a
2969
+ dedicated kernel on the decode path (with a dequant memo below the kernel
2970
+ threshold), and `mtplx_stats` reports the memo and kernel counters.
2971
+ - **Faster streaming under load.** The SSE hot path moved to a loop-fed
2972
+ queue with a constant envelope and cheaper per-chunk encoding, lowering
2973
+ per-token server overhead at high decode speeds.
2974
+ - **`/v1/messages` protocol conformance.** Parallel tool use, streamed
2975
+ usage accounting, and strict rejection of fields that were previously
2976
+ ignored silently.
2977
+ - **Bridge hygiene.** The OpenCode and Pi plugins no longer delete a
2978
+ client's explicitly configured capabilities, and the no-tools stream
2979
+ filter stops eating turns (code fences are exempt from tool-markup
2980
+ suppression).
2981
+
2982
+ ### Changed
2983
+
2984
+ - **Quickstart leads with "Auto (recommended)".** Auto pins nothing — the
2985
+ engine resolves the launch profile per model (Turbo for the quantized
2986
+ flagships). A previously saved default "sustained" state migrates to Auto
2987
+ once; deliberate picks (including Sustained Max) keep pinning. The macOS
2988
+ app's "auto" likewise emits no `--profile` flag anymore, so legacy hybrid
2989
+ and renamed model directories stop launching pinned to sustained.
2990
+ - **Discover shows every MTPLX build.** Any repo with "mtplx" in its name
2991
+ appears (case-insensitive, any position), results are collected until the
2992
+ page fills *after* filtering instead of slicing the top-30-by-downloads
2993
+ first, and the wall/CLI default page is 100.
2994
+ - **Config values are real pins.** A profile or sampler value from
2995
+ `config.toml` is honored as explicit in both directions (config
2996
+ "sustained" stays sustained; values equal to a family default are no
2997
+ longer treated as unset) and startup prints one line saying so. Injected
2998
+ launch defaults carry provenance and never override an artifact's stamp.
2999
+ - **KV quantization actually saves memory now.** The q8 dequant mirror is
3000
+ offset-sized with geometric growth and is released once a request latches
3001
+ onto the kernel path; q4 never allocates a mirror. Numerics route once
3002
+ per request (a q8 request starting below the kernel threshold stays on
3003
+ dequant math for its whole life instead of switching mid-generation), the
3004
+ byte stat counts the mirror, and the CLI help states the honest contract.
3005
+ - **Prompt-scoring (`/v1/completions` echo+logprobs) follows OpenAI array
3006
+ semantics:** all arrays length n with `null` at index 0, the scored token
3007
+ always present in its own top-K map, and a `token_ids` array for stable
3008
+ identity. `logprobs` without `echo` returns a 400 that names the exact
3009
+ requirement; `logprobs: 0` is a valid request.
3010
+ - **`reasoning_effort: "high"` maps up the engine's real effort ladder**
3011
+ (xhigh on Qwen 3.8) instead of silently falling back to the default;
3012
+ unknown values return 400.
3013
+ - **`MTPLX_CLIENT` is an observability label, not an owner.** Managed-client
3014
+ policy requires per-request evidence (headers/UA/body), so an anonymous
3015
+ benchmarker's sampling settings are honored against app- or
3016
+ hermes-launched daemons. Claude Code's `claude-cli/*` user agent is now
3017
+ recognized as a client hint.
3018
+ - **Draft-sampler provenance is explicit end-to-end:** a daemon launched
3019
+ with a bare `--draft-temperature` pins it as operator-explicit, while
3020
+ family-default launches ship unpinned values that keep the temperature
3021
+ curve live.
3022
+
3023
+ ### Fixed
3024
+
3025
+ - **Streamed text now concatenates to exactly the non-stream text.** The
3026
+ model separates `</think>` from its answer with a blank line, and the
3027
+ stream lane leaked that separator as content deltas while the non-stream
3028
+ lane stripped it, so diffing the two transports at temperature 0 showed a
3029
+ whitespace mismatch on every thinking response. Streamed content is
3030
+ edge-trimmed on the wire the same way the non-stream cleaner strips it;
3031
+ interior whitespace (markdown structure) is untouched, and transcript
3032
+ canonicalization stays byte-exact. One cosmetic shape remains: a stream
3033
+ that ends in a tool call can still carry a trailing blank line ahead of
3034
+ the tool call in some chunkings.
3035
+ - **Batched-AR responses no longer carry draft-sampler stamps.** Under
3036
+ concurrency, requests that ride the batched AR lane merged request-policy
3037
+ observability wholesale, so an AR response could report a draft sampler
3038
+ policy plus `draft_time_s: 0.0`, the exact fabricated-stats shape the
3039
+ serial lane already scrubbed. Both batched sites now scrub the same way.
3040
+ - **The launch environment can no longer steer request behavior.** Three
3041
+ branches (the OpenCode default-sampler override, the single-tool-call
3042
+ stream policy, and the post-request cache clear) keyed on a client hint
3043
+ that fell back to the daemon's `MTPLX_CLIENT` label, so an operator-set
3044
+ env var could change anonymous API callers' sampling. Behavior now keys
3045
+ on per-request evidence only; the env-inclusive hint remains as an
3046
+ observability label.
3047
+ - **Prompt scoring's `top_logprobs` is correct under string collisions and
3048
+ safe for harness parsers.** When two token ids decode to the same display
3049
+ string, the scored token's entry now always carries its true logprob
3050
+ (previously a higher-ranked collision kept the other token's value, which
3051
+ inflated string-keyed KL readings), and index 0 is an empty dict instead
3052
+ of null so parsers that iterate entries with `.items()` don't crash on a
3053
+ spec-shaped response. `token_logprobs[0]` stays null.
3054
+ - **`mtplx quickstart`/`serve` find branded local builds.** The canonical
3055
+ model id resolver only checked the Hub snapshot layout, so a forge-built
3056
+ pack living under its bare name (which `mtplx models` lists and bench
3057
+ model selection happily uses) was reported "not cached" with a 20 GB
3058
+ re-download suggestion. The resolver now falls back to the branded
3059
+ directory under the same contract validation.
3060
+ - **`mtplx doctor --summary` prints the compiled-verify fence** like the
3061
+ full report already did.
3062
+ - **The MTP lane's final session-bank commit is timed and guarded like
3063
+ AR's.** The post-response commit forward was billed into measured decode
3064
+ elapsed (understating MTP tok/s) and an allocation failure there could
3065
+ destroy a completed response; it is now outside the measured window and
3066
+ a failure downgrades to "no session commit this turn".
3067
+ - **The macOS app's model-family detection matches the engine's.** The app
3068
+ still classified stock `Qwen/Qwen3-8B` as the 3.8 family (temperature 1.0
3069
+ defaults on the wrong model) and let folder names outrank forge
3070
+ provenance; it now uses the engine's boundary guard and provenance-first
3071
+ order.
3072
+ - **The web chat UI no longer labels this launch's context cap as "the
3073
+ model's" context.** The max-tokens help fed `/health`'s `context_window` —
3074
+ which memory sizing can cap far below native — into a string attributing
3075
+ it to the model, so a 32GB machine serving a 256k-native model read
3076
+ "the model's 16.4k context". It now says whose number it is: this
3077
+ server's active context window.
3078
+ - **Stock `Qwen/Qwen3-8B` is no longer misclassified as the Qwen 3.8
3079
+ family** (a regression in the artifact-identity fix gave Alibaba's most
3080
+ popular size the wrong sampler defaults and reasoning codec), and family
3081
+ resolution now trusts forge provenance over the folder name, so renamed
3082
+ or symlinked model directories keep their true family (#268).
3083
+ - **Agentic sessions commit again.** The post-turn session commit now builds
3084
+ its prefix with the same committed-reasoning canonicalization the next
3085
+ request will send, so commits byte-extend instead of failing every turn
3086
+ ("retokenized_prefix_not_extending_session", 3–4% reuse — #269). The
3087
+ canonicalization gate also refuses on tool-call changes and dropped
3088
+ turns instead of substituting the wrong turn's reasoning, works for
3089
+ OpenCode's stripped preambles, and repair re-encodes preserve committed
3090
+ reasoning. Two system contracts became suffix contracts, ending
3091
+ full-context re-prefill when they flip.
3092
+ - **A prompt that fills the context window returns a clear 400**
3093
+ (`context_length_exceeded`) instead of silently generating one token,
3094
+ and a fitting prompt whose `max_tokens` exceeds the remainder is clamped
3095
+ visibly — no more phantom 0.4 tok/s rows at 256k.
3096
+ - **`finish_reason` is truthful everywhere:** a length cap beats
3097
+ `tool_calls` in non-streaming chat (matching streaming), `/v1/messages`
3098
+ maps priority `max_tokens` → `stop_sequence` → `tool_use`, the
3099
+ completions stream trims stop strings like non-stream, and empty content
3100
+ on a capped thinking row recovers or is stamped with the reason on both
3101
+ paths.
3102
+ - **AR mode reports honest numbers:** no fabricated draft temperature, and
3103
+ elapsed time no longer includes the post-response session-bank forward
3104
+ pass (which also can no longer destroy a finished response on failure).
3105
+ - **Draft-sampler telemetry equals engine reality.** The resolver no longer
3106
+ reports policy "none" while drafting at target temperature; the
3107
+ temperature-scale env applies before the stamp; artifact draft stamps
3108
+ survive launching under a different profile; OpenCode's server-side
3109
+ defaults are an honest `launch_default` tier with the coupling curve
3110
+ live; batch cohorts key on the target sampling triple.
3111
+ - **Repetition-guard stops stay off the wire:** the streaming paths (AR,
3112
+ MTP-K, and both Gemma-4 loops) hold back a detector-window tail while
3113
+ the guard is armed, so trimmed loop garbage never reaches clients, and
3114
+ the stop is visible in public stats.
3115
+ - **Benchmark rows stop paying hidden costs:** compiled-verify prewarm is
3116
+ no longer spent by the boot walk and warms the exact shared traces real
3117
+ rows dispatch; Metal memory caps and over-context refusal now apply to
3118
+ every bench, ladder, one-shot, and quickstart entry (#261) with
3119
+ per-row flushes.
3120
+ - **Streams end honestly:** the post-content commit wait is bounded with
3121
+ live heartbeats and a stall watchdog, explicit cancels emit a terminal
3122
+ frame + `[DONE]`, client disconnects are tagged as such, and
3123
+ `GeneratorExit` no longer logs runtime errors.
3124
+ - **No surface stamps "sustained" for a flagship anymore:** forge's
3125
+ artifact stamp, cached-model listings, doctor's support matrix, bench
3126
+ suites, tune, and the quickstart download branch all report what serve
3127
+ resolution actually picks.
3128
+ - **Sizing and accounting truths:** nested MTP-sidecar layouts are sized
3129
+ correctly (restoring the RAM-aware auto budget on small Macs), the
3130
+ dashboard no longer double-counts the sidecar, the NAX install report
3131
+ reflects the real probe, the batch histogram stops counting phantom
3132
+ width-1 units, and prompt scoring decodes each unique token once instead
3133
+ of ~65k times at long context.
3134
+
3135
+ ## [2.7.2] - 2026-08-16
3136
+
3137
+ An emergency fix for `mtplx pull`: on 2.7.1 and older, re-pulling a repo that
3138
+ changed upstream — such as the Qwen 3.8 repos re-published on 2026-08-15 with
3139
+ their vision towers restored — can corrupt the local copy. Upgrade before
3140
+ pulling.
3141
+
3142
+ ### Fixed
3143
+
3144
+ - **The Qwen 3.8 Hugging Face artifacts can see again (#263).** All six
3145
+ published 3.8 repos (Bare Speed, Optimized Speed, Optimized Quality and
3146
+ their FP16 siblings) shipped without their vision towers: the forge
3147
+ `mlx_lm.convert` lane serializes only the text model, so the 333
3148
+ `model.visual.*` tensors, `vision_config`, and the preprocessor sidecars
3149
+ were silently dropped. The repos were re-published on 2026-08-15 with the
3150
+ official bf16 tower grafted back as an index-registered
3151
+ `model-vision.safetensors` (byte-for-byte from `Qwen/Qwen3.8-27B`; language
3152
+ and MTP tensors untouched). Existing installs pick the delta up with
3153
+ `mtplx pull`; images, `/health` `vision.enabled`, prompt caching with
3154
+ images, and MTP decode were verified on all six builds. Thanks to
3155
+ @kjellix for the report and the proven graft procedure.
3156
+ - **Forge keeps vision towers from now on.** `mtplx forge build` grafts the
3157
+ source's vision tower, `vision_config`, and preprocessor sidecars into the
3158
+ converted artifact on every lane (`mtplx/vision_graft.py`), and fails
3159
+ closed when a source that declares `vision_config` would produce a blind
3160
+ artifact. A repair script for already-forged artifacts ships as
3161
+ `scripts/graft_vision_tower.py`, and the pillar gate now asserts
3162
+ `vision.enabled` before the vision-cache check so a blind build fails
3163
+ loudly with the real cause.
3164
+ - **`mtplx pull` no longer corrupts files that changed upstream (#258,
3165
+ #234).** The
3166
+ progress downloader treated a size-mismatched *complete* local file as a
3167
+ resumable partial and byte-range-appended the remote tail onto the old
3168
+ content — updating a repo in place (for example the restored 3.8 vision
3169
+ indexes) corrupted `config.json` and `model.safetensors.index.json` and
3170
+ left the local copy unloadable until re-downloaded. Stale files are now
3171
+ discarded and re-fetched whole; genuine `*.incomplete` partials still
3172
+ resume. **Users on ≤2.7.1 should upgrade before pulling repaired repos**;
3173
+ a failed pull from an older build is recovered by deleting the corrupt
3174
+ `config.json` + `model.safetensors.index.json` and pulling again.
3175
+
3176
+ ## [2.7.1] - 2026-08-15
3177
+
3178
+ Clears the 2.7.0 known-issues list: `xhigh` is now selectable everywhere it is
3179
+ offered, and the app's KV cache quantization toggle reaches Qwen 3.8.
3180
+
3181
+ ### Fixed
3182
+
3183
+ - **`xhigh` no longer bounces back to `medium` in the app.** Choosing it in
3184
+ Inference settings while the model was running, or running
3185
+ `mtplx config set reasoning_effort xhigh`, was rejected: those two writing
3186
+ surfaces carried their own hardcoded effort list that stopped at `high`,
3187
+ while the request path already knew `xhigh`. Because the live-settings POST
3188
+ is all-or-nothing, one unknown level threw away the whole payload and the
3189
+ picker snapped back to the family default. All four writing surfaces
3190
+ (serve/CLI `--reasoning-effort`, `mtplx config set`, the live-settings POST)
3191
+ now read one shared vocabulary in `mtplx/reasoning_effort.py`, and narrowing
3192
+ to what the loaded model actually supports stays where it belongs — the
3193
+ family's own `effort_levels`.
3194
+ - **The app's KV cache quantization toggle now applies to Qwen 3.8.** The
3195
+ launch gate listed only `qwen3_5`/`qwen3_6`, so a 3.8 launch exported
3196
+ nothing while Settings showed `q8`.
3197
+ - **`mtplx doctor` names the model it actually checks** instead of hardcoding
3198
+ the previous default, and turbo's profile note reports the real compiled
3199
+ verify fence (32,768, raised from 12,288 in 2.7.0) rather than the stale one.
3200
+ - **A built app can no longer rank below the release it supersedes.** Sparkle
3201
+ orders updates by `CFBundleVersion` alone, and the number derived from the
3202
+ version was narrow enough that 2.7.1 computed lower than the 2.7.0 that
3203
+ shipped — so a fresh build offered 2.7.0 to itself as an update. The
3204
+ derivation now has room for the whole version and the appcast reads its
3205
+ number back off the built bundle instead of being told one separately.
3206
+
3207
+ ### Still open from 2.7.0
3208
+
3209
+ - With reasoning switched off in a plain chat with no tools, Qwen 3.8 can
3210
+ still emit a stray tool call and cut the turn short. Keep thinking on.
3211
+
3212
+ ## [2.7.0] - 2026-08-15
3213
+
3214
+ Qwen3.8 support 🎉. Qwen3.8-27B came out on 2026-08-14; this release runs it
3215
+ the way the model card says, with three tuned MTPLX builds, FP16 versions for
3216
+ M1 and M2, and the compiled verify window extended to 32k. The release notes
3217
+ at docs/releases/v2.7.0.md carry the full story and every measurement (all on
3218
+ one M5 Max; nothing was measured on M1 or M2).
3219
+
3220
+ ### Added
3221
+
3222
+ - **Qwen 3.8 model family** (`qwen3_8`) with the official inference contract:
3223
+ temperature 1.0 / top-p 0.95 / top-k 20, reasoning effort `xhigh`, `medium`
3224
+ and `low` (coding sessions default to `medium`: 51.5 s against 314.9 s at
3225
+ xhigh on the same correct agent task), thinking preserved in history by
3226
+ default, `chat_template_kwargs.enable_thinking` honored. Live serving is
3227
+ capped at depth 3 for now (depth 4 killed the daemon on drop day).
3228
+ - **Three Qwen 3.8 builds.** Bare Speed (16.0 GB, flat 4-bit: quickest burst
3229
+ chat speeds, lower quality and slower on long coding tasks), Optimized
3230
+ Speed (20.4 GB, 4-bit dynamic quant: great coding speeds and good quality,
3231
+ recommended), Optimized Quality (29.4 GB, 8-bit dynamic quant: good coding
3232
+ speeds and perfect quality; KL to the bf16 teacher 0.00105). Each build
3233
+ states its recommended draft sampler, tuned depth (3) and measured peak in
3234
+ its own metadata; the runtime reads that ahead of profile fallbacks, and
3235
+ the app and CLI launch every build identically.
3236
+ - **FP16 builds for M1 and M2.** Every 3.8 build has an FP16 sibling on the
3237
+ Hub (`-FP16`): the same quantized packs byte for byte, every 16-bit tensor
3238
+ cast bf16 to fp16 (99.992% exact, none overflow). The M1/M2 tier of the CLI
3239
+ and the app routes to them automatically, same picks, order and text.
3240
+ - **`mtplx pull` mirror hint (#259).** A network-shaped download failure with
3241
+ no `HF_ENDPOINT` set now names the mirror knob (CLI env, or Settings,
3242
+ Advanced, HF download mirror in the app). Troubleshooting docs cover both.
3243
+ - **`mtplx tune --require-max-fans`** and a Forge `module_overrides` recipe
3244
+ lane (per-module quantization in one conversion pass; it built Optimized
3245
+ Speed).
3246
+
3247
+ ### Changed
3248
+
3249
+ - **Default model.** Fresh installs on M3/M4/M5 with 32 GB or more default to
3250
+ Qwen 3.8 Optimized Speed; M1/M2 get its FP16 sibling; under 32 GB still
3251
+ routes to the 9B. `mtplx quickstart`, `mtplx start` and the app's first-run
3252
+ picker offer the whole 3.8 line-up. `mtplx start` says once when the
3253
+ recommended default moved. Qwen 3.6 Optimized Speed V2 keeps turbo.
3254
+ - **Compiled verify to 32k.** The compiled verify graph stopped at 12,288
3255
+ tokens of context since July; that fence's reason is gone, so turbo now
3256
+ compiles verify to 32,768 tokens (interleaved A/B on Qwen 3.8 Bare Speed:
3257
+ +6.9% at 20k, peak memory flat at 20k and lower at 30k).
3258
+ `MTPLX_COMPILED_VERIFY` can be set by hand.
3259
+ - **Coding agents.** OpenCode and Pi no longer send an output cap of any kind
3260
+ for MTPLX models; Pi sessions carry their real session id so banked
3261
+ prefixes restore from RAM; the session bank's background re-render uses the
3262
+ effort the request ran with; reasoning cut off before the closing think tag
3263
+ is routed as reasoning.
3264
+ - **App.** Qwen 3.8 launch family (turbo, official sampler preset, reasoning
3265
+ effort control with xhigh, Tune AR to D3, exact sizes and measured peaks);
3266
+ the first-run picker shows the trio (FP16 on M1/M2); the Qwen 3.6 Optimized
3267
+ Quality row on M1/M2 resolves to its FP16 build.
3268
+ - **Thermal honesty.** Max-fan sessions hold an ownership token, so a daemon
3269
+ shutting down behind its replacement cannot switch fans back to Auto under
3270
+ it; Forge refuses to benchmark when verified max-fan mode cannot start.
3271
+ - **`mtplx doctor`** judges memory against the model this Mac would actually
3272
+ default to; M5 Max listed in the support matrix.
3273
+ - **Attribution is now required, not preferred.** MTPLX stays Apache-2.0, and
3274
+ the NOTICE file (which Apache-2.0 section 4(d) carries with every copy) now
3275
+ requires products built on MTPLX to show "Powered by MTPLX" inside the
3276
+ product, where a user can see it. A mention in a repo or on a website does
3277
+ not cover it.
3278
+
3279
+ ### Fixed
3280
+
3281
+ - **SSD session cache CPU drain.** The cache walked its whole store on every
3282
+ write and every `/health` poll (816,220 files, 89.9 GB, 41.7 s per walk).
3283
+ Reconciliation is now maintenance: only when the store changed, at most 5%
3284
+ of the time, off the writer lock, yielding to live traffic. Idle CPU with a
3285
+ health poller 35% down to 0.2%; per-write cap gate 71 to 159 s down to
3286
+ 3 to 6 s.
3287
+ - **macOS 27 crash in the inference settings overlay (#256, #257).** SwiftUI 8
3288
+ traps on a slider with no distinct values; the depth and context-window
3289
+ sliders are now built only when there is something to slide. Reported and
3290
+ fixed by @joshlacal.
3291
+ - Hardware detection uses absolute `/usr/sbin/sysctl` and
3292
+ `/usr/sbin/system_profiler` paths; `doctor` and `tune` no longer run `git`
3293
+ outside a repository (no Xcode Command Line Tools dialog on a clean Mac).
3294
+ - Depth-default resolution honors artifact metadata across profile
3295
+ mismatches; the degrade pin and the no-metadata path both survive. The
3296
+ public depth ceiling follows the artifact reference, not the served-name
3297
+ alias; `tune` validates depths against what the model supports and takes
3298
+ its sampler from the same family contract as `serve`.
3299
+ - `MTPLX_REQUEST_LOG_JSONL=1` logs to the default file instead of a file
3300
+ named `1`. xhigh boot no longer trips strict warmup.
3301
+
3302
+ ### Known issues (fixed in 2.7.1)
3303
+
3304
+ - Choosing `xhigh` in the app's Inference settings while the model is running
3305
+ is rejected by the server, as is `mtplx config set reasoning_effort xhigh`;
3306
+ set it before starting the model or pass `--reasoning-effort xhigh`.
3307
+ - The app's KV cache quantization toggle is not applied to Qwen 3.8 models.
3308
+ - With reasoning off in a plain chat with no tools, Qwen 3.8 emitted a stray
3309
+ tool call and cut the turn short on about half of our coding prompts; keep
3310
+ thinking on (the default).
3311
+
3312
+ ## [2.6.0] - 2026-08-11
3313
+
3314
+ ### Added
3315
+
3316
+ - **Concurrent MTP serving.** Until now speculative decoding was a
3317
+ single-request feature: under concurrency the scheduler fell back to the
3318
+ autoregressive batch lane and every simultaneous caller lost the MTP
3319
+ speedup. `--scheduler-mode mtp_batch` serves independent requests through
3320
+ fixed-width MTP cohorts — each row owns its state, drafts are verified in
3321
+ one batched target forward, and rows join and leave mid-flight without
3322
+ disturbing their neighbours. Two native cohort widths install side by side
3323
+ (three-wide and eight-wide) and the scheduler seals each cohort at the
3324
+ narrowest width that fits, so two concurrent agents no longer pay the
3325
+ padding of an eight-lane launch. Measured on `Qwen3.6-35B-A3B` on an M5 Max,
3326
+ the three-wide lane holds 1.7–1.8× the per-request decode of the padded
3327
+ eight-lane shape, and the composite lane below adds warm-prefix restores on
3328
+ top; against the previous production `ar_batch` route the same concurrent
3329
+ agent workloads decode at 1.6–2.25× per lane, sampled at the model's
3330
+ shipped settings.
3331
+
3332
+ The lane is exact where exactness is claimed and honest where it is not:
3333
+ `--mtp-batch-numerics` selects between `throughput`, `balanced`, and
3334
+ `b1-exact` profiles (documented trade-offs, install-time self-check), and
3335
+ per-request stats now report each row's own truth — its own accepted-depth
3336
+ histogram, its own cohort width, its own restore provenance — rather than
3337
+ cohort-averaged numbers.
3338
+
3339
+ The session bank composes with the cohorts: a request whose prefix is
3340
+ banked restores it at cohort admission (skipping the shared prefill for
3341
+ the covered span), prefills only its uncovered suffix, and commits its own
3342
+ prompt boundary before the merge, so agent fleets with shared system
3343
+ prompts keep warm TTFT under concurrency. The plain `ar_batch` lane learned
3344
+ the same trick: finished rows and prompt-only boundaries commit to the bank
3345
+ from batch mode too.
3346
+
3347
+ - **LiquidAI LFM2 / LFM2.5 support.** The LFM2 family serves natively with a
3348
+ bit-exact ShortConv decode fast-path, a verified think/tool grammar
3349
+ (parser stamp, native tool prompt, and the pythonic streaming dialect the
3350
+ family emits), and a catalog lane for the `llama-ar` shape. IQuest-Coder
3351
+ checkpoints serve target-only AR through the same registry honesty:
3352
+ recognized, served without MTP claims, refused cleanly when a quantization
3353
+ the runtime cannot execute is detected.
3354
+
3355
+ - **Embedding and reranking endpoints.** `POST /v1/embeddings` (OpenAI shape)
3356
+ and `POST /v1/rerank` (Cohere/Jina shape) are now served by the same daemon
3357
+ as chat, so a retrieval-backed setup no longer needs a second inference
3358
+ server beside MTPLX. Both are opt-in through repeatable `--embedding-model`
3359
+ and `--reranker-model` flags on `serve` and `quickstart`, accepting a Hugging
3360
+ Face id or a local path with an optional `REF=served-id` alias. With no flag
3361
+ the endpoints answer 404 and the chat runtime is untouched.
3362
+
3363
+ Retrieval models deliberately bypass the MTP generation path: multi-token
3364
+ prediction makes next-token decoding cheaper, which is meaningless for a
3365
+ model that emits a vector instead of a token stream. They run the transformer
3366
+ stack directly — last-token pooling with L2 normalisation for embeddings, a
3367
+ softmax over the `yes`/`no` logits for reranking, both padded on the right so
3368
+ causal attention leaves every real position bit-identical to an unpadded run.
3369
+
3370
+ Backends are cached by resolved path, so listing one reference as both an
3371
+ embedder and a reranker loads a single copy of the weights and serves both
3372
+ roles from it. `--retrieval-max-resident` caps how many retrieval models stay
3373
+ in memory; beyond it the least recently used one is unloaded. Models load on
3374
+ first request, so an unused endpoint costs nothing.
3375
+
3376
+ `/v1/models` keeps its default listing chat-only, so clients that enumerate
3377
+ models to build a chat picker (OpenCode, Cline, Continue, ...) are never
3378
+ offered an embedder as a conversation target. Retrieval models are listed
3379
+ via `?capability=embedding` or `?capability=rerank`, every entry carries a
3380
+ `capability` field, and a chat completion that requests a retrieval-only id
3381
+ is rejected with a clear 400 instead of being silently answered by the
3382
+ loaded chat model. The settings are configurable from the macOS app and
3383
+ persist in `~/.mtplx/config.toml` as `embedding_models`, `reranker_models`,
3384
+ and `retrieval_max_resident`.
3385
+
3386
+ Checkpoints that ship their own Python inference code (the jina MLX
3387
+ releases bundle `model.py`/`rerank.py`) are only executed after an explicit
3388
+ opt-in: `--retrieval-trust-remote-code` on `serve`/`quickstart`, or
3389
+ `retrieval_trust_remote_code = true` in the config file. Without it the
3390
+ request is refused with a clear 403 naming the flag; models served by
3391
+ MTPLX's own loaders are unaffected.
3392
+
3393
+ `/v1/embeddings` honours OpenAI's `dimensions` parameter with the
3394
+ Matryoshka recipe — truncate to the leading dimensions and re-normalise —
3395
+ matching how MRL-trained models like jina-embeddings-v5 (32→1024) are
3396
+ meant to be shortened. A `dimensions` beyond the model's native width is a
3397
+ clear 400 rather than a silently full-width vector that no longer fits the
3398
+ index the client sized.
3399
+
3400
+ ### Fixed
3401
+
3402
+ - **Temperature-0 speculative output matches plain decoding again.** The
3403
+ MTP lane's cold prefill fed the whole prompt through the model in one
3404
+ window, while plain autoregressive decoding splits it into body plus a
3405
+ final single-token step. The two shapes round differently at the last
3406
+ bit, so the speculative lane started from a cache one ulp apart from the
3407
+ AR lane's — enough to flip greedy argmax at a near-tie and break the
3408
+ "temperature 0 output is token-identical" contract on the promotion gate.
3409
+ All cold-prefill paths now partition the prompt identically; the Optimized
3410
+ Speed V2 artifact, which surfaced the flip, passes its greedy exactness
3411
+ gate at every depth again.
3412
+
3413
+ - **Single-request decode speed under sampling recovered.** The eight-way
3414
+ sampling unification routed the single-request lane's per-token sampling
3415
+ through a full-vocabulary host reference (a NumPy partition and float64
3416
+ softmax over 248k logits per sampled token), which measured as a 15-19%
3417
+ decode regression against 2.5.4 on the serving lane. The single-request
3418
+ lane now selects its top-k support on device with the same deterministic
3419
+ tie-breaking as the batched route and moves only those k tokens to the
3420
+ host. The batched cohort lanes keep their float64 host reference
3421
+ unchanged.
3422
+
3423
+ - **Prefix restores no longer corrupt the session bank (#247).** Since the
3424
+ zero-copy cache work, restoring a banked prefix installed the entry's own
3425
+ KV state objects into the borrowing request's cache; the borrower's suffix
3426
+ prefill and decode then wrote in place into pages the entry still
3427
+ referenced. An interleaved near-prefix request could silently rewrite a
3428
+ neighbour's banked span, and every later match served the poisoned pages,
3429
+ so a byte-identical repeat could answer under another request's system
3430
+ prompt. Restores now install fresh zero-copy views: the entry's stored
3431
+ state can no longer be written through, the borrower's first divergent
3432
+ write pays the single deferred copy the design always intended, and
3433
+ commits stay zero-copy. Reproduced and fixed from the reporter's
3434
+ reproducer, which ships as a regression test.
3435
+
3436
+ - **Streaming tool calls no longer duplicate argument values into `content`
3437
+ (#249).** When streaming with a prior tool call and result in history, the
3438
+ initial-fragment guard stripped tool markup and forwarded the remainder,
3439
+ which is the tool call's argument values, into `delta.content`, while the
3440
+ parser separately emitted the correct `tool_calls`. Clients then echoed the
3441
+ duplicated text back into context on every turn. Orphan remainders are now
3442
+ held until tool extraction resolves: a parser-claimed span is dropped, and
3443
+ only genuinely visible prose wrapped in stray markup still surfaces.
3444
+
3445
+ - **Solo requests with penalties answer on the composite scheduler.** A solo
3446
+ request carrying `presence_penalty` or `frequency_penalty` on the
3447
+ `mtp_batch` daemon hit the compiled lane's sampler validator and returned
3448
+ HTTP 500, while penalty cohorts already fell back to the host route. Solo
3449
+ penalty requests now steer to the same host lane up front.
3450
+
3451
+ - **`mtplx serve --no-auth` actually parses (#235 follow-through).** The
3452
+ 2.5.4 notes promised that exact spelling, but the flag existed only on the
3453
+ internal server module's parser; the public `mtplx serve` command rejected
3454
+ it with an argparse error before anything ran. The flag now parses on
3455
+ `serve` and is forwarded to the server. Non-localhost binds still require
3456
+ a key.
3457
+
3458
+ - **`qwen3_5_mtp` checkpoints validate and serve again.** The injector
3459
+ attached the MTP surface to the outer model wrapper while validation
3460
+ inspects the inner TextModel, so validation failed and the forward pass
3461
+ crashed. The surface now lives on the TextModel and is re-exposed on the
3462
+ outer wrapper by delegation. Cherry-picked from PR #242 (thanks @davidtai)
3463
+ together with its hermetic regression test.
3464
+
3465
+ - **Artifacts launch at their measured depth again.** The typed runtime
3466
+ contract kept only its schema fields, which silently dropped the
3467
+ measured-depth map and every artifact's declared depth default — so
3468
+ `quickstart`/`start` launched the 35B-A3B at its D3 *ceiling*, a measured
3469
+ ~22% decode loss against its fastest depth. Identity and depth defaults now
3470
+ resolve from the artifact's `mtplx_runtime.json` when the contract lacks
3471
+ them: the 35B Speed artifact launches at its measured D2, and artifacts
3472
+ declaring `recommended_mtp_depth` (Balance) are honored.
3473
+
3474
+ - **`--reasoning-parser` is authoritative.** A set parser was silently
3475
+ replaced by the backend's codec whenever they disagreed, which made the
3476
+ flag a no-op on shared lanes and left thinking impossible to enable for
3477
+ models whose templates fully support it. A parser resolved from family
3478
+ policy or typed by the operator now wins; the codec fills in only when no
3479
+ parser is set. Explicit `none` is still honored.
3480
+
3481
+ - **Metal buffer-object leak in long decodes.** mlx-lm's `ArraysCache`
3482
+ regrew Metal buffer objects on every `advance()`; over a long session that
3483
+ is unbounded growth. The fix is vendored in-tree and installed on both the
3484
+ server path and the batch lane, so pip installs against stock mlx-lm 0.31.x
3485
+ get it too.
3486
+
3487
+ - **Missing MTP heads degrade instead of refusing.** A checkpoint without
3488
+ `mtp_heads` now serves target-only AR (with the degrade reason surfaced,
3489
+ and propagated to the spawned server process) instead of being turned away.
3490
+ Checkpoints that ship custom Python (`auto_map`) are refused with the
3491
+ policy stated plainly — MTPLX never executes repository code — and
3492
+ quantizations MLX cannot run are refused with the offending bit-width named
3493
+ rather than failing later at load.
3494
+
3495
+ - **AR batch hardening.** Cache-removal errors in the batch lane now fail
3496
+ closed instead of corrupting the row; completed streams no longer starve
3497
+ behind still-running neighbours; and the vendored Metal shader cache is
3498
+ keyed by MLX ABI so an MLX upgrade cannot serve stale compiled kernels.
3499
+
3500
+ ## [2.5.4] - 2026-08-07
3501
+
3502
+ Agent sessions got the attention this cycle. If you drive MTPLX from Pi,
3503
+ OpenCode, or any tool-calling client, warm turns now stay warm. (This section
3504
+ was backfilled from the GitHub release notes, which carry the full narrative.)
3505
+
3506
+ ### Fixed
3507
+
3508
+ - **Warm-turn cache reuse in agent sessions.** Tool rounds carried a short
3509
+ transient hint that shifted the cached prefix by ~200 tokens per turn; the
3510
+ engine now records the stable boundary and restores from it directly. A
3511
+ postcommit within a bounded 0.6s of finishing is now awaited instead of
3512
+ discarded (a measured 1,449-token re-prefill became 436 tokens, cutting
3513
+ that turn's time-to-first-token from 2.7s to 1.1s). Background SSD cache
3514
+ maintenance no longer drains ahead of a starting turn (the unexplained
3515
+ ~0.8s stall at turn start), and the SSD tier skips hydrating candidates
3516
+ that cannot beat a fresher in-RAM match.
3517
+ - **The session cache says what it is doing (#229, and the observability half
3518
+ of #230).** The daemon prints the resolved cache budget at startup with the
3519
+ override variables; outgrowing the per-session cap warns once with the
3520
+ numbers and the setting that raises the ceiling; `MTPLX_SESSION_BANK_MAX_BYTES`
3521
+ parses "8G"/"8GB"/"8GiB" and warns on unparseable values; the app no longer
3522
+ drops explicit cache sizes set in Settings under the "target default"
3523
+ policy.
3524
+ - **Long-context decode on 32k+ sessions (#228).** The app forced a
3525
+ paged-attention route at 32k with launch-day thresholds that were never
3526
+ re-measured; reporters measured 4-7x slower decode at 43k. The app now
3527
+ defers to the engine's measured thresholds (64k on current kernels).
3528
+ - **Vision sessions.** The near-prefix restore lane is capped at the first
3529
+ image token, so it can never resurrect cache computed from a different
3530
+ image's pixels.
3531
+ - **Adaptive depth engages when it should.** The expected-value policy's cost
3532
+ constants now reflect measured reality on current kernels; depth-3 drafting
3533
+ fired on 13% of eligible rounds despite 65% acceptance.
3534
+
3535
+ ### Added
3536
+
3537
+ - **`mtplx serve --no-auth` (#235).** Explicit auth off-switch for localhost
3538
+ binds; non-localhost binds still require a key.
3539
+ - **llama.cpp-style `timings` object (#237).** Chat completion responses can
3540
+ include prompt/decode throughput for clients that read it from the response
3541
+ body (contributed).
3542
+
3543
+ ## [2.5.3] - 2026-08-06
3544
+
3545
+ Small release. A day of head-to-head benchmarking against another engine
3546
+ turned up a set of real latency bugs in our agent lane and a few places
3547
+ where the API surface misled external tools. All of them are fixed here.
3548
+
3549
+ ### Fixed
3550
+
3551
+ - Back-to-back agent requests no longer stall behind another session's
3552
+ background cache maintenance. Between requests the server commits session
3553
+ state to the reuse bank; since the 2.4 line that work could only be
3554
+ interrupted by its own session, so a request from any other session (a
3555
+ second chat, a subagent, an editor tool call) could wait out a
3556
+ multi-gigabyte commit and then decode slower on top of it. Worst measured
3557
+ hit on tight request cadences was a 44% slower follow-up turn and about
3558
+ 0.75s of added first-token latency. Commits now yield the moment any
3559
+ request is admitted, whoever it belongs to. A session's own follow-up
3560
+ keeps the short grace it always had, so streaming tool-call turns still
3561
+ resolve their prefix instead of re-prefilling.
3562
+ (`MTPLX_POSTCOMMIT_CROSS_SESSION_YIELD=0` restores the old behavior.)
3563
+ - Repeat requests skip prompt re-encoding. Rendering and tokenizing a long
3564
+ chat transcript costs 77-92ms per request; an exact-match cache now
3565
+ returns it in under a millisecond. Combined with the yield fix above,
3566
+ warm follow-up latency in our gate runs went from a 194-961ms band to a
3567
+ steady 65-74ms, and clean-room warm restores measure 2-3ms server-side.
3568
+ (A related opt-in knob,
3569
+ `MTPLX_COMPILED_VERIFY_POST_RESTORE_EAGER_ROUNDS`, can route the first
3570
+ verify round after a very large restore through the eager path; it ships
3571
+ off by default.)
3572
+ - API responses no longer end with the "MTPLX TPS" stats footer. External
3573
+ tools counted it as model output, which added ~430ms to their timing
3574
+ windows, made token counts disagree with `usage`, and broke output
3575
+ equality checks at temperature 0. The MTPLX app and browser chat keep
3576
+ the footer. (`MTPLX_STATS_FOOTER_SCOPE=all` restores it everywhere.)
3577
+ - `usage` now reports `completion_tokens_details.reasoning_tokens`, so
3578
+ clients can separate thinking tokens from visible output instead of
3579
+ inferring it from stream timing.
3580
+ - Probing unknown endpoints or posting malformed bodies returns clean JSON
3581
+ errors with the right status codes instead of Python exception text.
3582
+
3583
+ ### Changed
3584
+
3585
+ - Anonymous API clients now get standard OpenAI semantics for explicit
3586
+ request parameters: temperature, top_p, top_k, the thinking toggle,
3587
+ penalties, and generation mode in the request body are applied instead
3588
+ of being treated as hints. Requests that leave a field unset keep the
3589
+ server's launch and live settings, and clients MTPLX manages itself (the
3590
+ app, browser chat, configured OpenCode and editor lanes) stay
3591
+ server-owned exactly as before, so curated agent sampling is untouched.
3592
+ This closes the "temperature is ignored" class of report (#241).
3593
+ (`MTPLX_CLIENT_CONTROLS_DEFAULT=hints` restores the old policy.)
3594
+ - Temperature-0 requests now run the draft sampler greedy as well, so the
3595
+ speculative window matches the target's argmax choices more often:
3596
+ depth-2 acceptance rose from .526 to .590 in our runs.
3597
+
3598
+ ### Compatibility
3599
+
3600
+ - Model catalogs, model defaults, memory policy, and every managed-client
3601
+ behavior are unchanged. Each behavior change above has an environment
3602
+ switch that restores the previous policy.
3603
+
3604
+ ## [2.5.2] - 2026-08-04
3605
+
3606
+ Hotfix for a long-response slowdown that shipped in 2.5.1 with Optimized
3607
+ Speed V2 as the recommended coding model.
3608
+
3609
+ ### Fixed
3610
+
3611
+ - Long single responses no longer slow down and stutter partway through. The
3612
+ compiled verifier hands generation to the eager path after its growth
3613
+ reserve is exhausted, and that handoff carried unfinished GPU work into the
3614
+ rest of the response. Every later token paid for the old work, so decode
3615
+ throughput fell steadily on responses past a few thousand tokens. The
3616
+ reported 12,000-token response opened at 59 tokens per second and
3617
+ ended near 30. With the fix, the same seeded generation no longer
3618
+ decays: the closing windows now run as fast as or faster than the
3619
+ early ones.
3620
+ - The handoff now settles all cache and recurrent state exactly once at the
3621
+ ownership boundary and releases the compiled references before eager
3622
+ decoding continues. Handoff telemetry is exposed in the compiled verifier
3623
+ stats.
3624
+
3625
+ ### Changed
3626
+
3627
+ - The minimum MLX version is now 0.32. Fresh installs already resolved MLX
3628
+ 0.32.0; existing runtime environments could stay on 0.31.2 indefinitely
3629
+ because dependency upgrades only run when the declared floor requires
3630
+ them. Long-generation runs consistently read faster on the 0.32 stack,
3631
+ and existing installs now converge to what new installs already run.
3632
+
3633
+ ### Compatibility
3634
+
3635
+ - Model catalogs, defaults, sampler settings, memory policy, and the 2.5.1
3636
+ V2 recommendation are unchanged.
3637
+
3638
+ ## [2.5.1] - 2026-08-03
3639
+
3640
+ Optimized Speed V2 is now the recommended Qwen 3.6 27B model for coding on
3641
+ modern Macs with enough unified memory. It gives users a much higher-quality
3642
+ coding model while keeping the original, smaller Optimized Speed model one row
3643
+ below it.
3644
+
3645
+ ### Added
3646
+
3647
+ - Optimized Speed V2 is a first-class model in onboarding, the app catalog,
3648
+ CLI defaults, quickstart, downloads, served model identity, and OpenCode
3649
+ setup.
3650
+ - The model is published as a dynamic 4-bit hybrid quant with hand-tuned
3651
+ sensitive parts kept at up to 16-bit. It is faster on longer agent tasks,
3652
+ slightly larger, and a little slower for short chats.
3653
+ - RAM-aware defaults recommend V2 first on modern Macs with at least 32 GiB of
3654
+ detected memory. Smaller Macs keep the existing 9B and 4B recommendations.
3655
+
3656
+ ### Compatibility
3657
+
3658
+ - The original Optimized Speed model remains fully supported and appears
3659
+ directly below V2 wherever the machine can run both.
3660
+ - Runtime kernels, sampler defaults, cache behavior, and speculative depth are
3661
+ unchanged from 2.5.0.
3662
+
3663
+ ## [2.5.0] - 2026-08-03
3664
+
3665
+ The next-model release: MTPLX can load new architecture aliases without a
3666
+ hard-coded model-type wait, multi-layer MTP drafts are supported for the
3667
+ upcoming Qwen generation, HY3 becomes a first-class serving target, and the
3668
+ coding-agent bridge is materially more reliable. Experimental DeepSeek V4,
3669
+ Laguna, and GDN speed lanes from David Tai land behind explicit opt-ins; the
3670
+ shipping defaults remain unchanged.
3671
+
3672
+ ### Added
3673
+
3674
+ - Upcoming-Qwen architecture readiness: the native draft path now supports
3675
+ `mtp_num_hidden_layers = N`, and unknown `model_type` values can resolve
3676
+ through their declared architecture class. A synthetic `qwen3_8` alias
3677
+ drill exercised load, generate, CLI, and app discovery before public weights
3678
+ existed; this is compatibility preparation, not a claim that unreleased
3679
+ weights were benchmarked.
3680
+ - HY3 first-class serving: a vendored MTP-capable model class, official
3681
+ defaults, suffixed think-tag handling, model discovery, and native OpenCode
3682
+ tool calls. AR-only exports fail safely to the AR path rather than touching
3683
+ an uninitialized draft head.
3684
+ - Experimental DeepSeek-V4 shape-specialized verification lanes: prebound
3685
+ output-LoRA routes, adaptive speculative width, exact M3 attention
3686
+ projection, sinkhorn and attention-island kernels, and compiled
3687
+ post-attention verifier islands. On the 128 GB M5 Max release machine, the
3688
+ exact 2-bit DQ model plus official MTP shard measured about 31 AR tok/s and
3689
+ 36 MTP tok/s versus about 4/6 tok/s on the conservative path. These routes
3690
+ remain opt-in while broader model-quality validation continues. Contributed
3691
+ by David Tai (@davidtai, #223).
3692
+ - Experimental Laguna S-2.1 `mlx.fast` ports covering decode and prefill
3693
+ kernels, including size-gated prefill MoE combine; fail-loud guards and
3694
+ portable scratch space keep unsupported shapes on the safe route.
3695
+ Contributed by David Tai (@davidtai, #222).
3696
+ - Experimental GDN headquarter execution layout for verify tape capture,
3697
+ env-gated with bit-exact coverage and loud fallback. Contributed by David
3698
+ Tai (@davidtai, #209).
3699
+
3700
+ ### Fixed
3701
+
3702
+ - Coding-agent JSON tool calls can carry large write bodies without the hidden
3703
+ tool guard aborting them, and common near-miss argument keys are repaired at
3704
+ the protocol boundary. This was verified through real OpenCode CLI and
3705
+ Desktop sessions, including a multi-file edit with all generated tests
3706
+ passing.
3707
+ - App and CLI launches now share the same coding-agent engine environment, so
3708
+ a workflow does not silently change behavior depending on which Start button
3709
+ launched it.
3710
+ - `start` and `quickstart --dry-run` report the resolved profile instead of the
3711
+ parser's placeholder default.
3712
+ - Smart-fan mode holds through the post-generation heat-soak window before
3713
+ restoring automatic control, avoiding the early restore that could distort
3714
+ back-to-back performance runs (#227).
3715
+
3716
+ ### Compatibility
3717
+
3718
+ - `transformers` 5.14 is allowed after tokenizer and tool-template parity were
3719
+ verified; the incompatible 5.13.0 release remains excluded (#175).
3720
+ - No speculative-depth, cache, sampler, or model-speed default changed in this
3721
+ release. The opt-in model kernels fail closed to established implementations.
3722
+
3723
+ ## [2.4.2] - 2026-08-02
3724
+
3725
+ The agentic-cache release: the session cache stops losing warm state
3726
+ mid-run, tool-turn commits stop being ghosts, every serve keeps a durable
3727
+ per-request trail by default, an experimental DeepSeek-V4-Flash backend
3728
+ lands, and the documentation now matches the code everywhere it was
3729
+ audited.
3730
+
3731
+ ### Added
3732
+
3733
+ - DeepSeek-V4-Flash: experimental native AR backend
3734
+ (`model_type: deepseek_v4`) — Hyper-Connections, compressed sparse
3735
+ attention, hash-routed MoE, grouped output-LoRA — loading the
3736
+ mlx-community checkpoints directly, with an optional single-block MTP
3737
+ speculative lane when the checkpoint carries `mtp.0.*` weights
3738
+ (spec == AR gated; K=1-3 measured up to 2.28x on the 2bit-DQ build).
3739
+ MTP-declaring checkpoints that ship no draft weights degrade to AR
3740
+ with a clear message instead of failing at bind. Thanks @davidtai
3741
+ (#216).
3742
+ - Request log, default on: every serve writes numeric/hash per-request
3743
+ telemetry to `~/.mtplx/logs/request-log-<port>.jsonl` (64 MB x 4
3744
+ rotation; no prompt or completion content; disable with
3745
+ `MTPLX_REQUEST_LOG_JSONL=off`). Pairs with 2.4.1's opt-in bit-exact
3746
+ request capture to make agent-session incidents diagnosable after the
3747
+ fact (#196/#197). New helpers: `scripts/gauntlet_scoreboard.py`
3748
+ per-session summarizer, `scripts/oc_tap.py` recording proxy and
3749
+ `scripts/oc_tap_diff.py` request-mutation analyzer for content-level
3750
+ wire truth.
3751
+ - Session bank, active-session eviction protection: sessions that
3752
+ touched the bank within `MTPLX_SESSION_BANK_ACTIVE_PIN_TTL_S`
3753
+ (default 600 s) are eviction-last, so cross-session pressure evicts
3754
+ idle victims instead of the session that is mid-run.
3755
+ - Session bank, newest-K per-session snapshot retention
3756
+ (`MTPLX_SESSION_BANK_PER_SESSION_MAX_ENTRIES`, default 3): divergent
3757
+ per-turn sibling snapshots no longer accumulate unreclaimed.
3758
+ `/health` now reports active sessions, the pin TTL, and recent
3759
+ evictions.
3760
+ - Postcommit foreground grace (`MTPLX_POSTCOMMIT_FOREGROUND_GRACE_S`,
3761
+ default 2 s): a nearly-finished background cache commit lands instead
3762
+ of being preempted by the next fast agent-loop request.
3763
+ - Session identity honors `x-session-affinity` / `x-session-id` request
3764
+ headers (OpenCode sends these per request), ending cross-request
3765
+ identity churn on that client.
3766
+
3767
+ ### Fixed
3768
+
3769
+ - Tool-turn "ghost re-prefills": the tool-rewrite async commit rendered
3770
+ a canonical history that matched neither the generation nor the next
3771
+ prompt, burning full-history re-forwards (26.8 s observed) without
3772
+ ever storing. It is disabled pending a byte-proven canonical render
3773
+ (`MTPLX_IDLE_POSTCOMMIT_TOOL_REWRITE` re-enables);
3774
+ store-on-prefill and block salvage cover the lane.
3775
+ - The bridge's convergence guard now states explicitly that editing and
3776
+ verification tools remain allowed and that its restriction covers
3777
+ only the current reply — a model read the old wording as a
3778
+ session-wide tool ban and stalled an entire session.
3779
+ - `mtplx profile thermal`, `profile eval-attribution`,
3780
+ `profile dispatch --trace`, and `thermal fanmax-run` invoked
3781
+ research-workspace scripts that are not part of the distribution, and
3782
+ `--dry-run` printed those phantom paths as runnable commands. They
3783
+ now report availability honestly (exit 2, machine-readable
3784
+ `available: false`) and run the real script when present.
3785
+ - `mtplx doctor`: Python floor corrected to 3.11 (matching
3786
+ `requires-python`); remediation texts no longer tell end users to
3787
+ edit source constants or to move a healthy server off its port;
3788
+ `--port` is documented and, when passed explicitly, aims the server
3789
+ connectivity checks.
3790
+ - Session-bank near-prefix restores on backends with bounded rollback
3791
+ (DeepSeek-V4) pre-check `max_rollback` and fall back to a cold
3792
+ prefill instead of raising (#216).
3793
+ - Help surfaces match their own parsers: the onboarding help no longer
3794
+ promises a Turbo wizard choice that does not exist (Turbo
3795
+ auto-selects for the quantized flagships), `--strict-cold` names the
3796
+ enforced 59 tok/s gate, `--open-dashboard` opens alongside the chosen
3797
+ client (as it always did), and the command reference teaches
3798
+ `mtplx <command> --help`, which also works for multi-word commands.
3799
+
3800
+ ### Documentation
3801
+
3802
+ - Full truth sweep: ~450 documentation claims reconciled against the
3803
+ code across 27 files. Highlights: INSTALL.md no longer references an
3804
+ MLX fork removed in 2.0.0; turbo-verify.md no longer calls the
3805
+ shipped default "experimental, off by default" nor excludes the
3806
+ 6-bit lane that ships; the Anthropic base-URL instruction (docs and
3807
+ the canonical example) no longer 404s; `/metrics` no longer claims a
3808
+ Prometheus mode that never existed; the README modes table shows
3809
+ Turbo as the default for the quantized 27B/9B flagships; the Laguna
3810
+ memory requirement states the real ~85.3 GiB preflight gate;
3811
+ version-era staleness ("v0.1", "preview", v0.3.x runbook pins) is
3812
+ cleared; historical release notes gain bracketed corrections where
3813
+ they documented commands that never worked. Thanks
3814
+ @PhilipJohnBasile for #218 (removed the unsupported MTP-sidecar
3815
+ graft guidance; seeded by #215).
3816
+ - Dependency-record correction: the transformers pin has been
3817
+ `<5.14,!=5.13.0` since shortly after 2.0.0; the changelog never
3818
+ recorded the relaxation from `<5.13`.
3819
+
3820
+ ### Dependencies
3821
+
3822
+ - pypa/gh-action-pypi-publish 1.14.1 -> 1.14.2 (#217).
3823
+
3824
+ ## [2.4.1] - 2026-08-01
3825
+
3826
+ The smooth-streaming release: the app's chat render path is overhauled
3827
+ (no more freeze-then-catch-up stutter, scroll bounce, or plain-text code
3828
+ blocks — real syntax coloring, live code cards, tables, and actual math
3829
+ notation), and the 2.4.0 short-turn regression is fixed.
3830
+
3831
+ ### Added
3832
+
3833
+ - Live syntax coloring for code blocks (12 languages + generic) from a
3834
+ freeze-time lexer that colors each line exactly once; streaming cost
3835
+ is O(new text), never O(document).
3836
+ - Streaming code card: an open fence renders as a live card with
3837
+ colored lines and flips once to its settled form at close.
3838
+ - Pipe tables render as real tables; math renders as real notation
3839
+ (Unicode super/subscripts, stacked matrices and fractions, inline
3840
+ conversion instead of dollar-sign leaks).
3841
+ - Typewriter pacing for streamed text with geometric catch-up and a
3842
+ hard drain bound (`MTPLX_STREAM_TYPEWRITER=0` to disable), and a live
3843
+ tok/s chip computed over a sliding ~5 s window.
3844
+ - Performance mode is a true kill switch: plain text only, through both
3845
+ the streaming and settled render paths.
3846
+ - Opt-in per-request capture for bit-exact failure replay:
3847
+ `MTPLX_REQUEST_CAPTURE_DIR=<dir>` persists each request's
3848
+ reproduction envelope at dispatch time (#196/#197, third layer).
3849
+ - Opt-in frontend stream-performance probe (`MTPLX_UI_PERF=1`, HUD via
3850
+ `MTPLX_UI_PERF_HUD=1`) with a per-turn JSONL trace joinable to engine
3851
+ stats by request id.
3852
+ - Experimental: cost-model speculative-depth policy
3853
+ (`--adaptive-policy cost`) and blocked-sequential GDN prefill
3854
+ (`MTPLX_GDN_BLOCKED_PREFILL=1`). Defaults unchanged.
3855
+
3856
+ ### Fixed
3857
+
3858
+ - 2.4.0 short-turn regression: the compiled-verify path could reserve
3859
+ KV budget above the configured ceiling, taxing short requests with
3860
+ setup work they never used; the reserve is now clamped.
3861
+ - Warming prefills yield to real traffic within one small chunk instead
3862
+ of delaying a freshly arrived request.
3863
+ - Derivative model artifacts whose names extend a first-party model
3864
+ name are served under their own id, not the flagship's — the health
3865
+ payload, OpenAI `model` field, and app model chip now report the
3866
+ artifact actually loaded.
3867
+ - Streaming render: line-segment coalescing keeps realized view count
3868
+ bounded on long answers; the bottom-pin scroll correction runs in the
3869
+ same display cycle as layout so the streaming bubble can no longer
3870
+ visibly bounce; a per-display-cycle window-sizing walk that floored
3871
+ every update at ~50 ms is removed (`MTPLX_APP_SIZING_TUNER=0`
3872
+ restores it).
3873
+
3874
+ ## [2.4.0] - 2026-07-31
3875
+
3876
+ The 35B speed release: the 35B-A3B MoE gets a compiled decode stack and
3877
+ continuous batched serving, the 2.3.0 fan regression is root-caused and
3878
+ fixed, tool calling gets another round of contract hardening, and
3879
+ structured output can no longer be eaten by an unbounded reasoning
3880
+ prelude. Four community contributors landed code in this release.
3881
+
3882
+ ### Added
3883
+
3884
+ - 35B-A3B compiled decode stack: target-prefix compiled route, whole-MoE
3885
+ fusion, GDN post-conv fusion, and a row-owned router (David Tai, #174).
3886
+ - Continuous batched serving for the A3B lane: fixed-shape cohorts,
3887
+ ragged KV, fold-in repair, and AR row-packing (David Tai, #200).
3888
+ - Laguna S-2.1 support (exact-pin oQ4e, AR-only) with an app catalog
3889
+ entry, plus a Poolside `arg_key`/`arg_value` tool-call dialect parser
3890
+ (David Tai, #195).
3891
+ - Hy3 295B full-residency lane and generic MTP draft-contract hardening
3892
+ whose loud recurrent-cache failure also caught a real bug on the Qwen
3893
+ lane (David Tai, #208).
3894
+ - `/health` now reports smart-fan restore state (`restore_verified`,
3895
+ `restore_failures`, `stale_leases_reconciled`) so stuck-fan reports
3896
+ are diagnosable from the field (#201).
3897
+
3898
+ ### Fixed
3899
+
3900
+ - Fans no longer stay pinned at max after a request ends (#201). A
3901
+ failed fan restore was logged once and then treated as restored, so
3902
+ the hardware stayed ramped while the server believed it was clean;
3903
+ restores now verify the fan rows are back on the Apple auto curve and
3904
+ retry with backoff until they are. The ThermalForge daemon-socket
3905
+ restore path no longer trusts the daemon's "ok" reply without
3906
+ verifying, and falls back to the CLI in the same call. A stale-lease
3907
+ watchdog drops any fan lease held while the engine has been
3908
+ continuously idle (default 120s, `MTPLX_SMART_FAN_STALE_LEASE_S`).
3909
+ - A generation cut by `max_tokens` mid-tool-call now reports
3910
+ `finish_reason: "length"` instead of `"tool_calls"`, so agent clients
3911
+ continue the turn instead of executing a truncated call (#196, #197
3912
+ layers one and two).
3913
+ - The think-splitter no longer leaks reasoning into visible content when
3914
+ the text contains bare `function=` or `parameter=` strings (#196/#197
3915
+ companion fix).
3916
+ - Streaming tool-call parsing handles bracket-style dialects with a
3917
+ balanced string/escape-aware scanner, buffers incomplete calls instead
3918
+ of double-delivering them, and passes through calls to undeclared
3919
+ tools per the OpenAI contract instead of dropping them (David Tai,
3920
+ #195).
3921
+ - Constrained generation bounds the `<think>` prelude at 4000 characters
3922
+ (`MTPLX_THINK_PRELUDE_MAX_CHARS`, 0 restores unbounded), so an
3923
+ unclosed think block can no longer consume the entire token budget and
3924
+ return no document (Jozef Kristek, #213).
3925
+ - Forge model probes recover from slow Hugging Face config responses:
3926
+ 30s timeout, pinned-SHA retry, positive-MTP-only indexed acceptance,
3927
+ and revision-string validation (Philip John Basile, #210).
3928
+
3929
+ ### Changed
3930
+
3931
+ - Dependency bumps: pillow 12.3.0, actions/checkout 7.0.1,
3932
+ actions/setup-python 7.0.0, pypa/gh-action-pypi-publish 1.14.1.
3933
+
3934
+ ## [2.3.0] - 2026-07-21
3935
+
3936
+ The agent reliability release: the #170 tool-argument collapse is
3937
+ root-caused and fixed, structured output ships with full speculative
3938
+ speed (community-contributed), agent sessions keep their warm cache
3939
+ through client history rewrites, and eight findings from an independent
3940
+ source review of v2.2.0 are fixed, each with a regression test that
3941
+ fails on the pre-fix code.
3942
+
3943
+ ### Fixed
3944
+
3945
+ - Agent tool calls with nested arguments no longer collapse to empty `{}`
3946
+ arguments (#170). The streaming Qwen-XML tool parser silently discarded
3947
+ function-body text that was not wrapped in `<parameter=>` blocks, so a
3948
+ model that wrote its arguments as a JSON object inside the
3949
+ `<function=...>` envelope — the common slip on nested `edits` arrays —
3950
+ produced a schema-valid call with empty arguments on requiredless tools
3951
+ and a silently vanished call otherwise. Both the streaming and the final
3952
+ parser now accept a pure JSON-object function body as the arguments
3953
+ payload, and every unwrapped, lead, or trailing text lane is a loud
3954
+ protocol fallback with identical contracts in the two parsers.
3955
+ - The injected tool contract no longer shows degenerate `[]` examples for
3956
+ array parameters: exemplars are populated from the item schema's own
3957
+ keys (an `edits` array renders as
3958
+ `[{"search": "ARGUMENT_VALUE", "replace": "ARGUMENT_VALUE"}]`), removing
3959
+ an in-prompt template for empty-array emissions.
3960
+ - The final tool-call extraction lane (non-stream parsing and the stream
3961
+ fallback) no longer fabricates `{}`-argument calls out of function bodies
3962
+ it could not read — live-reproduced on v2.2.0 as a `grep` call arriving
3963
+ with empty arguments. All extraction dialects now share the strict
3964
+ parsers' contract: a pure JSON-object function body is the arguments
3965
+ payload, an unreadable body stays visible content, blank-body no-argument
3966
+ calls still parse, and partial arguments are delivered exactly as the
3967
+ model wrote them (schema validation remains the client's job, per the
3968
+ OpenAI protocol).
3969
+ - Context-copy speculative rounds now stop accepting at the first
3970
+ accepted stop token, exactly like the MTP acceptance loop. Previously
3971
+ the copy lane could accept an entire block past a stop, leaving the
3972
+ target cache, logits/hidden selection, and MTP history advanced beyond
3973
+ the emitted response while the final state was still marked safe to
3974
+ commit — a session-cache poisoning risk on recurrent (GDN) models.
3975
+ - Context-copy is prompt-only again at the boundary: proposal blocks are
3976
+ sliced from the prompt and capped at its edge instead of running into
3977
+ the model's own generated output (the self-repetition case the feature
3978
+ contract excludes). Boundary-less candidates are skipped inside the
3979
+ n-gram index so the best *valid* match still fires.
3980
+ - Non-divisor packed MTP quantization widths (5-bit, 3-bit, 6-bit) now
3981
+ infer the correct group size via total-bit arithmetic (#182; fix by
3982
+ @Jonathangadeaharder in #183). A 5-bit group-64 head previously
3983
+ inferred group 60, overwrote the artifact's correct declared contract,
3984
+ and made the model unloadable.
3985
+ - Long-running daemons no longer accumulate unbounded telemetry: the
3986
+ SessionBank eviction log is a bounded ring, the scheduler's
3987
+ started-by-batch-key counter collapses per-session key suffixes to
3988
+ stable classes, the dashboard per-session TPS map is LRU-bounded, and
3989
+ the OpenCode title fast path trims request metrics like every other
3990
+ lane (#145-adjacent slow-creep hygiene).
3991
+ - The desktop Hugging Face probe now recognizes rootless assistant-pair
3992
+ bundles (`mtplx_pair.json` + `target/config.json`) the same way the
3993
+ Python runtime does, so the official Gemma4 pair repos classify as
3994
+ ready to download instead of unreadable (#107); Forge routes them to
3995
+ install-instead-of-rebuild.
3996
+ - `mtplx doctor --json` stdout is now guaranteed machine-parseable: the
3997
+ report is built with stdout routed to stderr, so third-party lazy
3998
+ importers that print() their errors (huggingface_hub does, whenever its
3999
+ HTTP dependencies are broken or split across install locations) can no
4000
+ longer prefix the JSON document with prose and break `--json` consumers.
4001
+ - Agent tool-loop sessions no longer lose their warm cache when the
4002
+ client rewrites history (transcript compaction, retroactive tool-result
4003
+ digests): a common-prefix identity fallback (≥4096 shared tokens and
4004
+ ≥25% of the prompt) keeps the session id instead of minting an
4005
+ anonymous one, ending the cold full-context re-prefills those
4006
+ rotations caused mid-session.
4007
+ - Fan restore goes through the ThermalForge daemon socket instead of the
4008
+ app-killing CLI path, and `mtplx pull` detects interrupted downloads
4009
+ as incomplete instead of treating them as ready (thanks @titan550,
4010
+ #178/#179/#180/#181).
4011
+
4012
+ ### Changed
4013
+
4014
+ - `parallel_tool_calls` is now honored: a request that declares it gets
4015
+ the declared behavior in both directions (false → at most one tool
4016
+ call per turn), with the previous client-profile heuristic kept only
4017
+ as the fallback when the field is absent (thanks @PhilipJohnBasile,
4018
+ #190 — Android Studio declares false today and was being ignored).
4019
+ - Agent tool contract v13: instructs whole-file reads (the old
4020
+ "smallest read range" clause provoked storms of 1-to-5-line
4021
+ micro-reads) and forbids echoing file contents into visible text
4022
+ (double-emitted file bodies inflated agent turns by tens of
4023
+ thousands of characters). Measured on live agent sessions: thinking
4024
+ share of generated tokens 75% → 6-29%, double emissions eliminated.
4025
+
4026
+ ### Added
4027
+
4028
+ - Structured output: `response_format` `json_object` and `json_schema`
4029
+ are now enforced with llguidance token masks instead of silently
4030
+ ignored — and the grammar composes with the MTP verify loop, so
4031
+ constrained requests keep full speculative speed (measured: decode
4032
+ parity within noise, under 5ms total mask cost per request). On
4033
+ thinking templates a prelude grammar lets reasoning finish before the
4034
+ document is forced. Opt-in strict tool-call grammars
4035
+ (`MTPLX_TOOL_CALL_STRICT=1`) force every tool call to a declared tool
4036
+ name with schema-valid arguments — a real OpenCode session built a
4037
+ complete project through the strict lane with zero malformed calls.
4038
+ Thanks @PhilipJohnBasile (#186, #187, #188). Requires the `[server]`
4039
+ extra, which the desktop app installs by default; bare pip installs
4040
+ get a clear 400 with an install hint.
4041
+ - Durable per-request telemetry: `--request-log-jsonl` (env
4042
+ `MTPLX_REQUEST_LOG_JSONL`) appends every request record as one JSON
4043
+ line, and `scripts/session_forensics.py` correlates that log with an
4044
+ OpenCode database into a single timeline with detectors for re-prefill
4045
+ rewinds, TTFT stalls, thinking marathons, double emissions,
4046
+ session-identity rotations, and usage mismatches.
4047
+ - An **opt-in** agent-lane reasoning budget (`--agent-thinking-budget`,
4048
+ env `MTPLX_THINKING_BUDGET`; OFF by default): at the budget the
4049
+ reasoning segment is force-closed with a visible bridge so the turn
4050
+ proceeds to its answer or tool call, and every engagement is surfaced
4051
+ per-request in telemetry (`thinking_guard`). Below the budget decoding
4052
+ is bit-exact; plain chat is never touched.
4053
+ - Stream stall watchdog (#86 containment): if a stream receives nothing
4054
+ while the model owner's progress heartbeat is frozen for
4055
+ `MTPLX_STREAM_STALL_DEADLINE_S` (default 300s, 0 disables), the
4056
+ request fails with a structured, diagnosable error and releases its
4057
+ slot instead of hanging forever. Healthy long prefills and model loads
4058
+ tick the heartbeat and are never affected; the daemon is never killed.
4059
+ - `pip install mtplx` now includes Pillow, matching the advertised image
4060
+ support in the app and server (#103); previously vision failed at
4061
+ import unless the `[server]` extra was installed.
4062
+
4063
+ ## [2.2.0] - 2026-07-19
4064
+
4065
+ The copy-drafting and small-Mac release. Decoding: context-copy
4066
+ (prompt-lookup) drafting lands on by default (PR #151 by lBroth) with an
4067
+ exact temperature path — copied blocks are accepted with the target's own
4068
+ shaped probability, so the output distribution is unchanged at any
4069
+ temperature; measured +53% on edit-heavy agent turns at temp 0.6, parity
4070
+ on novel text (disable with MTPLX_CONTEXT_COPY=0). Models: the 4B
4071
+ zero-acceptance defect (#176) is root-caused and fixed — the engine heals
4072
+ raw delta-encoded MTP sidecars at load so existing downloads recover
4073
+ without re-downloading, the 4B Speed artifact is rebuilt (227.8 tok/s D3
4074
+ on M5 Max, 1.71x AR), and a new 4B Quality artifact ships (191.7 tok/s
4075
+ D3, 2.19x — the largest MTP multiplier in the fleet); sub-16GB Macs get
4076
+ first-class catalog recommendations. Tune (#177): a 0.0-acceptance depth
4077
+ can never win, be saved, or be replayed, and poisoned records are
4078
+ quarantined at load. Forge: FP16 precision option with M1/M2 auto-select
4079
+ (#166); the fp16 cast can no longer corrupt a sidecar in place;
4080
+ degenerate sidecars are quarantined on re-forge. Server: SSD session
4081
+ writer crash fixed — encode at enqueue (#169); live requests preempt idle
4082
+ cache maintenance. MoE: mtp_depth_max is a ceiling, not the default; the
4083
+ 35B-A3B launches at its measured D2 (#174 part 1 by davidtai). Also: the
4084
+ Qwen 3.6 27B AR decode-trace crash fix (#167 by davidtai), truthful
4085
+ per-model profile display, hybrid-model boundary retention across append
4086
+ churn, and an experimental --draft-core device (opt-in). Full details in
4087
+ docs/releases/v2.2.0.md.
4088
+
4089
+ ## [2.1.0] - 2026-07-17
4090
+
4091
+ The community-fixes release. Memory: the v2.x reports are root-caused and
4092
+ closed (MLX allocator cache bounded by default, per-session admission
4093
+ re-clamped on sub-96GB machines, paged pool bounded by the context
4094
+ window, pressure responder redesigned, q4 kv-quant crash fixed, new
4095
+ `--memory-budget` knob). Agent sessions: warm prefix reuse survives every
4096
+ tool turn (#121), hybrid-model near-prefix restores no longer collapse to
4097
+ the oldest boundary (measured 0.4s instead of 33.8s on a 22k follow-up),
4098
+ restart-warm sessions keep their boundary records across SSD generations
4099
+ (#159, #144), and cache hits are reported in standard `usage` fields.
4100
+ Sampling: presence and frequency penalties fixed in the batched AR lane
4101
+ (#156). App: startup and update hang fixed plus a full subprocess
4102
+ watchdog sweep (#158), the Hermes tile launches Hermes Desktop, raw
4103
+ tool-call XML no longer leaks into no-tools chats (#160). CLI: `start
4104
+ opencode` serves the same lane the app serves. Backends: qwen3_5_mtp and
4105
+ hy_v3 land (#142, #147). Performance: the model-owner thread is
4106
+ QoS-pinned for 8 to 10% faster decode under real multitasking load.
4107
+ Operators: `MTPLX_COMPILED_VERIFY_MAX_CONTEXT` is env-overridable. Full
4108
+ details in docs/releases/v2.1.0.md.
4109
+
4110
+ ## [2.0.2] - 2026-07-09
4111
+
4112
+ The agent-reliability release: multi-turn agent sessions now render
4113
+ reasoning history on the model's trained contract (the fix for the
4114
+ plan-execution repetition marathons), LAN serving works from the app,
4115
+ and every agent client gets warm prefix reuse.
4116
+
4117
+ ### Changed
4118
+
4119
+ - Reasoning history is now scoped to the active agent round by default
4120
+ on Qwen3.6/3.5 models, matching Qwen's trained multi-turn contract.
4121
+ The Qwen chat template keeps `<think>` blocks only for assistant
4122
+ messages after the last real user query (its built-in "rolling
4123
+ checkpoint"); MTPLX used to override that with `preserve_thinking`,
4124
+ which rendered an off-contract empty `<think>` scaffold on every
4125
+ completed assistant turn and replayed stale inline reasoning across
4126
+ turns, while silently dropping the structured `reasoning_content`
4127
+ fields agent clients such as OpenCode send. Scoped mode lets the
4128
+ template's own checkpoint govern: completed turns render with no
4129
+ think scaffold, and the active round (assistant -> tool -> assistant
4130
+ chains) keeps its reasoning, now including structured
4131
+ `reasoning_content` - strictly better in-round continuity than
4132
+ before. `--preserve-thinking on` restores the previous behavior
4133
+ byte-for-byte (including cache identity), `off` still strips
4134
+ everything, and the new explicit `scoped` value pins the scoped mode.
4135
+ Templates without the rolling checkpoint (Gemma 4, custom templates)
4136
+ keep the previous preserve-all behavior. The resolved policy is shown
4137
+ at startup ("Reasoning history: scoped (active round only)") and as
4138
+ `reasoning_history_mode` in `/v1/mtplx/settings` and the snapshot.
4139
+
4140
+ ### Added
4141
+
4142
+ - Loop Guard (opt-in): `MTPLX_LOOP_GUARD=1` enables a loop-armed
4143
+ anti-repetition steering mode for models prone to verbatim repetition
4144
+ marathons (for example repetition-damaged third-party quants). Unlike
4145
+ a static presence penalty, the guard is completely inert until a real
4146
+ loop is detected mid-response (bit-exact sampling otherwise, MTP
4147
+ acceptance math unchanged), then penalizes only the tokens that would
4148
+ extend a verbatim repeat and disarms once the loop is broken. Content
4149
+ inside tool calls is never steered (code legitimately repeats short
4150
+ token runs, so tool-call spans are masked token-exactly). The default
4151
+ is OFF: no synthetic steering touches sampling unless you ask for it,
4152
+ and the repetition fix that matters for MTPLX's own models is the
4153
+ scoped reasoning history change above. Detector/steering knobs are
4154
+ documented in `mtplx/loop_guard.py`; per-request guard activity is
4155
+ visible under `loop_guard` in `/v1/mtplx/snapshot`.
4156
+
4157
+ ### Fixed
4158
+
4159
+ - Hidden reasoning no longer leaks into visible chat content when a
4160
+ long reasoning tag (e.g. `</reasoning>`) splits across a streaming
4161
+ chunk boundary; the splitter held back too few bytes for tags longer
4162
+ than `</think>` (PR #149 by @Osamaali313).
4163
+ - `mtplx` quickstart onboarding screens now display the requested
4164
+ `--host`/`--port` in the Web UI and dashboard URLs instead of a
4165
+ hardcoded `http://127.0.0.1:8000` (PR #148 by @hasegaw).
4166
+ - Ctrl-C now returns control to the terminal within a bounded delay
4167
+ even when a browser tab holds an open chat/dashboard stream. The
4168
+ server previously waited forever for infinite SSE generators to
4169
+ finish; shutdown now drains in-flight requests with a 5-second
4170
+ deadline, and thermal/fan cleanup still runs (#124).
4171
+ - Serving on any host and port now works from the app. Setting host
4172
+ 0.0.0.0 (LAN serving) used to misreport free ports as occupied, bump
4173
+ the port, and kill the healthy daemon after a health-wait timeout,
4174
+ because the app probed the bind address verbatim; all app-side
4175
+ connections now resolve wildcard binds to a connectable loopback
4176
+ address (#109). The app also surfaces the "LAN serving requires an
4177
+ API key" rule before launch instead of a generic Degraded state, an
4178
+ API-key mismatch reads as a live-but-unauthorized daemon instead of
4179
+ a lost one, and the port-in-use preflight tests the address family
4180
+ the daemon will actually bind.
4181
+ - The app now passes the SSD session cache setting to the daemon
4182
+ explicitly, including "Off". Since 2.0.0 flipped the serve default to
4183
+ on, an explicit Settings "Off" was silently re-enabled with default
4184
+ limits on app-launched daemons, and `~/.mtplx/session-bank` kept
4185
+ growing (#140; also the "session-bank came back after I deleted it"
4186
+ half of #138). Generated `mtplx start` server commands carry the
4187
+ explicit `--ssd-session-cache off` for the same reason.
4188
+ - The app's runtime venv now self-heals after app updates. A venv whose
4189
+ base-interpreter symlink pointed into a replaced app bundle made every
4190
+ reinstall fail with "[Errno 2] No such file or directory:
4191
+ .../runtime-venv/bin/python3" and no DMG reinstall could fix it; venv
4192
+ creation now rebuilds with `--clear` when the existing venv python is
4193
+ broken or creation fails (#139).
4194
+ - Warm prefix reuse no longer freezes on the oldest short prefix for
4195
+ agent harnesses outside OpenCode (Pi, little-coder, Hermes, custom
4196
+ clients). The block-prefix restore lane was still gated to OpenCode's
4197
+ compact tool contract even though kvcache-v2's boundary-true restores
4198
+ made it exact for every client, so transcripts whose turns diverge
4199
+ more than a few tokens before the stored end re-prefilled a growing
4200
+ suffix every turn (#138). The lane now engages for all clients while
4201
+ boundary-true restore is on; `MTPLX_SESSION_BLOCK_PREFIX_RESTORE=0`
4202
+ still disables it.
4203
+
4204
+ ## [2.0.1] - 2026-07-07
4205
+
4206
+ Turbo for every Mac. The v2 turbo default now covers every dense catalog
4207
+ model on every Apple Silicon generation, with a load-time kernel
4208
+ self-validation safety net.
4209
+
4210
+ ### Added
4211
+
4212
+ - 6-bit affine verify kernels (split-K hexpack family): the 6-bit 9B tier
4213
+ gains 33-62% decode and 43% 2k-prefill under turbo (M5 Max, verified
4214
+ arms). Qwen 3.5 9B and 9B FP16 now default to turbo.
4215
+ - New model: `Youssofal/Qwen3.6-27B-MTPLX-Optimized-Quality-FP16`, the
4216
+ missing M1/M2 quality artifact. Wired into the app picker, CLI catalog,
4217
+ and chip-aware routing (a Quality pick on M1/M2 resolves the FP16
4218
+ sibling, mirroring the speed lane). Validated against the bf16 parent
4219
+ and measured at 2.5x over true AR under turbo.
4220
+ - Load-time kernel self-validation: every turbo lane checks itself against
4221
+ stock MLX in the model's exact dtype/quantization at boot; a mismatching
4222
+ lane falls back to the stock path for the session and the verdicts are
4223
+ surfaced as `kernel_selfcheck` in `/health`. Worst case is v2.0.0 speed,
4224
+ never wrong output.
4225
+ - `MTPLX_FORCE_GPU_FAMILY_FALLBACK=1` rehearses the exact M1-M4 code path
4226
+ on newer machines; `MTPLX_KERNEL_SELFCHECK=0` disables the probe.
4227
+ - CI kernel matrix on real M1 runners: kernel exactness across
4228
+ {bf16, fp16} x {4, 6, 8}-bit plus a live 4B turbo boot smoke.
4229
+
4230
+ ### Changed
4231
+
4232
+ - 27B Optimized-Speed-FP16 (the M1/M2 routing target) defaults to turbo:
4233
+ 19-31% faster decode than the v2.0.0 sustained default across 0.5k-32k
4234
+ context; true-AR multiplier 1.34x to ~2x. First-ever e2e measurement of
4235
+ this artifact; exactness gated (30/30 hot-shape logit-diff cases on real
4236
+ weights, greedy match turbo vs stock).
4237
+ - The published Speed-FP16 model card now recommends the turbo profile.
4238
+
4239
+ ### Unchanged on purpose
4240
+
4241
+ - 35B A3B MoE and Gemma 4 keep sustained (expert layers / assistant-pair
4242
+ architecture bypass these kernels; named 2.0.2 lanes). The 4B keeps
4243
+ sustained (turbo measured slightly slower at matched depth). Compiled
4244
+ verify stays off for 6-bit models.
4245
+
4246
+ ## [2.0.0] - 2026-07-06
4247
+
4248
+ MTPLX v2: the coding-agent release. Session-cache v2 (RAM + SSD), the
4249
+ turbo profile with NAX verify kernels and compiled verify, a new verify
4250
+ attention kernel wave for long context, and a long campaign of
4251
+ OpenCode/agent-bridge fixes measured on real sessions.
4252
+
4253
+ ### Added
4254
+
4255
+ - Session-cache v2: boundary-true GDN restores, O(1) RAM restores, SSD
4256
+ cold tier (default on) that survives daemon restarts — a 100k-token
4257
+ session restores in ~2s after a restart instead of a five-minute cold
4258
+ prefill. Prompt-cache reuse now chains across agent tool rounds.
4259
+ - Turbo profile: verify-specialized quantized-matmul kernels
4260
+ (`MTPLX_NAX_VERIFY`, vk_k/vk-q8 families) plus context-routed compiled
4261
+ verify with a per-model quantization gate. Measured on M5 Max: 27B
4262
+ Optimized-Speed 44.7 -> 58-60 tok/s chat lane; Optimized-Quality (q8)
4263
+ 31-36 -> 43-44 tok/s.
4264
+ - The quantized 27B flagships (Optimized-Speed, Optimized-Quality, and
4265
+ the legacy Optimized hybrid) now default to the turbo profile on the
4266
+ CLI and the bare OpenAI API — the same launch rule the macOS app
4267
+ applies. Explicit `--profile` flags and wizard picks still win; other
4268
+ models keep the sustained default.
4269
+ - `POST /admin/cache/clear` resets the MLX peak-memory counter after
4270
+ dropping the session bank, so per-request `peak_memory_bytes` reports
4271
+ the current phase instead of a process-lifetime ratchet (benchmark
4272
+ harnesses that clear between context rows now chart honest per-row
4273
+ peaks).
4274
+ - `--scheduler-mode ar_batch` now genuinely admits anonymous OpenAI
4275
+ clients into the concurrency-adaptive batch lane (lone requests keep
4276
+ solo MTP; real concurrency shares the batched AR decode lane), and
4277
+ the batched lane samples on the GPU (decode-heavy batch-8 aggregate
4278
+ 70.9 -> 79.2 tok/s). Serial remains the default: measured end to end,
4279
+ serialized solo-MTP still beats batched AR on prefill-heavy
4280
+ concurrent loads because MTP decode is ~4x faster per stream.
4281
+ - Long-context decode wave: a packed-GQA verify attention kernel plus
4282
+ commit-first KV donation in compiled verify. Measured on M5 Max
4283
+ (Optimized-Speed): 64k decode +12%, 128k decode 17 -> 20+ tok/s, and
4284
+ peak memory down 8 GB at 64k / 16 GB at 128k.
4285
+ - Startup warming without the wait: the daemon is ready in ~2s and the
4286
+ deeper kernel/shape warmup continues silently in the background,
4287
+ yielding instantly to real requests. First messages hit warm kernels
4288
+ without a slow boot.
4289
+ - RAM session-cache budget now scales to the machine (roughly half the
4290
+ RAM headroom above the model) instead of a flat cap, and the app's
4291
+ Settings tab exposes explicit RAM and SSD cache limits.
4292
+ - Per-request presence/frequency penalties end-to-end (server, CLI
4293
+ flags, dashboard slider, app dial), with MLX on-device penalty math.
4294
+ - App chat: markdown renders live during streaming at zero per-token
4295
+ cost; each turn gets one compact activity strip with grouped tool
4296
+ rounds and a sources footer for web results; turbo is a first-class
4297
+ mode in Settings.
4298
+ - Tool contracts are date-anchored (web-search answers stop regressing
4299
+ to the training cutoff) and post-search answers are no longer
4300
+ clipped to one sentence.
4301
+ - Vision: images flow through the OpenAI API into MTP decode; MoE
4302
+ multimodal checkpoints that store the tower under `model.visual.*`
4303
+ (for example Ornith 1.0) are now recognised (community PR #134).
4304
+ - Gemma 4 assistant-pair models default to their measured-best MTP
4305
+ depth; explicit `--depth` still wins.
4306
+
4307
+ ### Fixed
4308
+
4309
+ - Fresh installs no longer crash at model load: transformers 5.13.0
4310
+ broke mlx-lm's import (`AutoTokenizer.register` string key), which
4311
+ killed every new install and DMG first run. mtplx now pins
4312
+ `transformers<5.13` (#135, #136, community PR #137).
4313
+ - The app no longer kills a healthy engine. The health watchdog
4314
+ treated a response it could not parse the same as a dead server and
4315
+ terminated the daemon mid-session — the main driver of "Stream
4316
+ offline" / "server dies mid-session during agent workloads" reports
4317
+ (#105). Liveness is now transport truth: if the daemon answers, it
4318
+ lives.
4319
+ - SSD session-cache restores are boundary-true for recurrent (GDN)
4320
+ layers, fixing corrupted agent output after a prefix restore (prompt
4321
+ recitation, phantom tool calls, argument leakage) (#130).
4322
+ - Vision + MTP: the draft head's committed history now consumes the
4323
+ spliced vision rows instead of image-pad embeddings, fixing
4324
+ fabricated visual differences between similar screenshots (#103).
4325
+ - Smart fan control ramps at request arrival, verifies actual RPM, and
4326
+ holds through the post-response cache work instead of dropping to
4327
+ auto while the GPU is still pinned (#127).
4328
+ - The app no longer rewrites the Hermes profile config on every
4329
+ launch; user sections (memory/providers/delegation/auxiliary) are
4330
+ merge-preserved (#131).
4331
+ - Served model identity is contract-match-only: third-party builds no
4332
+ longer get coerced onto official `mtplx-*` model ids (#57).
4333
+ - OpenCode plan -> build mode switch no longer breaks the prompt cache
4334
+ or hides file tools. The bridge misread OpenCode's build-mode
4335
+ reminder ("no longer in read-only mode") as a read-only instruction,
4336
+ hid write/edit exactly when the user said "execute the plan", and the
4337
+ model spiralled re-planning files it could not create. OpenCode
4338
+ toolsets now pass through byte-stable; the negation is parsed
4339
+ correctly for other clients.
4340
+ - Agent transcripts render prefix-stable across rounds (historical
4341
+ bytes never rewrite), force-answer and Pi-convergence contracts ride
4342
+ as pure suffixes, and warm prefills inherit recurrent boundaries —
4343
+ together these take mid-session tool rounds from multi-second cold
4344
+ re-prefills to sub-2s warm restores.
4345
+ - One busy OpenCode conversation no longer evicts every other project
4346
+ from the RAM session cache (prefix-superseded entries + a wider
4347
+ high-memory entry budget); multitasking across projects keeps each
4348
+ project's cache warm.
4349
+ - `mtplx start`'s live-dashboard handoff no longer crashes on an
4350
+ ImportError (community PR #133).
4351
+ - The batch scheduler no longer accumulates every finished request
4352
+ forever (community PR #132).
4353
+ - Prefill disconnect-cancel: closing an agent client mid-prefill frees
4354
+ the engine immediately instead of finishing a 48k-token orphan.
4355
+ - CJK and dead-key input no longer drops composed characters in the
4356
+ app's chat composer (community PR #119).
4357
+ - Dense-layout prefill chunk cache-cleanup cadence relaxed 1 -> 4:
4358
+ 5-21% prefill TPS, memory byte-identical.
4359
+
4360
+ ### Changed
4361
+
4362
+ - Bumped to 2.0.0. The default OpenCode/agent daemon profile is turbo
4363
+ with the compiled-verify per-model gate; q8 Quality stays on the
4364
+ eager verify path it measures best on.
4365
+ - Removed the vestigial "required MLX fork" metadata from all profiles.
4366
+ MTPLX runs on stock PyPI MLX and always has in the shipped product;
4367
+ the speed stack (NAX verify kernels, packed-GQA verify attention,
4368
+ compiled verify) ships as in-package Metal kernels, not a patched
4369
+ MLX/qmm build. Profile payloads no longer carry
4370
+ `required_mlx_fork_commit`/`required_mlx_fork_fragment`, `/health`
4371
+ now reports a plain `mlx_runtime` diagnostic instead of a fork
4372
+ expectation, and `--strict-fast-path` /
4373
+ `--strict-mlx-fork-assert` are accepted as deprecated no-ops.
4374
+
4375
+ ## [1.0.4] - 2026-06-12
4376
+
4377
+ Same-day hotfix: 1.0.3 broke coding agents on their first tool turn,
4378
+ and pasting a GGUF repo got a wrong answer.
4379
+
4380
+ ### Fixed
4381
+
4382
+ - Coding agents crashed on 1.0.3. Any tool-using client (Pi, Hermes,
4383
+ OpenCode, or anything speaking the OpenAI tools protocol) hit
4384
+ "unexpected keyword argument 'vision_splice'" on its first tool
4385
+ turn after a cache miss: non-streaming callers got a 500, streaming
4386
+ agents lost the stream, and the app reported "Stream offline." One
4387
+ stray argument left behind by the vision work, in a diagnostics
4388
+ call only agent tool turns reach. Removed, with tests that drive
4389
+ the exact path and an audit test that fails if any call ever passes
4390
+ an argument its target does not accept (#99, #100).
4391
+ - Pasting a GGUF repo into "Add a model from Hugging Face" claimed
4392
+ the repository did not exist. The app now says what is actually
4393
+ going on: GGUF is llama.cpp's format and MTPLX runs MLX models. It
4394
+ names the source repo the GGUF was made from so Forge can convert
4395
+ it, and genuine typos get "check the name" instead.
4396
+ - The "Add a model" repo check now follows the configured Hugging
4397
+ Face download mirror instead of always probing huggingface.co, so
4398
+ it works on networks where huggingface.co is blocked.
4399
+
4400
+ ## [1.0.3] - 2026-06-12
4401
+
4402
+ The app can see. Vision lands across the Qwen models, and the
4403
+ compatibility gate stops blocking models that run fine.
4404
+
4405
+ ### Added
4406
+
4407
+ - Vision support in chat and the API. Attach PNG, JPEG, or WebP images
4408
+ in the app composer, or send OpenAI image_url content parts to
4409
+ /v1/chat/completions, and the model describes what it sees with MTP
4410
+ speculative decoding still running on top. Works on Qwen 3.6 27B
4411
+ (Speed and Quality), Qwen 3.6 35B, and Qwen 3.5 9B. The 9B repo on
4412
+ Hugging Face regained its vision weights; an explicit mtplx pull now
4413
+ syncs such repo updates into existing local copies automatically.
4414
+ - /health reports whether the loaded model supports vision, and the
4415
+ composer adapts to it.
4416
+
4417
+ ### Fixed
4418
+
4419
+ - Models that run fine are no longer refused for paperwork. The
4420
+ compatibility gate treated unverified runtime contracts (including
4421
+ the official Optimized Quality build) and even "slower than AR"
4422
+ speed evidence as reasons not to load. Verification is now a label:
4423
+ unverified models load with an honest note, and refusals are
4424
+ reserved for models that genuinely cannot execute (#98).
4425
+ - The gate's explanation message crashed with a traceback instead of
4426
+ printing since 1.0.0. It prints again, including the hint that was
4427
+ supposed to unblock you (#98).
4428
+ - Image attachments preview their actual pixels in the composer and
4429
+ the transcript instead of a "Could not read" placeholder.
4430
+
4431
+ ## [1.0.2] - 2026-06-11
4432
+
4433
+ Bug-fix release with one small feature.
4434
+
4435
+ ### Fixed
4436
+
4437
+ - Choosing the Auto or Sustained Max profile in the app's Settings left
4438
+ the engine unable to start, showing Degraded on every launch until
4439
+ the profile was changed back. Both values now resolve to real
4440
+ profiles (Sustained Max keeps its pinned-fans intent as the fan mode
4441
+ setting), existing configurations heal themselves on load, and the
4442
+ picker only offers values the engine accepts. `mtplx serve --profile
4443
+ auto` works from the command line too.
4444
+ - Parallel requests from agent tools that do not send session ids could
4445
+ fail with "session anon-... is already in flight" when they shared a
4446
+ prompt prefix. Busy sessions now fork to a fresh session instead of
4447
+ erroring, and anonymous session ids are random rather than clock
4448
+ derived. Reported and fixed by Frank Denis (@jedisct1) in #95.
4449
+ - A daemon launch that lost its port (another server bound it between
4450
+ checks, or a listener invisible to the local probe held it) now
4451
+ remediates and retries once before reporting a failure, and the
4452
+ failure message names the occupant when it can.
4453
+
4454
+ ### Added
4455
+
4456
+ - Optional Hugging Face download mirror for networks where
4457
+ huggingface.co is blocked (requested from mainland China in #96). Set
4458
+ it inline in the onboarding download step or later in Settings under
4459
+ Advanced; downloads and the engine then use the mirror endpoint. The
4460
+ stored HF token is never sent to a mirror, so gated repos stay on the
4461
+ official endpoint.
4462
+
4463
+ ## [1.0.1] - 2026-06-11
4464
+
4465
+ Bug-fix release.
4466
+
4467
+ ### Fixed
4468
+
4469
+ - First-run tuning no longer fails on Macs where fan control cannot
4470
+ verify a max ramp (for example when the passwordless helper grant is
4471
+ not in place yet). Tuning now runs with fans on automatic, the
4472
+ results are labeled accordingly, and `--require-max-fans` keeps the
4473
+ strict behavior for benchmarking.
4474
+ - The `mtplx` CLI accepts the official Gemma 4 assistant-pair repos
4475
+ directly from Hugging Face. The app already ran them; the CLI's
4476
+ preflight now reaches the same verdict.
4477
+
4478
+ ## [1.0.0] - 2026-06-10
4479
+
4480
+ The first full release: the native macOS app and the `mtplx` command line
4481
+ working as one product. Full notes:
4482
+ [mtplx.com/releases/notes/v1.0.0](https://mtplx.com/releases/notes/v1.0.0.html).
4483
+
4484
+ ### Added
4485
+
4486
+ - Native macOS app with onboarding (hardware check, model pick, guided
4487
+ setup, tuning), a live speed dashboard (decode gauge, acceptance by
4488
+ depth, verify waterfall, activity), native chat with attachments and
4489
+ web search, Forge, the AIME benchmark, and agent launchers for
4490
+ OpenCode, Pi, Hermes, and Open WebUI.
4491
+ - New models: Gemma 4 (assistant-pair drafting tuned by draft block
4492
+ size) and Qwen 3.6 MoE 35B-A3B (prequantized expert sidecars,
4493
+ normalized expert layouts, hard blocks on unrunnable layouts),
4494
+ alongside Qwen 3.5 4B and 9B for smaller machines.
4495
+ - KV cache reuse on two layers: warm-prefix reuse in RAM across turns
4496
+ and requests (multi-turn chats and agents like OpenCode hit the cache
4497
+ instead of re-processing the conversation), and an SSD session cache
4498
+ that persists KV state to disk with enforced size caps and restores
4499
+ near-instantly across server restarts.
4500
+ - Concurrency: continuous batching with presets, a scheduler mode, and
4501
+ explicit caps (`--max-active-requests`, `--decode-batch-max`,
4502
+ `--batch-wait-ms`).
4503
+ - Smart fan mode across the app, CLI, and server API: ramps while the
4504
+ model works, restores on idle, survives client handoffs, and keeps the
4505
+ crash-safe restore watchdog.
4506
+ - Forge: convert any Hugging Face repo to MLX (AWQ, compressed-tensors,
4507
+ NVFP4, BF16 sources), calibrate and train the MTP adapter, verify with
4508
+ quality gates that reject speed wins that degrade output, and publish
4509
+ with provenance. Vision towers are preserved through conversion. In
4510
+ the app and as `mtplx forge`.
4511
+ - Agent-grade serving: hardened tool contracts and dedicated lanes for
4512
+ OpenCode, Pi, and Hermes; long-context depth policy; client identity
4513
+ tagging; a live server-sent metrics stream plus snapshot, thermal, and
4514
+ prefill-history endpoints; honest cancellation that stops decode.
4515
+ - Automatic runtime setup during onboarding: the app installs its own
4516
+ Python engine, fan control (ThermalForge), and the `mtplx` terminal
4517
+ command without requiring Homebrew. Release builds bundle a pinned
4518
+ CPython interpreter, the engine environment ignores user pip
4519
+ configuration, and the interpreter is signed so installed packages
4520
+ load on macOS 14 and 15. A stale `mtplx` on PATH is updated
4521
+ automatically; a newer one is left alone.
4522
+ - Official Apple Silicon model catalog (Qwen 3.5/3.6, Gemma 4 in speed,
4523
+ balance, and quality builds) with device-aware defaults shared by the
4524
+ app and the CLI: chip generation picks precision and machines under
4525
+ 32 GiB route to the 9B model automatically.
4526
+ - App-aware `mtplx start`: detects a running MTPLX server and attaches
4527
+ instead of loading a second copy, lists installed models first, and
4528
+ adds a "Same as the MTPLX app" option. `mtplx stop` knows the app's
4529
+ persisted port.
4530
+ - New commands: `mtplx stop`, `mtplx settings get/set`, and
4531
+ `mtplx bench aime` for running the app's AIME benchmark from the
4532
+ terminal.
4533
+ - Sparkle automatic app updates with signed appcasts; the app verifies
4534
+ the installed engine against the shipped wheel and refreshes it after
4535
+ each update.
4536
+
4537
+ ### Changed
4538
+
4539
+ - Busy ports are now handled gracefully everywhere: the app moves to the
4540
+ next free port with a banner (and persists it), and the CLI explains
4541
+ exactly who owns a busy port and how to stop it.
4542
+ - The OpenAI-compatible server honors `stop` sequences (chat,
4543
+ completions, and Anthropic `stop_sequences`) and `/v1/completions`
4544
+ streams tokens as they are generated with real finish reasons.
4545
+
4546
+ [2.8.1]: https://github.com/youssofal/MTPLX/releases/tag/v2.8.1
4547
+ [2.8.0]: https://github.com/youssofal/MTPLX/releases/tag/v2.8.0
4548
+ [2.7.2]: https://github.com/youssofal/MTPLX/releases/tag/v2.7.2
4549
+ [2.7.1]: https://github.com/youssofal/MTPLX/releases/tag/v2.7.1
4550
+ [2.7.0]: https://github.com/youssofal/MTPLX/releases/tag/v2.7.0
4551
+ [2.6.0]: https://github.com/youssofal/MTPLX/releases/tag/v2.6.0
4552
+ [2.5.4]: https://github.com/youssofal/MTPLX/releases/tag/v2.5.4
4553
+ [2.5.3]: https://github.com/youssofal/MTPLX/releases/tag/v2.5.3
4554
+ [2.5.2]: https://github.com/youssofal/MTPLX/releases/tag/v2.5.2
4555
+ [2.5.1]: https://github.com/youssofal/MTPLX/releases/tag/v2.5.1
4556
+ [2.5.0]: https://github.com/youssofal/MTPLX/releases/tag/v2.5.0
4557
+ [2.4.2]: https://github.com/youssofal/MTPLX/releases/tag/v2.4.2
4558
+ [2.4.1]: https://github.com/youssofal/MTPLX/releases/tag/v2.4.1
4559
+ [2.4.0]: https://github.com/youssofal/MTPLX/releases/tag/v2.4.0
4560
+ [2.3.0]: https://github.com/youssofal/MTPLX/releases/tag/v2.3.0
4561
+ [2.2.0]: https://github.com/youssofal/MTPLX/releases/tag/v2.2.0
4562
+ [2.1.0]: https://github.com/youssofal/MTPLX/releases/tag/v2.1.0
4563
+ [2.0.2]: https://github.com/youssofal/MTPLX/releases/tag/v2.0.2
4564
+ [2.0.1]: https://github.com/youssofal/MTPLX/releases/tag/v2.0.1
4565
+ [2.0.0]: https://github.com/youssofal/MTPLX/releases/tag/v2.0.0
4566
+ [1.0.4]: https://github.com/youssofal/MTPLX/releases/tag/v1.0.4
4567
+ [1.0.3]: https://github.com/youssofal/MTPLX/releases/tag/v1.0.3
4568
+ [1.0.2]: https://github.com/youssofal/MTPLX/releases/tag/v1.0.2
4569
+ [1.0.1]: https://github.com/youssofal/MTPLX/releases/tag/v1.0.1
4570
+ [1.0.0]: https://github.com/youssofal/MTPLX/releases/tag/v1.0.0