Skip to content

fix(heartbeat): suppress fallback after route-matched message tool#84975

Open
luoyanglang wants to merge 1 commit into
openclaw:mainfrom
luoyanglang:wolf/heartbeat-suppress-after-message-tool
Open

fix(heartbeat): suppress fallback after route-matched message tool#84975
luoyanglang wants to merge 1 commit into
openclaw:mainfrom
luoyanglang:wolf/heartbeat-suppress-after-message-tool

Conversation

@luoyanglang

@luoyanglang luoyanglang commented May 21, 2026

Copy link
Copy Markdown
Contributor

When heartbeat runs in message-tool-only mode, a successful generic message tool delivery can still be followed by duplicate free-text fallback narration in the same Telegram chat.

Fixes #84217

Affected surface

  • src/auto-reply/reply/agent-runner.ts
    • Adds route-matched message tool delivery evidence for heartbeat replies only after the generic message tool sent to the same provider/target/account route.
  • src/auto-reply/reply-payload.ts
    • Adds internal reply-payload metadata for route-scoped message tool delivery evidence.
  • src/auto-reply/heartbeat-reply-payload.ts
    • Adds a heartbeat-specific evidence reader.
  • src/infra/heartbeat-runner.ts
    • Suppresses free-text fallback only when the reply carries route-scoped message tool delivery evidence.
  • src/auto-reply/reply/agent-runner-payloads.ts
    • Leaves heartbeat payloads available for heartbeat-specific route evidence instead of dropping them via generic message-tool dedupe first.
  • Tests:
    • src/infra/heartbeat-runner.tool-response.test.ts
    • src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts

Scope

This is a parameter/evidence threading fix. It does not add a config flag, does not change heartbeat scheduling, and does not touch Slack identity, commitment, or channel-specific private paths.

The important semantic boundary is that "generic message tool delivery evidence" is not the same as "has outbound reply content". This PR only marks heartbeat replies when existing message-tool target evidence matches the same delivery route via the existing resolveMessagingToolPayloadDedupe route matcher. Global messagingToolSentTexts without route targets is intentionally not enough.

After Vincent's thread-route follow-up, I found one extra current-head issue while refreshing proof: generic reply-payload dedupe could drop the matched heartbeat payload before heartbeat-runner saw the route evidence. The current head keeps heartbeat payloads out of that generic dedupe layer so the heartbeat-specific suppression path receives the evidence and can still preserve no-route / wrong-topic fallback behavior.

Prior art

Prior attempt #84273 was closed by contributor due to inability to provide live channel proof. This PR provides expanded source + runtime helper proof, with the helper semantic explicitly distinguishing "generic message tool delivery evidence" from "has outbound reply content" to address the message-delivery risk flagged in #84273.

The key difference from #84273 is route scope: #84273 marked heartbeat payloads after any message-tool send, while this PR only marks a heartbeat payload when committed messagingToolSentTargets match the reply route. If the message tool delivered to another route, or only global text evidence exists, heartbeat fallback still sends.

Current-head P1 follow-up proof

This update addresses the ClawSweeper P1 finding "Suppress same-route fallback even when the text differs" on head 25958135ed2135dede29e716668ac34cfaea8007.

The same-route heartbeat evidence rule now treats committed route-matched generic message tool target evidence as sufficient for a single plain heartbeat fallback payload, even when the fallback text differs from the tool body. Multi-payload heartbeat replies keep the existing text-match guard so unrelated fallback payloads are not suppressed. Rich payloads, media payloads, reasoning, no-route evidence, wrong-route evidence, and wrong-thread/topic evidence remain outside this suppression path.

Commands run locally:

$ pnpm test src/auto-reply/reply/agent-runner-payloads.test.ts -t "same-route message-tool delivery used different text"
RED before implementation:
AssertionError: expected undefined to be true

$ pnpm test src/auto-reply/reply/agent-runner-payloads.test.ts -t "same-route message-tool delivery used different text"
Test Files  1 passed (1)
Tests  1 passed | 46 skipped (47)

$ pnpm test src/auto-reply/reply/agent-runner-payloads.test.ts src/infra/heartbeat-runner.tool-response.test.ts
Test Files  1 passed (1)
Tests  17 passed (17)
Test Files  1 passed (1)
Tests  47 passed (47)

$ pnpm test src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts -t "heartbeat replies"
Test Files  1 passed (1)
Tests  9 passed | 55 skipped (64)

$ pnpm check:changed --base origin/main
exit 0

$ git diff --check origin/main..HEAD
exit 0

$ gitleaks protect --staged --redact
INF no leaks found

Updated live Telegram proof for head 25958135ed2135dede29e716668ac34cfaea8007

The earlier Mantis Telegram workflow could not run because this actor lacks write/maintain/admin permission, so I reran the current-head proof locally with the existing Telegram proof credential and a temporary harness outside the patch. The harness used the real current-head route-evidence path:

  • buildReplyPayloads(...) built heartbeat payloads from route-scoped messagingToolSentTargets.
  • runHeartbeatOnce(...) made the heartbeat delivery/suppression decision.
  • Telegram delivery used the real Bot API sendMessage path.
  • Route, chat, topic, token, local paths, and timestamps are redacted.

Visible Telegram screenshot: #84975 (comment)

{
  "proof": "openclaw-pr-84975-heartbeat-live-proof",
  "head": "25958135ed2135dede29e716668ac34cfaea8007",
  "mode": "live-telegram",
  "route": {
    "provider": "telegram",
    "to": "telegram:group:<redacted-chat>",
    "thread": "topic:<redacted-thread>"
  },
  "scenarios": [
    {
      "scenario": "same-route-different-text",
      "expected": "suppress",
      "result": "PASS",
      "decision": "suppress",
      "evidence": {
        "mode": "same-route",
        "targetCount": 1,
        "metadataDelivered": true,
        "targetRoute": "telegram:group:<redacted-chat>"
      },
      "transport": {
        "messageToolSendCount": 1,
        "heartbeatFallbackSendCount": 0,
        "sends": [
          { "phase": "message-tool", "method": "sendMessage", "status": "ok", "message_id": "15" }
        ]
      }
    },
    {
      "scenario": "same-route-same-text",
      "expected": "suppress",
      "result": "PASS",
      "decision": "suppress",
      "evidence": {
        "mode": "same-route",
        "targetCount": 1,
        "metadataDelivered": true,
        "targetRoute": "telegram:group:<redacted-chat>"
      },
      "transport": {
        "messageToolSendCount": 1,
        "heartbeatFallbackSendCount": 0,
        "sends": [
          { "phase": "message-tool", "method": "sendMessage", "status": "ok", "message_id": "16" }
        ]
      }
    },
    {
      "scenario": "no-route",
      "expected": "send",
      "result": "PASS",
      "decision": "send",
      "evidence": {
        "mode": "none",
        "targetCount": 0,
        "metadataDelivered": false,
        "targetRoute": "none"
      },
      "transport": {
        "messageToolSendCount": 0,
        "heartbeatFallbackSendCount": 1,
        "sends": [
          { "phase": "heartbeat-fallback", "method": "sendMessage", "status": "ok", "message_id": "17" }
        ]
      }
    },
    {
      "scenario": "wrong-thread",
      "expected": "send",
      "result": "PASS",
      "decision": "send",
      "evidence": {
        "mode": "wrong-thread",
        "targetCount": 1,
        "metadataDelivered": false,
        "targetRoute": "telegram:group:<redacted-chat> topic:<wrong-thread-redacted>"
      },
      "transport": {
        "messageToolSendCount": 0,
        "heartbeatFallbackSendCount": 1,
        "sends": [
          { "phase": "heartbeat-fallback", "method": "sendMessage", "status": "ok", "message_id": "18" }
        ]
      }
    }
  ],
  "summary": { "pass": 4, "fail": 0 }
}

Observed visible Telegram messages match the decisions above: same-route different-text only shows the message-tool visible body, same-route same-text only shows one visible body, and the no-route / wrong-thread cases show heartbeat fallback messages.

Current-head maintenance proof

After Vincent's route/thread follow-up moved this PR to f21343bfd4, CI exposed one current-head regression: checks-node-auto-reply-reply-agent-runner timed out in src/auto-reply/reply/agent-runner-payloads.test.ts at dedupes duplicate same-target reply text without suppressing unrelated finals.

Root cause: route dedupe called getChannelPlugin("telegram") from the generic reply-payload path, which lazily loads the bundled Telegram plugin in this test/runtime path and can block the shard. Current head df6f33925b keeps the route/thread semantics from Vincent's follow-up, uses only the already-loaded channel registry for plugin-specific matching, and preserves internal route-string fallback for forms like telegram:group:<chat>:topic:<thread> without loading bundled channel implementations.

Current-head commands run locally:

$ pnpm test src/auto-reply/reply/agent-runner-payloads.test.ts -t "dedupes duplicate same-target reply text without suppressing unrelated finals"
Test Files  1 passed (1)
Tests  1 passed | 46 skipped (47)
[test] passed 1 Vitest shard in 26.25s

$ pnpm test src/auto-reply/reply/agent-runner-payloads.test.ts src/auto-reply/reply/reply-payloads.test.ts src/infra/heartbeat-runner.tool-response.test.ts
Test Files  1 passed (1)
Tests  14 passed (14)
Test Files  2 passed (2)
Tests  77 passed (77)
[test] passed 2 Vitest shards in 41.90s

$ pnpm test src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts -t "heartbeat replies"
Test Files  1 passed (1)
Tests  9 passed | 55 skipped (64)
[test] passed 1 Vitest shard in 36.48s

$ pnpm check:changed --base origin/main
[check:changed] lanes=all
[check:changed] typecheck all
[check:changed] lint
[check:changed] runtime import cycles
Import cycle check: 0 runtime value cycle(s).
exit 0

