Every cache these providers keep is a prefix cache, and the working
directory of a terminal is the most volatile value the app has. Putting
one inside the other meant a single `cd` between turns changed byte zero
of the request and missed the entire replayed conversation — on a BYOK
key, in an app whose whole purpose is moving around a filesystem.
Nothing sets `cache_control` yet, so Anthropic was unaffected, but
OpenAI and Google cache prefixes automatically: this was already
costing money, and it was a trap laid for the day explicit caching lands.
The context now rides the tail of the message list, immediately before
the turn it describes. History the model has already seen is immutable,
so `[system + history]` keeps hitting and the divergence is the newest
turn, which was never cacheable anyway. The system prompt goes back to
being byte-identical across every run, conversation, and user.
It is sent but not kept. `snapshot.messages` is the durable transcript,
and a block added there would be persisted, replayed, and — since
restore projects user rows into the visible list — eventually shown in
the user's own chat bubble. Leaving it out costs the next run one
uncached message at the tail and nothing before it.
Two consecutive user messages are already routine here: `.tool` rows
become `user` rows on all three adapters, so any parallel tool call
produces them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`clamp` walks upward first, so `clamp(.low)` on a model whose levels are
`[off, high]` — several Kimi builds — returns high. Across the bundled
catalog that is 70 models spending full reasoning effort to write six
words, with `off` sitting right there unused.
The upward bias is correct where `SecurityReviewClient` uses it: a review
that does not reason is worse than a slow one, so failing upward fails
safe. Titling is the opposite. It needs no reasoning, and a rejected
title just falls back to the opening line.
`clamp(.off)` is the one request the clamp cannot answer by walking up,
so it takes off where it exists and the lowest available level where it
does not: 847 models now send no reasoning field at all, and the models
left reasoning are the ten with no lower rung to stand on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Context was decoration. Three defects, all reported from use.
Session context followed the conversation. `ConversationModel` owned the
chips and the remote system info, and `refreshAssistantContext` stamped
the focused pane onto whichever chat the sidebar was showing — so sending
from the sidebar carried a host and directory into the Assistant tab,
which has no session to describe. It now lives on `WindowSidebarState`
as a `SessionContext`, and `ComposerView` takes it as a parameter
defaulting to `.none`. The Assistant tab passes nothing, so it cannot
show or send a session it does not have. Two bugs fell out with it:
leaving a Workspace clears the context instead of freezing the last
value, and opening a chat in the sidebar no longer marks it.
Nothing was sent. Only `RemoteSystemInfo` reached the model; host and
cwd reached nothing, and `ContextChip` had no payload at all — the
SSH-failure attachment was a label for a log pasted into the draft.
`SessionContext.modelContext` now renders one fenced data-only block
into the run's system prompt, chips carry their content, and
`appendUserTurn` folds attachments into the turn it returns. What was
uploaded is literally what the bubble shows.
Titles were the first message. `ConversationTitleService` mirrors
`CommandComposerService` — tool-free, strict JSON, validated — and
resolves `.chat` rather than adding a Titler role, per the argument
already written into `AIRole`. It runs once after the first reply and
persists to the `title` column that existed but was never read back, so
no migration. It reads `transcript`, not `messages`: a failed run writes
its reason to the visible list alone, and that is not a name.
`Pane.remoteSystemInfo` already withholds the probe when Shell
Integration is off, so the second gate in `AppModel` is gone — the value
sent is the value the chips were drawn from, and the two cannot
disagree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three defects in how a conversation drives and shows a run.
Stop never reached the runtime. `Task { await runtime?.cancel() }`
resolves the property when the task runs, which is after `stop()` has
already set it to nil, so the run carried on executing tools with nobody
listening — secrets undiscarded, SSH and SFTP connections uncleaned.
`messages` was replayed as the next run's prior context, but it is a
display projection: no tool calls, and no tool results at all, because
those are drawn as cards. The model lost everything it had already done,
and a tool-only turn came back as an assistant message with empty
content, which Anthropic and Google both answer with a 400. The
model-facing transcript is now kept beside the visible one and rebuilt
from the same events.
The runtime held the conversation through both its approval broker and
its interaction handler while the conversation held the runtime, so every
chat that had ever run an agent stayed alive for the session. All three
edges are weak now, and a conversation that is gone denies and cancels.
Alongside: messages and tool cards are merged into one ordered timeline
rather than drawn as two lists, a tool-only turn no longer leaves an
empty bubble, the transcript follows streaming text instead of only new
rows, and conversations live in one app-wide store so two windows share
instances rather than each building their own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`bytes.lines` discards empty lines, and a blank line is the only thing
that dispatches an SSE event. Every streamed chat response therefore
arrived as one undelimited blob: `parse` failed to decode it, the turn
produced no text and no tool calls, and the agent loop ended after a
single request. That is both "the assistant never says anything" and
"tool calls never continue".
Line splitting now lives beside the field accumulator, so the streaming
and buffered entry points frame identically — they did not before, which
is why the bug hid on the side without tests. CRLF, LF and bare CR all
terminate a line; a body that ends without a trailing blank line still
delivers its last event; and a single line is bounded so an endless one
cannot exhaust memory.
The legacy MCP HTTP+SSE transport reads through the same parser. Its
stream stays open, so the accumulator never flushed and the endpoint
handshake waited until it timed out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tool the runtime does not have produced an error result and no events
at all, so the transcript showed an empty assistant turn and nothing
else — "the model said nothing" rather than "the model called a tool we
do not have". It gets a card now.
Registering tools trapped on a duplicate name, and names partly come from
remote MCP servers: one server exposing both `list-hosts` and
`list.hosts` sanitizes to a collision and took the app down. First
registration wins, so a remote name cannot shadow a built-in, and MCP
names are disambiguated where they are minted.
Every event was handed to the repository, which meant an unstructured
task and a `ModelContext.save()` per streamed token, all on the main
actor — while recording nothing, because the store ignores message
events. Only run-scoped events reach storage now, and the store no longer
saves when it has written nothing.
Run history also matched stored messages by run id, so a conversation's
earlier turns — replayed into every later run — were re-inserted on each
save, colliding with the unique constraint. Matching by message id
instead leaves the rows that are already correct alone. Restoring now
orders by run before time, so two runs with close timestamps cannot
interleave.
Also: one shared URLSession rather than one per run that was never
invalidated, a bounded read of a failed response body, the write-only
`sawOutput` plumbing removed, an orphaned event stream finished rather
than left suspended, and `AgentRunPersisting` no longer ships default
implementations that could silently become the witness.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>