Fix: detect Workflow subagents nested under subagents/workflows/ - #56
Fix: detect Workflow subagents nested under subagents/workflows/#56EdwinChua wants to merge 2 commits into
Conversation
Newer Claude Code spawns multi-agent runs via the Workflow tool and writes
each subagent's transcript to:
~/.claude/projects/<encoded>/<sessionId>/subagents/workflows/wf_<id>/agent-*.jsonl
These live two levels below the session's `subagents/` directory, but
scanSubagentsDir() used a flat fs.readdirSync, so it only ever saw
top-level `subagents/*.jsonl` files. As a result, Workflow-spawned
subagents never appeared in the visualizer — the orchestrator showed up,
but its children were invisible. (Workflow tool_use blocks are also not
emitted as Task/Agent in the main transcript and produce no agent_progress
events, so file tailing is the only path that can surface them.)
Changes:
- collectSubagentJsonlFiles(): recursively walk the subagents/ tree so
transcripts at any depth are discovered.
- scanSubagentsDir(): use the recursive collector, and prefer a recursive
fs.watch (Windows/macOS) with a flat-watch fallback; the existing
POLL_FALLBACK_MS poll covers platforms without recursive watch (Linux).
- resolveNameFromMeta(): when meta carries no descriptive name (Workflow
subagent meta is just {"agentType":"workflow-subagent"}), derive a stable
name from the transcript file id so each renders as a distinct node.
Adds extension/test/subagent-watcher.test.ts covering flat + nested
discovery, sidecar/non-jsonl exclusion, and the missing-dir case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
patoles
left a comment
There was a problem hiding this comment.
Thanks for this, and for the tests! The recursive discovery is a real gap and the fix is well done. Before merging I'd like to sort out three things, because they all bite the exact workflow sessions this PR targets:
- Session discovery is still flat.
scanForActiveSessionsandnewestMtimeinsession-watcher.ts(and the relay's copy inscripts/relay.ts:303) compute staleness from a top-level readdir, so a session whose only recent activity is nested workflow writes is considered inactive after 10 minutes and never attached, and the recursive tailing never starts. Since you already exportcollectSubagentJsonlFiles, reusing it in those three spots would complete the fix. Careful with the 10-minuteACTIVE_SESSION_AGE_Sbehavior there, it's a deliberate invariant. - The filename-derived fallback names can collide with the inline-progress path. That path still names no-meta agents
subagent-1(transcript-parser.ts:402) while the file watcher now producessubagent-<hash6>, and all duplicate suppression between the two paths is name-based (inlineProgressAgents,spawnedSubagents). If a workflow agent also streamsagent_progressinto the parent transcript, it will render as two nodes with doubled events. Do workflow subagents emit inline progress in your transcripts? If yes we need a dedup story before merge; if not, a comment noting the assumption is fine. - Workflow nodes never complete: subagent
agent_completeonly fires from aTask/Agenttool_result, which workflow agents don't get, so their nodes stay "running" forever. A transcript-inactivity completion (or an explicit "known limitation" note) would round this out.
Three tiny ones while you're in there: filter for agent-*.jsonl specifically (any stray .jsonl under subagents/ would currently become a phantom node), add a log.debug to the first watch catch so the recursive-to-flat fallback is diagnosable, and consider a short debounce on the recursive watcher callback since every nested append currently triggers a full tree walk (the 3s poll already covers correctness).
Happy to help with any of these if you're short on time. Thanks again!
…completion Concern 1 (session discovery still flat): extract newestSubagentMtime() — a recursive walk over the subagents tree — and use it in both scanForActiveSessions/newestMtime (session-watcher) and the relay's scanner, so sessions whose only recent activity is nested workflows/wf_*/ writes (including journal.jsonl, often the freshest file during a run) are no longer considered inactive. The walk only runs when the main JSONL alone doesn't qualify, keeping the common per-tick path flat. Concern 3 (workflow nodes never complete): when a Workflow tool_result arrives, the parser now re-scans the subagents dir (the flat fs.watch fallback can't see writes below workflows/) and emits subagent_return + agent_complete for every workflow-path subagent with no unmatched pending tool calls. Idle-check rather than run-id parsing because the result's "Run ID:" line is conditional (absent for remote launches) and completing all workflow-path agents would kill a concurrent run's live nodes. A new SubagentState.completed flag prevents re-completion, and a transcript that grows again revives its node. Also align the no-descriptive-meta fallback name with the SubagentStop hook convention (<agentType>-<id suffix>, e.g. workflow-subagent-87d5b) so hook-emitted completions resolve to the same node. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NdLrLGZKzuRkoRvJmtYB8U
|
Thanks for the thorough review — pushed 1efc5d2 addressing points 1 and 3, and I have a concrete answer for point 2. 1. Session discovery (fixed). Extracted a shared 2. Duplicate nodes (answered — no fix needed for current Claude Code). I verified this empirically against Claude Code v2.1.42 (live experiment plus inspecting the transcript-writing code in the binary): current versions never persist 3. Workflow nodes never complete (fixed). When a Tests: added a suite for the completion logic (idle vs. busy vs. never-spawned vs. re-completion) and for |
|
Thanks for the thorough response, and for the tests. I verified everything locally: the liveness walk behaves as described in both the watcher and the relay, the completion logic handles the late-discovery ordering correctly, and your point 2 checks out on my own transcripts too (zero persisted progress entries), so I'm happy to leave that path alone. One thing before this can merge. Your journal.jsonl observation surfaced a real gap in the tailing path: The idle-but-unread-tail race (complete then revive, node stays alive until session end) is fine by me given the revival mechanism; a short comment there would be plenty. Really nice work on this round, the completion heuristic reasoning is exactly right. |
Problem
Subagents spawned by the
Workflowtool (multi-agent orchestration) never appear in the visualizer. The orchestrator node shows up, but its children are invisible — including when running vianpx agent-flow-app.Root cause
Newer Claude Code writes each Workflow subagent's transcript to:
— two levels below the session's
subagents/directory.scanSubagentsDir()used a flatfs.readdirSync, so it only ever discovered top-levelsubagents/*.jsonlfiles and missed every nested Workflow subagent.The other two detection paths don't help here either: the
Workflowtool_use is not emitted as aTask/Agentblock in the main transcript (so the transcript parser never spawns a node), and there are noagent_progressinline events. File tailing is the only path that can surface these subagents — and it was looking one directory too shallow.Confirmed on a real session:
0Task/Agenttool_use,0agent_progress,1Workflowtool_use,1flat subagent file, and22nestedsubagents/workflows/wf_*/agent-*.jsonlfiles that were silently ignored. The nested meta is just{"agentType":"workflow-subagent"}.Changes
collectSubagentJsonlFiles()— recursively walks thesubagents/tree so transcripts at any depth are discovered.scanSubagentsDir()— uses the recursive collector; upgrades to a recursivefs.watch(Windows/macOS) with a flat-watch fallback. The existingPOLL_FALLBACK_MSpoll covers platforms without recursive watch support (Linux).resolveNameFromMeta()— when the meta carries no descriptive name (Workflow subagent meta has none), derives a stable name from the transcript file id (agent-<hash>.jsonl) so each renders as a distinct node instead of colliding on a generic label.extension/test/subagent-watcher.test.tscovering flat + nested discovery,.meta.json/non-jsonl exclusion, and the missing-directory case.Reproduction
npx agent-flow-app, connect a Claude Code session.Workflow-tool run in Claude.Testing
tsc --noEmitclean.Known related limitation (not addressed here, to keep the diff tight)
The "is this session still active?" mtime checks in
relay.tsandsession-watcher.tsalso scan only the flatsubagents/dir, so a session whose main transcript goes stale during a long workflow could be dropped from detection. Easy follow-up using the same recursive walk.🤖 Generated with Claude Code