$ git diff --check origin/main..HEAD
exit 0

$ gitleaks protect --staged --redact
INF 0 commits scanned.
INF no leaks found

Note: I did not rerun the earlier live Telegram helper on this current head. The current-head proof above is local source/runtime test coverage plus the prior PR body runtime helper proof retained below for the original route-suppression behavior.

Real behavior proof

  • Behavior or issue addressed: heartbeat message-tool-only delivery should not send duplicate free-text fallback after the generic message tool has already delivered to the same route, but it must still send fallback when no same-route delivery evidence exists.
  • Real environment tested: local OpenClaw checkout on Linux, Node 22.22.2, pnpm 11.1.0, branch wolf/heartbeat-suppress-after-message-tool, head 14b8854108.
  • Exact steps or command run after this patch: ran a local OpenClaw runtime helper that imports production runHeartbeatOnce, seeds a real openclaw.json-shaped config and session store, registers a local Telegram outbound adapter, then executes three heartbeat ticks: same-route evidence, no-route evidence, and wrong-thread/topic-unmarked evidence.
  • Evidence after fix: terminal output from the runtime helper:
$ OPENCLAW_PROOF_HEAD=14b8854108 pnpm exec tsx /tmp/openclaw-pr84975-proof/runtime-proof.mts
[runtime-helper-proof] repoHead=14b8854108
{
  "head": "14b8854108",
  "scenario": "same-route-message-tool-evidence-suppresses-fallback",
  "resultStatus": "ran",
  "routeEvidence": true,
  "telegramSendCount": 0,
  "eventStatus": "sent",
  "eventChannel": "telegram",
  "eventPreview": "Fallback narration that would duplicate same-route message-tool delivery."
}
{
  "head": "14b8854108",
  "scenario": "no-route-evidence-keeps-fallback-visible",
  "resultStatus": "ran",
  "routeEvidence": false,
  "telegramSendCount": 1,
  "sentTo": "<redacted-telegram-target>",
  "sentTextPreview": "Fallback narration still needs delivery without same-route evidence.",
  "eventStatus": "sent",
  "eventChannel": "telegram",
  "eventPreview": "Fallback narration still needs delivery without same-route evidence."
}
{
  "head": "14b8854108",
  "scenario": "wrong-thread-or-topic-unmarked-evidence-keeps-fallback-visible",
  "resultStatus": "ran",
  "routeEvidence": false,
  "telegramSendCount": 1,
  "sentTo": "<redacted-telegram-target>",
  "sentTextPreview": "Fallback narration still needs delivery for a different topic/thread.",
  "eventStatus": "sent",
  "eventChannel": "telegram",
  "eventPreview": "Fallback narration still needs delivery for a different topic/thread."
}
  • Observed result after fix: with route-scoped message-tool evidence, runHeartbeatOnce completes and does not invoke the Telegram send path (telegramSendCount: 0), so the fallback narration is suppressed. Without route evidence, and for the wrong-topic/wrong-thread-unmarked fallback case, the same runtime path invokes the Telegram send path once (telegramSendCount: 1), proving fallback remains available when the message tool did not prove same-route delivery.
  • What was not tested: I did not run a live Telegram heartbeat tick with a real bot token/chat. I did not test Slack/WhatsApp live channels; the patch is heartbeat reply evidence and generic runner fallback suppression, with Telegram-shaped runtime coverage for the reported duplicate free-text path.

Supplemental validation

RED before fix

The new regression was first run before the implementation. runHeartbeatOnce still sent fallback text even when the reply carried route-scoped message-tool delivery evidence:

$ pnpm test src/infra/heartbeat-runner.tool-response.test.ts src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts -t "message-tool delivery"
FAIL src/infra/heartbeat-runner.tool-response.test.ts > suppresses fallback text only after route-scoped message-tool delivery evidence
expected "spy" to not be called at all, but actually been called 1 times
received:
  ["-1001234567890", "Fallback narration that should not duplicate the message tool.", { verbose: false, cfg: [Object], accountId: undefined }]

Current-head GREEN after fix

$ pnpm test src/infra/heartbeat-runner.tool-response.test.ts
[test] starting test/vitest/vitest.infra.config.ts
Test Files  1 passed (1)
Tests  14 passed (14)
[test] passed 1 Vitest shard in 56.86s

$ pnpm test src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts -t "heartbeat replies"
[test] starting test/vitest/vitest.e2e.config.ts
Test Files  1 passed (1)
Tests  9 passed | 53 skipped (62)
[test] passed 1 Vitest shard in 37.25s

$ pnpm test src/auto-reply/reply/agent-runner-payloads.test.ts -t "delivers distinct same-target replies when messageProvider is synthetic but originatingChannel is set"
[test] starting test/vitest/vitest.auto-reply.config.ts
Test Files  1 passed (1)
Tests  1 passed | 37 skipped (38)
[test] passed 1 Vitest shard in 25.37s

$ node scripts/run-oxlint-shards.mjs --threads=8
[oxlint:core] finished
[oxlint:extensions] finished

$ git diff --check
exit 0

$ gitleaks protect --staged --redact
INF 1 commits scanned.
INF scan completed in 76ms
INF no leaks found

Previous changed gate

$ pnpm check:changed --base origin/main
[check:changed] lanes=core, coreTests
[check:changed] src/auto-reply/heartbeat-reply-payload.ts: core production
[check:changed] src/auto-reply/reply-payload.ts: core production
[check:changed] src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts: core test
[check:changed] src/auto-reply/reply/agent-runner.ts: core production
[check:changed] src/auto-reply/types.ts: core production
[check:changed] src/infra/heartbeat-runner.tool-response.test.ts: core test
[check:changed] src/infra/heartbeat-runner.ts: core production
[check:changed] conflict markers
[check:changed] changelog attributions
[check:changed] guarded extension wildcard re-exports
[check:changed] plugin-sdk wildcard re-exports
[check:changed] duplicate scan target coverage
[check:changed] dependency pin guard
[check:changed] package patch guard
[check:changed] typecheck core
[check:changed] typecheck core tests
[check:changed] lint core
[check:changed] media download helper guard
[check:changed] runtime sidecar loader guard
runtime-sidecar-loaders: local runtime sidecar loaders look OK.
[check:changed] runtime import cycles
Import cycle check: 0 runtime value cycle(s).
[check:changed] webhook body guard
[check:changed] pairing store guard
[check:changed] pairing account guard
exit 0

$ git diff --check origin/main..HEAD
exit 0

$ gitleaks protect --staged --redact
INF 1 commits scanned.
INF scan completed in 79.1ms
INF no leaks found

Coverage note

Push-before searches were rerun against open PRs:

gh search prs --repo openclaw/openclaw --state open "Fixes #84217" --json number,title,url,state,body --limit 10
[]

gh search prs --repo openclaw/openclaw --state open "#84217" --json number,title,url,state,body --limit 10
[]

gh search prs --repo openclaw/openclaw --state open "heartbeat-runner.ts" --json number,title,url,state,body --limit 10
#79869 fix(gateway): deliver targeted exec-event wakes when heartbeat.every is disabled — exec-event wake gating, not message-tool fallback.
#75469 fix: same-session inferred commitments can be starved indefinitely... — commitment/task scheduling, not message-tool fallback.
#83187 fix(heartbeat): clear pendingFinalDelivery* on send success — pending final delivery cleanup, not message-tool fallback.
#83819 fix(heartbeat-runner): add wakeGuard pre-flight hook... — opt-in wake guard, not message-tool fallback.
#84335 fix(slack): forward per-agent identity overlay on heartbeat and runtimeSend (#84297) — Slack identity, not message-tool fallback.

gh search prs --repo openclaw/openclaw --state open "resolveHeartbeatToolResponseFromReplyResult" --json number,title,url,state,body --limit 10
[]

gh search prs --repo openclaw/openclaw --state open "message-tool delivery evidence heartbeat" --json number,title,url,state,body --limit 10
[]

No open PR was found that fixes #84217 or covers the route-scoped message-tool fallback behavior.

Updated live Telegram proof for head 6c40273

This proof refreshes the current-head route-suppression behavior after the text-evidence guard fix. It uses the PR source from /code/workspaces/openclaw/openclaw-84975, calls current-head buildReplyPayloads(...), then runs runHeartbeatOnce(...) with real Telegram Bot API sends. Secrets, chat id, topic id, usernames, local temp paths, and process details are omitted or redacted.

Redacted route:

{"provider":"telegram","to":"telegram:group:<redacted-chat>","thread":"topic:<redacted-thread>"}

Covered cases:

  • same-route + different text -> suppress fallback
  • same-route + same text -> suppress fallback
  • same-route + media-only/textless delivery evidence -> fallback still sends
  • no route-scoped message-tool evidence -> fallback still sends
  • wrong-thread route evidence -> fallback still sends

Redacted live log:

{"proof":"openclaw-pr84975-live-telegram","head":"6c40273b3ec9881b131331932fd31e39a075e1fb","route":{"provider":"telegram","to":"telegram:group:<redacted-chat>","thread":"topic:<redacted-thread>"}}
{"scenario":"same-route-different-text","route":{"provider":"telegram","to":"telegram:group:<redacted-chat>","thread":"topic:<redacted-thread>"},"decision":"suppress-fallback","status":"PASS","heartbeatStatus":"ran","toolSendCount":1,"heartbeatFallbackSendCount":0,"sends":[{"phase":"tool","method":"sendMessage","httpStatus":200,"ok":true,"message_id":27}]}
{"scenario":"same-route-same-text","route":{"provider":"telegram","to":"telegram:group:<redacted-chat>","thread":"topic:<redacted-thread>"},"decision":"suppress-fallback","status":"PASS","heartbeatStatus":"ran","toolSendCount":1,"heartbeatFallbackSendCount":0,"sends":[{"phase":"tool","method":"sendMessage","httpStatus":200,"ok":true,"message_id":28}]}
{"scenario":"media-only","route":{"provider":"telegram","to":"telegram:group:<redacted-chat>","thread":"topic:<redacted-thread>"},"decision":"send-fallback","status":"PASS","heartbeatStatus":"ran","toolSendCount":1,"heartbeatFallbackSendCount":1,"sends":[{"phase":"tool","method":"sendPhoto","httpStatus":200,"ok":true,"message_id":29},{"phase":"fallback","method":"sendMessage","httpStatus":200,"ok":true,"message_id":30}]}
{"scenario":"no-route","route":{"provider":"telegram","to":"telegram:group:<redacted-chat>","thread":"topic:<redacted-thread>"},"decision":"send-fallback","status":"PASS","heartbeatStatus":"ran","toolSendCount":0,"heartbeatFallbackSendCount":1,"sends":[{"phase":"fallback","method":"sendMessage","httpStatus":200,"ok":true,"message_id":31}]}
{"scenario":"wrong-thread","route":{"provider":"telegram","to":"telegram:group:<redacted-chat>","thread":"topic:<redacted-thread>"},"decision":"send-fallback","status":"PASS","heartbeatStatus":"ran","toolSendCount":0,"heartbeatFallbackSendCount":1,"sends":[{"phase":"fallback","method":"sendMessage","httpStatus":200,"ok":true,"message_id":32}]}
{"summary":{"pass":5,"fail":0}}

Verification after the code fix:

pnpm test src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts -t "does not mark heartbeat text replies as delivered by media-only route evidence"  # RED before fix
pnpm test src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts -t "media-only route evidence|same route delivered different text"  # 2 passed
pnpm test src/auto-reply/reply/agent-runner-payloads.test.ts -t "same-route message-tool delivery used different text|same-route message-tool delivery was media-only"  # 2 passed
pnpm test src/auto-reply/reply/agent-runner-payloads.test.ts src/infra/heartbeat-runner.tool-response.test.ts  # 17 + 48 passed
pnpm test src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts -t "runReplyAgent typing \(heartbeat\)"  # 52 passed, 12 skipped
pnpm check:changed --base origin/main  # passed
git diff --check  # passed
gitleaks protect --staged --redact  # no leaks

Updated live Telegram proof for head dc6fa90

This refreshes the live proof after rebasing the branch onto latest origin/main to clear the PR conflict in src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts.

The temporary harness stayed outside the patch and used current-head source from this branch:

  • buildReplyPayloads(...) built heartbeat payload metadata from route-scoped messagingToolSentTargets.
  • runHeartbeatOnce(...) made the heartbeat suppression/fallback decision.
  • Telegram sends used the real Bot API. The fallback path went through a registered outbound adapter and runHeartbeatOnce(...); it was not mocked.
  • Token, chat id, user identity, local paths, timestamps, and process details are omitted or redacted.

Covered cases:

  • same-route + different text -> suppress fallback
  • same-route + same text -> suppress fallback
  • same-route + media-only/textless delivery evidence -> fallback still sends
  • no route-scoped message-tool evidence -> fallback still sends
  • wrong-route evidence -> fallback still sends

Redacted live log:

{"proof":"openclaw-pr84975-live-telegram","head":"dc6fa9070a83366c1878bcd62351062578f65c9a","route":{"provider":"telegram","to":"telegram:group:<redacted-chat>","thread":"root"}}
{"scenario":"same-route-different-text","decision":"suppress-fallback","status":"PASS","heartbeatStatus":"ran","toolSendCount":1,"heartbeatFallbackSendCount":0,"sends":[{"phase":"tool","method":"sendMessage","httpStatus":200,"ok":true,"message_id":42}]}
{"scenario":"same-route-same-text","decision":"suppress-fallback","status":"PASS","heartbeatStatus":"ran","toolSendCount":1,"heartbeatFallbackSendCount":0,"sends":[{"phase":"tool","method":"sendMessage","httpStatus":200,"ok":true,"message_id":43}]}
{"scenario":"media-only","decision":"send-fallback","status":"PASS","heartbeatStatus":"ran","toolSendCount":1,"heartbeatFallbackSendCount":1,"sends":[{"phase":"tool","method":"sendPhoto","httpStatus":200,"ok":true,"message_id":44},{"phase":"fallback","method":"sendMessage","httpStatus":200,"ok":true,"message_id":45}]}
{"scenario":"no-route","decision":"send-fallback","status":"PASS","heartbeatStatus":"ran","toolSendCount":0,"heartbeatFallbackSendCount":1,"sends":[{"phase":"fallback","method":"sendMessage","httpStatus":200,"ok":true,"message_id":46}]}
{"scenario":"wrong-route","decision":"send-fallback","status":"PASS","heartbeatStatus":"ran","toolSendCount":1,"heartbeatFallbackSendCount":1,"sends":[{"phase":"tool","method":"sendMessage","httpStatus":200,"ok":true,"message_id":47},{"phase":"fallback","method":"sendMessage","httpStatus":200,"ok":true,"message_id":48}]}
{"summary":{"pass":5,"fail":0}}

Verification after rebase:

pnpm test src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts -t "media-only route evidence|same route delivered different text"  # 2 passed
pnpm test src/auto-reply/reply/agent-runner-payloads.test.ts -t "same-route message-tool delivery used different text|same-route message-tool delivery was media-only"  # 2 passed
pnpm test src/auto-reply/reply/agent-runner-payloads.test.ts src/infra/heartbeat-runner.tool-response.test.ts  # 17 + 48 passed
pnpm test src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts -t "runReplyAgent typing \(heartbeat\)"  # 52 passed, 12 skipped
pnpm check:changed --base origin/main  # passed
git diff --check origin/main..HEAD  # passed
gitleaks detect --redact --source . --log-opts origin/main..HEAD  # no leaks

@openclaw-barnacle openclaw-barnacle Bot added size: M triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 21, 2026
@clawsweeper

clawsweeper Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 17, 2026, 11:00 AM ET / 15:00 UTC.

Summary
Threads successful terminal message-tool-only delivery evidence through route matching into heartbeat payload metadata so a single plain fallback is suppressed only for the matching provider, target, account, and thread.

PR surface: Source +110, Tests +536. Total +646 across 11 files.

Reproducibility: yes. at source level. The linked issue provides production logs for a generic message-tool send followed by heartbeat fallback, and current code still lacks this candidate route-scoped suppression path; this review did not rerun the live channel scenario.

Review metrics: none identified.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #84217
Summary: This PR is the active candidate fix for the canonical duplicate-heartbeat-delivery issue; the earlier unmerged attempt used broader evidence and was replaced by this route-scoped implementation.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦐 gold shrimp
Proof: 🦐 gold shrimp
Patch quality: 🐚 platinum hermit
Result: blocked until stronger real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Add redacted exact-head Telegram proof covering terminal success, progress, failure, and wrong-thread controls.

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: A real Telegram screenshot and transcript prove an older head, but the exact candidate SHA changes terminal, progress, and failure-sensitive suppression; rerun those scenarios with private identifiers redacted, update the PR body, and request re-review if it does not trigger automatically.

Mantis proof suggestion
Native Telegram proof would directly show that the final head suppresses only the duplicate terminal same-route fallback and preserves control cases. A maintainer can ask Mantis to capture proof by posting this exact PR comment:

@openclaw-mantis telegram desktop proof: verify on head 894d4c152c449eb58c9bdaab8b221c5eaa7ef50c that same-route terminal message-tool delivery suppresses one plain heartbeat fallback while progress, failure, and wrong-thread cases remain visible, returning redacted evidence.

Risk before merge

  • [P2] Merging without exact-head live proof could silently suppress a fallback when a real channel reports progress, failure, or route identity differently from the tested in-process cases.

Maintainer options:

  1. Prove the final head live (recommended)
    Capture redacted Telegram evidence on the exact candidate SHA for same-route terminal success plus progress, failure, and wrong-thread controls before merging.
  2. Accept test-backed delivery risk
    A maintainer may explicitly accept the older live proof plus exact-head regression suite, owning the remaining real-transport classification uncertainty.

Next step before merge

  • [P1] Contributor-supplied exact-head Telegram proof and a refreshed final-candidate review are required; there is no narrow code defect for an automated repair worker to fix.

Security
Cleared: The final diff adds internal delivery-state metadata and tests without changing dependencies, permissions, credentials, secret access, or authorization boundaries.

Review details

Best possible solution:

Land the route- and terminal-aware fix after exact-head Telegram proof shows one same-route terminal send, while progress, terminal failure, wrong-route or wrong-thread, rich payload, and relayable exec cases remain visible.

Do we have a high-confidence way to reproduce the issue?

Yes, at source level. The linked issue provides production logs for a generic message-tool send followed by heartbeat fallback, and current code still lacks this candidate route-scoped suppression path; this review did not rerun the live channel scenario.

Is this the best way to solve the issue?

Yes, subject to exact-head proof. Keeping the decision at the heartbeat owner boundary and reusing shared route matching is narrower and safer than suppressing all free text after any message-tool activity.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 75697ad3bf6c.

Label changes

Label changes:

  • add rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🐚 platinum hermit.
  • remove rating: 🧂 unranked krab: Current PR rating is rating: 🦐 gold shrimp, so this older rating label is no longer current.
  • remove merge-risk: 🚨 automation: Current PR review merge-risk labels are merge-risk: 🚨 message-delivery.
  • remove merge-risk: 🚨 security-boundary: Current PR review merge-risk labels are merge-risk: 🚨 message-delivery.

