Skip to content

Heartbeat exec_completion event spawns phantom user-facing reply when command output is not attached #70552

@danielt69

Description

@danielt69

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/openclaw@2026.4.21):

// 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions