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>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-06 19:54:36 -04:00
1 parent 5711c2568a
commit ef1aad8776
11 files changed
+342 -120

No files matched your search

+24 -11
View File
@@ -678,22 +678,35 @@ mid-sentence with not even a space between them.
Both halves were wrong and both are fixed. The fold now refuses to grow a
*settled* reply, so a turn boundary is always a message boundary whatever
caused it (`joinPages` carries the same rule across a page boundary). And the
notification is recorded as `Event::TaskNote`, drawn as a card naming who
reported and what they said — a card rather than a divider, because 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 and would change where no reader
is looking.
notification is recorded as `Event::TaskNote`, on its own row rather than as an
update to the Task call's — that row is wherever the call was made, above
everything the session has said since, and it would change where no reader is
looking.
**Drawn as a divider, closed, and not carrying the subagent's words.** It
marks a boundary, which is what the reader needs from it; the subagent's
closing report is recorded as that subagent's own transcript's closing text,
and repeating it in the parent puts the same paragraph in two places for
somebody who did not ask for it. Opening the divider shows it anyway, because
leaving the conversation to read one line has its own cost — and because 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.
`status` is carried beside `summary` rather than folded into it because the
summary is absent exactly when things went wrong, and "finished" is the wrong
word for a task that was killed. `title` is the subagent's; a backgrounded
command has none and its summary names itself, so the card says "a background
task" rather than inventing one.
word for a task that was killed.
Reported once. The two lifecycle shapes (`task_notification` and
`task_updated`) can both arrive for one task, and the translator's `tasks` map
is what says which got there first — removing the entry is also what stops the
task counting as outstanding, which is what decides `Status::Waiting`.
`task_updated`) can both arrive for one task, and whichever gets here first is
the one that finds the task open — in the translator's own `open_tasks`, or
failing that in the registry, which is what makes an **adopted** session work.
A backend restart picks a session's stdout back up from a recorded offset, so
the `task_started` lines for anything already running are behind it and the
translator never sees them; `Subagents::any_open` is the measurement that
covers those, and `open_tasks` covers the backgrounded command, which has no
subagent to be found in the registry at all. Both are needed and neither
subsumes the other.
### A limit a subagent hits is the session's (2026-09-06)