- The model menu and the inline `/model <query>` suggestions match each query word against the full `provider/model` ID, so `anthropic opus` and `claude sonnet 4.5` find their models.
- Punctuation inside a query word is ignored, so `sonnet45` finds `claude-sonnet-4.5` and `z.ai glm` finds the `zai/glm-*` models.
- A query word also matches when its letters appear in order from the start of a word in the ID, so `cs45` finds `anthropic/claude-sonnet-4.5`.
- Results that contain the query as typed come first, then word matches, then fuzzy matches, each in catalog order, so queries that already worked keep the same top results.
The retry of an empty reply cut short by the provider's default used a
flat 32,768-token limit and kept it for every later step, even when the
catalog named a limit of its own. A known context window now bounds the
limit, as it bounds a full-window limit, and a catalog limit wins on
later steps.
ChatGPT's Codex endpoint chooses its own output limit, so a retry there
sent the same request again and said it was raising the limit. Providers
now say whether a request carries fx's limit, and the retry runs only
through one that does.
The model cache and command replay comments now describe the bounded
wait and the run id in each handle.
Main now pluralizes subagent counts in tool group summaries the same way
this branch pluralizes MCP calls, so the label keeps both: "subagents"
and "MCP calls".
Tests that read command replays matched the old handle, fx-command-replay-
and the hash, so they found none once each run's handle gained a run id.
They now accept the run id and take the blob's name from the hash after
it. D91 in the session manager's design record amends D44 with the new
handle shape.
A name the registry lacks but that has the mcp_ prefix now counts as an
MCP call, so the unsupported-tool test expects "1 command · 1 MCP call".
The ACP length test scripts the retry's reply and checks that the retry
carries a 32,768-token limit and never asks for a summary.
A command's full_output_handle named the blob that holds its output, so
two commands with identical output got the same handle, and a later
result looked like a replay of an earlier one.
The handle now starts with a random id for the run and still ends with
the blob's hash. Runs with the same output keep sharing one stored copy
under handles of their own, and existing handles still resolve.
When a request named no output limit and the provider's own default ran
out before any text or tool call, fx ended the turn with "Done." for an
empty reply.
fx now sends the request again once, with a limit of 32,768 tokens, and
says it is retrying. If that reply is also empty, or the request already
had the catalog's limit, the turn ends with "The reply ran out of output
tokens before any text." instead of "Done.".
When the model catalog failed to load, the retry a submitted prompt
starts ran behind the failed state, so the prompt's first request went
out without the model's limits. With no output limit, some providers
stop a reply at 4,096 tokens.
A request now waits up to two seconds for that retry, as it waits for a
first load. A retry that lands in time gives the request the catalog's
limits. A slower one leaves the request as it was before, so a retry
that hangs never holds a prompt for longer than that.
A tool the model loads with capability_search or mcp_select_tool stays
loaded only for that turn. When the model called it again in a later
turn, or after a resume, the terminal app and fx ask failed the call
with "Dynamic MCP tool not selected for this model step", and the model
had to load the tool again before it could use it.
ACP already loads such a call's current definition from the live
catalog and sends the call through normal validation and permission
review. The terminal app and fx ask now do the same. A name that no live
server has still fails without reaching any server.
A tool the registry does not hold was counted as a command, so a turn's
MCP calls showed up as "2 commands" in its tool group summary and as
commands started in the status line.
Every MCP tool name has the mcp_ prefix, so those calls now have their
own activity kind. The summary reads "1 MCP call" or "2 MCP calls", and
the status line counts MCP calls made.
- An MCP tool call over HTTP after the server dropped an idle connection now goes out on a new connection, instead of failing with "the server went away" on every call until fx restarts.
- Renewing an MCP login after the sign-in server dropped an idle connection now goes through, instead of failing with "the sign-in server didn't answer" until fx restarts.
- A connection the server closed cleanly while idle is replaced before the call is sent, so the call no longer fails once with "the tool may have run".
- Connections unused for more than a minute are replaced instead of reused, so a call after the network silently dropped one no longer waits out its timeout.
- A request whose send fails goes out once more on a new connection. A request that was fully sent is still never sent twice.
The repeated-login driver read the sign-in address the transcript printed.
The transcript now links it, so the driver records the shell's output and
reads each link's target instead.
A typed /mcp login and the Vercel sign-in picker printed the whole sign-in
address. Both now show a few underlined words whose OSC 8 link is the
address: "Open the sign-in page" for MCP and "Authorize with Vercel", the
way Codex and Grok already link theirs. Notices underline a link on every
row it wraps onto. An address that can't be a link target is still printed
so it can be copied.
- `/effort` opens a menu of the current model's reasoning effort levels with the one in use marked. ↑↓ moves, Enter applies the highlighted level, and Esc closes it.
- `/effort <level>` applies a level without the menu, and `/effort default` goes back to the model's default.
- Tab after `/effort` opens a level column under the prompt, as Tab after `/model` opens the model column. ↑↓ previews the level in the status line, typing filters the list, Tab fills in the highlighted level, and Enter applies it.
- A new level is saved like a level chosen in `/settings`, and fx prints `Reasoning effort set to <level> for <model>`.
- During a response, the line reads `Next turn will use reasoning effort <level> for <model>`. The running turn keeps its level.
- A model without levels, a level the model doesn't offer, or a malformed level prints a notice and leaves the effort unchanged. If the model list is still loading, the notice asks to try again.
- In a short terminal, the menu leaves the end of the conversation in view and scrolls.
- `/model` still asks for the effort after a model is chosen.
- `/help` lists `/effort` with the model commands.
The SignIn model and its driver still had every reload replace the host and
end the sign-in. A reload now keeps a sign-in when it leaves that server
alone and ends it when it removes or changes the server; the host swap
remains for reloads the loader can't apply in place. KeptSignInGoesOn checks
the first, with a planted bug for the old behavior.
- The model can ask up to 20 questions in one ask_user_question call, so a related set of questions opens as one prompt instead of several prompts of 4.
- The tool schema and description tell the model the new limit.
- A call with more than 20 questions is rejected with "provide 1 to 20 questions", and no prompt opens.
- Resumed sessions show every answer from a batch of up to 20 questions.
Running /permissions with no arguments printed the mode, configured
rules, session grants, and saved rules to the transcript. It now opens a
read-only panel in the footer, like /status, and prints nothing. Each
configured rule, session grant, and saved rule is a bullet, and saved
rules show the ID that /permissions revoke takes. The panel updates when
an approval adds a grant. Subcommands such as /permissions auto and
/permissions remember still confirm in the transcript.
The /status panel becomes a shared info panel that both commands use.
Tests that ran /permissions only to put text in the transcript now run
/allowlist.
wasm32 has no 64-bit atomics, so the SDK's WASM builds failed to compile
the catalog's generation counter. Bindings only compare it for equality,
so 32 bits is enough.
With FX_RECORD set, fx prints a recording notice under the banner, so a
12-row terminal has no room left for the banner above the panel. The
tests now check that the last row of the transcript stays in view.
An /mcp add, remove, approve, or reject used to build a new MCP host and
restart every server. The runtime now applies the change in place through
Engine.reload: servers with the same config keep their process, connection,
sign-in, and calls; removed or changed servers stop and their calls end;
a changed or re-added server starts once its own old process has exited.
The engine copies each server's environment and compares it by content, so
reading the same config again no longer looks like a change.
In a short terminal the panel could take every row but three and cover
the turn in progress. Closing it redrew the rows it had covered, which a
terminal could already have scrolled into history, so they showed twice.
Output written while the panel was open could lose them instead.
The panel now gets the rows an inline picker gets, which leave the end of
the transcript in view, and scrolls the rest with the arrow keys. The E2E
reader scrolls a panel that does not fit to read every line.
- /status opens a panel in the footer instead of writing a notice to the transcript. Esc or Enter closes it and leaves nothing behind.
- The panel's first line shows the open session's ID and whether it is saved yet, so it can be resumed with fx --resume <id>. A shell that can't save sessions shows none.
- The open panel updates when a running turn saves the session or finishes.
- When the panel doesn't fit, the arrow keys scroll it and the hint says so. Long values such as the workspace path wrap instead of being cut off.
- When profile settings can't be read, the panel shows a Settings line and leaves out auth, instead of printing an error with no status.
- A fork made before the first prompt now counts as saved: /status shows it as saved, and the exit line prints its resume command.
- When a stdio MCP server replies with more than 16 MiB, the model is told the reply was too large and fx restarted the server, instead of "the server went away".
- When an HTTP MCP server replies with more than 16 MiB, fx ends the call at once and tells the model the reply was too large, instead of "the server went away".
- fx no longer resumes an HTTP stream (2025 protocol) to fetch an oversized reply again. Before, it downloaded the same reply up to two more times.
- fx no longer requests an oversized tool list again from an HTTP server on the 2026 protocol.
- The debug trace logs each dropped reply with the server, transport, and reason.
- /mcp logout in a shell now also revokes the login another terminal saved after the shell started. Before, that login stayed valid at the server.
- After another terminal logs out of an MCP server, a shell that still holds the login revokes it the next time it would renew it, instead of renewing it and saving it back.
A shell with job control runs the rest of a list when its foreground job
stops, as it does when the job dies. Stopping children first gave such a
shell the same gap the kill pass had, so the stop pass now goes parents
first: the root, then each tracked process in the order the tracker found
it. The kill pass keeps its order, because by then nothing is running.
When fx force-kills a command after a cancel, a timeout, or a lost owner, it
sends SIGKILL to each process it tracks, children before parents. A parent
shell whose child died first could run its next command before its own
SIGKILL arrived, so cancelling `sleep 30; rm -rf out` during the sleep could
start `rm -rf out`. The tracker now sends SIGSTOP to every process it would
kill before the SIGKILL pass, so none of them can act on another's death.
Cleanup after a command finishes on its own gets the same order.
- Write a character that a terminal read splits right before a CAN or SUB byte as a replacement character, as the same bytes in one read already do. It was dropped, or joined with continuation bytes after the CAN into a different character.
- Stop fx from crashing on a cursor position report in origin mode when the cursor sits above the top margin, for example after DECRC or VPA. The report now gives row 1.
The helper in the subagent decoder tests printed the correction it could not find, and the direct-write audit rejects a print outside a test block. It now fails with a plain assertion.
The command approval prompt now shows the forced-removal note and the
safer-tool hint for a command such as CI=$(echo a b) rm -rf dir, because the
assignment no longer ends inside the substitution. Assignments the analysis
doesn't peel, such as X=$(...), still get no note.
Before next dev --webpack compiles anything, it waits for Next's version
check, a fetch to registry.npmjs.org with no timeout of its own. While the
registry was slow, dev printed that it was ready and then answered nothing,
so test-next-package.mjs gave up after 30 seconds. Each Next server the test
starts now preloads sdk/tests/no-npm-registry.mjs, which fails that request at
once, so Next takes its "staleness unknown" path and the test needs no npm
access.
A fuzz test reads its input through std.testing.Smith, which reads a slice
as a little-endian u32 length and then that many bytes. The inline seeds of
the older fuzz tests were plain strings, so each lost its first four bytes
to the length: "one" replayed as an empty input, and "split every field"
read "spli" as a length. The fx tests now wrap their seeds in
fuzz_seed.slices, which writes that length first. The session manager
imports nothing from fx, so its two targets keep their seeds as saved
inputs in tests/fuzz/corpus.
A shell call without `action` now runs when its fields fit exactly one action: `command` with run fields means run, `session_id` with `chars` or `yield_time_ms` means interact, and `session_id` with `force` means stop. fx adds the action before the call is stored or checked against permissions, so the approval covers the command that runs. A bare `session_id`, or a `command` next to a `session_id`, is still rejected with a correction.
Both tools list their fields at the top level instead of under a `request` object with one variant per action or selector, so a model no longer writes that nested JSON by hand. Each field's description says which action or selector it belongs to. A rejected subagent call now names the field it got wrong and the fields each action takes, instead of returning only an error code. Calls that still wrap their arguments in `request` keep working, and saved wrapped calls replay with top-level arguments.
The shell tool schema lists `action`, `command`, and the other fields at the top level instead of nesting them under `request` with one variant per action. With the wrapper, models that wanted several commands at once sometimes sent two `request` keys in one call, which fx rejected as invalid JSON arguments. Each field's description now names the actions it applies to, a field used with the wrong action is still rejected with a correction, and corrections suggest a top-level retry. A call that wraps its arguments in `request` still runs, and saved shell calls are replayed as they were sent.
Tool schemas also keep the description of an object-valued property, which the `shell` field of the shell tool and the `request` field of `read_tool_result` were missing.
A remembered command approval is not meant to cover a shell call that reloads the user's startup files, because the reload changes the environment the command runs in. The check read `reload` only from inside a `request` wrapper, but shell calls are admitted with top-level arguments, so `reload: true` never matched and the remembered approval still applied. The check now reads top-level arguments as well as an exact wrapper.
word_end, which finds where a leading NAME=value assignment ends before the
command classifier looks for the command, now reads $(...), ${...},
backquotes, and $'...' the way the shell does, so a space inside a
substitution no longer ends the assignment early. The command_classification
fuzz target now generates substitutions and leaves them out of its lexer
comparison, because the lexer reads $ and backquotes literally.
ThreadSanitizer does not intercept mremap, so when std.heap.PageAllocator shrinks or moves a block on Linux, the released pages keep their access history and the next thread to use those addresses appears to race with the one that released them. That produced the race reports in the MCP HTTP large-answer test. Sanitized x86_64 and aarch64 Linux builds now link an mremap that makes the same change with munmap, and with mmap, a copy, and munmap when a block moves.
When fx could not confirm that a model accepts images, because the model catalog was unreachable or did not list the model, every later message in a session that had attached an image failed with "Unable to verify image support for this model". Only an image attached to the current message can fail it now. Earlier images go inline to a model the catalog lists with vision, and through the vision tool's image references to a model without vision. Otherwise they stay in the session and leave the request as a note for the model, and an unreachable catalog tells the user once per message.
- The image route core separates images attached this turn from earlier ones, which follow the tool image rule.
- `bun run verify` in `tests/tla/image_route` gains a stage that drives the built binary through `fx ask` sessions and has TLC check every turn against the model.
- The `image_route` and `earlier_image_notes` fuzz targets check the core's laws on generated event sequences and the notes left for earlier images.
git runs a git-remote-<helper> program, which it may find on PATH, for a repository written <helper>::<address> and for a URL whose scheme has no built-in transport. Auto mode now sends those git fetch commands to review, including after --. https, ssh, git, and file URLs still run without review.
fx ask writes one notice when converting a session an older fx saved takes over 150 ms. The tests that resume a copied v1 session through fx ask now strip that one leading notice before requiring empty stderr, so a slow runner no longer fails them.
Tests never set up the environment `io.getenv` reads, so the shell parity tests never saw `FX_SHELL_PARITY_ALL_SHELLS` and skipped a missing shell even when it was set. They now read it from `std.testing.environ`.
A parity failure now shows what fx parsed against what the shell ran, and which command or shell failed, through `std.testing.expectEqualStrings` instead of printing it, and keeps its error name.
Eleven functions in oauth.zig, chatgpt_oauth.zig, and grok_oauth.zig
used snake_case names, against the repository's camelCase rule for
functions. They and their callers are renamed, and nothing else
changes.
Both tools replaced a wrong-type value with its default and ran anyway.
A string "true" for case_insensitive searched case-sensitively, a
numeric path searched the whole workspace, and an unknown mode fell
back to the default, while the model believed it got what it asked for.
They now answer with a message that names the field, as the other tools
do. A missing field or null still means the field was not given.
Grok refused to save an account ID that it would reject when loading,
but ChatGPT saved it anyway. A sign-in or refresh with such an ID looked
fine, and the next start then reported that saved credential storage
was unavailable.
Both providers now check before saving, so that sign-in or refresh
fails right away. The setting that kept the two apart is gone.
chatgpt_session.zig and grok_session.zig were the same store with a
different file, lock, log label, and pair of error names, and the two
copies had started to drift. Both now come from subscription_session.zig
with those values. Each still returns its own error names, and only the
errors it returned before.
The one real difference stays, as an explicit setting: Grok refuses to
save an account ID that it would reject when loading, and ChatGPT
doesn't. ChatGPT's issuer and auth file name exports had no users and
are gone.
Six transports turned an HTTP error status into a failure kind with the
same switch, and four wrapped their stream callbacks around the event
sink with the same adapter struct. A new status had to be added in six
places, and a missed copy would misclassify retries for one provider.
FailureKind.fromHttpStatus and the EventSink callback adapters now live
in stream_provider.zig, next to the types they produce, and every
transport uses them. Each status maps to the same kind as before and
each adapter emits the same event. New tests pin both.
The Linux unit test job installs zsh, so it now sets `FX_SHELL_PARITY_ALL_SHELLS` and the shell parity tests fail there instead of skipping when sh, dash, bash, or zsh is missing.