Skip to content

[Bug]: relayable exec completion heartbeat leaks HEARTBEAT_OK as user-visible alert #78197

Description

@songshikang0111

Bug type

Behavior bug: wrong user-visible output, no crash.

Beta release blocker

No.

Summary

A heartbeat run triggered by a relayable async exec completion can deliver the raw protocol token HEARTBEAT_OK to the user. HEARTBEAT_OK is a heartbeat no-op/ack token and should be swallowed regardless of whether the heartbeat was triggered by a timer, cron event, or exec completion.

Observed on the shiki-tokyo host on 2026-05-06 (hostname, paths, config, and transcript contents redacted). Reproduced against current main source at 7f71e84248.

Steps to reproduce

  1. Enable heartbeat delivery to a user-visible target, for example target: "last" with directPolicy: "allow".
  2. Queue a relayable exec completion system event, for example Exec completed (review-run, code 0) :: review-worker spawn finished.
  3. Run heartbeat processing for the exec event and have the model/provider return exactly HEARTBEAT_OK.
  4. Observe that outbound delivery receives and sends text: "HEARTBEAT_OK".

Expected behavior

If the final normalized heartbeat response is pure HEARTBEAT_OK, gateway should treat it as a protocol no-op and skip user delivery. Exec completion reminders should still be delivered when the model returns a real summary or warning.

Actual behavior

normalizeHeartbeatReply correctly strips a pure HEARTBEAT_OK reply to shouldSkip=true, but the relayable exec completion branch restores the original reply text and clears the skip flag. That turns the protocol token into ordinary alert text.

Relevant current-source path:

  • src/agents/bash-tools.exec-runtime.ts:318 queues the exec completion event and requests heartbeat for exec-event.
  • src/infra/heartbeat-runner.ts:1040 collects exec completion events.
  • src/infra/heartbeat-runner.ts:1046 computes hasRelayableExecCompletion.
  • src/infra/heartbeat-events-filter.ts:36 treats structured exec completions with output, or failed completions, as relayable.
  • src/auto-reply/heartbeat.ts:147 strips heartbeat ack tokens.
  • src/infra/heartbeat-runner.ts:1689 intentionally prevents skip for exec completion events.
  • src/infra/heartbeat-runner.ts:1693 restores the original reply text when the normalized text is empty.
  • src/infra/heartbeat-runner.ts:1704 excludes relayable exec completions from shouldSkipMain.
  • src/infra/heartbeat-runner.ts:1861 sends normalized.text as an outbound payload.

OpenClaw version

Current-source repro: upstream/main at 7f71e84248.

Observed shiki-tokyo installed version: NOT_ENOUGH_INFO. The fresh logs show npm-global runtime paths and Node v22.22.0, but openclaw was not on the noninteractive SSH PATH for an exact --version check.

Operating system

Source-level repro: macOS development checkout.

Observed host: shiki-tokyo, Ubuntu Linux kernel 5.15.0-164-generic, x86_64.

Install method

Local repro: source checkout with pnpm.

Observed host: appears to be npm-global install from runtime log paths. Exact launch/install method: NOT_ENOUGH_INFO.

Model

Observed host logs: openai-codex/gpt-5.5.

Local deterministic repro: mocked heartbeat model boundary returning exactly HEARTBEAT_OK.

Provider / routing chain

Observed host: OpenClaw gateway -> openai-codex -> gpt-5.5, channel delivery through Feishu.

Local deterministic repro: runHeartbeatOnce with a lightweight test messaging plugin and mocked model response.

Additional provider/model setup details

Observed host heartbeat config, redacted to non-secret fields: target=last, directPolicy=allow, ackMaxChars=300, lightContext=true, skipWhenBusy=true, active hours in Asia/Shanghai.

Logs, screenshots, and evidence

Issue template/rules checked before filing: .github/ISSUE_TEMPLATE/bug_report.yml, .github/ISSUE_TEMPLATE/config.yml, and CONTRIBUTING.md.

Duplicate search was performed for HEARTBEAT_OK exec completion, notifyOnExit heartbeat, and HEARTBEAT_OK. Related issues are listed below, but this issue has a narrower deterministic current-source reproducer.

Local deterministic repro output, redacted:

event = Exec completed (review-run, code 0) :: review-worker spawn finished
model reply = HEARTBEAT_OK
result = {"status":"ran"}
outbound send = {"text":"HEARTBEAT_OK","to":"<redacted-target>"}

Targeted existing tests around adjacent behavior pass but do not cover this regression:

pnpm test src/infra/heartbeat-runner.ghost-reminder.test.ts -t "metadata-only successful exec completions|includes untrusted exec completion details"
=> 1 file passed, 2 tests passed, 18 skipped

Fresh shiki-tokyo log excerpts, redacted:

2026-05-06T09:13:29+08 Stripped stray HEARTBEAT_OK token <redacted> reply
2026-05-06T09:21:52+08 Stripped stray HEARTBEAT_OK token <redacted> reply
2026-05-06T09:21:57+08 Feishu final delivery preview referenced that HEARTBEAT_OK had been shown to the user and should have been swallowed.
2026-05-06T09:30:09+08 Stripped stray HEARTBEAT_OK token <redacted> reply

A plain heartbeat poll control case in the same log family showed assistantTexts:["HEARTBEAT_OK"] with didSendViaMessagingTool:false, which is the expected silent path. The failing path is the relayable exec completion branch above.

Impact and severity

Affected users: anyone with tools.exec.notifyOnExit enabled and heartbeat delivery allowed to a user-visible target.

Severity: medium-low but user-visible. It leaks an internal protocol token, creates confusing background notifications, and undermines the intended no-op semantics for heartbeat. The feature should still keep useful background exec completion notifications; disabling tools.exec.notifyOnExit is only a workaround.

Frequency: deterministic when a relayable exec completion is inspected and the model decides no user notification is needed by returning pure HEARTBEAT_OK.

Suggested fix

Do not restore or deliver original reply text when the normalized heartbeat response is a skip token. In particular, a pure normalized HEARTBEAT_OK should remain shouldSkip=true even when hasRelayableExecCompletion is true.

A regression test should cover: structured relayable exec completion with output + model reply HEARTBEAT_OK -> no outbound send, while the inspected exec event is still consumed/recorded according to the existing event lifecycle.

Related issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions