feat(desktop): attribute a sub-agent's work to it, and show what it did (R5) - #574
Merged
Conversation
…id (R5)
The Sub-agents chip listed delegated agents but could not say what any of
them did. Clicking one now opens what it was asked to do and everything
it has done — its tool calls, its notes, and what it is running now.
Building it measured the premise. The plan said "events already carry
`subagent` marking"; the only producer of that marking was the kimi M4
wire-tail (kimi_code/mapper.go:235), so the panel would have been
permanently empty for claude and codex — the two engines the Companion
is built around.
The probe that settled it: a real `claude --print --output-format
stream-json` run making one Agent call, because no session log on this
box had a sidechain in it and the field appears in no doc we hold.
claude-code 2.1.220 carries MORE than the plan assumed:
- parent_tool_use_id on every assistant/user frame — null for the main
agent, the spawning Agent call's tool_use_id for a sub-agent's work
- subagent_type + task_description alongside it
- four system subtypes: task_started (full prompt), task_progress
(last_tool_name + running usage), task_updated, task_notification
- the tool is named `Agent`, not `Task`
Our translator dropped all of it. So a sub-agent's tool calls and prose
landed in the transcript INDISTINGUISHABLE from the main agent's own
work — read as the agent doing it itself — and #374's guard, which keeps
a sub-agent's usage out of the session's turn counts, skipped nothing on
claude and quietly inflated them.
So R5 ships the producer first: the frame profile stamps
parent_tool_use_id plus a DERIVED `subagent` boolean — the same one kimi
already sets, so one vocabulary answers for both engines and the two
existing consumers (digest_fold.go, transcriptStats.ts) start working
with no change. Deriving it at the producer rather than in each consumer
is deliberate: a predicate re-derived per consumer is the duplication
this repo has been bitten by. task_progress regains tool_use_id +
subagent_type, which #374 dropped along with the noise it meant to drop.
The legacy translator mirrors it field-for-field (the parity test
compares key sets, so the fields are set unconditionally, nil included),
and the 15-frame recording is now in the parity corpus — its first
sidechain coverage, scrubbed of the recording box's paths and session
id. The desktop's TS interpreter is pinned to the same regenerated
fixture, so the local Companion gets it too.
Honesty: codex and gemini publish no per-sub-agent correlation. The
panel still opens for them — the header comes off the spawning call,
which every engine has — and says "this engine does not report which
events are a sub-agent's". That is a different sentence from "this
sub-agent has not done anything yet", which is what an empty stream
means when the engine DOES report, and the panel picks between them by
asking the data, not the engine name.
Tests: 8 new, three mutation-checked. The separating input is two
sub-agents running at once — a filter keyed on the `subagent` boolean
alone puts both agents' work under each row, and passes every
single-agent test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… kind #374's guard reads R5 stamped text / thought / tool_call / tool_result, which fixes the transcript's attribution — but the token-inflation half of the bug it describes lives on `usage`: digest_fold's InTokens/OutTokens fold and transcriptStats' turn accounting both skip `subagent:true` USAGE frames and read nothing else. A sub-agent's assistant frames carry `message.usage` like any other (corpus frame 25, the recorded sidechain frame, does), so the unstamped usage emit kept the delegated agent's tokens counted inside the main turn — the exact inflation the change set out to remove, surviving its own fix. The pinned fixture showed it: frame 25's usage event had no `subagent` key at all. Both translators now stamp the same three fields on usage (`$.` scope, not the sub_rules' `$$.` — this rule has no for_each, so the frame IS the inner scope). Fixture + families artifact regenerated; frame 25's usage row now reads `subagent: true`. New TestProfile_ClaudeCode_SubagentUsageIsFlagged pins the requirement semantically for BOTH translators — deliberately independent of the parity fixture, which is regenerated on drift and would simply re-pin a future regression. Mutation-verified both ways: dropping the legacy stamp fails parity (keyset diff on every usage frame), dropping the YAML field fails this test on both paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…atus docs Both PRs rewrote the plan's W4 status paragraph and appended under the changelog's Unreleased headings — the collision called out at review time. - plan: united to "W4 complete (2026-08-17)" naming all of F4 / L4a–L4c / R5 / R6, remaining = L3c only (deferrable). - changelog: union — both Added entries kept (R5 above R6), both Fixed entries under one heading. Everything else auto-merged; verified on the merged tree: agentfamilies + hostrunner drift/parity tests (the auto-merged generated artifact matches a regeneration of the merged YAML), electron 720/0, frontend 908/0, docs lint 323. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the R5 wedge of
docs/plans/desktop-companion-vision-parity.md. W4 is complete with R6 (#573).What the director gets
The Sub-agents chip listed delegated agents but could not say what any of them did. Clicking one now opens what it was asked to do and everything it has done — its tool calls, its notes, and what it is running right now. The drill-down lives in the dock's existing panel with a back button, so no new region.
The premise was true of one engine, and not one we drive
The plan said "events already carry
subagentmarking". The only producer of that marking is the kimi M4 wire-tail (kimi_code/mapper.go:235). Built as written, the panel would have been permanently empty for claude and codex.No session log on the dev box had a sidechain in it, and the field appears in no vendor doc we hold — so I made the observation: a real
claude --print --output-format stream-jsonrun making one Agent call. claude-code 2.1.220 carries more than the plan assumed:parent_tool_use_idon every assistant/user frame —nullfor the main agent, the spawning Agent call'stool_use_idfor a sub-agent's work. This is the correlation the panel needs.subagent_type+task_descriptionalongside it.systemsubtypes:task_started(full prompt),task_progress(last_tool_name+ running usage),task_updated,task_notification(summary).Agent, notTask— the dock's name set already matches it.The bug that fell out
Our translator dropped all of it. A sub-agent's tool calls and prose landed in the transcript indistinguishable from the main agent's own work — read as the agent you're talking to having run them. And #374's guard, which keeps a sub-agent's token usage out of the session's turn counts, checks
subagent:trueand therefore skipped nothing on claude, quietly inflating them.So R5 ships the producer half first. The frame profile stamps
parent_tool_use_idplus a derivedsubagentboolean — the same one kimi already sets, so one vocabulary answers for both engines and the two existing consumers (digest_fold.go,transcriptStats.ts) start working with no change. Deriving it at the producer rather than in each consumer is deliberate: a predicate re-derived per consumer is the duplication this repo has been bitten by.task_progressregainstool_use_id+subagent_type, which #374 dropped along with the noise it meant to drop.Honesty for the engines that report nothing
codex and gemini publish no per-sub-agent correlation. The panel still opens for them — the header comes off the spawning call, which every engine has — and then says "this engine does not report which events are a sub-agent's". That is a different sentence from "this sub-agent has not done anything yet", which is what an empty stream means when the engine does report. The panel picks between them by asking the data, not the engine name, so an engine that starts reporting tomorrow needs no change here.
Verification
subagentboolean alone puts both agents' work under each row and passes every single-agent test. Mutations caught: subagent-bool-only join (2 fail),toolId= event id (7 fail),reporteddropping the provenance probe (1 fail).translate()mirrors the profile field-for-field (the parity test compares key sets, so fields are set unconditionally, nil included).go test ./internal/hostrunner ./internal/agentfamilies ./internal/server✅ · desktopnode --test883 pass · electron 715 pass, 6 skipped ·tsc --noEmit✅ · fullvite build✅ · token ratchet clean, no phantom tokens · CI's whole lint set ✅ including openapi.🤖 Generated with Claude Code