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/current — no 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)
- 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.
- 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.
- 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).
- 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
Improvement / robustness gap (filed during dogfooding)
Filed while dogfooding armory-fleet v0.11.1 (using its own
subagenttool 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
subagentdispatch returns literally(no tool output)— norunId, 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.tsexecutehandler always returns a structured{ content, details, isError }object — it never returnsundefined/empty:So
(no tool output)meansexecutenever returns — theawait spawnSubagent(...)hangs forever and pi's tool-call layer times it out to empty. InsidespawnSubagent:src/engine/concurrency-lock.tsSingleSlotLockis a plain closure withtryAcquire/release/current— no timeout, no reaper.src/engine/spawnSubagent.tshasfinally { opts.lock.release(); }(line ~350), butfinallyonly runs if theawaitinside thetrysettles.await …never resolves →finallynever runs → the single-slot lock is leaked forever and the tool call hangs until pi's tool timeout →(no tool output)./reloaddoes 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)
SingleSlotLockshould 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.{ isError: true, content: "child session hung (no events for Ns)" }instead of lettingexecutehang to a silent(no tool output).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