v0.3.18
Tool calls reach the client again on a --api claude / --api codex-cli seat, and a
conversation stops repeating itself.
Fixes
-
codex-cli: the client never ran a tool the model asked for
On the Responses wire the seat announced tool calls nowhere but inside the terminal
response.completedevent. Codex CLI builds its output from the incremental item events, so it
saw a stream that announced nothing and ran nothing — the model asked for a command, the client
never executed it, and the turn ended with the model reporting it could not read the repository.A call now arrives as the item sequence the dialect defines:
output_item.added→
function_call_arguments.delta→.done→output_item.done.output_indexis assigned in
emission order and reused inresponse.completed, so the indices a client saw stream are the
indices it finds in the final object — it used to be hardcoded to 0, which mis-numbered every
item after the first. -
claude: a second question was answered with the first question's answer
The seat replayed the transcript through
--input-format stream-json, which reads everyuser
line as a live turn rather than as history. A three-message replay produced two answers, the
first re-answering the opening message, and the streaming path forwarded both — glued together
with no separator. What the user saw was the model repeating an earlier reply in front of the
real one, and the same tool call issued over and over.The transcript now travels as one prompt, one turn. It is sent as verbatim JSON rather than
flattened intoUser:/Assistant:prose, so atool_usestays a tool_use and atool_result
stays a tool_result instead of becoming sentences the model has to parse back. -
claude: every non-streaming request failed with a 502
--input-format stream-jsonrefuses to pair with--output-format json; the CLI exited without
writing an answer and the seat reported "no result envelope". Claude Code sends about half its
requests non-streamed, so this was not a rare path. -
cli-seat: a tool call was printed to the user as raw JSON
A model writing a shell command puts real newlines inside the JSON string. The scan that reads
tool calls back rejected those as control characters, found no call, and handed the whole
{"tool_calls": …}object back as prose — the user saw it printed instead of run. A malformed
call is still forwarded verbatim, never repaired or dropped, but the seat now says so in its log
instead of passing it off silently as an ordinary answer. -
codex-cli: an approval refusal ended the turn instead of redirecting it
The seat answered the app-server's approval request with
{"decision": "decline"}.declineis
not a member of that enum — the valid refusal is{"denied": {"rejection": …}}, the one form
that carries a reason and lets the agent continue and try something else. Two more requests
(item/fileChange/requestApproval,item/tool/requestUserInput) had no handler at all and got a
generic JSON-RPC error. Every refusal is now recorded, so a turn that quietly gave up is
answerable after the fact. -
codex-cli: the model list is read from the account, not a hardcoded table
The static list had fallen behind by two — including
gpt-5.6-sol, the model Codex CLI selects
by default, which the grid answered with "No providers available for this model". The seat now
asks the CLI's ownmodel/listand caches the answer per account, falling back to the static
table when the CLI cannot be asked. -
[apps._default.tools.apply_patch]removed from the seat configcodex --strict-configrejects it as an unknown field, so it blocked nothing and only read as
though it had. Codex ignores unrecognised keys silently without that flag.
Improvements
-
Thinking is streamed instead of silence. A reasoning turn sent nothing until the answer
began — measured at 4.3s of a 5.2s turn with a dead wire. The seat now forwards the model's
reasoning as it arrives: asreasoningitems on the Responses wire andreasoning_contenton
the chat wire, never mixed into the answer text. Enabled at the source too, since codex emits no
summaries by default. On the Anthropic wire it is deliberately dropped: athinkingblock is
replayed with a vendor signature this seat cannot mint, and an unsigned one is rejected on the
next turn. -
The caller's tool rules are honoured.
parallel_tool_calls: falseandtool_choicewere
read from no request; the tool block invited several calls at once while the client had asked
for one. Tools the caller cannot execute — aweb_searchwith no name, anamespacecontainer —
are no longer offered to the model as though it could call them. -
output_config.effortis read. Claude Code sends its effort there alongside
thinking: {"type": "adaptive"}; grading only the latter turned a high-effort request into a
medium one. -
A
role: "system"message insidemessages[]is no longer dropped. Claude Code sends one
alongside the top-levelsystemfield — 6379 characters of instructions that never reached the
model.
Full Changelog: v0.3.17...v0.3.18