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>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-06 21:39:19 -04:00
1 parent ef1aad8776
commit 1bbb642973
13 files changed
+171 -407

No files matched your search

-22
View File
@@ -67,10 +67,6 @@ pub struct SubagentInfo {
/// session's but with no driver behind it.
pub struct Subagent {
dir: PathBuf,
/// What a reader knows this subagent as -- `Meta::title`, kept here so
/// naming one costs no file read. Never changes: a subagent is titled
/// once, when it is created.
title: String,
transcript: Mutex<Transcript>,
events: broadcast::Sender<SeqEvent>,
/// Mirrors the transcript's last `Status` event, kept live rather than
@@ -100,13 +96,6 @@ impl Subagent {
*self.status.lock().unwrap() != SessionStatus::Exited
}
/// What a reader knows this subagent as. Empty for one started from a
/// child line before its Task call was seen and never renamed since --
/// see `Subagents::get`, which passes no title on a reopen.
pub fn title(&self) -> &str {
&self.title
}
fn append(&self, event: Event) {
let mut transcript = self.transcript.lock().unwrap();
match transcript.append(event, super::now()) {
@@ -211,7 +200,6 @@ impl Subagents {
let (events, _) = broadcast::channel(EVENT_BUFFER);
Ok(Arc::new(Subagent {
dir,
title: meta.title.clone(),
transcript: Mutex::new(transcript),
events,
status: Mutex::new(status),
@@ -270,16 +258,6 @@ impl Subagents {
}
}
/// What the subagent named `id` is called, or `None` for an id that is
/// not a subagent's at all -- a backgrounded command's tool call reaches
/// here with the same shape, and answering it with a made-up name is
/// worse than answering "this is not one".
pub fn title_of(&self, id: &str) -> Option<String> {
self.get(id)
.map(|subagent| subagent.title().to_string())
.filter(|title| !title.is_empty())
}
/// Whether the subagent named `id` exists and has not finished. `false`
/// for an id that is not a subagent's at all -- a backgrounded command's
/// tool call reaches here with the same shape.