Skip to content

subagent tool: failed explicit-model override returns empty output instead of a clean error #26

Description

@rz1989s

Improvement / robustness gap (filed during dogfooding)

Filed while dogfooding armory-fleet v0.11.1 (using its own subagent tool to build armory-fleet). Not a confirmed single-trigger bug — a robustness gap exposed when a child session hangs.

Symptom (observed 2026-08-03)

A subagent dispatch returns literally (no tool output) — no runId, no error, no fleet-run TODO. Reproducible across /reload. Earlier in the same session, dispatches worked (Tasks 1–4 implementers/reviewers returned real results); onset coincided with the host Mac sleeping mid-session and never recovered via /reload.

Root-cause analysis (from reading the installed source)

The installed src/tools/subagent.ts execute handler always returns a structured { content, details, isError } object — it never returns undefined/empty:

const res: SpawnResult = await spawnSubagent({ ... });
return { content: [{ type: "text", text: isError ? (res.error ?? res.status) : res.finalText }], details: {...}, isError };

So (no tool output) means execute never returns — the await spawnSubagent(...) hangs forever and pi's tool-call layer times it out to empty. Inside spawnSubagent:

  • src/engine/concurrency-lock.ts SingleSlotLock is a plain closure with tryAcquire/release/currentno timeout, no reaper.
  • src/engine/spawnSubagent.ts has finally { opts.lock.release(); } (line ~350), but finally only runs if the await inside the try settles.
  • There is no overall timeout around the child-session event loop. If the child session's first model call hangs (e.g. a provider HTTP connection that died during host sleep and never errors out before TCP keepalive), await … never resolves → finally never runs → the single-slot lock is leaked forever and the tool call hangs until pi's tool timeout → (no tool output).

/reload does not fix it because it reloads extension code but does not reset pi's provider connection pool / the child-session SDK path that holds the hung connection. A full pi restart resets it.

The gaps (improvement asks)

  1. Spawn timeout. Wrap the child-session event loop in an overall timeout (configurable, default e.g. 10 min) so a hung child fails loudly instead of hanging forever.
  2. Lock reaper. SingleSlotLock should auto-release after a max-hold TTL (or on a heartbeat miss), so a crashed/hung holder can't deadlock the single subagent slot permanently.
  3. Never hang the tool — always return. If the child session hasn't produced an event within N seconds, surface a clean { isError: true, content: "child session hung (no events for Ns)" } instead of letting execute hang to a silent (no tool output).
  4. Surface lock contention as a clean error, not empty. A contended/hung slot should return the existing "a subagent is already running" message, not empty output (the empty path masks the real state from the controller).

Why it matters

Dogfooding gotcha #6 already showed reviews miss what smokes catch. This gap makes the subagent-driven flow silently dead after a host sleep — a controller sees empty outputs and can't tell "broken tool" from "no work". A timeout + reaper + clean-error would make the tool self-diagnosing.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions