Skip to content

server: a warm pass keeps the state the next turn resumes (chunk cut, tool-result opener, reasoning) - #2686

Closed
ashhart wants to merge 10 commits into
mainfrom
lead/warm-turns
Closed

ashhart wants to merge 10 commits into
mainfrom
lead/warm-turns

Conversation

@ashhart

@ashhart ashhart commented Nov 9, 2026

Copy link
Copy Markdown
Owner

What this changes

After a reply, the server runs a warm pass: it renders the conversation as the client's next turn will send it, and keeps that prompt state, so the next request resumes instead of prefilling the reply again. Three gaps made the warmed state differ from the one the next request needs, so agent tool loops missed it and re-prefilled:

  1. The chunk cut. Chat requests cut their prompt chunks just before the conversation's own text (after the system prefix), but the warm pass used the plain chunk starts. On a family with planned chunks, the warm state was built from different chunks than the next request's fresh run, so it couldn't be the same state. One helper, chat.requestChunks, now gives both the same starts and cut.
  2. The next turn's opener. The warm pass probed the next turn as a user message. After a reply that called tools, the next request opens with a tool result instead, and on templates whose tool opener differs (Kimi, for one), the shared prefix stopped short. After tool calls, the probe now opens as the first call's result.
  3. The reply's reasoning. The warm pass rendered the reply without reasoning_content, while a client sends back the message it received, reasoning included. On templates that render past reasoning, the history then differed from the next request's. The warm pass now renders the same message the client got (openai.replyMessage, which the whole-body reply uses too, so responses are unchanged).

A warm pass only changes which prompt states are kept. No reply's tokens change.

Checks

  • New zig/src/server/warm_turns_test.zig. Each test asserts that the warmed history is a prefix of the next request's prompt (and, for the cut, that the chunk starts below it are equal). Each fails when its fix is reverted (each control checked).
  • zig build test passes on macOS on this head. zig fmt --check, the lean check (no new problems), and files at most 600 lines.
  • Not changed: the Anthropic route already warms (warmNext runs on both reply paths). The warm pass being cancelled while other streams are active is a separate change.

Not yet run

A served multi-turn tool-loop cell, main against this branch (token hashes per turn, cached_tokens and first-token time on the next turn). It runs before landing.

The warm pass took the plain chunk starts, while chat requests add a cut before the
conversation's own text (the system prefix). On a planned family the pass then kept
a state built from other chunks than the next request's fresh run. One helper,
chat.requestChunks, now gives both the same starts and cut. Requests that don't warm
are unchanged.
…esult

The warm pass always rendered the next turn as a user message. When the reply
called tools, the next request opens with a tool message, and a template whose tool
opener differs from its user opener (Kimi's, for one) left the warmed prefix off
that request, so the pass was never resumed. The probe now opens as the first call's
result when the reply made calls. Replies without calls warm as before.
…ient got it

The warm pass rendered the reply as role, content and tool calls, without the
reasoning_content the client received. A client that sends its reasoning back, and
a template that renders past reasoning, then made the next request differ from the
warmed prefix at the reply. One replyMessage now builds the whole reply's message
and the warm pass's alike; the message cortheon remembers is unchanged.
…se history sent reasoning back

A client that drops reasoning from the replies it sends back (most do) would
miss a warm pass that rendered it, and on a first turn there is no history to
tell, so the pass leaves it out as main does. Tests cover a first turn, a
client that keeps reasoning and one that drops it.
@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto today's main (head 3094e15a55), with one change to the third fix.

The warm pass now keeps the reply's reasoning only when the request's own earlier replies came back with reasoning_content. On a first turn, and for a client that drops reasoning (most do, as #2791 found), it leaves the reasoning out, as main does. Rendering it for every client would cost those clients the warmed resume, and #2811's fix and its test were built around main's reasoning-free warm pass.

  • zig/src/server/warm.zig:66 decides this from the request's history.
  • Three tests in zig/src/server/warm_turns_test.zig cover a first turn, a client that keeps reasoning and one that drops it. With the change removed, the first-turn and drop-client tests fail.
  • The chunk cut and the tool-result opener are unchanged.

The server host tests pass on this head (1,330 passed, 7 skipped). zig fmt is clean and the lean count is unchanged.

Still owed before landing: the served tool-loop cell on Qwen3.8-27B and on Flash Next on Metal, main against this branch, once with a client that keeps reasoning and once with one that drops it. It reports token hashes per turn, and cached_tokens and first-token time on each next turn.

  • Synthena (Ash's Agent)

@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner Author

Restacked: this branch now carries the four follow-up commits on top (724f3d28b4, a fast-forward). With TF_QWEN_REQUEST_STARTS, the warm pass is deferred rather than cancelled when the queue is busy, kept ends follow the request's own message cuts (system message included), and a kept state's cut plan must match before it is reused. The stale recipe sentence about the system-cut mismatch is updated, and the changelog line is one line naming this PR. All 57 CUDA family host steps pass, and 17 single-rule breaks each fail their test. Note for landing: #2824 also enables warm turns, by default, and conflicts with this in qwen3_5/cuda.zig, native/cuda_ledger.zig and native/cuda_snaps.zig; whichever lands second reconciles.

  • Synthena (Ash's Agent)

@ashhart

ashhart commented Nov 14, 2026

Copy link
Copy Markdown
Owner Author

Closing: its first-turn reasoning rule is the opposite of #3264's, which is queued to land, so #3264's rule stands. Any other part of this series that is still wanted can come back as a small PR rebased on main.

  • Synthena (Ash's Agent)

@ashhart ashhart closed this Nov 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant