Skip to content

fix(*): live spawn record, live usage and a leaner node panel subtitle - #567

Merged
0xKT merged 6 commits into
refactor/ui_web_architecturefrom
fix/task_pane_header_trace
Sep 21, 2026
Merged

0xKT merged 6 commits into
refactor/ui_web_architecturefrom
fix/task_pane_header_trace

Conversation

@0xKT

@0xKT 0xKT commented Sep 20, 2026

Copy link
Copy Markdown
Member

Summary

Two defects in the desk tasks tab's node panel, both reported from the page.

  1. 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 frame 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 reads subagent.context on (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 a run_completed frame that no node_updated announced.

  2. 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.

  3. A running node's usage was invisible until it settled: tasks.list read 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 account subagent.context and dag.node serve a transcript from), keyed by the record id for a spawn and by node_live_key for 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 through tasks.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; reconcile keeps 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

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

Run in the fix worktree, base refactor/ui_web_architecture at 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, reconcile keeping 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-python and make 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 off node_live_key).

  • python3 ui-web/build.py: both boot snapshots match their golden (250 and 251 nodes).

  • Real gateway (raven serve from 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 read Raven-Code / running / 6s and then Raven-Code / done / 51s (the acp lane reports usage once, at the end of its turn).

  • Real gateway on an isolated RAVEN_HOME whose default model is deepseek, so the in-process Raven lane runs: a three-step spawn (each step a sleep 15 shell command that needs the previous output). Sampled every 3 s with the panel left open, the subtitle read Raven / running / 6s / 1,812 tokens, then 3,769 tokens, then 5,852 tokens, and Raven / done / 49s / 8,021 tokens once settled; the board card's tool chip went 1, 2, 3; tasks.list reported 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

  • Security impact considered: none; the change is confined to rendering and to calling an existing read-only method (subagent.context) once a second per open panel while a spawn runs, the cadence the transcript card already uses.
  • Backward compatibility considered: the two removed catalogue keys had one consumer, removed in the same change; gui.tasks.tools_n stays in use by the board card.
  • Rollback path is clear for risky changes: revert the squash commit.

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.

0xKT and others added 3 commits September 20, 2026 23:16
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>
@0xKT
0xKT requested a review from gloryfromca September 20, 2026 16:05

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in boot-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>
@0xKT 0xKT changed the title fix(ui-web): live spawn record and a leaner node panel subtitle fix(*): live spawn record, live usage and a leaner node panel subtitle Sep 20, 2026
@0xKT

0xKT commented Sep 20, 2026

Copy link
Copy Markdown
Member Author

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 fix(*) and the description has a third item.

  • tasks.list overlays a running node's tokens_in / tokens_out, tool counts and files from the activity being collected for it in this process (run_activity.live(<record id>) for a spawn, run_activity.live(node_live_key(run_id, node_id)) for a dag node), only for what the lane has reported so far; a lane that has not spoken keeps its null. The wire shape and its null semantics are unchanged.
  • The page re-reads a running node's row (store.reconcile) on each read of its record, so the subtitle's token total follows the beat and every node_updated frame. reconcile now keeps a frame that landed while its read was out (the rule refresh already follows), so a read started on the beat cannot put a settled row back to running.
  • Tests: three backend (tests/test_rpc_tasks.py, 43 passed) and two frontend (tasks domain 189 passed). Real-gateway pass with the in-process lane: the subtitle read Raven / running / 6s / 1,812 tokens, then 3,769, 5,852, and Raven / done / 49s / 8,021 tokens once settled. The acp lane reports usage once at the end of its turn, so its total appears when the run settles.

Please take another look at the new commit.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --check checks: 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>
@0xKT
0xKT requested a review from LivXue as a code owner September 20, 2026 16:58
@0xKT

0xKT commented Sep 20, 2026

Copy link
Copy Markdown
Member Author

Pushed 6578c17, from a second read of the live usage commit; the description's third item says the same in prose.

  • _overlay_live 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 collected under the same id. (The same key serves subagent.context's live transcript read, which predates this PR; scoping that key by session would be its own change.)
  • A dag node that finished while its run was still going read as reporting nothing again -- its account reaches disk with the manifest, written once the whole run is over, and collecting() drops the live entry as the node's block exits -- so the total the panel showed while the node ran vanished until the run ended. The runner now sets the account aside (activity.record_settled) at the node's end, tasks.list reads it there when the manifest has no entry yet, and the run forgets the copies once the manifest carries them, or on a hard stop.
  • One row read is out at a time per open panel, so a dag node's per-tool-call frames do not stack tasks.list requests.
  • Tests: tests/test_rpc_tasks.py 45 passed (settled account read and forgotten; a settled spawn ignores a live activity under its key), tests/test_subagent_activity.py and a two-node runner test (node b sees a's set-aside account mid-run; the index is empty after the manifest), tasks domain 191 passed (dag row re-read on node_updated; one row read at a time).

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --check checks: passed.

Comment thread ui-web/src/features/tasks/TasksPage.tsx
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>
@0xKT

0xKT commented Sep 20, 2026

Copy link
Copy Markdown
Member Author
Finding Verdict Change
Inline at TasksPage.tsx:389: a dag node's terminal node_updated frame did not re-read the row, so the settled account stayed off the page until dag.run_completed accept applyNodeUpdated now answers a refetch for a node's own terminal frame (completed, failed, skipped, cancelled, interrupted, exception), the way applySubagentStatus and applyRunCompleted already do; onNodeUpdated goes through apply, so the store reconciles the row on that frame. 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.

Verified: the frame is emitted after _run_node records the set-aside account (activity.record_settled in the block's finally, before the emit at the end of the node), so the read the frame triggers finds it.

Tests (pushed as 2612503): live.test.ts pins which statuses ask for a reconcile and that a running frame does not; store.test.ts pins that a node's terminal frame reads the row and a running frame does not; TasksPage.test.tsx covers the case you asked for -- a node completes while its sibling runs, with the panel's own row read still out: the frame's reconcile lands the final 4,910 tokens and the stale read, released afterwards, does not put the node back to running. Tasks domain 194 passed.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 passed
  • npm test --prefix ui-web -- src/features/tasks: 8 files, 194 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 pre-existing warnings in untouched files
  • diff, source-language, and large-file checks: passed

@0xKT
0xKT merged commit 9656dd9 into refactor/ui_web_architecture Sep 21, 2026
23 checks passed
@0xKT
0xKT deleted the fix/task_pane_header_trace branch September 21, 2026 03:18
0xKT added a commit that referenced this pull request Sep 21, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants