openclaw - 💡(How to fix) Fix ACP spawn + streamTo=parent does not wake parent session on child completion (heartbeat enabled; distinct from #52249) [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#70264Fetched 2026-04-23 07:27:00
View on GitHub
Comments
1
Participants
2
Timeline
1
Reactions
0
Participants
Timeline (top)
commented ×1

With heartbeat enabled, sessions_spawn({ runtime: "acp", streamTo: "parent" }) + sessions_yield() does not wake the parent session when the child completes. The ACP terminal event mirrors into the parent transcript, but no new parent turn runs.

Native subagent spawn (runtime: "subagent") on the same parent + same heartbeat config wakes correctly — both with and without sessions_yield. So the gap is specifically in the ACP completion → parent-wake path, not in heartbeat delivery generally.

This is adjacent to #52249 (which scopes the same symptom to heartbeat-disabled deployments) but presents as a distinct case: the wake does fire but produces a HEARTBEAT.md prompt instead of a prompt that surfaces the mirrored ACP completion event.

Root Cause

With heartbeat enabled, sessions_spawn({ runtime: "acp", streamTo: "parent" }) + sessions_yield() does not wake the parent session when the child completes. The ACP terminal event mirrors into the parent transcript, but no new parent turn runs.

Native subagent spawn (runtime: "subagent") on the same parent + same heartbeat config wakes correctly — both with and without sessions_yield. So the gap is specifically in the ACP completion → parent-wake path, not in heartbeat delivery generally.

This is adjacent to #52249 (which scopes the same symptom to heartbeat-disabled deployments) but presents as a distinct case: the wake does fire but produces a HEARTBEAT.md prompt instead of a prompt that surfaces the mirrored ACP completion event.

Fix Action

Fix / Workaround

in one patch. Still opt-in at spawn via wakeOwnerOnDone to preserve current "notify and stop" default.

Code Example

sessions_spawn({
  runtime: "acp",
  agentId: "claude",
  mode: "run",
  streamTo: "parent",
  label: "sa-wake-test-<hhmm>",
  runTimeoutSeconds: 120,
  task: 'Wake test <hhmm>. Print "WAKE-TEST-DONE-<hhmm>" on its own line. Do not run any commands, do not read files, do not spawn anything.',
});
sessions_yield({ message: "Waiting for wake..." });

---

requestHeartbeatNow(scopedHeartbeatWakeOptions(parentSessionKey, { reason: "acp:spawn:stream" }));

---

hasExecCompletion ? buildExecEventPrompt(...)
                     : hasCronEvents ? buildCronEventPrompt(...)
                                     : resolveHeartbeatPrompt(cfg, heartbeat)

---

streamTo is only supported for runtime=acp; got runtime=subagent

---

await sendMessage({, mirror: { sessionKey: ownerSessionKey,} });
+ if (latest.wakeOwnerOnDone) {
+   requestHeartbeatNow({ reason: "exec-event", sessionKey: ownerSessionKey });
+   //                  ^^^^^^^^^^^^^^^^^^^^^
+   //                  reuse existing ACTION-priority reason so
+   //                  preflight builds an event-carrying prompt
+ }
RAW_BUFFERClick to expand / collapse

Summary

With heartbeat enabled, sessions_spawn({ runtime: "acp", streamTo: "parent" }) + sessions_yield() does not wake the parent session when the child completes. The ACP terminal event mirrors into the parent transcript, but no new parent turn runs.

Native subagent spawn (runtime: "subagent") on the same parent + same heartbeat config wakes correctly — both with and without sessions_yield. So the gap is specifically in the ACP completion → parent-wake path, not in heartbeat delivery generally.

This is adjacent to #52249 (which scopes the same symptom to heartbeat-disabled deployments) but presents as a distinct case: the wake does fire but produces a HEARTBEAT.md prompt instead of a prompt that surfaces the mirrored ACP completion event.

Environment

  • OpenClaw 2026.4.15
  • macOS 15.7.3 / Mac Mini M4
  • ACP backend: acpx (embedded)
  • Child agent: claude (Claude Code)
  • Parent heartbeat: every: 1h, enabled (explicitly overrode isolatedSession: false on parent agent)

Reproduction

Three independent ACP repros and two native-subagent controls run on one system-architect parent session today:

#runtimestreamTosessions_yieldheartbeatisolatedSessionchild modelparent woke?
1acpparentyesevery: 1htrue (default)claudeno
2acpparentyesevery: 1hfalse (override)claudeno
3acpparentyesevery: 1hfalseclaudeno
4subagent— (rejected)yessamesamedefaultyes
5subagentomittednosamesamezai/glm-5.1yes

ACP pattern (repeated 3×):

sessions_spawn({
  runtime: "acp",
  agentId: "claude",
  mode: "run",
  streamTo: "parent",
  label: "sa-wake-test-<hhmm>",
  runTimeoutSeconds: 120,
  task: 'Wake test <hhmm>. Print "WAKE-TEST-DONE-<hhmm>" on its own line. Do not run any commands, do not read files, do not spawn anything.',
});
sessions_yield({ message: "Waiting for wake..." });

Observed (each of 3 runs):

  • Child starts within seconds (Started claude session agent:claude:acp:…).
  • Child prints WAKE-TEST-DONE-<hhmm> ~30s later.
  • claude run completed. fires, delivery-mirror writes all three lines into the parent transcript.
  • Parent session does not run a new turn. I had to send a manual message 10–20 minutes later in each run.

Native-subagent control (#5), using the exact same parent + heartbeat config, woke in ~7 seconds without a yield.

Root-cause trace (current dist/, equivalent to current main)

  1. ACP child end-of-run calls:

    requestHeartbeatNow(scopedHeartbeatWakeOptions(parentSessionKey, { reason: "acp:spawn:stream" }));

    — [acp-spawn-u8L_0p9o.js:175]

  2. resolveHeartbeatReasonKind maps acp:spawn:*"wake" — [heartbeat-wake-a3GAt85y.js:18].

  3. "wake" is not an isHeartbeatActionWakeReason — only manual, exec-event, hook are — [heartbeat-wake-a3GAt85y.js:22-25]. So acp:spawn:stream gets DEFAULT priority rather than ACTION.

  4. In resolveHeartbeatPreflight ([heartbeat-runner-CInrztfM.js:452+]):

    • isWakeReason = true
    • isolatedSession = false so shouldInspectWakePendingEvents = true
    • shouldBypassFileGates = true
    • Preflight returns OK.
  5. But resolveHeartbeatRunPrompt ([heartbeat-runner-CInrztfM.js:508+]) then returns:

    hasExecCompletion ? buildExecEventPrompt(...)
                      : hasCronEvents ? buildCronEventPrompt(...)
                                      : resolveHeartbeatPrompt(cfg, heartbeat)

    Neither hasExecCompletion (only true for isExecCompletionEvent) nor hasCronEvents matches the queued ACP terminal system event, so the parent runs a generic HEARTBEAT.md turn that replies HEARTBEAT_OK and ignores the ACP event. The mirrored completion message never surfaces into a prompt the agent is expected to act on.

  6. Separately, maybeDeliverTaskTerminalUpdate in [task-registry-BO2EMqj7.js:1780-1866] sends the "Background task done: …" message via sendMessage({ … mirror: { sessionKey: ownerSessionKey, … } }) and never calls requestHeartbeatNow on the mirror branch. So the user sees the message on channel, the transcript gets the mirror, but the agent is never woken by this path either.

Compare native subagent (deliverSubagentAnnouncement in subagent-registry-BrNWizSY.js): it either queues an inter-session message handled by sessions_send-style processing, or calls the agent gateway method directly — both paths wake a real parent turn, independent of heartbeat infrastructure. That's why control #4 and #5 above work on the exact same heartbeat config.

Two orthogonal gaps

Gap 1 — streamTo: "parent" is misleading on runtime=acp

On runtime=subagent, sessions_spawn rejects streamTo: "parent" with:

streamTo is only supported for runtime=acp; got runtime=subagent

On ACP, streamTo: "parent" only guarantees progress/terminal events mirror into the parent transcript. It does not guarantee the parent session runs a new turn on child completion. Users reasonably read streamTo: "parent" as "parent sees it and handles it" — the current semantics only cover half of that.

Gap 2 — ACP terminal wake is a HEARTBEAT turn, not an ACP-completion turn

Even when the heartbeat infrastructure is available and unblocked, the wake produces a generic HEARTBEAT.md prompt rather than a prompt that contains the queued ACP completion event. The orchestrator agent runs, sees no actionable context, replies HEARTBEAT_OK, and does not advance the orchestration.

Proposed fix (three options)

Option A (minimal, opt-in flag)

In maybeDeliverTaskTerminalUpdate, after the mirror-path sendMessage(...):

await sendMessage({, mirror: { sessionKey: ownerSessionKey,} });
+ if (latest.wakeOwnerOnDone) {
+   requestHeartbeatNow({ reason: "exec-event", sessionKey: ownerSessionKey });
+   //                  ^^^^^^^^^^^^^^^^^^^^^
+   //                  reuse existing ACTION-priority reason so
+   //                  preflight builds an event-carrying prompt
+ }

Add wakeOwnerOnDone?: boolean (default false, preserving today's "notify user and stop" semantics for fire-and-forget tasks) to:

  • sessions_spawn tool schema
  • createTaskRecord / task-registry schema
  • Docs: docs/reference/sessions-spawn.md, skills/taskflow/SKILL.md

Orchestrator sessions that want to continue iterating opt in.

Option B (fix the prompt routing)

Promote acp:spawn:stream to an ACTION-priority reason in isHeartbeatActionWakeReason, and extend resolveHeartbeatRunPrompt to detect queued ACP terminal events (not just exec completions / cron events) and build an event-carrying prompt for them. Keep wakeOwnerOnDone as opt-in to avoid churning idle-bound sessions.

Option C (route ACP through native subagent announce, best long-term)

Route ACP terminal delivery through deliverSubagentAnnouncement (or its direct-send path), the same mechanism native subagent uses. This decouples ACP parent-wake from heartbeat entirely, giving both:

  • a fix for this bug (heartbeat-enabled, prompt-mismatch case), and
  • a fix for #52249 (heartbeat-disabled, no-handler case)

in one patch. Still opt-in at spawn via wakeOwnerOnDone to preserve current "notify and stop" default.

Rename / doc clarification (any option)

Either rename streamTo: "parent" to something less implication-heavy (e.g. relayTo: "parent"), or clarify in tool description and skills/taskflow/SKILL.md:

streamTo: "parent" only relays progress/terminal text into the parent transcript. It does not cause the parent session to run a new turn on child completion. To continue orchestration after a child completes, pair streamTo: "parent" with wakeOwnerOnDone: true, or use mode: "run" (blocking).

Test plan

  1. runtime=acp, streamTo=parent, wakeOwnerOnDone=true — parent runs a new turn on child completion, with the completion event in the turn's input (not a generic HEARTBEAT prompt). ✅
  2. runtime=acp, streamTo=parent, wakeOwnerOnDone=false (or omitted) — mirror still happens, parent does not auto-turn. Matches today. ✅
  3. runtime=acp with heartbeat disabled — works if Option C is chosen; still fails (by design of opt-in + heartbeat coupling) if Options A/B alone.
  4. runtime=subagent — unchanged.

Relationship to #52249

Distinct but adjacent:

  • #52249: ACP parent does not wake when heartbeat is disabled. Maintainer's active seam: heartbeat-wake handler is not registered, so the queued wake is a no-op.
  • This issue: ACP parent does not produce a useful turn even when heartbeat is enabled. Preflight/wake fires, but the prompt routing doesn't surface the ACP completion event.

Option C resolves both. Options A/B resolve only this issue.

Evidence artifacts available

Full session transcripts, heartbeat config, and spawn/yield call sequences for all 5 repros can be provided if useful for writing a regression test.

extent analysis

TL;DR

The most likely fix for the issue where the parent session does not wake up when an ACP child completes, despite heartbeat being enabled, involves modifying the maybeDeliverTaskTerminalUpdate function to request a heartbeat with an "exec-event" reason when the wakeOwnerOnDone flag is set.

Guidance

  • Identify the root cause of the issue, which is the mismatch between the expected and actual behavior of streamTo: "parent" with ACP runtime.
  • Consider implementing one of the proposed fix options: A (minimal, opt-in flag), B (fix the prompt routing), or C (route ACP through native subagent announce).
  • Verify the fix by testing the scenarios outlined in the test plan, including runtime=acp with wakeOwnerOnDone=true and wakeOwnerOnDone=false (or omitted).
  • Review the code changes to ensure that the wakeOwnerOnDone flag is properly handled and that the prompt routing is corrected to surface the ACP completion event.

Example

// maybeDeliverTaskTerminalUpdate function
await sendMessage({, mirror: { sessionKey: ownerSessionKey,} });
if (latest.wakeOwnerOnDone) {
  requestHeartbeatNow({ reason: "exec-event", sessionKey: ownerSessionKey });
}

Notes

The fix may require additional changes to the sessions_spawn tool schema, createTaskRecord/task-registry schema, and documentation to support the wakeOwnerOnDone flag.

Recommendation

Apply workaround Option A (minimal, opt-in flag) to introduce the wakeOwnerOnDone flag and request a heartbeat with an "exec-event" reason when set, as it provides a straightforward solution to the issue while preserving the current "notify and stop" default behavior.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix ACP spawn + streamTo=parent does not wake parent session on child completion (heartbeat enabled; distinct from #52249) [1 comments, 2 participants]