fix(subagent): preserve steered task text on restart redispatch#77539
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 1, 2026, 12:14 AM ET / 04:14 UTC. Summary PR surface: Source +61, Tests +127. Total +188 across 15 files. Reproducibility: yes. at source level: current main preserves the previous task during replacement while orphan recovery reconstructs restart text from the persisted task. I did not run a live failing restart in this read-only review. Review metrics: 1 noteworthy metric.
Stored data model Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the focused registry-level task override after maintainers explicitly accept prompt-as-task persistence for replacement runs. Do we have a high-confidence way to reproduce the issue? Yes, at source level: current main preserves the previous task during replacement while orphan recovery reconstructs restart text from the persisted task. I did not run a live failing restart in this read-only review. Is this the best way to solve the issue? Yes as an implementation shape, pending maintainer acceptance. Adding the override at AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 5a5913a98b03. Label changesLabel justifications:
Evidence reviewedPR surface: Source +61, Tests +127. Total +188 across 15 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
|
There was a problem hiding this comment.
Pull request overview
This PR fixes a restart-recovery correctness gap in the subagent registry: when a running subagent is steered (or woken/resumed) and the gateway restarts mid-flight, restart redispatch should continue with the steered instruction rather than reverting to the original task.
Changes:
- Adds an optional
task?: stringoverride toreplaceSubagentRunAfterSteerand threads it through steer/wake/orphan-resume call paths. - Updates the subagent run manager replacement logic to persist the caller-supplied replacement task (fallbacking to the prior task when omitted).
- Adds regression tests to ensure task override is persisted and that legacy callers still preserve the prior task.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/agents/subagent-registry.ts | Extends replaceSubagentRunAfterSteer wrapper params to include optional task. |
| src/agents/subagent-registry.steer-restart.test.ts | Adds regression tests for task replacement/preservation during steer replacement runs. |
| src/agents/subagent-registry-steer-runtime.ts | Documents and types the new optional task override in the steer runtime shim. |
| src/agents/subagent-registry-run-manager.ts | Implements task override selection when replacing a run after steer/wake/resume. |
| src/agents/subagent-orphan-recovery.ts | Passes the constructed orphan-resume message as the replacement run’s persisted task. |
| src/agents/subagent-control.ts | Passes the steered message as the replacement run’s persisted task. |
| src/agents/subagent-announce.ts | Passes the descendant-wake message as the replacement run’s persisted task. |
| src/agents/subagent-announce.format.e2e.test.ts | Updates expectations to assert the wake replacement call includes a task string. |
| CHANGELOG.md | Adds an entry describing the restart-redispatch steer-task preservation fix. |
cfd90f4 to
bcfdece
Compare
bcfdece to
a1384c5
Compare
a1384c5 to
ee7bb27
Compare
ee7bb27 to
2884472
Compare
2884472 to
cd60564
Compare
0f14ccc to
c09e0f8
Compare
c09e0f8 to
50eefdf
Compare
|
@clawsweeper re-review The requested latest-head runtime proof for the completed-session reactivation path is now in the PR body (addendum section, captured 2026-06-12 against exact head |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
50eefdf to
9889fec
Compare
|
Clownfish 🐠 reef update Thanks for the work here. Clownfish got this branch swimming again without needing a replacement PR. Source PR: #77539 fish notes: model gpt-5.5, reasoning medium; reviewed against 9889fec. |
Threads an optional task override through subagent run replacement so restart redispatch rewraps the actual prompt dispatched on steer, descendant wake, and orphan recovery instead of the stale `task` from the original run record. Also threads it through completed-session reactivation (`reactivateCompletedSubagentSession` in `src/gateway/session-subagent-reactivation.ts`), which `sessions.send` and `agent.run` dispatch use to swap a completed subagent run id for a new follow-up run id. Without this, a follow-up sent via `sessions.send` or a fresh `agent.run` on a completed child session would reactivate the prior runRecord with its old task; a later gateway restart's orphan recovery would then rewrap the stale original prompt instead of the follow-up text the operator just sent. `expectSubagentFollowupReactivation` now accepts an optional `task` parameter and propagates it into the `replaceSubagentRunAfterSteer` assertion. The two existing callers (`agent.test.ts` and `sessions.send-followup-status.test.ts`) pass `task: "follow-up"` matching the dispatched message, so the focused gateway follow-up tests now prove the new completed-session task-passing contract instead of asserting the old `task`-free shape. Test totals on the new head: 322 tests pass (helpers + sessions follow-up + agent + reactivation).
Summary
When a user steers an in-flight subagent task, the new task text is recorded in a steer event. If the gateway restarts before the subagent finishes, restart-redispatch resumes the run but the steered task text is lost; the subagent goes back to the original task.
This adds an optional
task?: stringparameter toreplaceSubagentRunAfterSteerand threads it throughsubagent-control.ts,subagent-announce.ts, andsubagent-orphan-recovery.ts. Backwards-compatible: omitted callers preserve the previous task.Repro
launchctl kickstart -k gui/$UID/ai.openclaw.gatewayon macOS, equivalent on systemd).Test plan
node scripts/run-vitest.mjs run src/agents/subagent-registry.steer-restart.test.ts src/agents/subagent-orphan-recovery.test.ts src/agents/subagent-control.test.ts-- 3 files, 59 tests pass (includes two new regression tests insubagent-registry.steer-restart.test.ts).node scripts/run-oxlint-shards.mjs --only=core --threads=4clean.pnpm buildclean.taskpreserve the previous task field; new call sites pass the steered/wake/resume message explicitly.Thanks @amittell.
Real behavior proof
replaceSubagentRunAfterSteeraccepts atask?: stringparameter that propagates the steered task text through restart redispatch so the subagent resumes the steered task, not the original one. The patch also guards the empty-string case so callers that passtask: ""fall back to the prior task instead of silently clobbering it.~/.openclaw/openclaw/dist/subagent-registry-CCMhICN7.jscontains the patched lineconst nextTask = typeof replaceParams.task === "string" && replaceParams.task.length > 0 ? replaceParams.task : source.task;at line 1468. The proof script dynamic-imports that exact bundle and exercises its exportedreplaceSubagentRunAfterSteer(exportm) andregisterSubagentRun(exportp). State is isolated viaOPENCLAW_STATE_DIRredirected to a fresh tmpdir so the live gateway'sruns.jsonis not touched.scp /tmp/proof-77539-amittell.mjs user@gateway-host:/tmp/proof-77539-amittell.mjs ssh user@gateway-host 'cd ~/.openclaw/openclaw && /opt/homebrew/bin/node /tmp/proof-77539-amittell.mjs'task: "do A: investigate widget metrics for last 24h", then exercises four scenarios against the live deployed bundle:"do B: steered instruction, switch to gauge reset"-> new run task = "do B".taskparam -> new run task preserves "do A" (back-compat).task: ""-> new run task preserves "do A" (empty-string fallback).buildResumeMessage(replicated verbatim from the deployedsubagent-orphan-recovery-VcexvSiP.jslines 51-58) -> resume message wraps "do B", not "do A".~/.openclaw/subagents/runs.jsonwas not touched:total runs: 0,proof-77539 keys: [].replaceSubagentRunAfterSteerhonors the caller-suppliedtaskparam and falls back to the prior task for the no-arg and empty-string cases. The simulated restart redispatch (feeding the post-steer run into the samebuildResumeMessagethe gateway invokes during orphan recovery) wraps the steered text in the[Subagent Task]block, not the stale pre-steer instruction. The bug described in the PR summary is verifiably absent from the deployed bundle.launchctl kickstartof the rh-bot gateway because that would interrupt unrelated production traffic.Real behavior proof addendum: completed-session reactivation (2026-06-12)
Covers the path added in
1607b5e8fe(now squashed into head50eefdf243):sessions.send/agent.runfollow-ups to a completed subagent session.Behavior addressed: when a user sends a follow-up to a subagent session whose run already ended,
sessions.send(src/gateway/server-methods/sessions.ts:913) callsreactivateCompletedSubagentSessionwith the follow-up message text. Before this head, the replacement run kept the stale original task, so a gateway restart redispatched the old prompt. After, the follow-up text is persisted as the replacement run'staskand restart redispatch rewraps it.Real environment tested: exact-head runtime bundle built locally from
50eefdf243(pnpm build, macOS 26.5.1, node v26.3.0). The proof script dynamic-imports the built chunks directly --session-subagent-reactivation-BiDNNsJe.js(exportt=reactivateCompletedSubagentSession) andsubagent-registry-D8kvjzdz.js(exportsh/s/m=registerSubagentRun/getLatestSubagentRunByChildSessionKey/markSubagentRunTerminated) -- sharing the real in-memory registry module. State isolated viaOPENCLAW_STATE_DIRredirected to a fresh tmpdir; no live gateway state touched. Same bundle-driving methodology as the accepted proof above.Exact steps or command run after this patch:
The script: (1) registers a run with task "do A: investigate widget metrics for last 24h" and drives it to ended state; (2) reactivates via the sessions.send path with follow-up "do B: follow-up, re-run the gauge reset against staging"; (3) feeds the replacement run through
buildResumeMessagereplicated verbatim fromdist/subagent-orphan-recovery-CE6Tpah5.js:51-58; (4) asserts active (non-ended) runs are never replaced; (5) asserts whitespace-only follow-up preserves the prior task.Evidence after fix:
Observed result after fix: the replacement run record persists the follow-up text as
task(scenario 2), restart-redispatch orphan recovery rewraps the follow-up rather than the stale original (scenario 3), the path refuses to touch active runs (scenario 4), and the empty/whitespace fallback contract from the original proof holds on this sibling path too (scenario 5).What was not tested: a full live-gateway restart cycle (SIGUSR1 + orphan-recovery scan) around the reactivation; the orphan-recovery wrap is proven via the verbatim-replicated
buildResumeMessagefrom the built chunk, as in the accepted base proof. A realsessions.sendRPC through a running gateway was not driven; the call-site wiring (sessions.ts:913-917, agent.ts sibling) is covered by the unit tests on this head (322 passing).Real behavior proof addendum: live gateway capture (2026-06-14)
Live capture on a real gateway process built from this PR head (
50eefdf243), driving the actualreactivateCompletedSubagentSessionover a real websocket against the qa-lab mock-openai model. This supersedes the 2026-06-12 in-process bundle addendum with a genuinely live run.Behavior addressed: When a user sends a follow-up (
sessions.send) to a subagent session whose run already ended,reactivateCompletedSubagentSession(src/gateway/session-subagent-reactivation.ts) now threads the follow-up text intoreplaceSubagentRunAfterSteerastask, so the replacement subagent run record persists the follow-up prompt. On a gateway restart, orphan-recovery redispatch (buildResumeMessagein src/agents/subagent-orphan-recovery.ts) then wraps the follow-up rather than the stale original task. Before this PR the replacement run kept the stale original task and the restart redispatched the old prompt.Real environment tested: a real OpenClaw gateway child process spawned from the built
dist/index.jsat this PR head (50eefdf243); macOS 26.5.1, node v26.3.0, sqlite3 3.51.0. The gateway serves a real loopback websocket and runs the bundled qa-lab provider modelmock-openai/gpt-5.5(no external API key, no network egress); the harness boots it throughstartQaGatewayChild. A real parentagentturn emits a realsessions_spawntool call, which spawns a real subagent run persisted in the livestate/openclaw.sqlite. State is isolated to a fresh temp dir, so no live~/.openclawdata is touched.Exact steps or command run after this patch:
Observed result after fix: Over a real websocket RPC to a live gateway,
sessions.sendof the follow-up drovereactivateCompletedSubagentSession, which persisted the follow-up text as the subagent run'staskcolumn in the livestate/openclaw.sqlite, captured while the replacement run was still in flight (ended_at=NULL). The original subagent task ("Inspect the QA workspace...") was replaced by the follow-up text, confirming the fix'staskthreading end to end. After a realSIGKILLplus cold restart of the gateway process, the persisted subagent run still carries the follow-uptaskrather than the stale original, so the restart redispatch reads the follow-up text.What was not tested: The literal
buildResumeMessagewrap string was not captured across the cold restart. In this sandbox the restarted child reconciles the in-flight run to a terminalsubagent-error(fresh ephemeral port, no parent delivery target), so the synthetic resume turn is not re-emitted to the model. The wrap behavior itself (orphan recovery readingrun.task) is covered by the accepted base proof above and by the unit regression tests on this head (src/agents/subagent-registry.steer-restart.test.ts,src/gateway/session-subagent-reactivation.test.ts).