Skip to main content

Module subagent_watcher

Module subagent_watcher 

Source
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 detected
  • subagent: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 — a SubAgent.display_name resolved (on-demand click-triggered, or eager at dispatch time — see trigger_eager_naming)
  • dispatch:updated — an AgentDispatch’s member count, status, or dispatch_name changed (both Solo and Workflow kinds)
  • dispatch:activity — coalesced new events across a dispatch’s members, flushed every DISPATCH_ACTIVITY_FLUSH_INTERVAL. Workflow dispatches are ONLY ever represented here (no per-member broadcast). Solo dispatches are ALSO queued here in addition to the immediate subagent:activity above (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 by spawn_block_prune_subscriber reacting to Event::BlockDeleted/TabDeleted/WorkspaceDeleted). Without this, ListActive/ListDispatches kept 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_block also 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; see session_belongs_to_block and 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 from jsonl::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, and scan_subagents_dir (the capped cold-backfill file walk).
types 🔒
State/event types for the subagent watcher: the public SubAgent/ AgentDispatch API types (re-exported at subagent_watcher::), their internal SessionWatch/SubagentState/DispatchState/WatchedAgent tracking counterparts, and the PendingDispatchActivity coalescing buffer filled and flushed by subagent_watcher::jsonl.

Structs§

SubagentWatcher

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 🔒

Functions§

global
set_global