Expand description
Subagent watcher: monitors Claude Code session directories for subagent JSONL files and broadcasts activity events to WebSocket clients.
Claude Code spawns “subagents” via the Task tool. Each subagent writes its
conversation to a JSONL file under:
<claude-config>/projects/<encoded-workspace>/subagents/agent-<id>.jsonl
A Workflow tool run nests its member agents one level deeper, under a
per-run directory, and adds a run-level journal:
<claude-config>/projects/<encoded-workspace>/subagents/workflows/<run-id>/agent-<id>.jsonl
<claude-config>/projects/<encoded-workspace>/subagents/workflows/<run-id>/journal.jsonl
This module watches those directories and emits:
subagent:spawned— new subagent JSONL file detectedsubagent:activity— new events for a Solo dispatch’s one member, broadcast immediately (existing per-member consumers, e.g. the agent-pane activity dock, still rely on this)subagent:completed— subagent finished (result event seen)subagent:named— aSubAgent.display_nameresolved (on-demand click-triggered, or eager at dispatch time — seetrigger_eager_naming)dispatch:updated— anAgentDispatch’s member count, status, ordispatch_namechanged (both Solo and Workflow kinds)dispatch:activity— coalesced new events across a dispatch’s members, flushed everyDISPATCH_ACTIVITY_FLUSH_INTERVAL. Workflow dispatches are ONLY ever represented here (no per-member broadcast). Solo dispatches are ALSO queued here in addition to the immediatesubagent:activityabove (dual emission, not a replacement) — see docs/specs/SPEC_SWARM_DISPATCH_NAMING_AND_ROW_MODEL_2026_07_19.md Phase A, added so a solo dispatch’s row can use the same concatenated-feed expand mechanism a Workflow row already had.subagent:block_pruned— a deleted block’s subagents/dispatches were just pruned from this watcher’s state (prune_block/unwatch_agent, driven byspawn_block_prune_subscriberreacting toEvent::BlockDeleted/TabDeleted/WorkspaceDeleted). Without this,ListActive/ListDispatcheskept returning a closed block’s subagents forever, so the Swarm pane kept rendering a ghost row for it until the whole app/srv restarted.prune_blockalso tears down the block’s own filesystem watcher (unwatch_block) — without that, a closed block’s watcher leaked indefinitely and kept re-creating fresh (mis-)attributions the next time another agent sharing its watched directory wrote to its own subagent transcript; seesession_belongs_to_blockand docs/retro/retro-subagent-watcher-shared-dir-fanout-and-leak-2026-07-23.md.
Split into submodules (pure relocation, no behavior change): types (the
state/event types), query (the read/naming API), scan (session/dir
backfill scanning + stale-subagent reconciliation), jsonl (the JSONL/
journal file-change state machine and dispatch-activity coalescing), and
parse (free-standing path/JSONL parsing utilities). This file keeps the
SubagentWatcher struct definition and its lifecycle methods (new/
spawn/watch_agent/unwatch_agent/prune_block*).
Re-exports§
pub use parse::derive_claude_config_dir;pub use parse::resolve_claude_config_dir;pub use parse::spawn_block_prune_subscriber;pub use types::AgentDispatch;pub use types::DispatchKind;pub use types::DispatchStatus;pub use types::SubAgent;pub use types::SubAgentStatus;pub use types::SubagentEvent;pub use types::SubagentEventType;
Modules§
- completion 🔒
- Did this subagent actually finish, or was it cut off?
- events
- The subagent watcher’s events, as typed payloads (
mps_events). Each serializes exactly the camelCase JSON the watcher built by hand before. - jsonl 🔒
- The JSONL/journal file-change state machine:
process_jsonl_change(one continuous state machine, moved whole rather than decomposed further) and its dispatch-membership/coalescing plumbing —flush_pending_dispatch_ activity,update_dispatch_membership,queue_dispatch_updated,process_journal_change,dispatch_entry,refresh_dispatch_info,refresh_dispatch_status,broadcast_dispatch_updated. - parse 🔒
- Free-standing parsing/path utilities used by the rest of
subagent_watcher— none of these need&self. Covers: JSONL line parsing, journal-count reading, workflow/session id derivation from a path, the block-delete cascade subscriber, and Claude config dir resolution. - query 🔒
- Query/naming API:
list_active/list_dispatches/solo_dispatch/get_history/get_info/set_display_name/set_dispatch_name/trigger_eager_naming— the read-mostly surface RPC dispatch calls into, plus the eager Haiku-naming trigger fired fromjsonl::process_jsonl_change. - scan 🔒
- Session/directory backfill scanning and stale-subagent reconciliation:
scan_session_subagents(pane-reopen backfill entry point),reconcile_stale_subagents(Active -> Abandoned correction once a parent’s turn is confirmed idle),broadcast_subagents_abandoned, andscan_subagents_dir(the capped cold-backfill file walk). - types 🔒
- State/event types for the subagent watcher: the public
SubAgent/AgentDispatchAPI types (re-exported atsubagent_watcher::), their internalSessionWatch/SubagentState/DispatchState/WatchedAgenttracking counterparts, and thePendingDispatchActivitycoalescing buffer filled and flushed bysubagent_watcher::jsonl.
Structs§
Constants§
- DISPATCH_
ACTIVITY_ 🔒FLUSH_ INTERVAL - Flush cadence for buffered dispatch activity (SPEC §9.5 — 250ms-1s was floated as a plausible range, not measured; 500ms is the midpoint, picked as a reasonable default pending real usage data).
Statics§
- GLOBAL 🔒