Commit Graph
84 Commits
Author SHA1 Message Date
iris-aiandClaude Opus 5 bb5ac1a242 Draw a model's thinking, and what a reply cost to produce
A llama.cpp session's `reasoning_content` becomes `Event::Thinking` deltas
closed by an `Event::ThinkingDone` carrying the span the driver measured, and
the phone draws it as a card of its own: "Thinking" with the spinner a running
command has, then "Thought for 12.4s". Deliberately not a tool call, so a run
of calls cannot collapse the reasoning into "Called 6 tools"; the reasoning is
also kept out of the next prompt, which `conversation` already ignored.

`UsageDelta` gains `tokensPerSecond`, the provider's own figure or nothing --
llama.cpp reports `timings.predicted_per_second` and the coding CLIs report no
such thing -- and a finished reply carries a small line under it saying when it
was sent and, where there is one, how fast it came out: "3:00 PM · 149 tok/s".

The compact usage bar drops the provider's name for the window and puts its
length after the time left instead: "42% · 3h 20m left / 5h".

Three things that had to come with it: the transcript coalesces runs of
thinking deltas as it does reply deltas, so one block is one row of a page
rather than a page of its own; `joinPages` welds a block cut by a page boundary
(`healSplitThinking`), since the half with no ending spun for ever; and
`UsageDelta` now reaches the fold, which is what carries the rate to the reply.

Verified on the emulator against a real Qwen3-0.6B session and the echo rig's
new `/think [seconds]`: the spinner while it runs, "Thought for 1.4s" and
"2:54 PM · 149 tok/s" after, the reasoning on tapping the card, and the usage
bar reading "42% · 3h 19m left / 5h".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 15:07:25 -04:00
iris-aiandClaude Opus 5 81ab564a09 Declare provider settings, and give the context figure a denominator
Two things a session could not say, and one it was saying wrongly.

**Every provider setting is reachable.** `-np 1`, the MTP draft depth, the
tool set, the sampling parameters -- most were hardcoded to what measured
best on this machine, which is right as a default and wrong as a constant:
the next machine has a different GPU and a different core count, and
nobody running this app can edit the source. `DriverKind::params` now
declares what a provider takes -- key, label, shape, what blank means, and
whether a change waits for a restart -- and the phone renders whatever
arrives, on the spawn form and in the session settings dialog. Adding a
setting to a driver is one entry in that table and no app change.
`POST /sessions/{id}/params` takes the whole map, so an absent key is the
instruction to unset; the sampling half applies at once and the session is
told in words which of the rest are waiting for a restart.

`tools` is one of them, because it is the biggest lever on a tight
context: the seven built-in definitions are ~1,300 tokens of every prompt
(2,191 against 887 with none). `"none"` omits the flag rather than passing
it on, since `--tools none` is `unknown tool "none"` and a server that
exits.

**The context figure has a denominator.** `Event::ContextWindow` carries
it, read from `llama-server`'s `/props` once the model is up -- the
measurement rather than the request, since a session that named no context
size gets the model's own. Neither coding CLI states its window, so those
keep the bare figure: "2,042" and "2,042 / 8,192" are deliberately
different-looking, and a missing ceiling is never drawn as a proportion of
an assumed one.

**And the numerator was wrong**, by the length of the last reply: it was
the prompt alone, so a five-word answer reported 2,042 against a slot
holding 2,355. It is the turn's total now, which matches `llama-server`'s
own `n_tokens` to within a token.

Two defects the review found, both of which would have shipped: changing
settings on a *stopped* session reported "no process running, so it can't
take new settings", when a stopped session is exactly when you would set
them for the next start; and `GET /tools` answers **403** rather than an
empty list on a server started without `--tools`, so reading it as a
failure made the no-tools session one that never started.