Label justifications:

  • P1: The PR addresses duplicate heartbeat messages in a real channel workflow, while an incorrect suppression decision could hide a user-visible fallback.
  • merge-risk: 🚨 message-delivery: The patch intentionally suppresses outbound heartbeat content based on route and terminal-delivery evidence, so a classification error could drop or duplicate messages.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🐚 platinum hermit.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: A real Telegram screenshot and transcript prove an older head, but the exact candidate SHA changes terminal, progress, and failure-sensitive suppression; rerun those scenarios with private identifiers redacted, update the PR body, and request re-review if it does not trigger automatically.
  • proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. A real Telegram screenshot and transcript prove an older head, but the exact candidate SHA changes terminal, progress, and failure-sensitive suppression; rerun those scenarios with private identifiers redacted, update the PR body, and request re-review if it does not trigger automatically.
  • mantis: telegram-visible-proof: Mantis should capture Telegram visible proof. The change directly alters which heartbeat messages appear in Telegram and can be demonstrated clearly in a short native Telegram recording.
Evidence reviewed

PR surface:

Source +110, Tests +536. Total +646 across 11 files.

View PR surface stats
Area Files Added Removed Net
Source 6 115 5 +110
Tests 5 537 1 +536
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 11 652 6 +646

What I checked:

  • Successful-send intent capture: The PR records terminal versus progress intent on message-tool-only sends and attaches it to committed message-target evidence after tool execution completes. (src/agents/embedded-agent-subscribe.handlers.tools.ts:1405, 894d4c152c44)
  • Route and terminal gate: The shared dedupe resolver reports terminal delivery only when a matching target has terminal intent and visible text, media, or rich content. (src/auto-reply/reply/reply-payloads-dedupe.ts:394, 894d4c152c44)
  • Narrow payload eligibility: Heartbeat evidence is applied only when one visible payload remains and it is plain sendable text rather than an error, fallback notice, reasoning, rich, interactive, or media payload. (src/auto-reply/reply/agent-runner-payloads.ts:518, 894d4c152c44)
  • Heartbeat owner boundary: The runner suppresses the main fallback only when route delivery evidence exists and no heartbeat response-tool result, terminal tool failure, or relayable exec completion requires delivery. (src/infra/heartbeat-runner.ts:1989, 894d4c152c44)
  • Regression coverage: Focused tests cover same-route terminal delivery, progress sends, wrong routes and threads, multiple or rich payloads, failures, and fallback-preserving cases. (src/auto-reply/reply/agent-runner-payloads.test.ts:491, 894d4c152c44)
  • Exact-head proof gap: The live Telegram screenshot and transcript were captured on an older head; the previous completed review examined the current SHA and requested a rerun because the final implementation materially changed terminal, progress, and failure handling. No newer commit or exact-head live artifact was supplied afterward. (894d4c152c44)

