[Bug]: Collector child calling sessions_yield strands agents_wait forever while status surfaces report done; outputSchema silently inert on claude-cli backends
Build: OpenClaw 2026.9.2 (3928bad). Backend: claude-cli. Both defects reproduced live with control arms in both directions, then traced to source in the installed dist/.
Two independent defects in the swarm collector path. D2 is the serious one: it is a silent, unbounded hang with a green status surface, and it is the default behaviour rather than an edge case. D1 is filed with it because D1 is what usually triggers D2 — but they are independent, and fixing D1 alone leaves D2 armed.
D2 — a collector child that calls sessions_yield strands its parent forever, while the status surface reports "done"
Symptom
A collect: true child that calls sessions_yield ends up with:
execution.status: "terminal",execution.outcome.status: "ok"— the run genuinely finishedpauseReason: "sessions_yield"insubagent_runs.payload_json- no collector completion record written, ever
agents_waitpends forever — no error, no timeout of its own, no diagnosticsubagents(action="list")recentreports the same run asstatus: "done"subagents(action="list")tasksreports the same run asstatus: "running"
Three surfaces, three different answers, for one run. The one an operator is most likely to check (recent → done) is the one that says everything is fine.
Why children reach for it
sessions_yield is in a collector child's tool catalog. structured_output is not (that is D1). So a child instructed to "submit structured output" reaches for sessions_yield as the nearest available thing. Two of the first two test children did this unprompted, with no mention of yielding in their prompt.
Minimal reproduction
sessions_spawn({ task: "Reply with the single word gamma.", collect: true,
groupId: "g", label: "arm", model: "haiku", lightContext: true })
agents_wait({ ids: [runId], timeoutSeconds: 30 })
| arm | outputSchema |
child called sessions_yield |
agents_wait result |
|---|---|---|---|
| A (positive control) | no | no | status:"done", result:"gamma" — 5.1s |
| B | yes | no | status:"failed", schemaError:"structured_output was not called", result:"delta" — 7.4s |
| C | no | yes | never returns. recent: done, endedAt set, runtimeMs 15855, 52576 tokens. tasks: running. |
Arm A proves the harness observes success. Arm C carries no outputSchema at all — sessions_yield alone is sufficient to hang the parent, so D2 does not depend on D1.
Root cause
In dist/subagent-completion-admission.store-*.js, three separate paths exempt pauseReason === "sessions_yield" unconditionally, with no check for entry.collect:
// result-capture path — aborts before any completion state is created
const liveEntry = params.runs.get(entry.runId);
if (entry.pauseReason === "sessions_yield" || liveEntry?.pauseReason === "sessions_yield"
|| context.newerGenerationOwnsSession(entry)) return false;
const completion = ensureCompletionState(entry);
// resolveFinalizedSubagentTaskState — returns undefined, so `tasks` reports "running"
if (typeof endedAt !== "number" || !outcome || entry.pauseReason === "sessions_yield"
|| completion?.resultText === void 0 && typeof completion?.capturedAt !== "number") return;
// listPendingCompletionRunsForSession — run is skipped entirely
if (entry.pauseReason === "sessions_yield") continue;
The exemption looks correct for an announce child: a yielded child is paused and will resume, so admitting a completion early would be wrong. But a collector child has no resume path and no announcement — the collector completion record is its only output channel. The same guard that is right for announce children permanently strands collector children, and the recent projection reads execution.endedAt directly so it happily reports done over the stranded row.
Suggested fix shape
Gate these three exemptions on entry.collect !== true, so a terminal collector run admits its completion regardless of pauseReason. Failing that, at minimum: make agents_wait surface a terminal-without-completion run as a failed result with a real diagnostic instead of pending forever, and reconcile recent / tasks so they cannot disagree.
A cheaper partial mitigation would be to keep sessions_yield out of a collector child's tool catalog entirely — it has no meaning for a one-shot collector — though that alone would not fix a run already in this state.
D1 — structured_output is never injected into claude-cli-backed collector children, so outputSchema is silently inert
Symptom
Passing outputSchema to a collect: true spawn on a claude-cli backend has no effect on the child. agents_wait then returns status: "failed" with schemaError: "structured_output was not called". The child's answer is still present as raw text in result, so the caller gets a "failed" result that actually contains the correct answer.
Asked directly, the child reports: "A tool named structured_output was not present in my tool list."
Root cause
createStructuredOutputTool is present in the tools bundle (dist/openclaw-tools-*.js), but the string structured_output does not appear in any dist/cli-backend*.js — nor does forceToolNames. The CLI backend has no code path that injects the synthetic tool or forces the model to call it. The schema is accepted at the API boundary and then dropped on the floor for this backend class.
Impact
outputSchema is not merely unenforced — it is accepted, documented, and silently ineffective for every claude-cli-backed agent. The failure is also mislabelled: status: "failed" on a run that succeeded trains callers to ignore the status field.
Suggested fix shape
Either inject the synthetic tool on the CLI backend path, or reject outputSchema at spawn time with a clear error when the resolved backend cannot support it. Accepting a parameter that cannot be honoured is the worst of the three options.
Relationship to existing issues
- #138018 (Swarm collector can freeze missing structured output before a valid terminal tool call is recorded) — related but distinct. That issue is a race:
structured_outputis called, and arrives after the freezer has already recorded the missing-output failure. D1 here is the case where the tool is never injected at all, so it can never be called and there is no late callback to lose. A fix for #138018 would not change any arm above. - #132765 (
agents_waitignorestimeoutSeconds) — adjacent. Note the interaction: D2 produces a wait with no completion record to ever return, soagents_waittimeout behaviour is the only thing standing between a caller and an unbounded hang.
Workarounds currently in force on our fleet
- Never pass
outputSchema; ask for JSON in the child's final text and parse it parent-side. - End every collector child prompt with "do not call
sessions_yield; end your turn normally." - Give every parent wait loop its own wall-clock deadline — never trust
agents_waitto terminate.
All three are a standing tax on every swarm caller, and because a fix upstream would be silent (the workarounds keep working afterwards), we run a daily tripwire against these code paths to know when to drop them.
Source: openclaw/openclaw