Verified against real models: settings spawned and changed live, the
restart note, a session with two tools and one with none, and the counter
checked against the server's own slot occupancy each time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:58:08 -04:00
iris-aiandClaude Opus 5 ac476ab0c9 Give llama.cpp sessions tools, web search and a model picker
A llama session was a chat box: no tools, a fixed model, no permission
mode, and a model name drawn as the path the file sits at. It now runs the
agent loop itself, which is what the pieces below all hang off.

Tools are `llama-server`'s own (`--tools all`), which that server both
publishes and runs -- `GET /tools` for the definitions, `POST /tools` to
call one. Web search is Exa's MCP server, reached from this backend rather
than from the machine serving the model: that is what llama.cpp's own web
UI does, and it puts the search on the machine with a route out instead of
the one with the GPU. `llama-server`'s `--mcp-servers-json` can only spawn
local commands, so using it would have meant a Node bridge on every
machine that serves a model.

Driving the loop is what makes the permission gate ours. Two modes,
`manual` and `bypassPermissions`, which is what the mechanism has: the web
UI asks before every call and remembers the tools you say "always" to. The
allowances fold back out of the transcript's own answers, so they survive
a restart and a model change without being stored anywhere else.

Also here, because tools made each of them matter:

- **Loading is a state.** A 12 GB model takes twenty seconds to reach
  memory and refuses everything until it has; the session used to report
  `running` for that whole time, and a message sent meanwhile came back as
  an error. It is `loading` now, and the message waits.
- **The model can be changed.** A `llama-server` holds one model, so this
  stops it and starts another. The conversation survives because it was
  never in the server.
- **Models are named, not pathed.** `general.name` read out of the file
  itself -- over ssh too, in the round trip the spawn was already making.
  Where two models share a name the file name breaks the tie.
- **`-np 1`, and the MTP draft head where the file has one.** Measured on
  the 27B here: 41.5 tok/s plain, 61.4 with `--spec-type draft-mtp` at one
  slot, and 28 with it at four -- speculating against a split KV cache is
  worse than not speculating. The flag is conditional because asking for a
  head that is not there makes `llama-server` exit.
- **A refusal says what to do.** Tool results are thousands of tokens, so
  an overrun context is now ordinary; it was "http status: 400" and is now
  the server's own "exceeds the available context size, try increasing it".

`GET /machines/{id}/models` is gone: the provider models route answers the
same question, and two answers to one question is how a picker comes to
offer a model the spawn screen does not.

Verified end to end against real models: a tool call asked and allowed, an
Exa search, a shell command, a 27B loaded while a message waited on it, a
model switch mid-session, a second message queued behind a running turn,
and the whole of it again on a session running over ssh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 08:11:49 -04:00
iris-aiandClaude Opus 5 392cc5413d Recover Claude sessions with a stale resume token
A resume token the CLI will not accept made the session unrecoverable
rather than merely failed: every later start passed the same `--resume`,
died the same way, and nothing ever forgot it, so the chat could not be
opened again from the phone.

The reader now recognises both refusals the CLI gives, forgets the token
and emits `Cleared`, which is what the Codex path already does for a
thread whose rollout has gone. The transcript is this server's and
survives; only the model's context restarts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 06:56:41 -04:00
iris-ai 827a30768c Count Codex background terminals 2026-09-16 01:07:40 -04:00
iris-aiandClaude Opus 5 cbae7ee8c0 Order the session list by when each agent was turned on
A running session no longer moves: the ones with a process come first,
oldest start first, so starting one appends it to the bottom of that
group and nothing it goes on to do -- beginning a turn, finishing one,
asking a question -- can shift it. Sorting by activity with the
awaiting-answer ones floated to the top is what this replaces; the
status word and its colour already say which session wants something
without the row having to move to say it. Stopped sessions are a group
below, most recently active first.

The order is the server's: `SessionConfig::started` is written each time
a process is started for a session and reported as `started`, so it is
the same on every device and survives a backend restart -- which adopts
processes rather than starting them, and so could not work the times out
for itself. Applied on the phone, because presentation order is a
display decision.

