akashic-intelligence lost /trekplan Phase 9 twice to reviewer agents that never returned. Reproduced here and measured: the cause is the Agent tool's `name` parameter, not the agent definitions. Passing `name` does not label a subagent - it changes its kind. The spawn is recorded as taskKind "in_process_teammate" (spawnDepth 0) instead of a real subagent (spawnDepth 1). A teammate's final assistant text is not a return value; it reaches the parent only if the teammate itself calls SendMessage(to: "main"). plan-critic and scope-guardian declare tools: [Read, Glob, Grep] - no SendMessage - so as teammates they are structurally incapable of returning, whatever the prompt says. Denominators: named 0/5 returned; named + explicit SendMessage 1/1; unnamed 3/3 (plan-critic and scope-guardian each returned full findings + the JSON block in ~110s). Model override 2/2 non-returning, so the override is not the variable. All 5 named agents produced correct final text in their transcripts - only delivery failed, and that output is recoverable on disk. The defect is a harness behaviour, so it is documented rather than silently worked around: docs/agent-return-channel-defect.md carries the mechanism, every denominator, the two broken queries that nearly became facts, the unmeasured cells, and a recommended working shape for consumers (use agents, drop `name`; do not fall back to inline review, which costs the dedup step). Prevention is pinned, TDD red->green: the four spawning commands each state the rule and name the mechanism, and doc-consistency derives the spawning set from the command files so the pin cannot go vacuous. Suite 1013 (1011/0/2), +5 from baseline 1008. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014MfB5Ecp8vvGyou8uFGxBV
207 lines
11 KiB
Markdown
207 lines
11 KiB
Markdown
# The Agent tool's `name` parameter silently destroys the return channel
|
||
|
||
**Date:** 2026-08-17 (voyage S85)
|
||
**Claude Code:** 2.1.226 · **Plugin:** voyage 5.9.1 (installed cache — verified byte-identical to the repo working tree for both reviewer agents before measuring)
|
||
**Status:** Root cause MEASURED and mechanistically confirmed. The defect is in
|
||
how the harness treats one Agent-tool parameter, not in voyage's agent
|
||
definitions. A one-line workaround exists and is now pinned in every voyage
|
||
spawn site.
|
||
|
||
## The report that triggered this
|
||
|
||
`akashic-intelligence` lost `/trekplan` Phase 9 twice. S25 (2026-08-17, plan
|
||
rev 5.0): four subagents spawned via the Agent tool — `voyage:plan-critic` ×2,
|
||
`voyage:scope-guardian` ×2, all `model: "opus"`, not Workflow. Nine
|
||
`SendMessage` reminders, zero replies. The round before had the same outcome
|
||
with tighter scope and an explicit tool-call budget. Consequence: the
|
||
hand-dedup step fell away (one source instead of two) and review coverage came
|
||
out **narrower** than the four preceding rounds.
|
||
|
||
Their own conclusion was "the channel, not the agents" — directionally right.
|
||
Two details in it are wrong and matter: they attributed it to the agents having
|
||
no `Write` tool, and they concluded the remedy is to abandon agents and run the
|
||
review inline. `Write` is irrelevant, and the agents do not need abandoning.
|
||
|
||
## Root cause
|
||
|
||
**Passing `name` to the Agent tool does not name a subagent. It changes what
|
||
kind of thing gets spawned.**
|
||
|
||
The spawn metadata says it outright. From
|
||
`~/.claude/projects/<project>/<session>/subagents/agent-*.meta.json`, same
|
||
session, same model, same `subagent_type`:
|
||
|
||
| spawn | recorded `agentType` | `taskKind` | `spawnDepth` |
|
||
|---|---|---|---|
|
||
| without `name` | `voyage:plan-critic` | *(none — a real subagent)* | `1` |
|
||
| with `name` | `pc-opus` (the name) | `in_process_teammate` | `0` |
|
||
|
||
The two kinds have different return semantics:
|
||
|
||
- **Subagent** (`spawnDepth: 1`) — the agent's final assistant text **is** the
|
||
return value. It arrives at the parent as a task notification.
|
||
- **Teammate** (`in_process_teammate`, `spawnDepth: 0`) — a peer of the main
|
||
session, not a child of it. Its plain final text is **not** a return value; it
|
||
is only transcript. A teammate reaches the parent **only** by calling
|
||
`SendMessage(to: "main")`.
|
||
|
||
Every voyage agent declares a `tools:` allowlist. `plan-critic` and
|
||
`scope-guardian` declare `["Read", "Glob", "Grep"]`. **No SendMessage.** As
|
||
teammates they are therefore *structurally* incapable of returning anything —
|
||
no prompt, no scope tightening, and no tool-call budget can change that. This
|
||
is exactly why akashic's second attempt with a stricter prompt failed
|
||
identically: the prompt was never the variable.
|
||
|
||
### Why it looks like a hung agent
|
||
|
||
The teammate runs. It reads the plan, reasons, and writes a complete, correct
|
||
final answer to its transcript. Then it stops. From the orchestrator's side
|
||
this is indistinguishable from an agent that stalled — so the natural response
|
||
is to poke it with `SendMessage`, which produces more transcript that also
|
||
never comes back. Nine reminders, zero replies.
|
||
|
||
**The work is not lost.** It is on disk at
|
||
`~/.claude/projects/<project-slug>/<session-id>/subagents/agent-a<name>-<hash>.jsonl`.
|
||
The final assistant text block in that file is the answer the orchestrator never
|
||
received. This is the recovery path when a run has already been burned.
|
||
|
||
## Measurements
|
||
|
||
One session, one target unless noted. Target for the reviewer cells:
|
||
`docs/devils-advocate-plan.md` (66 lines) — a real plan file, small enough that
|
||
runtime cannot be confused with hanging.
|
||
|
||
| # | `subagent_type` | `model` | `name`? | Returned to parent? | Time |
|
||
|---|---|---|---|---|---|
|
||
| 1 | `general-purpose` ("reply PONG", 0 tools) | `opus` | yes | **no** | >17 min, never |
|
||
| 2 | `general-purpose` (read file, count headings) | `opus` | yes | **no** | never |
|
||
| 3 | `voyage:plan-critic` | `opus` | yes | **no** | never |
|
||
| 4 | `voyage:scope-guardian` | `opus` | yes | **no** | never |
|
||
| 5 | `voyage:plan-critic` | *(no override)* | yes | **no** | never |
|
||
| 6 | `general-purpose`, told to call `SendMessage(to: "main")` | `opus` | yes | **yes** | ~30 s |
|
||
| 7 | `general-purpose` ("reply PONG", 0 tools) | `opus` | **no** | **yes** | **2.19 s** |
|
||
| 8 | `voyage:plan-critic` | `opus` | **no** | **yes** — full findings + JSON block | **110.3 s**, 7 tool uses |
|
||
| 9 | `voyage:scope-guardian` | `opus` | **no** | **yes** — full findings + JSON block | **109.1 s**, 8 tool uses |
|
||
|
||
Denominators, per varied dimension:
|
||
|
||
- **`name` present, no explicit SendMessage: 0/5 returned.** `name` absent:
|
||
**3/3 returned.** This is the only dimension that moved the outcome.
|
||
- **`name` present *with* an explicit `SendMessage(to: "main")` instruction:
|
||
1/1 returned** (cell 6). This is the positive control for the teammate
|
||
channel: it is not broken, it is merely never used by tool-less agents.
|
||
- **Model override: 2/2 non-returning.** Cells 3 (`model: "opus"`) and 5 (no
|
||
override) are identical in outcome — the override is not the variable, which
|
||
rules out akashic's stated `model: "opus"` detail as causal.
|
||
- **Agent type: 2/2 in each arm.** `plan-critic` and `scope-guardian` behave
|
||
identically named (both silent) and unnamed (both complete). The agent
|
||
definitions are not the variable.
|
||
- **Work actually performed while named: 5/5.** All five named agents produced
|
||
correct, complete final text in their transcripts within ~3 minutes —
|
||
`PONG`, `8`, plan-critic's blocker list with the correct legacy-format
|
||
advisory, and scope-guardian's coverage table. Nothing failed except delivery.
|
||
|
||
### Known-positive discipline (Verifiseringsloven face 4)
|
||
|
||
Two negative results in this investigation were produced by broken queries and
|
||
would have been consumed as facts:
|
||
|
||
1. **`ListAgents` returned no in-process subagents** while five were running. It
|
||
listed 26 peer sessions and none of mine. Probing one directly with
|
||
`SendMessage` proved the agents existed and had live inboxes. Had the empty
|
||
listing been believed, the diagnosis would have been "the spawns silently
|
||
no-op" — wrong.
|
||
2. **The first known-positive control failed.** Cell 1 (named PONG) was meant to
|
||
prove the spawn mechanism works, and it did not return either. A failed
|
||
control proves nothing; it only means the control shared the defect. The
|
||
control that discriminates is cell 7 — same prompt, same model, same agent
|
||
type, `name` removed — which returned in 2.19 s.
|
||
|
||
**Unmeasured cells, stated as unmeasured:** the returning arm was only exercised
|
||
against the 66-line target. Large targets (voyage's 573-line plan;
|
||
akashic's 3730-line / 277 KB `features/01-sun-position/plan.md`) were **not**
|
||
measured in either arm. Nothing here rules out a *separate*, size-dependent
|
||
failure at akashic's scale — it only establishes that the failure they observed
|
||
reproduces at 66 lines, where size cannot be the cause.
|
||
|
||
## Where the defect lives
|
||
|
||
**Outside this repository.** Nothing in `agents/plan-critic.md` or
|
||
`agents/scope-guardian.md` is wrong: correct frontmatter, a `tools:` list
|
||
appropriate to a read-only reviewer, a terminating prompt, and an output format
|
||
that both agents produced verbatim when spawned as real subagents. The failure
|
||
is a harness behaviour — a parameter that silently changes a spawn's kind and
|
||
its return contract, with no error, no warning, and no observable difference at
|
||
the call site beyond the wording of the tool result.
|
||
|
||
Per the standing rule, this is **not** worked around silently. It is documented
|
||
here, reported to `.claude` (which owns cross-repo quality and can escalate),
|
||
and guarded in voyage's own spawn instructions so no consumer re-enters it.
|
||
|
||
## In-repo fix
|
||
|
||
Every voyage command that spawns agents — `trekbrief`, `trekplan`,
|
||
`trekresearch`, `trekreview` — now states the rule at its spawn site, naming
|
||
the mechanism (`in_process_teammate`) so it cannot be mistaken for style
|
||
preference. `trekexecute` spawns nothing (Hard Rule 10) and is excluded.
|
||
|
||
Pinned by `tests/lib/doc-consistency.test.mjs`:
|
||
|
||
- `S85: the set of agent-spawning commands is exactly the four that carry the
|
||
no-name rule` — derives the spawning set from the command files themselves, so
|
||
the pin cannot go vacuous when a command starts or stops spawning.
|
||
- `S85: commands/<cmd>.md forbids the Agent tool's name parameter at its spawn
|
||
sites` — one per command.
|
||
|
||
Verified red before the prose was written, green after.
|
||
|
||
## Recommended working shape for consumers
|
||
|
||
For anyone running voyage's reviewers (akashic included), grounded in the
|
||
measurements above rather than in preference:
|
||
|
||
**1. Use agents. Spawn them without `name`.** Cells 8 and 9 are the evidence:
|
||
both reviewers returned complete, schema-conformant output — human-readable
|
||
findings *and* the machine-readable JSON block the dedup helper consumes — in
|
||
~110 seconds, in parallel, from a single message. This is the intended shape and
|
||
it works. Removing `name` is the entire fix.
|
||
|
||
**2. Do not switch to inline review as a remedy.** akashic's fallback was
|
||
rational under their diagnosis but it pays a real, measured price: with one
|
||
source there is no dedup step, and their own record shows `scope-guardian`
|
||
finding blockers in the rev 3.0/4.0 rounds that `plan-critic` did not see at
|
||
all. The two agents are not redundant — cells 8 and 9 here returned largely
|
||
*different* findings on the same 66-line plan (plan-critic led on
|
||
placeholder/headless defects; scope-guardian led on the plan already having been
|
||
executed and its deliverable never being written). Two independent passes with
|
||
different mandates is the property worth keeping, and unnamed agents deliver it
|
||
for free. Running two inline passes is the fallback if agents fail *for a
|
||
different, verified reason* — not for this one.
|
||
|
||
**3. If you deliberately want a named teammate, give it SendMessage and say so.**
|
||
Cell 6 shows the teammate channel works when used. That means adding
|
||
`SendMessage` to the agent's `tools:` list *and* instructing it in the prompt to
|
||
call `SendMessage(to: "main")` with its result. voyage does not do this: its
|
||
reviewers are read-only by design, and the subagent path already returns.
|
||
|
||
**4. A hard tool-call budget is not the lever here.** akashic tried a tighter
|
||
budget and it changed nothing, which this measurement explains: a 0-tool-call
|
||
agent (cell 1) failed exactly like a 7-tool-call one. Budget caps are a cost
|
||
control, not a liveness control.
|
||
|
||
**5. When an agent looks hung, check its transcript before re-prompting.** The
|
||
final assistant block in
|
||
`~/.claude/projects/<project-slug>/<session-id>/subagents/agent-*.jsonl` tells
|
||
you within seconds whether the agent failed or merely could not deliver. Reading
|
||
that file directly recovers a burned run's output. Extract only the last
|
||
assistant text block — these transcripts run to 140 KB and reading one whole
|
||
will flood the orchestrator's context.
|
||
|
||
## Open
|
||
|
||
- The `--gates`-adjacent "idle-agent gap" already logged as open operator
|
||
decision #3 in `STATE.md` (`/trekreview` Phase 5, `/trekplan` Phases 5/6/9,
|
||
`/trekresearch` Phase 4 have no empty-return detection) now has a measured
|
||
root cause for its most likely trigger. Whether to add active detection — as
|
||
opposed to the prevention pinned here — remains open and is not decided by
|
||
this document.
|