openclaw - ✅(Solved) Fix Heartbeat exec_completion event spawns phantom user-facing reply when command output is not attached [1 pull requests, 1 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#70552Fetched 2026-04-24 05:56:38
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×1

The heartbeat exec_completion handler can spawn a user-facing agent turn with no command output attached, producing phantom replies like "I don't see any system messages above with command output to relay..." delivered to end users on Telegram/Discord/etc.

Root Cause

  1. Agent runs an exec with background: true that completes quickly (e.g. brew install <pkg>, openclaw doctor, a shell script).
  2. The originating agent turn ends normally.
  3. When the detached process exits, the runtime posts a system event (exec completed (session-id, code 0)) to the timeline.
  4. heartbeat-runner detects via isExecCompletionEvent() and spawns an isolated agent turn using buildExecEventPrompt({ deliverToUser: canRelayToUser }).
  5. In the isolated session, the actual stdout is not attached — the "system messages above" referenced in the prompt do not exist.
  6. The agent correctly notes "I don't see any system messages above..." and, because canRelayToUser=true (Telegram channel configured with visibility.showAlerts=true), the reply is delivered to the end user.

Fix Action

Fix / Workaround

Workaround users can apply today

PR fix notes

PR #70598: fix: include exec event details in heartbeat relays

Description (problem / solution / changelog)

Summary

  • Problem: heartbeat exec-completion runs told the model that results were "shown in the system messages above" even when the actual user-visible context only contained the synthesized prompt.
  • Why it matters: the model can respond with phantom apologies like "I don't see any system messages above..." and relay that to users instead of describing the real exec completion event.
  • What changed: exec heartbeat prompts now embed the actual pending exec event text directly, and regression tests verify both prompt rendering and runner wiring.
  • What did NOT change (scope boundary): this does not change heartbeat delivery policy, event classification, or exec completion queueing.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #70552
  • Related #
  • This PR fixes a bug or regression

Root Cause / Regression History (if applicable)

  • Root cause: buildExecEventPrompt() referenced "system messages above" instead of embedding the actual exec completion event text, unlike the cron reminder prompt path.
  • Missing detection / guardrail: tests covered exec event classification, but not that the prompt body included the event payload itself.
  • Prior context (git blame, prior PR, issue, or refactor if known): issue #70552 documents the phantom relay symptom; I did not trace a specific introducing PR.
  • Why this regressed now: this looks like a prompt-shape gap rather than a recent runtime regression.
  • If unknown, what was ruled out: ruled out a delivery adapter bug; the missing event detail was already absent before outbound delivery.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • src/infra/heartbeat-events-filter.test.ts
    • src/infra/heartbeat-runner.ghost-reminder.test.ts
  • Scenario the test should lock in: exec-event prompts include the actual event text, and the heartbeat runner passes that text through to the isolated exec-event context.
  • Why this is the smallest reliable guardrail: the bug sits at the prompt-building seam plus runner wiring, so one prompt test and one runner test cover the failure path without a full channel integration.
  • Existing test that already covers this (if any): none.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

Heartbeat exec-completion follow-ups now describe the actual exec completion event text instead of asking the model to infer missing "system messages above" context.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22+, pnpm workspace
  • Model/provider: N/A
  • Integration/channel (if any): heartbeat exec-event path
  • Relevant config (redacted): heartbeat wake / exec completion

Steps

  1. Queue an exec completion system event.
  2. Run heartbeat on the exec-event path.
  3. Inspect the generated prompt body passed to the heartbeat reply flow.

Expected

  • The prompt includes the actual exec completion event details.

Actual

  • Before this fix, the prompt only referenced "system messages above," which could be absent from the model-visible context.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios:
    • Ran pnpm test -- src/infra/heartbeat-events-filter.test.ts src/infra/heartbeat-runner.ghost-reminder.test.ts
    • Verified exec prompts now include event text for both user-relay and internal-only paths
    • Verified heartbeat runner wires the exec event text into the prompt body
  • Edge cases checked:
    • empty exec event content falls back to HEARTBEAT_OK
    • internal-only path still avoids user-relay wording
  • What you did not verify:
    • a live end-to-end channel repro on Telegram/Discord
    • broader heartbeat alert policy changes outside exec-event prompts

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps:

Risks and Mitigations

  • Risk: exec heartbeat prompts are now slightly more verbose because they include the event body directly.
    • Mitigation: only exec-event prompts change, and the added text is the already-queued event content the model needed to answer correctly.

Changed files

  • src/infra/heartbeat-events-filter.test.ts (modified, +31/-5)
  • src/infra/heartbeat-events-filter.ts (modified, +23/-3)
  • src/infra/heartbeat-runner.ghost-reminder.test.ts (modified, +51/-1)
  • src/infra/heartbeat-runner.ts (modified, +25/-1)

Code Example

// dist/heartbeat-runner-DV6vG6Se.js
function isExecCompletionEvent(evt) {
  return /^exec finished(?::|\s*\()/.test(evt) || /^exec (completed|failed) \(.../.test(evt);
}

function buildExecEventPrompt(opts) {
  if (!(opts?.deliverToUser ?? true)) return "...Handle the result internally. Do not relay it to the user...";
  return "An async command you ran earlier has completed. The result is shown in the system messages above. Please relay the command output to the user in a helpful way. If the command succeeded, share the relevant output. If it failed, explain what went wrong.";
}

// later
const shouldSkipMain = normalized.shouldSkip && !normalized.hasMedia && !hasExecCompletion;
//                                                                   ^^^^^^^^^^^^^^^^^^^^^^^
// exec completion forces a reply even when the agent returns empty.
RAW_BUFFERClick to expand / collapse

Summary

The heartbeat exec_completion handler can spawn a user-facing agent turn with no command output attached, producing phantom replies like "I don't see any system messages above with command output to relay..." delivered to end users on Telegram/Discord/etc.

Environment

  • OpenClaw 2026.4.21
  • Channel: Telegram (direct chat)
  • Trigger: any exec call with background: true whose process later exits

Steps to reproduce

  1. Agent runs an exec with background: true that completes quickly (e.g. brew install <pkg>, openclaw doctor, a shell script).
  2. The originating agent turn ends normally.
  3. When the detached process exits, the runtime posts a system event (exec completed (session-id, code 0)) to the timeline.
  4. heartbeat-runner detects via isExecCompletionEvent() and spawns an isolated agent turn using buildExecEventPrompt({ deliverToUser: canRelayToUser }).
  5. In the isolated session, the actual stdout is not attached — the "system messages above" referenced in the prompt do not exist.
  6. The agent correctly notes "I don't see any system messages above..." and, because canRelayToUser=true (Telegram channel configured with visibility.showAlerts=true), the reply is delivered to the end user.

Reference in built code (paths relative to @openclaw/[email protected]):

// dist/heartbeat-runner-DV6vG6Se.js
function isExecCompletionEvent(evt) {
  return /^exec finished(?::|\s*\()/.test(evt) || /^exec (completed|failed) \(.../.test(evt);
}

function buildExecEventPrompt(opts) {
  if (!(opts?.deliverToUser ?? true)) return "...Handle the result internally. Do not relay it to the user...";
  return "An async command you ran earlier has completed. The result is shown in the system messages above. Please relay the command output to the user in a helpful way. If the command succeeded, share the relevant output. If it failed, explain what went wrong.";
}

// later
const shouldSkipMain = normalized.shouldSkip && !normalized.hasMedia && !hasExecCompletion;
//                                                                   ^^^^^^^^^^^^^^^^^^^^^^^
// exec completion forces a reply even when the agent returns empty.

Expected

If the completion event has no attached stdout/stderr, the runtime should either:

  1. Attach the actual command output before spawning the relay turn, or
  2. Skip the user-facing turn entirely (no delivery, no phantom reply).

Actual

A fresh isolated agent session is spawned with only the relay prompt (no output), the agent apologises for missing output, and hasExecCompletion forces the reply to be delivered to the end user. The user sees an out-of-context apology that has no relation to anything they asked.

Suggested fix

  • Before spawning the relay turn, verify the exec result payload is non-empty. If empty, emit the completion to internal logs only and skip the agent turn.
  • Alternatively, add a config knob: agents.defaults.heartbeat.suppressExecCompletionRelay: true (or channels.*.heartbeat.suppressExecCompletionRelay) that short-circuits the relay path without affecting genuine heartbeat alerts (unlike the existing showAlerts: false, which is too coarse).
  • Consider removing the !hasExecCompletion forced-override in the shouldSkipMain calculation so that agent-side NO_REPLY works as the fallback.

Workaround users can apply today

  1. Agent-side: avoid exec with background: true for short-running commands. Use synchronous exec with adequate yieldMs so the process completes in-turn.
  2. Runtime-side: channels.<name>.heartbeat.showAlerts: false will switch the prompt variant to "handle internally" — but it also suppresses legitimate heartbeat alerts, so it is too blunt for most users.

Evidence

In my deployment, three orphan sessions were generated today with only the relay prompt and no attached output — timestamps correlate exactly with backgrounded exec calls (brew install, gateway-restart, doctor checks). Each produced the phantom apology and delivered it to the user's Telegram chat.

extent analysis

TL;DR

Verify the exec result payload is non-empty before spawning the relay turn to prevent phantom replies.

Guidance

  • Before spawning the relay turn, check if the exec result payload is empty. If it is, consider logging the completion internally and skipping the agent turn.
  • Review the shouldSkipMain calculation to ensure that agent-side NO_REPLY works as the fallback when hasExecCompletion is true.
  • Consider adding a config knob, such as agents.defaults.heartbeat.suppressExecCompletionRelay, to short-circuit the relay path without affecting genuine heartbeat alerts.
  • As a temporary workaround, users can avoid using exec with background: true for short-running commands or set channels.<name>.heartbeat.showAlerts: false to suppress the relay prompt.

Example

No code snippet is provided as the issue is more related to the logic and configuration of the system rather than a specific code error.

Notes

The suggested fix requires careful consideration of the system's behavior and potential impact on legitimate heartbeat alerts. The workaround may not be suitable for all users, and a more targeted solution may be necessary.

Recommendation

Apply the suggested fix by verifying the exec result payload before spawning the relay turn, as it directly addresses the root cause of the issue and prevents phantom replies.

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 - ✅(Solved) Fix Heartbeat exec_completion event spawns phantom user-facing reply when command output is not attached [1 pull requests, 1 participants]