`LiveSession::info` takes the session's config entry rather than a
parameter per field read from it, which is what `AutoResumeView` existed
to bundle; that goes.

Verified on the emulator against the sandbox: three echo sessions kept
their order while the newest-active one was messaged; a stopped and
restarted session moved below one started after it; a stopped session
dropped below every running one; and after a backend restart the
recorded times came back unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 23:54:36 -04:00
iris-ai a9cfea89e5 Show Codex background task counts 2026-09-15 23:26:37 -04:00
iris-ai 3d1b1e304d Render whole-file patches from change metadata 2026-09-15 16:18:18 -04:00
iris-ai 06bf1c8f81 Preserve messages during Codex thread recovery 2026-09-15 15:30:22 -04:00
iris-ai 3f94eeb6d6 Create Codex threads on first message after clear 2026-09-15 14:43:23 -04:00
iris-ai 1c60e78b55 Keep Claude commands out of subagents 2026-09-15 14:29:18 -04:00
iris-ai 8262ceb786 Show live background task counts 2026-09-15 13:44:32 -04:00
iris-ai 9fd21af4e8 Reconcile Claude background task state 2026-09-15 12:49:22 -04:00
iris-ai f0661919bb Offer Claude sign-in from failed sessions 2026-09-15 12:24:25 -04:00
iris-ai 036eb375aa Keep the running tool call outside its group
A run of adjacent calls is drawn as one collapsed card, which hid the one
thing worth seeing without opening anything: the command the session is
running right now. It is a row of its own while it runs and folds back into
the run when it ends.

Grouping stays a display decision, so the pieces a running call cuts a run
into are keyed there. The first piece keeps the run's name -- that name is
what survives a page of history landing in front of it -- and later pieces
take their own first call's id behind it, since the call a run was named
after can itself be the one running.

The echo rig's /tools gap now runs between a call's start and its end rather
than between one call and the next, which is where a real session's time goes
and what makes the running state observable at all.

Checked with ktfmtFormat, compileDebugKotlin, testDebugUnitTest (new
ToolRowsTest) and lintDebug, cargo fmt/clippy/test, and on the emulator
against the sandbox: "Called 2 tools" with the live Bash card beneath it.
2026-09-15 01:13:03 -04:00
iris-ai b9b777acaf Release messages after Codex recovery 2026-09-14 15:16:05 -04:00
iris-ai 46831520e3 Recover Codex sessions with missing rollouts 2026-09-14 15:01:47 -04:00
iris-aiandClaude Opus 5 3af2502982 Tell the model when a message was a steer
A message typed during a turn reaches the model at the next model call if
the turn has one left, and otherwise as the opening line of the next turn --
Claude's read out of the fifo after the turn ended, Codex's requeued when
turn/steer is refused. Read there it is indistinguishable from a reply, so
the model treats the answer it just gave as seen.

Both drivers now compose the text the CLI receives through
driver::message_body, which prefixes a note saying the message was written
without having seen the rest of that turn. The transcript still holds the
words that were typed; only the CLI's copy carries the note.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 22:30:32 -04:00
iris fe25108c51 Count every Codex model request, not just the last
App-server sends `thread/tokenUsage/updated` once per *model request*, and a
Codex turn makes as many as it made tool calls. The translator held the last
one until `turn/completed`, so a turn's cost was reported as its final
request alone -- measured against the real rollout, 28,878 tokens for a turn
that spent 51,399 -- and the gap grows with how much work the turn did. The
context figure also stood still for the whole turn, which is exactly when it
is moving most.

Reported as each arrives instead: `tokens` now adds up to what the turn
spent, and the context figure climbs during the turn (28,921 -> 33,190 ->
35,978 on a two-file read here, matching Codex's own `last_token_usage`
exactly at every step).
2026-09-13 03:58:47 -04:00
iris 898e6b92d0 Clarify subagent coordination cards 2026-09-13 02:48:21 -04:00
iris cad0cbcfbe Keep subagent delivery out of assistant text 2026-09-13 01:05:35 -04:00
iris 83b113ef0f Support Codex subagent transcripts 2026-09-13 00:41:59 -04:00
iris 7d9df5d572 Rename setups and add provider reauthentication 2026-09-12 22:56:43 -04:00
iris 559e6c9226 Suppress errors for requested session stops 2026-09-12 20:49:38 -04:00
iris 57e1cec09c Render Codex web searches as common tools 2026-09-11 02:31:21 -04:00
iris 3c19b5a9bb Fix Codex transcript convergence 2026-09-10 01:24:16 -04:00
iris f9c8f640ce Fix quoted Bash tool titles 2026-09-10 00:46:09 -04:00
iris cbdd8493ed Unwrap double-quoted Codex Bash commands 2026-09-09 22:17:14 -04:00
iris 26fe9895e7 Unwrap rendered Codex Bash commands 2026-09-09 22:11:52 -04:00
iris b00e89795e Parse Codex app-server patch payloads 2026-09-09 21:30:15 -04:00
iris 10ce1a216b Defer Codex patches until their diff arrives 2026-09-09 20:58:48 -04:00
iris b507656abd Normalize shell and patch tool cards 2026-09-09 20:30:52 -04:00
iris 4dc3e3d784 Fix Codex transcript streaming and images 2026-09-09 15:14:24 -04:00
iris 14dd520719 Fix explorer back and session usage selection 2026-09-09 13:01:27 -04:00
iris 8c88a7e991 Use native Codex steering and transcript deletion 2026-09-09 12:19:11 -04:00
iris 00538cc19b Show separate Codex usage pools 2026-09-08 00:41:06 -04:00
iris 7ee88dfd9c Make model and permission choices provider-specific 2026-09-08 00:08:09 -04:00
iris 6a0202b1b5 Add Codex JSON sessions and usage limits 2026-09-07 23:29:15 -04:00
irisandClaude Opus 5 0862b47f76 Record the loose end the taskNote outage exposed, and the last of its checks
A session whose transcript will not parse is skipped with only a log line, so
from the phone it is indistinguishable from an idle unresponsive one. That is
why the outage needed a report from Bryan rather than showing itself. The
cause is fixed; the class is not, and it is the "design the unknown state
first" rule rather than a bug in one code path.

Also rustfmt on the parse path, which the fix landed unformatted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 22:43:01 -04:00
irisandClaude Opus 5 fd71d876e1 Never let one unreadable line take a transcript down
Removing `Event::TaskNote` hours after adding it made every transcript that
had recorded one unreadable. `Transcript::open` parses every line, so `launch`
failed for those sessions and `SessionManager::new` logged
"couldn't relaunch session <id>" and skipped them -- and a skipped session has
no pump and no driver. On the phone that is no status, no history and nothing
sendable, for every live session that had run a background task. One
unfamiliar word took down every conversation it appeared in.

A transcript is append-only and permanent, so the set of kinds one can hold
only ever grows: what this build writes is not what it may have to read. A
line can come from a newer server, or from an older one that wrote a kind
since dropped, and neither may be able to end the file.

`Indexed::parse_at` degrades a line it cannot make sense of to
`Event::Unreadable { kind }` instead of failing the whole read. It keeps the
line's seq -- the cursors, the page bisection and the next-seq counter are all
addressed by it, and dropping the line would hand out a seq the file already
contains -- and carries the word the line called itself, so the phone can say
what is missing rather than that something is. A line with no readable seq is
still an error: that one cannot be placed at all.

`Event::TaskNote` comes back retired rather than deleted: deserializable,
never constructed, dated, with the reason on it. The phone folds it to no row,
which is the point -- an unreadable line correctly draws a placeholder, and
one per background task is the wall the row was removed for in the first
place.

Found while diagnosing a report that live sessions had lost their status and
could not be sent to. 173 server tests pass, including the new one, which
fails on the old code within a second.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 22:23:45 -04:00
irisandClaude Opus 5 9cc52beb09 Report a backgrounded command into the card that launched it
A backgrounded command has no subagent, so there is no second transcript for
its report to live in and its own tool card is the only record of it anywhere
-- and until the task notification arrives that card is showing the launch
result, which says the command is running. It was left saying that for ever.

The report now updates the call's own row (`Event::ToolUpdate` against its
tool_use id), so the card ends up holding what became of the command instead
of a claim nothing was ever going to correct. That includes the endings that
carry no summary: those are exactly the ones that went wrong, and a stale
"running in background" reads worst on them, so they say the status word
rather than nothing. A task with a subagent behind it is untouched and its
report stays where it was, in that subagent's own transcript.

Echo grew `/background [seconds]` for the shape end to end: the Bash call, the
launch result, a turn that ends `waiting`, and the completion arriving later
to correct the card and start a second turn.

Verified on the emulator: the card reads `Background command "sleep 5 && echo
done" completed (exit code 0)` where it had said "Command running in background
with ID: ...". 172 server tests, ktfmt, clippy, rustfmt, Android lint and the
JVM unit tests clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 21:47:54 -04:00
irisandClaude Opus 5 1bbb642973 Take subagent reports out of the main transcript, and separate turns with a rule
A row per finished background task is a screenful of dividers about work the
reader was not asking after, and one of them turned out to be a whole shell
command drawn as centred prose, because its words came from somewhere with no
reason to keep them short. `Event::TaskNote` is gone entirely, along with the
row that drew it. A subagent's closing report is recorded as that subagent's
own transcript's closing text and is read in the subcard, which is where it
was already going; what the parent gets a row for is a message a subagent
genuinely sends it, which arrives by the peer path and has had one all along.

What remains is the actual defect and the smallest thing that fixes it. The
fold still refuses to grow a settled reply, so a turn boundary is always a
message boundary, and where two replies then abut it puts a `TurnBreak`
between them: a hairline, no words, no colour. Made by the fold rather than
sent by the server, because it is not something that happened -- it is the
boundary between two things that did. `joinPages` puts one in at a page seam,
which the fold never gets to see.

The task notification is still what closes a task in `Status::Waiting`'s
bookkeeping, and the registry lookup that recognises one this translator never
saw start is what makes that work for a session adopted across a restart.

Verified on the emulator: three replies, three rules, and nothing about the
helpers anywhere in the parent. 170 server tests, 85 JVM tests, ktfmt, clippy,
rustfmt and Android lint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 21:39:19 -04:00
irisandClaude Opus 5 ef1aad8776 Keep a subagent's words in its own transcript, and count the ones already running
Two corrections to the previous commit.

A subagent's closing report belongs in the subagent's transcript, which is
where it already is; drawing it as a card in the parent's put the same
paragraph in two places for a reader who did not ask for it. The row is a
divider now -- a boundary, which is what the transcript actually needed there
-- closed, saying only what reported and how it went. Opening it shows the
report anyway, since leaving the conversation to read one line has its own
cost, and a backgrounded command has no transcript of its own so this is the
only place its report exists at all: that one names itself from its summary
and has nothing left to open. `TranscriptDivider` grew a `trailing` slot for
the chevron rather than the row growing its own copy of the rules.

And the status was wrong for a session that was already running before the
update, which is every session when the backend is replaced under it.
Adoption picks a session's stdout back up from a recorded offset, so the
`task_started` lines for subagents launched earlier are behind it and the
translator never saw them -- it started with an empty set and reported `idle`
with a subagent plainly still working. `Subagents::any_open` reads the
directory instead, which is a measurement rather than bookkeeping and is right
for a session this process did not start. Both sources are kept and neither
subsumes the other: the translator's own set is the only thing that knows
about a backgrounded *command*, which has no subagent to be found. The same
pair decides whether an ending has already been reported, so a task that began
before the restart still gets its divider.

Echo's helpers now record their report as their own subagent's closing text,
the way the real driver does, so the fixture has the shape being tested.

Verified on the emulator: three dividers closed, one opened to its report, and
each reply drawn as its own message. 170 server tests, ktfmt, clippy, rustfmt,
Android lint and the JVM unit tests all clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 19:54:36 -04:00
irisandClaude Opus 5 5711c2568a Never run two turns into one, and say when a session waits on its own work
A turn started by something with no row of its own -- a subagent reporting
back, a peer message the CLI only owns up to at the end -- met the previous
reply with nothing between it, and the fold grew that reply rather than
starting a new one. Two answers were drawn as one paragraph, running together
mid-sentence with not even a space between them. The fold now refuses to grow
a settled reply, and `joinPages` carries the same rule across a page boundary.

The other half is the row. `Event::TaskNote` records a background task
reporting back -- a subagent that finished, or a backgrounded command -- with
its title, how it ended and what it said; `TaskNoteRow` draws it as a card,
since somebody said this, and its own row rather than an update to the Task
call's, which is above everything the session has said since. Reported once
however many of the CLI's two lifecycle shapes arrive.

`SessionStatus::Waiting` is a session whose own turn is over while work it
started is not. `Idle` means "waiting for a person" and this means the
opposite, so reporting it as idle sent a "finished" notification at the one
moment that was untrue. Drawn as "waiting" in `waitingColor`; the queue and
the held-command boundary release on either end-of-turn status, so a message
sent while a subagent runs is not held until it finishes.

And a usage limit the account hits inside a subagent now reaches the session
as well as the subagent's transcript. `resume.rs` can only schedule against a
session, and a background Task outliving its parent's turn is the ordinary
case, so auto-resume was doing nothing at all for it.

The status word and its colour were two `when`s on two screens, and the second
missed `waiting` silently; they are `sessionStatusWord`/`sessionStatusColour`
now. Echo's `/subagent n` reproduces the whole shape, staggered a second
apart. Verified on the emulator against the sandbox: 169 server tests, ktfmt,
clippy, rustfmt, Android lint and the JVM unit tests all clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 18:57:30 -04:00
irisandClaude Opus 5 74c07d687a End subagents on the CLI's own task lifecycle, and detect a limit two ways
Subagents were showing "running" long after they had finished. Measured
against 2.1.237 by running a session that launched one Task agent and
reading its stdout: a subagent's lines carry no `stream_event` at all --
they are whole `user`/`assistant` lines with a null `stop_reason` -- and no
`result` line is sent for one. So `ends_a_turn`, which watches for a raw
`message_delta` saying `end_turn`, could never fire for a subagent, and
nothing finished one until its session's process exited.