Likely related people:

  • vincentkoc: They were assigned to this PR and repeatedly carried the route and thread semantics through its earlier heads, making them the strongest routing candidate for the final delivery invariant. (role: agents area collaborator and reviewer; confidence: high; commits: f21343bfd4; files: src/auto-reply/reply/agent-runner-payloads.ts, src/auto-reply/reply/reply-payloads-dedupe.ts, src/infra/heartbeat-runner.ts)
  • agocs: Their merged pending-final delivery repair recently changed the same heartbeat send-success and recovery boundary, providing relevant context for suppression versus preserved delivery. (role: recent heartbeat delivery contributor; confidence: medium; commits: 305fa9c4ddbe; files: src/infra/heartbeat-runner.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (5 earlier review cycles)
  • reviewed 2026-07-01T06:54:17.328Z sha d79eaa0 :: needs maintainer review before merge. :: none
  • reviewed July 13, 2026, 8:13 PM ET / July 14, 2026, 00:13 UTC sha unknown :: needs maintainer review before merge. :: none
  • reviewed 2026-07-17T09:32:10.170Z sha 0b854c2 :: needs real behavior proof before merge. :: [P1] Restore this branch to the scoped heartbeat change
  • reviewed 2026-07-17T10:13:38.008Z sha 0b854c2 :: needs real behavior proof before merge. :: [P1] Restore this branch to the scoped heartbeat change
  • reviewed 2026-07-17T14:18:52.768Z sha 894d4c1 :: needs real behavior proof before merge. :: none

@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 21, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. labels May 21, 2026
@clawsweeper

clawsweeper Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

✨ Hatched: ✨ glimmer Pearl Crabkin

Hatch command

Comment @clawsweeper hatch when this PR is hatchable.

Hatchability rules:

  • Merged PRs are hatchable.
  • Open PRs are hatchable when they are status: 👀 ready for maintainer look, status: 🚀 automerge armed, or labeled clawsweeper:automerge.
  • Closed unmerged PRs are hatchable only when one of those hatchable labels is still present in the durable record.

Rarity: ✨ glimmer.
Trait: collects tiny proofs.
Image traits: location review cove; accessory miniature diff map; palette sunrise gold and clean white; mood patient; pose waving from a small platform; shell frosted glass shell; lighting clean product lighting; background small review tokens.
Share on X: post this hatch
Copy: My PR egg hatched a ✨ glimmer Pearl Crabkin in ClawSweeper.

What is this egg doing here?
  • Eggs appear after the PR passes real-behavior proof. It is here for vibes, not verdicts: it does not change labels, ratings, merge decisions, or automation.
  • The shell reacts to review momentum: open follow-up work warms it up, re-review makes it wobble, and a clean final review lets it hatch.
  • Hatchability usually comes from sufficient real-behavior proof, no blocking P0/P1/P2 findings, no security attention needed, and clean correctness. A merged PR is already final, so merge makes the egg hatchable independently.
  • The hatch is seeded from this repository and PR number, so the same PR keeps the same creature; the reviewed head SHA can only change safe visual details.
  • Rarity is just collectible sparkle: 🥚 common, 🌱 uncommon, 💎 rare, ✨ glimmer, and 🌈 legendary.

@vincentkoc vincentkoc self-assigned this May 21, 2026
@vincentkoc
vincentkoc force-pushed the wolf/heartbeat-suppress-after-message-tool branch from 9f3e455 to 78ecbc4 Compare May 21, 2026 16:40
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 21, 2026
@vincentkoc
vincentkoc force-pushed the wolf/heartbeat-suppress-after-message-tool branch from 78ecbc4 to 87d31f6 Compare May 21, 2026 16:45
@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels May 21, 2026
@vincentkoc
vincentkoc force-pushed the wolf/heartbeat-suppress-after-message-tool branch 5 times, most recently from 3efd23c to 0bb15b5 Compare May 21, 2026 17:41
@vincentkoc
vincentkoc force-pushed the wolf/heartbeat-suppress-after-message-tool branch 2 times, most recently from 88f67b0 to 48b7aaf Compare May 21, 2026 18:44
@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

8 similar comments
@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

3 similar comments
@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@luoyanglang

Copy link
Copy Markdown
Contributor Author

Rescued and refreshed the PR on exact head 0b854c20d2939469aa111662780db8a5161dd442 (including main 5c86a224c0af9f299c3f5a25ac5056d677688575).

The final implementation now suppresses heartbeat fallback narration only when the matching provider/target/account/thread has a successful terminal visible message_tool_only delivery. Progress sends, wrong routes/threads, multiple visible payloads, rich/error/fallback payloads, terminal tool failures, and relayable exec completions remain unsuppressed.

Exact-tree local validation:

  • focused heartbeat/payload regressions: 140 passed
  • runReplyAgent E2E: 117 passed
  • message-tool handler suite: 130 passed
  • pnpm tsgo:core and pnpm tsgo:core:test: passed
  • target oxlint, oxfmt --check, conflict-marker check, and git diff --check: passed
  • scoped gitleaks scan: no leaks
  • final high-reasoning autoreview: no findings (patch is correct, confidence 0.87)

GitHub now reports this head mergeable; exact-head CI is running. It currently reports BEHIND because main advanced again after the refresh, so I am monitoring the new checks before deciding whether another refresh is warranted.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Security-sensitive guard cleared

This PR no longer has blocked security-sensitive file changes. A future security-sensitive change requires a fresh /allow-security-sensitive-change comment after the guard blocks that new head SHA.

  • Current SHA: 894d4c152c449eb58c9bdaab8b221c5eaa7ef50c

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Dependency graph guard cleared

This PR no longer has blocked dependency graph changes. A future dependency graph change requires a fresh /allow-dependencies-change comment after the guard blocks that new head SHA.

  • Current SHA: 894d4c152c449eb58c9bdaab8b221c5eaa7ef50c

@luoyanglang

Copy link
Copy Markdown
Contributor Author

Authorized force-with-lease correction completed on exact head 894d4c152c449eb58c9bdaab8b221c5eaa7ef50c.

The previous merge topology expanded GitHub's comparison to unrelated base-history changes. The branch is now a single fix commit on main 16f658d0c61f12572128e47befd09196e66f3b2b; GitHub has recalculated the PR to the intended 11 files (+652/-6) and reports it mergeable.

Exact-candidate validation:

  • focused heartbeat/payload tests: 140 passed
  • runReplyAgent E2E: 117 passed
  • message-tool handler suite: 130 passed
  • tsgo:core and tsgo:core:test: passed
  • target lint/format, conflict-marker and diff checks: passed
  • scoped gitleaks: no leaks
  • final autoreview: no findings, patch correct (0.88 confidence)

Exact-head CI is running now. The old dependency/security guard failures belonged to the expanded comparison and must not be attributed to this head.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

7 similar comments
@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling mantis: telegram-visible-proof Mantis should capture Telegram visible proof. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P1 High-priority user-facing bug, regression, or broken workflow. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. size: L stale Marked as stale due to inactivity status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Heartbeat dispatch delivers free text block alongside message-tool call (chatty non-Codex providers, v2026.5.18)

3 participants