Skip to content

Expose stopReason in message_sending plugin hook metadata #66235

Description

@ludovicfourrage

Problem

When a model generates text before a tool call (e.g., "Let me check that..."), that text block gets delivered to the messaging channel as a separate message. In non-streaming mode, these reasoning preamble messages arrive after the actual response, creating visible inner monologue leaks in channels like Discord.

Current behavior

The message_sending plugin hook receives:

type PluginHookMessageSendingEvent = {
    to: string;
    content: string;
    metadata?: Record<string, unknown>;  // currently: { channel, accountId, mediaUrls }
};

There is no way to distinguish between:

  • A text block that precedes a tool call (stopReason: "toolUse") — always inner monologue
  • A final response (stopReason: "stop" / "end_turn") — the actual reply

Proposed change

Pass stopReason (and optionally hasToolCalls: boolean) through to the message_sending hook metadata:

metadata: {
    channel,
    accountId,
    mediaUrls,
    stopReason,        // "stop" | "end_turn" | "toolUse" | "tool_use" | ...
    hasToolCalls,      // boolean
}

The relevant code is in applyMessageSendingHook() in the delivery pipeline, which constructs the metadata object. The stopReason is available in the agent command layer but is not currently forwarded to delivery.

Why this matters

This would allow plugins to structurally suppress reasoning preambles:

api.on("message_sending", (event, ctx) => {
    if (event.metadata?.stopReason === "toolUse" || event.metadata?.hasToolCalls) {
        return { cancel: true };
    }
});

This is more reliable than content-based pattern matching (regex for "Let me..." / "Now I need to...") which can false-positive on legitimate responses.

Context

Running an OpenClaw Discord bot that monitors guild channels. In non-streaming mode, text blocks from stopReason: "toolUse" responses consistently leak as separate messages. The existing output-sanitizer plugin catches most cases via META_PLANNING_PATTERNS regex, but a structural filter would be definitive.

Workaround

Re-enable the output-sanitizer plugin with content-based filtering.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.staleMarked as stale due to inactivity

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions