Skip to main content

Module completion

Module completion 

Source
Expand description

Did this subagent actually finish, or was it cut off?

SubAgentStatus::Abandoned is — as its own doc comment says — “always an inference, never an observation”. Until this module existed, the inference was “the subagent transcript has no terminal Result line, therefore it was interrupted” (scan.rs::reconcile_stale_subagents). That premise does not hold: no AgentMux transcript contains a "type":"result" line at all — not the subagent sidechain files, not the parent session file. The line type the old check waited for is absent from the format this deployment writes (entrypoint: sdk-cli), so SubAgentStatus::Completed was unreachable and every Agent-tool dispatch ended up displayed as “interrupted”, including the ones that returned full results the parent went on to use.

Evidence and the full chain: docs/reports/REPORT_SUBAGENT_COMPLETION_NEVER_DETECTED_2026_09_05.md.

The replacement uses the strongest evidence available, and it is an observation rather than an inference: the parent’s own tool_result for the dispatch’s tool_use_id. If the parent recorded a result, the subagent demonstrably returned — whatever its transcript does or doesn’t end with. The correlation key is already on disk: Claude Code writes an agent-<id>.meta.json sidecar next to each subagent transcript carrying the parent-side toolUseId.

Deliberately kept free of watcher state so the decision is unit-testable on its own — the same reasoning that split parse.rs out.

Structs§

SubagentMeta 🔒
One subagent’s .meta.json sidecar. Only the fields we use are modelled; the sidecar also carries agentType/spawnDepth/model. serde ignores the rest.

Constants§

DESCRIPTION_NAME_MAX_CHARS 🔒
Longest description kept as a name. The tool asks for 3–5 words; this only stops a runaway one from filling the row.

Functions§

description_for 🔒
The subagent’s display name from the parent’s description of its task, read from the same sidecar. None when there is none (a workflow member, a sidecar not written yet); the subagent is then named by Haiku on demand.
display_name_from_description 🔒
A description as a one-line name: whitespace collapsed, capped, None if nothing is left.
parent_tool_results 🔒
Every tool_use_id the parent transcript has recorded a tool_result for, mapped to that result’s is_error flag.
parent_tool_results_at 🔒
Read an already-resolved parent transcript. An unreadable/absent file yields an empty map, which makes terminal_status fall back to the old Abandoned inference — conservative, and identical to pre-fix behaviour.
parent_transcript_for 🔒
The parent session transcript for a subagent transcript.
read_meta 🔒
sidecar_path 🔒
…/agent-<id>.jsonl → …/agent-<id>.meta.json.
terminal_status 🔒
What an Active subagent’s status should become once its parent turn is confirmed idle.
tool_use_id_for 🔒
The parent-side tool_use_id for a subagent transcript, read from its sidecar. None when there is no sidecar, it is unreadable, it is malformed, or it predates the field — all of which must degrade to the old conservative behaviour rather than erroring.
transcript_for_sidecar 🔒
…/agent-<id>.meta.json → …/agent-<id>.jsonl; None for any other file. The inverse of sidecar_path, so the watcher can route a sidecar write to the transcript whose tool_use_id it carries.