Skip to content

[Bug]: TTS engagement is skipped on all message-tool sendMessage* paths (cross-provider; structurally unmet in group channels with visibleReplies="message_tool"; regardless of tts.auto mode) #81598

Description

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

Final-reply payload delivery engages TTS correctly via maybeApplyTtsToPayload in run-delivery.runtime. The proactive message(action=send) tool — used for visible group/channel posts when messages.groupChat.visibleReplies = "message_tool" — does not engage TTS at all. [[tts:text]]…[[/tts:text]] directives render as literal body text on the recipient surface and no audio attaches. Scope is the message-tool path only; bug is independent of messages.tts.auto mode (off | always | inbound | tagged).

Steps to reproduce

  1. Install OpenClaw 2026.5.7 (or any release built on this dist tree).
  2. Configure a working TTS provider (e.g. ElevenLabs eleven_v3).
  3. Set messages.groupChat.visibleReplies = "message_tool" (the doctrine-correct lurk default; also the effective default when the path is unset).
  4. From an agent session, call the proactive message tool: message(action=send, target="channel:<id>", message="[[tts:text]]Hello world.[[/tts:text]]").
  5. Observe the Slack channel: tags render visibly, no audio attached.
  6. Repeat with messages.tts.auto set to each of tagged, always, and inbound (the latter with an audio user-input present). All three modes reproduce identically — TTS pipeline is never invoked on the message-tool path.
  7. For comparison, send the same content as the agent's final-reply payload in a direct chat where messages.visibleReplies defaults to "automatic". Audio attaches correctly. This isolates the bug to the message-tool send path.

Expected behavior

message(action=send) should engage the same TTS pipeline that run-delivery.runtime invokes on final-reply payloads via maybeApplyTtsToPayload (gated on shouldAttemptTtsPayload). The [[tts:text]]…[[/tts:text]] markers should be stripped, audio generated by the configured provider, and audio attached to the outbound message.

Actual behavior

Tags are forwarded verbatim into the underlying channel API call (e.g. Slack chat.postMessage). No TTS conversion is invoked. No audio attaches. Recipients see raw control directives in the message body.

OpenClaw version

2026.5.7 (eeef486)

Operating system

macOS 15.x (Darwin 25.2.0 arm64)

Install method

npm global (/opt/homebrew/lib/node_modules/openclaw/dist/...)

Model

anthropic/claude-opus-4-7 (irrelevant — bug is runtime-side, model-agnostic)

Provider / routing chain

openclaw runtime → channels.slack (proactive message tool) → chat.postMessage. TTS provider chain that would normally be invoked: openclaw → elevenlabs (eleven_v3).

Additional provider/model setup details

  • messages.tts.auto = "tagged" confirmed reproducer on host A. Independently confirmed by host B with messages.tts.auto = "inbound" (zero successful tts.convert events on either host for message-tool sends).
  • messages.tts.provider = "elevenlabs" with valid apiKey SecretRef and modelId = "eleven_v3".
  • messages.groupChat.visibleReplies valid values per plugin-sdk/src/config/types.messages.d.ts: "automatic" | "message_tool".
  • TTS works correctly on final-reply payload delivery in direct chats. Bug only manifests on the message-tool path used for group-channel visibility.

Logs, screenshots, and evidence

$ grep -ln "maybeApplyTtsToPayload" dist/extensions/slack/*.js dist/extensions/telegram/*.js dist/extensions/signal/*.js dist/extensions/imessage/*.js
(no matches in any extension)

$ grep -ln "maybeApplyTtsToPayload" dist/**/*.js
dist/tts.runtime-By6eJ978.js
dist/dispatch-acp-tts.runtime-DcDLudXS.js
dist/tts-runtime-Btvk-HT-.js
dist/run-delivery.runtime-Cbm9rJpU.js
dist/dispatch-acp-HFOhX_U-.js
dist/plugin-sdk/tts-runtime.js
dist/plugin-sdk/agent-runtime.js
dist/extensions/speech-core/runtime-api.js
dist/tts-62RsnUZe.js
dist/dispatch-8E8vi2HV.js

Per-provider send-message TTS coverage (independently verified on two hosts):

Provider sendMessage files TTS refs
Slack 2 0
Telegram 3 0
Signal 2 0
iMessage 1 0

Working pattern in run-delivery.runtime-Cbm9rJpU.js that should be mirrored at the message-tool send path:

async function maybeApplyTtsToCronPayloads(params) {
  if (!shouldAttemptTtsPayload({
    cfg: params.cfg, ttsAuto: params.ttsAuto,
    agentId: params.agentId, channelId: params.delivery.channel,
    accountId: params.delivery.accountId
  })) return params.payloads;
  const { maybeApplyTtsToPayload } = await loadTtsRuntime();
  return await Promise.all(params.payloads.map((payload) => maybeApplyTtsToPayload({
    payload, cfg: params.cfg, channel: params.delivery.channel,
    kind: "final", ttsAuto: params.ttsAuto,
    agentId: params.agentId, accountId: params.delivery.accountId
  })));
}

Impact and severity

  • Affected: all OpenClaw deployments that combine any non-off messages.tts.auto mode with messages.groupChat.visibleReplies = "message_tool" (the doctrine-correct lurk-by-default mode for shared agent channels). Slack, Telegram, Signal, and iMessage outbound paths all exhibit the same gap.
  • Severity: Workflow-blocking for any "voice on every reply" policy in group/channel surfaces. Tags rendering as visible body text is also a UX defect — recipients see raw control directives.
  • Frequency: Deterministic, every send through the message-tool path. Independent of tts.auto mode.
  • Consequence: "Voice on every group/channel reply" rules are structurally unsatisfiable without either flipping visibleReplies to "automatic" (regresses selective participation defaults for shared channels) or the runtime patch proposed below.

Additional information

Proposed fix (Option 1 — recommended):
Hoist the maybeApplyTtsToPayload call out of the per-pipeline runtimes (run-delivery.runtime, dispatch-acp-tts.runtime) into the shared outbound payload normalization layer that all sendMessage* provider paths invoke before handing off to the channel API. Reuse the existing gate shouldAttemptTtsPayload. Single landing point covers Slack, Telegram, Signal, iMessage, and any future provider.

Proposed fix (Option 2 — lighter but additive):
Extend the visibleReplies enum to add a third value (e.g. "message_tool_with_tts") that preserves lurk-default discipline but engages TTS on tool sends. Less invasive but adds config surface and only helps installs that opt in.

Workaround: None agent-side. Operator-side workaround is flipping visibleReplies to "automatic", which restores TTS but breaks intentional lurking — net regression for shared agent channels.

Related (separate issue worth filing): During the same investigation, fetchWithSsrFGuard was observed to leak socket/agent slots when an in-flight model_call is classified stalled_agent_run without a terminal embedded run agent end event firing. After accumulation, all outbound calls through the wrapper time out at exactly 30s. Resolved by gateway restart in this incident. Happy to file separately with repro details.


Filed on behalf of Agent Jasmin (CG Intelligence). Investigation + RCA: Jasmin. Cross-host verification: Jack.

Metadata

Metadata

Assignees

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