What the CLI does send is a task lifecycle, as top-level `system` lines:
`task_started` (with the tool_use id), `task_progress`, `task_updated`
(status, naming the task only) and `task_notification` (tool id, status, and
the agent's own summary). `translate_task` keeps the task -> tool mapping,
records the summary as the subagent's closing text -- the run showed its
child lines stop at its last tool_result, so without this a finished
subagent reads as stopping mid-tool -- and ends it. A `completed` update is
deliberately not the end, since its notification carries the summary; any
other terminal status is, because the failure to avoid is a subagent nothing
ever finishes. `ends_a_turn` stays as a second detector and must never be
the only one again. Verified by replaying the captured stream through the
server as a fake CLI: running, prompt, Bash call, output, report, exited.

`finish_all` now reads the directory rather than the live map, which is what
clears the ones already stuck: a subagent left running by an earlier run of
the server is exactly the one this process never touched, so it read
"running" again every time its session was started.

Auto-resume gets the same treatment on its own single point of failure. The
only thing that scheduled a resume was the CLI's error sentence at the end
of a failed turn; the CLI also sends `rate_limit_event` lines saying where
the account stands, and this server ignored them entirely. Both are read
now. Anything that is not an `allowed...` status counts as refused and is
logged if unfamiliar -- being wrong that way costs one question to the usage
meter, which is still what decides whether anything is sent, and being wrong
the other way is the feature silently not existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 13:29:23 -04:00
irisandClaude Opus 5 13d2d11c2d Order a session's subagents by activity, and delete finished ones by holding
The subcards were oldest first, which buried whatever is working now. They
are ordered on the phone -- still running first, then most recently active --
over the server's stable oldest-first answer, since presentation order is a
display decision and a subagent that is thinking reports nothing meanwhile.

Holding a subcard selects it and several at a time, the import list's gesture
and its confirmation, so selecting is learned once. The selection bar sits
inside the session's card rather than at the bottom of the screen: it belongs
to one card, and one Delete is one request against one parent, so picking a
row in another card moves the selection rather than adding to it. Delete is
disabled, with the reason in words, while anything selected is still running
-- its transcript is still being written to and its process is the session's
to stop, so the server refuses that batch outright.

`POST /sessions/{id}/subagents/delete` takes the batch and checks every id
before removing any, so a set naming a running one is left exactly as it was
rather than half-deleted. It is `Subagents::start`'s path out. What counts as
running is shared with the list route through `has_a_process`, so the two
cannot disagree. On success the phone takes those rows out of that one card
and off the session's count, purges its cached copies, and drops the
expansion when nothing is left -- nothing else is refetched.

Driven on the emulator against the sandbox with ui-trace's new hold-by-name:
selecting two, the dialog, the rows going, a running one holding Delete
disabled, and the expander leaving with the last subagent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 13:10:21 -04:00
irisandClaude Fable 5.1 cf10b17c5b End a subagent on its own end_turn, not the parent's tool_result, and give the expander a touch-sized row
The Agent tool runs subagents in the background, so the parent's result
arrives at launch while the subagent works on for minutes; finishing on it
read a running agent as finished with a transcript cut off at launch. A
subagent now ends on its own message_delta end_turn, and a later line for a
finished one reopens it, since a background agent can be messaged again.

The card's expander row was only the chevron's height, so a tap for it
landed on the first subcard; it is the platform's 48dp minimum now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 14:44:00 -04:00
irisandClaude Fable 5.1 9fa09b0af1 Show a session's subagents as subcards, each with a read-only transcript
A subagent is a second transcript owned by a session, in the same event
model, with no process and no controls. The claude translator routes lines
carrying parent_tool_use_id to a per-subagent translator and transcript
under <session>/subagents/<tool_use_id>; three routes expose the list, a
transcript page and the SSE stream. Echo grows /subagent [n] as the rig.

On the phone a card with subagents ends in a chevron expander, collapsed by
default, opening to outlined subcards styled like dev-updater's components;
a subcard opens SessionScreen in read-only form, addressed through
TranscriptAddress so paging, cache and stream are shared.

Design in SUBAGENTS.md; choices awaiting review in DECISIONS.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 13:41:15 -04:00
iris eff5c8b0c0 Let the machine's own CLI refresh an expired token, and retry once
A 401 from the usage endpoint means the stored access token has expired.
Refreshing it here is not an option: Anthropic's OAuth rotates the refresh
token, so a second refresher invalidates the CLI's copy and forces a
re-login on a machine that usually has a live session on it. So run the CLI
there instead and re-read what it wrote.

`doctor` rather than `auth status`: probed against 2.1.258 with an invalid
token, `auth status` answers loggedIn:true from the file alone and never
reaches the network. The same probe showed a failed refresh blanks both
tokens, which is why this stays on the 401 path.

Also gives ProviderConfig one program() so the CLI's default path is not
written down twice.
2026-09-05 12:07:34 -04:00
iris 7b63330aaa Say when a usage 401 is an expired login, not an unreachable endpoint
A 401 is the endpoint answering and refusing the stored OAuth token, which
Claude Code refreshes as it runs -- so a machine whose CLI has been idle
hands us a stale one. Reporting it as "usage endpoint unreachable" pointed
at the network instead of at the one thing that fixes it.
2026-09-05 11:58:17 -04:00