fix(*): live spawn record, live usage and a leaner node panel subtitle - #567
Conversation
The node panel's subtitle carried the spawn's minted handle, a "usage not reported" fragment for every lane that reports none, and the tool and failure counts -- none of which the reader asked that line for. It now reads agent, status word, duration, and the token total only when the lane reported one; the tool count stays on the board card and in the process fold. The two catalogue keys nothing reads any more are removed, the TUI's generated copy follows, and the design note says what the header shows. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…anel A spawn task's node panel read its record once, when opened, and never again: useNodeRecord re-read only on store.nodeVersion, which only a dag's node_updated event bumps, and its key held no status, so neither the steps a running spawn kept making nor its final answer ever reached a panel left open on it. The transcript's own spawn card already reads subagent.context on a one-second beat while the run is live; the panel now does the same for a running spawn (skipping a beat while a read is still out), and re-reads once on any status transition, which also covers a dag node settled by a run_completed frame that no node_updated announced. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…beat stops Review follow-ups on the node panel's record beat. A pane with no tasks source would have re-run the read effect into its failed branch once a second, so the beat now requires a source. The subtitle comment no longer credits the process fold with a tool count it does not show, and the design note's node-context section says how often a record is re-read. Tests cover usage reported on one side only, the beat stopping when the spawn settles and when the panel closes, and the token line on the settled shape the server actually sends. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
I reviewed the full target diff and the surrounding task source, store, live-event reducers, node-panel lifecycle, transcript polling precedent, and relevant history. The running-spawn beat is scoped to an open panel, serializes reads, stops on close or settlement, and the status dependency provides the final refresh. The subtitle/catalogue changes have no remaining consumers and preserve the board/process presentation of tool data.
I also checked AGENTS.md, CONTEXT-MAP.md, ui-web/CONTEXT.md, and ui-web/CONTRIBUTING.md; backward compatibility; test changes for weakening; and the architecture gates. I found no plain error or blocker.
Verification:
npm test --prefix ui-web: 186 files, 2551 tests passed.npm test --prefix ui-web -- src/features/tasks: 8 files, 187 tests passed.npm test --prefix ui-web -- scripts/gates: 40 files, 158 tests passed. An initial concurrent run timed out inboot-order.test.mjs; the isolated rerun and the complete rerun both passed.npm run type-check --prefix ui-web: passed.npm run lint --prefix ui-web: 0 errors, 5 warnings on untouched files.npm run lint:i18n --prefix ui-tui: generated catalogue is current.- Large-file and source-language checks against
github/refactor/ui_web_architecture...HEAD: passed. git diff --check: passed.
…ivity A node's tokens, tool counts and files reached tasks.list only from the record on disk, which the run writes when it finishes, so a running node read as reporting nothing until it settled. The reader now overlays those facts from the activity being collected for the node in this process -- the same in-memory account subagent.context and dag.node already serve a transcript from -- keyed by the record id for a spawn and by node_live_key for a dag node, and only for what the lane has reported so far; a lane that has not spoken keeps its null. On the page, each read of a running node's record also re-reads its row through tasks.list(kind, id), so the subtitle's token total moves with the record on the beat and on every node_updated frame. reconcile keeps a frame that landed while such a read was out, the rule refresh already follows, so a read started on the beat cannot put a settled row back to running. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
Pushed b97a49c on top of the reviewed head: the user asked for the running node's token total to move during the run, which needed one backend change, so the PR title now reads
Please take another look at the new commit. |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
I reviewed the new live-usage commit and rechecked the complete target diff. The activity overlay uses the same spawn and DAG keys and lifetimes as the existing record readers, preserves null for facts a lane has not reported, and does not change the wire shape. On the page, the row reread follows the existing record cadence and the reconcile guard prevents a live frame from being overwritten by an older response.
I covered the changed callers and data flow through activity collection, tasks.list, the task source/store, the node panel, and the concurrent RPC transport; relevant history; AGENTS.md and the UI context/contributing rules; backward compatibility; test changes for weakening; and architecture constraints. I found no concrete failure or outstanding suggestion.
Verification:
uv run pytest tests/test_rpc_tasks.py: 43 passed.npm test --prefix ui-web -- src/features/tasks: 8 files, 189 tests passed.npm test --prefix ui-web: 186 files, 2553 tests passed.npm test --prefix ui-web -- scripts/gates: 40 files, 158 tests passed.npm run type-check --prefix ui-web: passed.npm run lint --prefix ui-web: 0 errors, 5 warnings in untouched files.- Relevant Python Ruff check and format check: passed.
npm run lint:i18n --prefix ui-tui: generated catalogue is current.- Large-file, source-language, and
git diff --checkchecks: passed.
… the overlay Two holes in the live usage read. A dag node's account reaches disk with the run's manifest, written once the whole run is over, and collecting() drops the live entry the moment the node's block exits -- so a node that finished while its run was still going read as reporting nothing again, and the total the panel had shown while it ran vanished until the run ended. The runner now sets the node's account aside (activity.record_settled) as the node settles, tasks.list reads it there when the manifest has no entry yet, and the run forgets the set-aside copies once the manifest carries them, or when a hard stop means no manifest ever will. The overlay itself now applies to a running node only: the live index is keyed by a record id that is unique per conversation, so a finished spawn could have taken the numbers of another conversation's run under the same id. On the page, one row read is out at a time, so a dag node's per-tool-call frames do not stack tasks.list requests. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
Pushed 6578c17, from a second read of the live usage commit; the description's third item says the same in prose.
|
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: the terminal DAG node update must fetch the retained final usage instead of leaving the subtitle stale until the whole run ends.
I reviewed the new corrective commit and rechecked the complete target diff. I covered the activity index lifecycle, DAG runner event ordering and cleanup paths, tasks.list, the task live reducer/store, the node-panel effects, relevant history, backward compatibility, test changes for weakening, AGENTS.md and the UI architecture/domain rules. The settled-account backend path is sound, but the page does not consume it on the node's terminal event; the inline finding is the one blocker.
Verification:
uv run pytest tests/test_rpc_tasks.py tests/test_subagent_activity.py tests/test_subagent_dag_runner.py -k 'tasks or activity or finished_nodes_account_is_set_aside_until_the_manifest_is_written': 61 passed.npm test --prefix ui-web -- src/features/tasks: 8 files, 191 tests passed.npm test --prefix ui-web -- scripts/gates: 40 files, 158 tests passed.- UI type check: passed.
- UI lint: 0 errors, 5 warnings in untouched files.
- Relevant Python Ruff check and format check: passed.
- TUI generated-catalogue, large-file, source-language, and
git diff --checkchecks: passed.
A dag node's terminal node_updated frame moved the node and bumped its record version, but re-read no row: the node panel's row read is for a running node, and the frame itself scheduled no reconcile. The account the runner sets aside as the node settles therefore stayed off the page until dag.run_completed -- with a sibling still running, the rest of the run. applyNodeUpdated now answers a refetch for a node's own terminal frame, the way the spawn and run frames already do, and onNodeUpdated applies it; a row read that was out when the frame landed is still dropped by the liveTick guard, and the frame's own reconcile is the replacement. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
Verified: the frame is emitted after Tests (pushed as 2612503): |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
The terminal-frame change fixes the outstanding stale-usage failure: applyNodeUpdated requests reconciliation for terminal node statuses, onNodeUpdated routes it through the store's apply path, and the regression test covers a completed node while a sibling continues plus rejection of the older in-flight pane read.
Review coverage: the new commit and the full target diff; AGENTS.md/CLAUDE.md, CONTEXT-MAP.md, and ui-web/CONTEXT.md; callers and event/settled-record ordering; history and backward compatibility; the source/seam/store architecture constraints; and whether tests were weakened (they were not). No new candidate finding survived refutation.
Verification:
npm test --prefix ui-web: 186 files, 2,558 tests passednpm test --prefix ui-web -- src/features/tasks: 8 files, 194 tests passednpm test --prefix ui-web -- scripts/gates: 40 files, 158 tests passednpm run type-check --prefix ui-web: passednpm run lint --prefix ui-web: 0 errors, 5 pre-existing warnings in untouched files- diff, source-language, and large-file checks: passed
…alone (#577) ## Summary While a spawned sub-agent runs, its live account (transcript, usage, tool calls) is held in a process-wide index. A spawn was keyed into it by the node id the model chose, which is unique for one conversation only, so two conversations running a spawn under the same id shared one entry: the later `collecting` overwrote it, so the earlier conversation's context panel showed the other run's transcript (and, since #567, its live usage), and the first run to finish popped the entry, so the other run's live view went blank until it ended. This has been possible since a92514e (2026-09-07), when the spawn record id became the model's own `node_id`; before that the key was a minted, globally unique call id. The key is now the record's own address, the conversation's node root plus the id (`history.spawn_live_key`, the spawn side of `dag_store.node_live_key`). The writer holds the root as the record's directory and the readers as the node files' root, both resolved from the same session directory, so the two sides agree without depending on how a session key is spelled. `subagent.context` and `tasks.list` read by the same helper; the dag side already carried its run id. No wire change. ## Type - [x] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification Run in the fix worktree, base `refactor/ui_web_architecture` at 9656dd9: - `uv run pytest tests/test_rpc_tasks.py tests/test_rpc_subagent_calls.py tests/test_subagent_activity.py`: 104 passed. Two tests are new: two conversations running a spawn under the same id each read their own usage through `tasks.list`, and each read their own transcript through `subagent.context` driven through a real `SubagentManager.spawn` with a backend that waits to be released, with the first run to finish taking nothing of the other's. The second test was run against the pre-fix source and failed there (conversation A read conversation B's transcript). - `uv run pytest tests/test_subagent_manager.py`: 159 passed. - `make lint-python` and `make test-python`: 23989 passed, 109 skipped. - Relevant Ruff check and format check: passed. - Real gateway (`raven serve` from this branch on the user's home, Raven-Code lane): two conversations dispatched a spawn under the same `node_id` at once, one saying ALPHA in two steps and one saying BRAVO in five; polled every 3 s, each conversation's `subagent.context` carried only its own word while both ran, and after the ALPHA run settled the BRAVO conversation's live view stayed up and kept growing (6 to 10 messages) instead of going blank. The #567 regression driver on the same gateway still passed its trace and subtitle checks (tool rows 0, 1, 3, 4 while running; answer landed; subtitle `Raven-Code / done / 1m02s`). - Real gateway on an isolated `RAVEN_HOME` (in-process `Raven` lane): the `tasks.list` reader, which now reads by the same key, still served the live usage -- subtitle `Raven / running / 5s / 1,881 tokens` up to `Raven / done / 49s / 8,601 tokens`, board chip 1, 2, 3 tools. ## Risk - [x] Security impact considered: this closes a cross-conversation leak of a running sub-agent's transcript and usage inside one gateway process; nothing new is exposed. - [x] Backward compatibility considered: the key is in-memory only, so no record on disk changes shape; a reader and a writer of one process always run the same code. - [x] Rollback path is clear for risky changes: revert the squash commit. ## Related Issues Fixes #580. #567 added the second reader of this index (`tasks.list`) and gated it to running nodes; the collision itself predates it. --------- Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
Summary
Two defects in the desk tasks tab's node panel, both reported from the page.
A spawn task's node panel read its record once, when opened, and never again.
useNodeRecordre-read only onstore.nodeVersion, which only a dag'snode_updatedframe bumps, and its key held no status; so a panel left open on a running spawn kept the first read's single step while the transcript's own spawn card moved on, and the final answer never landed either. The panel now re-reads a running spawn's record on the one-second beat the transcript card already readssubagent.contexton (skipping a beat while a read is still out, and only while a tasks source is wired), and re-reads once on any status transition, which also covers a dag node settled by arun_completedframe that nonode_updatedannounced.The subtitle read
Raven-Code @raven-code-db8bbb / done / 2m55s / usage not reported / 14 tools / 1 failed. It now reads agent, status word, duration and, only when the lane reported usage, the token total:Raven / running / 1m50s / 4,910 tokens. The handle stays on the board card and the work-order tab; the tool count stays on the board card, and the calls themselves are in the process fold (whose folded multi-call rows still say how many failed). The two catalogue keys nothing reads any more are removed, the TUI's generated copy is regenerated, and the design note says what the header shows and how often a record is re-read.A running node's usage was invisible until it settled:
tasks.listread tokens, tool counts and files from the record on disk, which the run writes when it finishes. The reader now overlays those facts from the activity being collected for the node in this process (the same in-memory accountsubagent.contextanddag.nodeserve a transcript from), keyed by the record id for a spawn and bynode_live_keyfor a dag node, only for a node that is running and only for what the lane has reported so far. A dag node that finishes while its run is still going keeps the account the runner set aside for it (activity.record_settled) until the manifest is written, so its total does not vanish between the two. On the page, each read of a running node's record also re-reads its row throughtasks.list(kind, id), so the subtitle's token total moves during the run (one row read out at a time, so a dag node's per-tool-call frames do not stack requests), and a dag node's own terminal frame re-reads the row too, so the node's final total lands even while a sibling keeps the run going;reconcilekeeps a frame that landed while such a read was out, so a read started on the beat cannot put a settled row back to running.No wire change:
TaskNode's fields and their null semantics are unchanged; a lane that has not reported usage yet still reads null. The in-process lane reports usage after every model call, so its total moves during the run; the acp lane reports once at the end of its turn, so its total appears when the run settles.Type
Verification
Run in the fix worktree, base
refactor/ui_web_architectureat 6062b36:npm test --prefix ui-web -- src/features/tasks: 8 files, 189 tests passed. Seven tests are new (the beat, the skipped beat while a read is out, no beat for a dag node, the settle re-read and the beat stopping, the beat stopping on close, the row re-read on the beat reaching the subtitle,reconcilekeeping a frame that landed during its read) and three changed for the subtitle; the tests that reproduce each defect were run against the pre-fix source and failed there.npm run lint --prefix ui-web: 0 errors, 5 pre-existing warnings in other domains.npx tsc --noEmit -p ui-web: clean.npm test --prefix ui-web -- scripts/gates: 40 files, 158 tests passed.npm run lint:i18n --prefix ui-tui: generated catalogue up to date.make lint-pythonandmake test-python: 23980 passed, 109 skipped.In
ui-tui:npm run lint,npm run lint:rpc,npm run type-check,npm test(2074 passed),npm run build.uv run pytest tests/test_rpc_tasks.py: 43 passed (3 new: a running spawn's usage, counts and files off the live activity and nothing once the block closes; a live lane that has not spoken keeps its nulls; a running dag node's usage offnode_live_key).python3 ui-web/build.py: both boot snapshots match their golden (250 and 251 nodes).Real gateway (
raven servefrom the worktree with the rebuilt page): a Raven-Code spawn dispatched over RPC, its node panel opened while running and left open. Sampled every 3 s, the process fold's tool rows went 1, 3, 4 without the panel being closed; the answer appeared when the run settled; the subtitle readRaven-Code / running / 6sand thenRaven-Code / done / 51s(the acp lane reports usage once, at the end of its turn).Real gateway on an isolated
RAVEN_HOMEwhose default model is deepseek, so the in-processRavenlane runs: a three-step spawn (each step asleep 15shell command that needs the previous output). Sampled every 3 s with the panel left open, the subtitle readRaven / running / 6s / 1,812 tokens, then3,769 tokens, then5,852 tokens, andRaven / done / 49s / 8,021 tokensonce settled; the board card's tool chip went 1, 2, 3;tasks.listreported the same growing usage over RPC.Relevant tests pass locally
Relevant lint / type checks pass locally
User-facing docs or screenshots are updated when needed (docs/specs/2026-09-18-desk-tasks-list-design.md, sections 4 and 7)
Risk
subagent.context) once a second per open panel while a spawn runs, the cadence the transcript card already uses.gui.tasks.tools_nstays in use by the board card.Related Issues
N/A for closing. #461 is the instance surface's version of the same class of gap (a running sub-agent's tool calls not streamed live) and is not closed by this.