This release fixes correctness first and gains speed along the way. Three separate audits of the decode path were run after two kinds of report, one that the 8-bit Quality pack loops and one that the answers are worse than the competition. Every defect the audits agreed on is fixed here, and each fix has a test. Speculative decoding on both flagship models is now measured exact by token id, at temperature 0 and at the native sampler, and warm agent turns restore the state they were saved with. None of this costs speed. On the same Mac, runtime and prompt, Flash-Next decodes up to 27 percent faster than 2.11.2. The release also brings 261k-token prompts on Flash-Next, a working draft head on 32 GB Macs, an SSD session cache that cleans itself, deep sessions on 64 GB Macs, Gemma 4 warm turns, ordered model folders, and a set of app and agent-harness repairs. Two repairs from the final day are measured the same way. A 19k-token app conversation with web search used to prefill from the start on every turn; its follow-ups now answer in 0.6 s instead of 15. A 109k-token OpenCode turn decodes at 61.8 tok/s instead of 48.8. Both were measured against the same build without the repair.
Measured on an M5 Max with 128 GB, 2.11.2 against 2.11.3 on the same Mac and the same Python runtime, alternating boots, fans at maximum:
| 2.11.2 | 2.11.3 | |
|---|---|---|
| Flash-Next decode, 9k-token code prompt, 1,500-token answer, thinking off | 62.5 tok/s | 79.3 tok/s (+27%) |
| Flash-Next decode, 8,848-token code prompt, September 8 measurement | 82.1 tok/s | 91.7 tok/s (+12%) |
| Flash-Next decode, 88-character Flappy Bird prompt at effort xhigh, 45k to 56k generated tokens | 66.5 and 62.6 tok/s | 66.8 tok/s (tie) |
| OpenCode and Pi agent sessions, mean decode per step over ten steps | 63.6 tok/s | 65.4 tok/s (+3%) |
| Draft acceptance by depth on the 9k-token prompt | 0.808 / 0.663 / 0.515 | 0.875 / 0.713 / 0.586 |
| Flash-Next at 261,120 tokens of context, first speculative verify step | out of GPU memory | decodes |
| Draft head on a 32 GB Mac | zeroed head, MTP acceptance under 1 percent | bit-identical head, built in row chunks |
| Hindi sentence, tokens produced (Thai, Arabic) | 32 (17, 45) | 20 (9, 31) |
| Hermes turn after a token that is not the standard encoding of its text, the next request | 29,842 tokens re-prefilled, 41 s to the first token | the saved token ids are reused, no re-prefill |
| OpenCode on an 8K window, reply budget advertised | the whole window (48 summaries in 98 turns) | half the window |
| SSD session store after three weeks of uptime, reporter's machine | 394,155 orphan files, 44.1 GB | reconciled at every open |
The first four rows are pairs measured on the same runtime on September 8 and September 16. Each speed section below says what was measured and how. The remaining rows are before-and-after states from the reports and the fixes. Every number is either measured here or quoted from a report, and the text says which.
Exactness: eight defects, each with a test
The reports pointed at the 8-bit weights. The weights are fine. The problems were in the code around them, and each one was reproduced on a small case before it was fixed.
| Defect | What it did | Fix | |
|---|---|---|---|
| 1 | Block verification at depth 3 was not distribution-exact | On a small vocabulary the acceptance rule is exact at depths 1 and 2 and off by up to 4e-2 total variation at depth 3, in the cases where the last depth's acceptance probability had been clipped to 1 | The rule now limits each depth's budget to what the earlier depths actually left it, passes that budget down, and corrects a rejection by the shortfall. A test enumerates the emitted distribution against the target for both rules at depths 1 to 4, exact to 1e-12 |
| 2 | A warm agent turn restored the wrong recurrent state on the hybrid models | A near-prefix restore whose gap to the banked turn was 1 to 8 tokens, the shape of a re-rendered agent turn, trimmed the attention cache but kept the recurrent state of the longer sequence, then ran one token on it. The whole turn decoded on a state the new prompt never produced, later turns inherited it, and the 8-bit Quality pack was affected exactly like the 4-bit ones | Every partial restore of a recurrent entry now lands on a stored recurrent boundary at or below the match point |
| 3 | The banked state after a speculative turn was one token short | The final token of a response (a deferred greedy correction, a fresh sample, or any max_tokens exit) was committed but never run through the model, so the next warm turn decoded as if the closing token never existed. Temperature-0 clients such as Cline hit it every turn | The final token is run through the model before the state is saved |
| 4 | Non-finite logits became token 0 | Token 0 is ! in the Qwen vocabulary: the thousands of exclamation marks of #311 |
The request ends with finish_reason: error and code non_finite_logits, with the NaN and inf counts in the message; the session's cached state is dropped and the daemon stays up. A row that constrained decoding masks to -inf with a finite winner is legitimate and passes the guard |
| 5 | The pipelined Flash-Next AR sampler measured top-p on the top-k slice | It kept one to five fewer of the top-20 tokens than the other sampling paths at 1.0 / 0.95 / 20 | The nucleus is measured on the full-vocabulary softmax, like the CPU reference |
| 6 | The Qwen3 pre-tokenizer regex had been replaced by the Qwen2 one | transformers' Qwen2Tokenizer split combining marks off their letters: Hindi 32 tokens instead of 20, Thai 17 instead of 9, Arabic 45 instead of 31 on the same sentences. Latin, code and Chinese were unchanged | The loader restores the Qwen3 regex on every Qwen3-family pack. The six 27B packs on the Hub keep the old regex in their tokenizer.json until they are re-pushed; the loader repair covers them either way |
| 7 | A copied block at the verify cache's growth edge lost its rows | On Metal the functional slice update silently drops the rows past the end of the buffer while the offset still advances over them, so the rest of the turn attended over missing rows. Present since 2.10.0, when the context-copy path and the fixed verify buffers were first combined | Both copy-block routes reserve their rows before the forward, the write grows the buffer as a last guard, and the request log counts the growths under context_copy_capacity_growths |
| 8 | Reasoning substitution served an old tool body | The check that decides whether a tool call was already served compared calls by their command or path only, so a write to the same file with new content counted as already served and the prompt carried the old body |
The check now compares the complete argument set, in both tool-call formats |
Defect 1 never ran on a shipped daemon, because the block-verify path was never switched on (see the serving settings below). The fix is what makes that path safe to switch on. Defect 7 is the biggest of the eight for a user: it is behind the mid-code cuts and the files written twice on 2.10.0 through 2.11.2.
The growth-edge defect in detail
The context-copy path proposes a block of up to 24 tokens copied verbatim from the prompt and verifies it in one forward pass. That pass writes its key and value rows into the fixed-capacity buffers the compiled verify installs, on both model families. The 27B's buffers grow in 512-token steps. A Flash-Next entry restored from the warm cache has only its rounding slack until the cache grants it more room, and the first rounds after a restore are often copy rounds. A copy block that straddled a growth edge was written with the functional slice update, and its rows past the edge were lost.
Reproduced with a six-turn coding session on the Optimized Quality pack. 2.11.2's fourth answer matches the fixed engine's for 3,001 characters and then ends the turn in the middle of parser.add_argument(, where the fixed engine goes on to write the option and the subcommands. With reasoning on, the same turn from the same restored point and seed diverges from the fixed engine inside the imports copied from context, then writes the complete cli.py twice, verbatim, up to the token cap. The fixed engine writes it once and stops.
Warm reuse: three more repairs
-
A long agent turn stays warm across the model's own token boundaries. The tokens a model samples are not always the standard encoding of their own text. In a Hermes session at effort xhigh the 27B wrote
"Nothingas a single token, 8,498 tokens into a write_file call, where the tokenizer encodes"and thenNothing. The bytes were identical and the ids were not, and everything downstream treated that as an edit. The re-tokenized history could never match the saved stream, the end-of-turn snapshot was refused, the fallback re-prefill did not finish before Hermes' next request arrived, and that request re-prefilled the whole 29,842-token turn, 41 seconds to the first token. Wherever the resent prompt and the session's saved ids decode to the same text, the saved ids are now used, both when a request is matched to its session and when a finished turn is stored. A real edit still ends the match where it happens. The request log records the splice undercommitted_reasoning_canonicalization.token_splice, andMTPLX_COMMITTED_TOKEN_SPLICE=0turns it off. -
A reply's own whitespace no longer breaks its warm restore. A turn cut by the token budget right after a newline used to come back with the end-of-turn marker where the newline was, and the exactness rule then re-prefilled the whole turn on the next request. The same splice puts back the whitespace a reply's visible content is served without, and it runs whether or not thinking is on.
-
A mid-conversation system message no longer rewrites message 0 (#477). Claude Code's per-turn
<system-reminder>notes arrive as late system messages. Hoisting them into the leading system message changed the first message every turn and cost a cold re-prefill of the whole history. A late system message now becomes a user turn in place.
Gemma 4 warm turns (PR #283, Craig Tollifson)
Gemma 4 sessions restore from the warm cache across edited or re-rendered turns instead of re-prefilling the conversation. Two exactness holes in the sliding-window trim the restore relies on were closed while the PR was merged. A restored entry's trim left stale rows that the next step counted as history. And the cache identity of a Gemma 4 entry now comes from the runtime's declared history policy rather than a fixed value, so a session served without the MTP head cannot hit a policy mismatch on every turn.
Speed
The Flash-Next serving settings reach the running daemon
Four hot-path settings (MTPLX_QWEN4_OPDIET, MTPLX_QWEN4_VERIFY_GLUE, MTPLX_QWEN4_DRAFT_K20_PRESCATTER, MTPLX_QWEN4_BLOCK_VERIFY) were read once at import, before the server applied the Flash-Next defaults, so /health reported them configured while the daemon ran with all four off. David Tai's PR #475 found the same problem. The settings are read again when the model's runtime environment is applied, before the load, and /health now shows every one of them with observed equal to expected on a running daemon.
Measured against 2.11.2 on the same Python runtime, the same Mac, the same 8,848-token code prompt and sampler, alternating boots with the fans at maximum: 82.1 tok/s on 2.11.2, 91.7 tok/s on this release, +11.7 percent, with identical acceptance by depth.
The exact block verify is off by default
The block verify is the one setting that is not on by default. On the 8,848-token code prompt it was a tie: off 91.65, 97.92 and 93.34 tok/s, on 91.44, 97.26 and 92.01 tok/s, with acceptance by depth 407 / 364 / 323 against 408 / 361 / 324 of 441 drafts. On our reference workload, an 88-character Flappy Bird prompt at effort xhigh with the native sampler, which generates 45k to 56k tokens a turn, it accepted fewer draft tokens per round. Eight runs on 2026-09-16, alternating boots on the same runtime, fans at maximum, the chip cooled to 70 C or lower before each run, and every run ending on a hot chip:
| Run | Version | Tokens | Wall | Whole-turn tok/s | Tokens per round | Acceptance by depth |
|---|---|---|---|---|---|---|
| R1 | 2.11.2, boot 1 | 44,876 | 676 s | 66.5 | 3.03 | 0.829 / 0.666 / 0.530 |
| R2 | 2.11.2, boot 2 | 50,115 | 801 s | 62.6 | 3.00 | 0.827 / 0.656 / 0.514 |
| B1 | 2.11.3, block verify on, boot 1 | 50,797 | 784 s | 64.9 | 2.90 | 0.804 / 0.618 / 0.481 |
| B2 | 2.11.3, block verify on, boot 2 | 45,040 | 708 s | 63.7 | 2.91 | 0.804 / 0.624 / 0.480 |
| B3 | 2.11.3, block verify on, boot 3 | 46,212 | 719 s | 64.3 | 2.90 | 0.799 / 0.623 / 0.480 |
| V0 | 2.11.3, shipped default | 46,117 | 690 s | 66.8 | 3.00 | 0.825 / 0.653 / 0.518 |
| P0 | 2.11.3, shipped default with the op diet off | 56,285 | 855 s | 65.9 | 2.98 | 0.819 / 0.647 / 0.511 |
| V1 | 2.11.3, shipped default, started on a cold chip | 48,490 | 738 s | 65.8 | 2.94 | 0.811 / 0.634 / 0.496 |
The block verify accepted 2.90 draft tokens per round against 3.00 for the standard verify and 3.01 on 2.11.2, a 3.5 percent loss that the whole-turn speed follows. Both are distribution-exact. The faster one is the default, and MTPLX_QWEN4_BLOCK_VERIFY=1 switches the block verify on. With the shipped default this release runs at the same speed as 2.11.2 on this workload, with the same acceptance, and it has the best number of the eight runs. The whole turn heats the chip within its first minute, so the start temperature changes the whole-turn number by about one tok/s, not ten.
9k context: 27 percent faster
The same 9k-token code prompt on both versions, 1,500 generated tokens, seeded native sampler, thinking off, alternating boots, fans at maximum, the chip cooled to 70 C before each run and 60 s of cooling between runs:
| Run | Version | Decode tok/s | Prefill tok/s | Time to first token | Acceptance by depth | Tokens per round |
|---|---|---|---|---|---|---|
| R1 | 2.11.2 | 62.1 | 1,293 | 11.64 s | 0.808 / 0.663 / 0.515 | 2.99 |
| B1 | 2.11.3 | 79.0 | 1,280 | 11.76 s | 0.875 / 0.713 / 0.586 | 3.17 |
| R2 | 2.11.2 | 62.8 | 1,298 | 11.60 s | 0.808 / 0.663 / 0.515 | 2.99 |
| B2 | 2.11.3 | 79.7 | 1,308 | 11.50 s | 0.875 / 0.713 / 0.586 | 3.17 |
At 9k context this release decodes 79.3 tok/s against 62.5 on 2.11.2, with the draft acceptance up from the sampler and draft-tie repairs and the prefill speed a tie. The pooled row-selection kernel from David Tai's PR #475 was measured on the same prompt with the flag forced on (79.4 to 79.6 tok/s against 79.6 to 79.7 without it) and is not in this release.
Long agent turns: the launchers and the depth policy
Two defects were behind a 108,919-token OpenCode turn that decoded at 26 tok/s on this Mac. Neither was an acceptance collapse: the turn restored 92,521 tokens from RAM in 6 ms and ran mostly at depth 3, and target verification took 75 percent of the decode time at 78.8 ms per call against 50.7 ms one turn earlier. The hardware state of that turn was not recorded, so the two repairs below are measured on their own and are not credited with every millisecond of the original.
-
The agent launchers overrode the model's verify path. The app's OpenCode, Pi and Hermes presets and the CLI's
mtplx opencode,mtplx piandmtplx hermescommands exported two evaluation flags from an older tuning,MTPLX_LAZY_TARGET_DISTRIBUTIONS=1andMTPLX_LAZY_BONUS_VERIFY=1(Hermes exported only the second). On Flash-Next the first one switched off the batched, compiled fixed-width verifier that the chat launch already used, and with the first removed the second would have verified three rows per cycle instead of four and bypassed the compiled verifier entirely (zero compiled calls, 39.7 and 41.1 tok/s in the rejected single-flag experiment). Both exports are gone: the model's own defaults choose the path, the 27B keeps its profile defaults, and an explicit operator export still wins. -
The adaptive depth policy learned a startup spike as the recurring cost. On a restored 109k-token request the first depth-3 verify cost 125.19 ms and the next three 30.44, 30.77 and 31.00 ms. No new graph trace was reported, so the existing rule that excludes tracing cost did not apply, and the exponentially weighted estimate still read 95.11 ms after four samples against about 38 ms for the eager depth-2 path. The policy chose the slower path for 453 of 464 cycles. It now takes the minimum over its existing four calibration samples before the weighted average starts, the same warm-up rule the other cost-based policy already used, and a sustained increase still lowers the depth. A test replays the measured 125 to 31 ms sequence and the sustained-rise case.
Measured on the captured OpenCode request, the same Mac, one resident model, fans verified at maximum, every run started under 70 C, seed 1731 at the native sampler, alternating with the same build without the two repairs:
| Context | Without, run 1 | With, run 1 | With, run 2 | Without, run 2 | Means |
|---|---|---|---|---|---|
| 108,919 tokens | 48.02 tok/s | 60.83 tok/s | 62.72 tok/s | 49.64 tok/s | 48.83 to 61.77 tok/s (+26.5 percent) |
| 200,073 tokens | 47.59 tok/s | 49.97 tok/s | 50.55 tok/s | 50.23 tok/s | 48.91 to 50.26 tok/s, within run-to-run variation |
At 109k the compiled verify calls per 1,024 output tokens went from 11 to 376 and the verification calls from 470 to 396; active memory stayed at 94.7 GB and the peak at 95.4 GB in every run. At 200k the warm route stays on the eager verifier by design, because a matching snapshot plus the 5.7 GB the compiled verifier needs does not fit the admission budget, and that guard is unchanged. In the installed app a cold 200,073-token OpenCode prompt decoded at 42.5 tok/s after a 243 s prefill, with 2.1 GB of OS compression during the decode, and its warm follow-up restored 200,068 tokens and ran at 50.6 tok/s with the first token in 0.89 s. Fresh OpenCode, Pi and Hermes tasks on the installed build ran at 54.6 to 122.4, 53.6 to 72.1 and 56.1 to 82.4 tok/s per request, with 27 of their 30 follow-up requests served from the session bank.
Flash-Next at full context (PR #482, davidtai)
At the pack's 261,120-token context the first speculative verify step ran out of GPU memory after the whole prompt had been read: the verify's KV-cache update reallocated the entire cache buffer on every step (about 6.4 GB across the twelve full-attention layers at that context), and its multi-row attention built a score table that grows with the context. The KV update now writes its rows into the existing buffer, and the verify attention is split across query-head groups so each call stays on the fused kernel in the three-to-eight-row band the verify uses. Both are exact. MTPLX_QWEN4_VERIFY_SDPA_HEAD_CHUNK=0 turns the split off and /health reports verify_sdpa_head_chunk when it engages. Speed and peak memory at 16k are unchanged, and the 27B models are not affected.
Session cache
- A conversation with tools stays warm from turn to turn. Three defects made the app's web-search conversations prefill from the start on every turn: an 18,776-token turn took 14.7 s to its first token with 0 cached tokens, and the next one 15.2 s. First, the app's JSON encoder wrote the keys of its tool declarations in a different order on every request, the native chat template renders those declarations verbatim in the leading system prompt, and the server kept the order it received, so the conversations diverged between token 58 and 116 and nothing after that could be reused. Tool declarations are now put into one fixed key order once, at the server boundary, with every field, value and list order preserved. Second, the turn that closes a web-search round with
tool_choice: nonedropped the declarations from the prompt and changed the cache identity, and the next turn put them back; the declarations now stay in the prompt while calls stay disabled, on one identity through auto, none and auto. Third, the server's own closing instruction for such a turn was banked as history the client never sends back, so the next follow-up diverged before the whole 2,820-token answer (10,323 prompt tokens, 7,269 cached, 2.8 s); the history is now captured before the instruction is added, and both closing instructions are registered as prefill boundaries. Seven tests pin the three cases for the app, OpenCode, Pi and Hermes. This is a server fix and applies to every client.
Measured on the saved 19k-token conversation, replayed against the same build without the repair, two runs per build, fans verified at maximum:
| Turn | Without, first token | With, first token | Without, decode | With, decode |
|---|---|---|---|---|
| Cold prime | 15.16 s | 15.04 s | 59.3 tok/s | 60.1 tok/s |
| Follow-up after a re-encoded declaration | 15.26 s | 0.57 s | 58.5 tok/s | 69.9 tok/s |
| The turn that closes the tools | 13.93 s | 0.66 s | 59.2 tok/s | 59.3 tok/s |
| The turn that reopens them | 1.06 s | 1.00 s | 65.4 tok/s | 69.9 tok/s |
In the installed app a web search, its answer and a follow-up ran uncapped at effort xhigh: the follow-up sent 11,461 tokens with 11,421 cached and saw its first token in 0.29 s at 69.2 tok/s, and the original conversation, continued in place, sent 12,536 tokens with 12,510 cached and saw its first token in 0.28 s at 62.3 tok/s. The answer to the search itself still prefills its 8,419 new tokens (6.4 s), and the background commit that removes the closing instruction from the banked history took 2.2 s on an 11k-token answer; a request that arrives inside that window waits under the existing bounded scheduler.
-
SSD cache eviction yields to the request in flight. On a populated SSD store, the eviction that makes room for a new snapshot re-read every surviving payload for every victim while it held the store lock, so a 66-entry eviction ran for about 89 seconds alongside two app replies that measured 33.5 and 42.3 tok/s. Ordinary blob writes already paused for foreground work; eviction did not. How much of that slowdown the eviction caused was not quantified, because the thermal state of that session was not recorded. Entries are now removed under short lock sections. Their files are reclaimed in one paced pass that gives way to requests between payload reads and every 64 deletion candidates. The store's version is checked under the lock before anything is deleted, files shared with other entries or still being written are protected, and a write started on the request's own thread gives way instead of waiting behind a request and rechecks the budget afterwards. A test fails on the previous build and passes here. In a controlled A/B on a private 64-entry store with 65,536 files, alternating boots with the fans verified at maximum and identical output on every run, the reply that starts while the eviction runs went from 61.2 to 65.9 tok/s, +8 percent, against the same build without the fix. This is a server fix shared by the native app, OpenCode, Hermes and Pi.
-
Deep sessions persist where the machine can restore them (PR #496, Dizzler7). A 12 GiB warm snapshot (Qwen3.8-27B, Q8 KV, past 100k tokens) hit the flat 8 GiB per-session cap on Macs under 96 GB, so those conversations never reached the SSD tier and came back cold after a restart. The per-session cap now follows the memory plan: two thirds of the bank budget, held under half of what the engine budget leaves after the weights and the transients, because a restore holds the snapshot next to its banked copy. That is 13.2 GiB on a 64 GB Mac with the 27B (the report's snapshot fits), 32 GiB on a 128 GB Mac with the 27B, and 10.5 GiB with Flash-Next. The flat 32 GiB the PR proposed would have pushed a 64 GB Mac into swap. Also from the PR: a 64 GB Mac with 150 GiB of free disk defaults its SSD cache cap to 100 GiB, and the hourly SSD write budget defaults to 128 GiB.
-
The SSD store cleans itself, and its size limit counts what is on disk (#493). Three people measured the same thing:
~/.mtplx/session-bank/holding hundreds of thousands of blob files the manifest no longer named (394,155 files and 44.1 GB against 17 live entries after three weeks; 471,541 files and 67 GB on a 60 GB cap after three days of uptime). The reconciliation that deletes them existed, but it only ran when a write found the store near its cap, and a generous cap never got there. After each cleanup the size check assumed every unaccounted byte was gone. But files shared by a later snapshot of the same conversation stay on disk when the entry that paid for them is evicted, so a store could sit above--ssd-session-cache-max-sizeindefinitely. The daemon now reconciles the store every time it opens the cache. That runs in the background, gives way to requests, never blocks the boot or the first request, and writes onemtplx_ssd_session_cache_reconcileline to the log with what it reclaimed and what the store holds. The size check counts the directory as it is on disk, reclaims garbage on the writer thread before it would evict live entries to make room, and never walks the store on the request thread. A write in progress keeps its files through a pass. -
mtplx gc(PR #502, ArctifoxNL). To inspect the store or sweep it by hand, with or without a daemon running,mtplx gcreports what is live and what is orphaned and--applydeletes the orphans. It runs the same scan as the daemon and does not need MLX.docs/server.mddocuments the cap, the pass and the command next to theMTPLX_SESSION_BANK_*variables. -
There is no five- or ten-minute timer on the warm cache (#481). The idle sweep is one hour, and the ten-minute window only orders eviction victims under pressure. What the report shows is a 2.10.2 cache miss that 2.11.2 already fixed. The idle limit is now a setting,
MTPLX_SESSION_BANK_IDLE_TTL_S(0keeps entries until memory needs them), and the fiveMTPLX_SESSION_BANK_*variables are documented together indocs/server.mdfor the first time. -
A session banked to the SSD tier survives a daemon restart. Measured on this release: the next turn restored 1,552 of its 1,597 prompt tokens from disk in 43 ms, first token in 0.58 s against 2.40 s cold, and its seeded text differs from the cold one at one token, where the two candidates sit 0.125 nats apart.
Agent harnesses
-
OpenCode compacted after every reply on small context windows (#480). MTPLX registered the model with the reply limit equal to the context window. OpenCode reserves the reply limit out of the window before deciding whether the conversation still fits, so on an 8K window the usable conversation was zero tokens and OpenCode wrote its
## Objective / Important Details / Work Statesummary after every turn (48 summaries in 98 turns in the report).mtplx connect opencode, the quickstart and the app now advertise a reply budget of half the window, capped at the 32,000 OpenCode uses on large windows. -
mtplx connect opencodefollows the live daemon (#472). The command names a host and a port, never a pack, and its image-input flag came from a pack probe on an empty model reference (always text-only on 2.11.1) and then from the catalog default. It now asks/healthon the given port: the daemon's public model id replaces the catalog default unless--model-idwas passed, and the daemon's vision block decides whether images are advertised. -
Streams no longer hold for minutes when the first line looks like tool-control markup (#468). The guard that buffers the start of a streamed channel while it may still become dangling tool-control markup classified from the first line only, so an answer whose first line was exactly a bare name such as
value, a prefix of one, or an openedname=form stayed buffered for the entire stream and was released only at the end. That meant minutes of silence on Claude Code and every other client with a stream watchdog. The buffer is released once the first line closes. -
The Hermes profile warning on every launch is gone. Hermes v0.21 deprecates the
TERMINAL_CWDline, and neither the app nor the CLI writes it into the Hermes.envany more. -
Flash-Next agent launches draft to the chosen depth. The app's Pi and Hermes launches on Flash-Next draft to the chosen depth instead of the expected-value policy, which measured 7 to 8 percent slower than a fixed depth 3 with the shipped settings. The adaptive policy stays behind its switch, and
docs/server.mddocuments the flag and the cost of flipping it mid-session. -
OpenCode 2 loads the session-header plugin (#498). The MTPLX plugin that OpenCode loads is now a small package with the version 1 entrypoint and a version 2
setuphook that registers one request hook for the MTPLX provider only. A real 1.18.29 client and a real 2.0.5 client both send the session headers, the managed registration migrates in place, and other plugins in the configuration are preserved. The plugin rewrites no prompt, tool schema or sampling option.
The app
-
The app keeps its port (#503). A daemon that hard-froze mid-inference can keep its listening socket after
/healthhas stopped answering. The app's port preflight read that as another app on the port, moved to the next free one, saved it, and every client pinned to the configured port (an agent connector on 8001, in the report) was stranded while the engine showed Running. The preflight now asks the OS who holds the port: a listener carrying the app's own launch marker is stopped in place and the configured port is kept. When something else really holds the port, the launch still moves to a free port, but only for that launch: settings keep the configured port, a save made in the meantime writes the configured port back unless the port itself was changed on purpose, the banner stays until the next start, and the next start tries the configured port again. A listener without the marker is never touched. -
A busy daemon is not a dead one (#487). The app's watchdog killed a live daemon four times in one day of HenriGrimm's logs: a generation-final prefix commit on a 110k to 150k-token vision agent session took 19 to 25 seconds, the daemon answered nothing on its health route for that long, and two missed probes were read as death while the commit succeeded. The watchdog now reads process and port evidence before it acts: a daemon whose process is alive and whose port still accepts connections is busy, and is killed only after 90 seconds of silence or the moment its process is gone or its port closes. The commit was slow for a reason the log could not name: a prompt whose screenshots exceeded the vision embed cache's row budget was evicting its own images and re-running the image tower for every one of them. The prompt's images are pinned for the pass, and the commit now records how long each phase took in its log event.
-
Model folders (PR #387, Philip John Basile). One primary model folder, where downloads and Forge output go, plus an ordered list of additional read-only folders that the app and the CLI both search (issue #388). The first complete copy wins, duplicates are collapsed by real path, and an unplugged drive is skipped and explained. The app has a Settings card for the folders, the CLI takes
--model-search-dirormodel_dirsin~/.mtplx/config.toml, andmtplx doctorlists the roots. Known gap: the app's folder setting and the CLI's config file are two stores, so a folder chosen in the app does not move amtplx pullrun from Terminal. -
Thirteen languages. Turkish is new (#470). The Appearance row, the chat sidebar timestamps, the launch chooser, the Hermes overlay, the tune-step captions and the Forge buttons are localized in all thirteen languages, and relative times follow the app language.
-
A stale custom model can be removed from the picker (PR #471, nRanzo).
Models, packs and downloads
-
32 GB Macs get a working draft head (#483). The fast draft LM head was built by dequantizing the whole 8-bit head into one 2.4 GiB dense tensor and requantizing it in a single pass. On an M1 Pro with 32 GB the requantize hit a silent Metal out-of-memory and the head read back as zeros, collapsing MTP acceptance to under 1 percent and failing the tuner in its first warmup. The head is now built in row chunks (a bit-identical result, no dense intermediate), a zeroed head is refused, and the install falls back to the resident target head as the drafter, which the reporter's own measurements show is equivalent in quality. The startup report records which path was taken.
-
The Flash-Next converter accepts the official FP8 checkpoint (PR #474, Graham Jenkins). The FP8 n-gram scalar scales are read correctly.
-
Remote Qwen checkpoints without a draft head inspect as autoregressive (PR #489, Philip John Basile) instead of
missing-model-weights, and Forge refuses to build a speculative artifact from an AR-only trunk it cannot give a head. -
Packs already in the machine-wide Hugging Face cache are found without a second download (#445). The model resolver consults
HF_HOMEandHF_HUB_CACHEafter MTPLX's own cache, honoursHF_HUB_OFFLINE, holds the copy to the same completeness gate, and Forge builds from the local copy and stamps its revision. -
Retrieval models are validated at startup. A missing embedding or reranker model refuses to launch with the
mtplx pullhint instead of answering the first request with a 500, and/v1/modelslists only the ones that resolved. -
aria2 is an opt-in download backend (PR #452, zeeshanhaque21). The built-in downloader stays the default.
-
The README carries a model table with recommended settings (#238, #408).
Server, doctor, dashboard and docs
-
Responses API (PR #219, Philip John Basile).
POST /v1/responsesis served as a stateless, text-only endpoint over the chat runtime, with client-executed function, custom and namespace tools. The Codex CLI settings that make its first request work are indocs/api.md. -
mtplx doctorreports which MTPLX answers (#479).runtime.identitycarries the version, the path, whether themtplxfirst on PATH runs the same interpreter, the GPU architecture, and whether the M5 tensor-unit route is available. The kernel-build failure of #479 on macOS 15 is unreachable on 2.11.2 code; the check answers which MTPLX actually ran. -
Desktop web views can be allowlisted for CORS (#473). Any scheme is an origin scheme now, so
--cors-origin tauri://localhostworks for Jan.app. Wildcards and the admin routes stay closed. -
The dashboard request log prints real per-depth acceptance totals and completion times (#401). Non-finite floats are serialized as null on the dashboard stream and the JSONL request log, as they already were on
/health, soMTPLX_SESSION_BANK_IDLE_TTL_S=0no longer makes every live dashboard snapshot unparseable in the browser. -
The request-capture registry no longer grows for the life of the daemon (PR #356). Pruned request ids are forgotten, and the
MTPLX_REQUEST_CAPTURE_KEEPring size is documented. -
/healthfast-path checks compare against the server's own runtime overrides. -
Reasoning effort is documented (#484):
reasoning_effortper request,--reasoning-efforton the server, the app's picker, and the per-family tiers. The OpenCode CLI's per-turn system-prompt rebuild (one cold re-prefill after a file write; the Desktop app keeps one process) is documented as a client-side cost. -
The flight recorder counts tokens for non-streaming requests. A 24k-token non-streaming generation used to read as a prefill with no tokens for its whole life.
-
transformers is floored at 5.10.0 (Dependabot alert 24, the
save_pretrainedpath-traversal range), with the lock at 5.14.1. -
The dead M=8 K-split verify kernel is removed (#322). Nothing dispatched it.
Validated with the real harnesses
Everything below ran on this code, on an M5 Max with the fans at maximum and one resident model at a time.
| Check | Result |
|---|---|
| Python suite, final code (436d0713) | 6,959 passed, 33 skipped, 0 failures, rerun after the last three fixes |
| Swift app suite | 965 tests, 2 skipped, 0 failures, after two test-target repairs (a socket call that Swift resolved to an instance method, and a model-library test that read the machine's installed packs instead of its own temporary root); rerun on the final code after the launcher change to the app's presets |
| OpenCode and Pi, ten steps per run, three runs (2.11.2, this release before the final fixes, the final code) | 30 of 30 steps exit code 0, 30 of 30 follow-up requests restored from the session bank, 74 to 100 percent of each follow-up prompt's tokens cached, no repetition stop, the agents' own test suites green on all twelve projects |
| Mean decode per step, the three runs | 63.6, 65.4 and 64.0 tok/s |
| OpenCode, Pi and Hermes on the final code (build 2011048), a task and a follow-up each | 25 of 29 requests restored from the session bank (the first request of each client is cold by design), decode 50.0 to 84.1, 57.8 to 79.8 and 58.5 to 73.6 tok/s per request, each client's own tests green |
The client runs ran against a Flash-Next Optimized Speed daemon launched with the app's own flags at effort xhigh: OpenCode wrote a multi-file Flappy Bird over three turns ending with its own passing node --test cases and a multi-file 2048 over two turns, and Pi wrote the same two projects.
A candidate app build (2011042, the release wheel and the signed native kernels inside it, its own bundle id) was driven through its own daemon by a UI automation tool on this code:
-
The reference prompt at effort xhigh produced 54,460 tokens at 61.7 tok/s whole-turn (first token in 0.32 s, natural stop) on a chip that sat at 91 to 100 C with the fans at maximum for the whole quarter hour.
-
The second turn, a full rewrite of the 89k-character game file with a persistent high score and a pause key, restored all 54,530 tokens of the first turn from the session bank in 4 ms (30 new tokens prefilled, first token in 0.39 s) and generated 42,200 tokens at 73.2 tok/s.
-
The third turn restored 96,760 tokens in 8 ms and answered in 5.8 s.
-
The rewritten game parses, boots, plays, pauses on P and keeps its records in localStorage, checked in a browser. The first turn's file carried one stray token in 66k characters of script, a sampling slip at temperature 1, gone in the rewrite. The same long turn in this Mac's history ended at 36 to 51 tok/s on 2.10 and 2.11.0.
The SSD eviction fix landed after that drive, so a second signed candidate (2011044, the same app binary with the fixed wheels) was verified again through its own daemon: five turns in the native app at 53 to 75 tok/s with the warm follow-ups restored from the bank; a coding task and a follow-up in OpenCode, Pi, Hermes and OpenCode Desktop, each on its own scratch project, 33 of the 38 requests restored from the bank (the first request of each client is cold by design), decode between 46 and 103 tok/s, and the five tests each client wrote passing in an independent run; 13 SSD writes and four SSD restores over that QA with no failure.
The launcher and depth-policy repair and the tool-prefix repair landed after that, so two more signed candidates were driven the same way: 2011046 (a rebuilt app binary and both wheels) through the app's OpenCode preset, with the 200,073-token cold and warm pair in the table above and a fresh task in each of OpenCode, Pi, Hermes and OpenCode Desktop; then 2011048 (the same binary with the final wheels) through a native web-search conversation and its follow-ups, the original conversation continued in place, and a fresh task and follow-up in OpenCode, Pi and Hermes. The full Python and Swift suites in the table were rerun on the final code after both.
Exactness measurements
Both packs, scored by exact token ids through the completions endpoint with echo and logprobs, never by decoded text.
| Test | Flash-Next Optimized Speed | 27B Optimized Quality |
|---|---|---|
| Temperature 0, speculative against autoregressive, six prompts, 512 tokens | One prompt byte-identical; the other five differ from token 60, 46, 117, 158 and 39 on, where the model's two best candidates sit 0.000 to 0.250 nats apart | One prompt byte-identical; the other five differ from token 483, 182, 427, 58 and 18 on, where the two best candidates sit 0.000 to 0.125 nats apart |
| Median gap between the top two candidates over the agreed prefixes | 2.0 to 10.7 nats | 1.9 to 11.1 nats |
| Native sampler, 1,000 four-token draws, total variation speculative against autoregressive at joint length 1 to 4 | 0.027 / 0.042 / 0.089 / 0.222 | 0.025 / 0.053 / 0.068 / 0.131 |
| The same distance between two halves of the autoregressive sample (the noise floor) | 0.022 / 0.080 / 0.122 / 0.274 | 0.042 / 0.060 / 0.086 / 0.162 |
| Seeded warm restore against the same turn cold, 400 tokens | Differs at one token, where the two candidates carry p 0.493 each, an exact tie; the three-token tiny-gap restore is byte-identical | Differs at one token, where the two candidates carry p 0.488 each, an exact tie; the tiny-gap restore is byte-identical |
| The 243 padding rows of the 248,320-row logit table | Never in the top 128 of 2,957 scored positions | Never in the top 128 of 2,878 scored positions |
At temperature 0 the two paths differ only where the model's own two best candidates are tied or one bf16 step apart, against median gaps of 2 to 11 nats over the agreed prefix. At the native sampler the speculative distribution sits within the autoregressive sample's own split-half distance at every joint length: below it at every length on the 27B and at lengths 2 to 4 on Flash-Next, and the same size at length 1 on Flash-Next (0.027 against 0.022). A warm restore matches the cold state except where the model's two best candidates are an exact tie.
Against the bf16 Qwen3.8-27B checkpoint, teacher-forced over 2,389 positions of a fixed corpus, the 27B Optimized Speed pack sits at KL 0.012 nats with 96.0 percent top-1 agreement and the 27B Optimized Quality pack at KL 0.0005 nats with 99.3 percent.
What remains at temperature 0 is the model's own arithmetic, measured rather than assumed, with the prompt scored teacher-forced through the completions endpoint. On the 27B Quality pack at 53k tokens a warm turn and the same turn cold still differ at one token, where the model's two best candidates sit 0.125 nats apart, a single bf16 step at that logit size. Every earlier token is the model's own argmax, and the median gap between its top two candidates is 8 nats. The difference is at the same token on 2.11.2, and the three texts of that measurement are byte-identical between 2.11.2 and this release. On Flash-Next at 106k tokens a cached request and a stateless request of one prompt, both prefilled from scratch, differ once in 88 tokens, at a position where the two best candidates sit 0.375 nats apart, in the tightest twentieth of the answer, and the cached path holds the model's argmax at every position. The two paths draft differently and verify in different block shapes, and on a model that routes tokens to experts and selects keys by score, a rounding difference can move a candidate by more than one step. Neither is a wrong state.
Known in this release
On a 48 GB Mac the 27B packs cannot bank a session past about 95k tokens: the memory plan caps one session's bank entry at half the bank (7.4 GB on a 48 GB budget) and a 142k-token snapshot is 11 GB, so the entry is refused and, when the client holds no streaming lease, nothing reaches the SSD tier either. Every following turn then prefills the whole conversation cold (325 to 390 s for 142k tokens on an M5 Max with the budget forced to 48 GB, measured 2026-09-16). Issue #499's trace is the same shape with a streaming lease, whose SSD copy lands without the draft head's history and is refused on restore. The turn after the refusal now reports it as its miss reason (cache_miss_reason: oversized_snapshot_skipped, with the refused size and the cap in the bank's last_oversized_skip) instead of an SSD prefix miss, which hid the cause. The fix, an SSD write-only spill for oversized snapshots and a restore that rebuilds the draft head's history over the restored trunk, is queued for 2.11.4. Macs with 64 GB and more are not affected at these lengths.
The turn right after a reply that closed its tools can wait on the background commit that removes the closing instruction from the banked history (2.2 s on an 11k-token answer), under the existing bounded wait. The app's own compaction of large tool results past 64,000 characters of history is unchanged, so an older web-search conversation may render shorter than it was captured.
Upgrading
brew upgrade mtplx, pip install -U mtplx, or the app's own update check. No settings change. The six 27B packs on the Hub keep their tokenizer.json until they are re-pushed; the loader repair applies either way. MTPLX_QWEN4_BLOCK_VERIFY=1 switches on the exact block verify; the standard verify, also exact, is the default. A conversation that declares tools prefills once more on its first turn after the upgrade, because the declarations now render in one fixed key order; every later turn stays warm.