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>
This commit is contained in:
1 parent
74c07d687a
commit
5711c2568a
17 files changed
+891
-85
No files matched your search
@@ -118,7 +118,17 @@ seq N", so there is no separate history path to drift from the live one.
|
||||
a permission is a question with two bare options, not a different kind.
|
||||
- `Answered { id, answer }` — so a question card resolves on every connected
|
||||
device, not just the one that answered.
|
||||
- `Status { state }` — idle / running / awaiting-input / compacting / exited.
|
||||
- `Status { state }` — idle / running / awaiting-input / compacting /
|
||||
**waiting** / exited / unknown. `waiting` (2026-09-06) is the session's own
|
||||
turn being over while work it started is not: a backgrounded subagent, or a
|
||||
command left running. Its own state because `idle` and it differ in *kind* —
|
||||
`idle` means the session is waiting for a person, and this means it is
|
||||
waiting for itself and will speak again with nobody having typed anything.
|
||||
Reporting it as idle sent a "finished" notification at the one moment that
|
||||
was untrue.
|
||||
- `TaskNote { about, title, status, summary }` (2026-09-06) — a task the
|
||||
session started in the background reporting back. See "A task reporting
|
||||
back".
|
||||
- `UsageDelta { tokens, context }` — what a turn cost and how much the model
|
||||
was holding when it ended. `context` is prompt plus both cache figures,
|
||||
taken from the **last assistant message** rather than the turn's `result`:
|
||||
@@ -654,6 +664,49 @@ to end that way on 2026-09-05: the wait moved from the dialect's two minutes
|
||||
to the meter's seven when the meter changed its mind, and the message went
|
||||
out on the first check after the meter came back under the limit.
|
||||
|
||||
### A task reporting back (2026-09-06)
|
||||
|
||||
**A subagent finishing is a message the session receives, and it gets a row.**
|
||||
The CLI says so on a `system/task_notification` line carrying the task's
|
||||
status and its own closing summary; the parent then wakes up and runs a turn
|
||||
because of it. Before this the parent's transcript had nothing between the
|
||||
reply that ended the previous turn and the reply that answered the
|
||||
notification, and the phone's fold grew the older message rather than starting
|
||||
a new one — so two answers were drawn as one paragraph, running together
|
||||
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.
|
||||
|
||||
`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.
|
||||
|
||||
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`.
|
||||
|
||||
### A limit a subagent hits is the session's (2026-09-06)
|
||||
|
||||
A background Task runs on long after its parent's turn ended, so **the account
|
||||
running out while the main agent is idle is the ordinary shape of the problem
|
||||
rather than an edge of it.** `translate_child` used to record everything a
|
||||
subagent produced into the subagent's own transcript and return nothing, which
|
||||
meant `Event::LimitReached` never reached the session — and the session is the
|
||||
only thing `resume.rs` can schedule against. That session then waited for a
|
||||
person for ever, with nothing anywhere saying so. The limit is hoisted now: it
|
||||
goes into the subagent's transcript, where it happened, *and* out to the
|
||||
session, which is what auto-resume needs.
|
||||
|
||||
### Subagents (2026-09-05)
|
||||
|
||||
**A subagent is a second transcript owned by a session, in the same event
|
||||
|
||||
Reference in new issue
Block a user