fix(subagents): wake yielded parent on no-deliverable-payload completions (closes part of #89095)#96189
Conversation
|
RC4 RFC issue filed: #96190 — opt-in |
|
Codex review: needs maintainer review before merge. Reviewed July 12, 2026, 6:04 PM ET / 22:04 UTC. Summary PR surface: Source +38, Tests +311. Total +349 across 3 files. Reproducibility: yes. Current main's fallback has no deliverable text for terminal empty or '(no output)' subagent events, and the report is reinforced by independent live reproductions plus the PR's after-fix gateway transcript. Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Keep one canonical terminal-completion invariant: every expected subagent completion wakes the requester exactly once, with the resumed parent session owning yielded delivery where applicable and this bounded secret-safe notice covering direct no-payload fallbacks without adding persisted state or configuration. Do we have a high-confidence way to reproduce the issue? Yes. Current main's fallback has no deliverable text for terminal empty or '(no output)' subagent events, and the report is reinforced by independent live reproductions plus the PR's after-fix gateway transcript. Is this the best way to solve the issue? Yes. Extending the existing direct fallback with a bounded closed vocabulary is the narrowest maintainable fix for no-payload completion delivery; the remaining merge check is exact-once composition with the adjacent sessions_yield suppression work. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 2c06dfdd2fe1. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +38, Tests +311. Total +349 across 3 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (7 earlier review cycles)
|
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
215f73f to
63c8981
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
… slice RC1 (wait-timer pending-timeout forwarding) is superseded on main: shared terminal-outcome classifier with fresh-wait isolation. This branch now carries only the delivery-layer slice: - Synthesize a bounded, closed-vocabulary terminal notice for no-deliverable-payload subagent completions (statusLabel is never echoed; reasons are limited to no visible reply / timed out / failed (details withheld) / incomplete). - Reorder the message-tool-only delivery branch so the text-direct fallback runs before the failed-no-output early-out. - Regression tests for the notice vocabulary (incl. sensitive-label cases) plus wait-timer hard/soft coverage locking main's openclaw#89367 semantics. Restart-cancelled completions resolve through main's direct terminal path by design and are not re-tested here.
63c8981 to
6476dae
Compare
|
This assigned pull request has been automatically marked as stale after being open for 27 days. |
What Problem This Solves
Closes two distinct silent-drop paths inside subagent completion delivery that left a parent waiting via
sessions_yieldhung instate=processinguntil restart. Both paths were verified live by independent reporters on the issue thread (sunnydongbo for RC1; jrex-jooni for RC2 on 2026.6.9), and ClawSweeper's most recent review explicitly recommends pairing both fixes:This PR is the minimal-surface implementation of that pairing.
Linked context
subagent-announce.ts,subagent-registry-lifecycle.ts,subagent-registry.types.ts,subagent-registry-run-manager.ts— no file overlap with this PR).src/tasks/— no file overlap).Root cause and fix
RC1 — wait-timer fallback ignored pending timeout snapshots
src/gateway/server-methods/agent-job.tswaitForAgentJob's innersetSafeTimeoutfallback only consultedgetPendingAgentRunError. When a subagent was force-killed byrunTimeoutSeconds, a pending timeout snapshot (not a pending error) is recorded; the wait-timer fired near-simultaneously with the gateway-side kill and calledfinish(null), discarding the timeout snapshot.isTerminalWaitTimeoutthen resolved false downstream, the announce flow never fired, and the parent (sessions_yield) hung instate=processinguntil restart.The eager-check path higher up at lines 411–418 already consulted both maps. The patched timer mirrors that ordering: pendingError →
createPendingErrorTimeoutSnapshot, else pendingTimeout → snapshot, else null.RC2 — no-deliverable-payload synth notice + reordered fallback
src/agents/subagent-announce-delivery.tsresolveTextCompletionDirectFallbackonly returned content for completions withstatus:\"ok\"and non-empty payload. Any other terminal completion leftdeliverTextCompletionDirectreturningundefined, the four guarded return-false sites at ~lines 1614/1629/1672/1686 fired `visible_reply_missing`, announce retried 3× and abandoned, and the yielded parent never saw a terminal message.Per jrex-jooni's 2026-06-22 verified data point (OpenClaw 2026.6.9): the trigger is not timeout-specific — context-overflow-driven mid-turn compaction reliably produces a cleanly-stopped (`stopReason=stop`) child with an empty completion payload that hits the same drop. The fix has to key off "child produced no deliverable payload" rather than the timeout case specifically.
Two changes:
Second-pass loop in `resolveTextCompletionDirectFallback` synthesizes a bounded terminal notice when no ok-with-content completion exists but at least one `task_completion` event is present:
No raw child output is leaked; `statusLabel` is used as a hint when result has content, otherwise the reason is "no visible reply". Length is well under any per-channel budget.
Reordered the message-tool-only delivery branch (~line 1646) so the text-direct fallback is attempted before the `hasFailedSubagentNoOutputCompletion` early-out. Without the reorder, the early-out short-circuits to `visible_reply_missing` for the exact `(no output)` case the synth path is meant to cover.
Tests
5 new tests across the two test files ClawSweeper named as acceptance criteria:
`src/agents/subagent-announce-delivery.test.ts` (4 new, 1 modified):
`src/gateway/server-methods/server-methods.test.ts` (1 new):
568 tests pass across the four files ClawSweeper named (subagent-announce-delivery.test.ts: 216 across its two project configs, server-methods.test.ts: 162, subagent-registry-lifecycle.test.ts + subagent-registry.test.ts: 190), including the review-response regression tests below. Broader sweep of 7 adjacent subagent-* test files (`subagent-announce*`, `run-wait`, `subagent-run-timeout`, `subagent-delivery-state`) also passes — no regressions. `oxlint:core` and `tsgo` type-check clean.
Evidence
Vitest output for the new behavior (run on this branch)
```
✓ |agents| src/agents/subagent-announce-delivery.test.ts > deliverSubagentAnnouncement completion delivery > delivers a synthesized terminal notice when a direct-message subagent fails with no output (#89095) 3ms
✓ |agents| src/agents/subagent-announce-delivery.test.ts > deliverSubagentAnnouncement completion delivery > delivers a synthesized terminal notice for a clean completion with empty payload (#89095) 2ms
✓ |agents| src/agents/subagent-announce-delivery.test.ts > deliverSubagentAnnouncement completion delivery > delivers a synthesized terminal notice for a timeout-killed subagent with no output (#89095) 1ms
✓ |agents| src/agents/subagent-announce-delivery.test.ts > deliverSubagentAnnouncement completion delivery > synthesized terminal notice does not leak raw child output (#89095) 1ms
✓ |gateway-methods| src/gateway/server-methods/server-methods.test.ts > waitForAgentJob > resolves with the pending timeout snapshot when the wait timer expires before the grace fires (#89095) 0ms
```
Live-environment validation (this workspace, OpenClaw 2026.6.8)
Both fixes have been applied as byte-equivalent runtime patches to the installed `dist/` on this workspace's gateway for ~24 hours of real use (multi-agent workflows, sessions_yield-based ACP/Codex orchestration, Workflow tool harness runs). The local-patch sandbox suites are:
No silent-drop incidents observed in 24h of normal use. The vitest tests in this PR are the source-level translation of those sandbox assertions.
Issue-thread live reproductions (independent verification)
Hard maintainer requirements satisfied
Out of scope / follow-up
Review Response (2026-07-08)
All three rank-up moves addressed in commit
215f73f1d:now reuses the shared terminal-outcome classifier and forwards a pending
timeout snapshot only when
terminalOutcomeFromSnapshot(...).reason === "hard_timeout". Soft queue/draining timeouts and restart-cancelled snapshotsstay inside grace instead of being published as a false terminal cause.
Regression tests: soft
timeoutPhase: "queue"andstopReason: "restart"pending snapshots both resolve the waiter to
nullwhen the wait timer fires.notice no longer reads
event.statusLabelat all — it isoutcome.error-derived and could echo provider error text (paths, secrets) to an external
DM when the completion carried non-empty content. The reason is now strictly
one of
no visible reply/timed out/failed (details withheld)/incomplete, and the status is normalized to the closed event-status set.Regression tests: an error completion with a secret-bearing
statusLabelandpartial output, and a timeout completion with partial output, both produce
the exact closed-vocabulary notice and never contain the label text.
Real behavior proof (redacted)
Live run on a real gateway (v2026.6.11, macOS, dist patched with this PR's
delivery-path change), performed 2026-07-08:
no-deliverable-payload completion:
terminal, delivered (ids truncated):
Before this fix, this exact shape ((no output) placeholder result) abandoned
delivery after the retry limit with
visible_reply_missingand the waitingparent never received a terminal message. With the fix, the completion is
announced once (
delivery: delivered) and the parent wakes with the boundedterminal notice. No sensitive values in the artifacts above; session ids are
machine-local UUIDs.
Rebase note (2026-07-09)
Rebased onto current main per review. RC1 is superseded: #89367 landed the
wait-timer pending-timeout forwarding on main, already gated through the shared
terminal-outcome classifier with fresh-wait isolation — so this branch drops its
agent-job.tschanges entirely and now carries only the delivery-layerslice (the no-deliverable-payload synth notice with closed vocabulary, plus
the delivery-branch reorder). One commit, conflict-free against main.
Tests rerun on the exact rebased head (
63c8981d7c):Semantics notes for the rebase review ask:
waitForAgentJobare untouched; the retained wait-timer tests (hard forwarded,soft queue-timeout not forwarded) lock main's fix: forward pending timeout snapshot in waitForAgentJob fallback timer #89367 behavior in place.
(centralized precedence, fix(agents): centralize terminal run outcome precedence #88136) rather than timeout grace; that behavior is
main's by design and this PR does not alter it.
byte-identical before/after the rebase apart from context lines.
Sibling sequencing: with RC1 gone this PR no longer touches the wait layer at
all, so the only remaining composition surface with the yielded-completion and
blocked-completion PRs is the single synth-notice fallback in
resolveTextCompletionDirectFallback— additive, and inert whenever anok-with-content completion exists.