Bug
When a backgrounded exec session completes, the heartbeat fires correctly via maybeNotifyOnExit, but the prompt sent to the heartbeat model says "the result is shown in the system messages above" without actually injecting the exec completion payload into the heartbeat context. The model then correctly reports it cannot see any results, and delivers that confused response to the user.
Root Cause (two-part)
-
No dedup between process poll and notifyOnExit: When a cron run launches an exec with background: true, yields, and later polls the result with process poll, the manual poll does not clear notifyOnExit or cancel the pending auto-exit wake. So both the cron (via poll) and the heartbeat (via auto-exit event) consume the completion — the cron gets the real output, and the heartbeat gets a bogus prompt.
-
Heartbeat prompt uses classification only, not actual event content: The heartbeat runner checks pending system events with isExecCompletionEvent, then calls buildExecEventPrompt, which produces a generic template: "An async command you ran earlier has completed. The result is shown in the system messages above." But the actual queued exec event text is never injected into the heartbeat body, making the prompt wrong in-context.
Reproduction
- Create a cron that runs
openclaw status via exec (which yields as a background process session)
- The cron later consumes the result via
process poll
- The backgrounded exec completes and triggers
notifyOnExit
- Heartbeat fires on the cron session with the generic async-completion prompt
- The heartbeat model sees no actual command output and responds with confusion
- If
heartbeat.target is "last" and directPolicy is "allow", that confused response is delivered to the user's DM
Evidence
- Cron session
7aa43c60 launched openclaw status, yielded as process session briny-nexus, then polled the result successfully
- Heartbeat session
8e88de9f received only the generic prompt at 2026-04-14T05:00:58Z: "An async command you ran earlier has completed. The result is shown in the system messages above."
- Model responded: "I don't see system messages with results in my current context"
- A second heartbeat cycle (
d9acd5de) repeated the same pattern, with the model getting increasingly defensive across iterations
- The confused responses were delivered to the user's Telegram DM
Suggested Fix
Either:
- Option A: Have
process poll clear notifyOnExit on the polled session so heartbeat never fires for already-consumed results
- Option B: Have
buildExecEventPrompt inject the actual exec completion event content into the heartbeat context, not just a classification-based template
- Option C (ideal): Both — dedup via Option A, and fix the prompt via Option B as defense-in-depth
Workaround
Set heartbeat.target to "none" and heartbeat.directPolicy to "block" to prevent heartbeat from delivering to user-facing channels.
Environment
- OpenClaw v2026.4.12
- Heartbeat model:
claude-haiku-4.5 via GitHub Copilot
heartbeat.isolatedSession: true
- macOS (arm64)
Bug
When a backgrounded
execsession completes, the heartbeat fires correctly viamaybeNotifyOnExit, but the prompt sent to the heartbeat model says "the result is shown in the system messages above" without actually injecting the exec completion payload into the heartbeat context. The model then correctly reports it cannot see any results, and delivers that confused response to the user.Root Cause (two-part)
No dedup between
process pollandnotifyOnExit: When a cron run launches an exec withbackground: true, yields, and later polls the result withprocess poll, the manual poll does not clearnotifyOnExitor cancel the pending auto-exit wake. So both the cron (via poll) and the heartbeat (via auto-exit event) consume the completion — the cron gets the real output, and the heartbeat gets a bogus prompt.Heartbeat prompt uses classification only, not actual event content: The heartbeat runner checks pending system events with
isExecCompletionEvent, then callsbuildExecEventPrompt, which produces a generic template: "An async command you ran earlier has completed. The result is shown in the system messages above." But the actual queued exec event text is never injected into the heartbeat body, making the prompt wrong in-context.Reproduction
openclaw statusviaexec(which yields as a background process session)process pollnotifyOnExitheartbeat.targetis"last"anddirectPolicyis"allow", that confused response is delivered to the user's DMEvidence
7aa43c60launchedopenclaw status, yielded as process sessionbriny-nexus, then polled the result successfully8e88de9freceived only the generic prompt at2026-04-14T05:00:58Z: "An async command you ran earlier has completed. The result is shown in the system messages above."d9acd5de) repeated the same pattern, with the model getting increasingly defensive across iterationsSuggested Fix
Either:
process pollclearnotifyOnExiton the polled session so heartbeat never fires for already-consumed resultsbuildExecEventPromptinject the actual exec completion event content into the heartbeat context, not just a classification-based templateWorkaround
Set
heartbeat.targetto"none"andheartbeat.directPolicyto"block"to prevent heartbeat from delivering to user-facing channels.Environment
claude-haiku-4.5via GitHub Copilotheartbeat.isolatedSession: true