Skip to content

[Bug]: Heartbeat mode leaks [TOOL_CALL] bracket-format blocks into user messages #54138

Description

@Deniable9570

Bug Description

When a heartbeat handler runs tool calls (e.g. exec, cron list), the embedded agent outputs [TOOL_CALL]{tool: "exec", args: {...}}[/TOOL_CALL] blocks in its response. These leak into user-facing Telegram/Discord messages instead of being silently discarded.

Steps to Reproduce

  1. Have HEARTBEAT.md content that triggers an exec or other tool call
  2. Any inbound message or scheduled heartbeat poll fires
  3. The heartbeat embedded agent runs
  4. User receives a message containing [TOOL_CALL]{...}[/TOOL_CALL] — raw agent tool-call output

Example

[TOOL_CALL]
{tool => "exec", args => {
--command "ls memory/..."
}}
[/TOOL_CALL]

Root Cause

In agent-runner-payloads.ts, the buildReplyPayloads function has two branches:

  • Normal messages: HEARTBEAT_TOKEN stripping + full sanitization
  • isHeartbeat=true: payloads pass through without any text filtering
const sanitizedPayloads = params.isHeartbeat
  ? params.payloads  // ← no stripping, no sanitization
  : params.payloads.flatMap(...)

When the embedded agent executes a tool, the result is injected back into the model as an observation. The model then generates a response that may contain [TOOL_CALL] bracket-format blocks. Since no stripping happens in heartbeat mode, this text flows into replyPayload.text and gets delivered to the user.

Note: [TOOL_CALL] (bracket format) is distinct from <tool_call> (XML format). PR #42260 addresses the XML format but does not cover the bracket format.

Expected Behavior

Heartbeat tool executions should never produce user-visible [TOOL_CALL] blocks.

Related

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