openclaw - 💡(How to fix) Fix [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) [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#81598Fetched 2026-05-14 03:30:34
View on GitHub
Comments
1
Participants
2
Timeline
2
Reactions
2
Timeline (top)
commented ×1cross-referenced ×1

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).

Root Cause

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).

Fix Action

Fix / Workaround

$ 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


- **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.

**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.

Code Example

$ 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

---

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
  })));
}
RAW_BUFFERClick to expand / collapse

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):

ProvidersendMessage filesTTS refs
Slack20
Telegram30
Signal20
iMessage10

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.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

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.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [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) [1 comments, 2 participants]