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
- Install OpenClaw
2026.5.7 (or any release built on this dist tree).
- Configure a working TTS provider (e.g. ElevenLabs
eleven_v3).
- Set
messages.groupChat.visibleReplies = "message_tool" (the doctrine-correct lurk default; also the effective default when the path is unset).
- From an agent session, call the proactive message tool:
message(action=send, target="channel:<id>", message="[[tts:text]]Hello world.[[/tts:text]]").
- Observe the Slack channel: tags render visibly, no audio attached.
- 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.
- 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.
Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
Final-reply payload delivery engages TTS correctly via
maybeApplyTtsToPayloadinrun-delivery.runtime. The proactivemessage(action=send)tool — used for visible group/channel posts whenmessages.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 ofmessages.tts.automode (off | always | inbound | tagged).Steps to reproduce
2026.5.7(or any release built on this dist tree).eleven_v3).messages.groupChat.visibleReplies = "message_tool"(the doctrine-correct lurk default; also the effective default when the path is unset).message(action=send, target="channel:<id>", message="[[tts:text]]Hello world.[[/tts:text]]").messages.tts.autoset to each oftagged,always, andinbound(the latter with an audio user-input present). All three modes reproduce identically — TTS pipeline is never invoked on the message-tool path.messages.visibleRepliesdefaults 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 thatrun-delivery.runtimeinvokes on final-reply payloads viamaybeApplyTtsToPayload(gated onshouldAttemptTtsPayload). 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 withmessages.tts.auto = "inbound"(zero successfultts.convertevents on either host for message-tool sends).messages.tts.provider = "elevenlabs"with validapiKeySecretRef andmodelId = "eleven_v3".messages.groupChat.visibleRepliesvalid values perplugin-sdk/src/config/types.messages.d.ts:"automatic" | "message_tool".Logs, screenshots, and evidence
Per-provider send-message TTS coverage (independently verified on two hosts):
Working pattern in
run-delivery.runtime-Cbm9rJpU.jsthat should be mirrored at the message-tool send path:Impact and severity
offmessages.tts.automode withmessages.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.tts.automode.visibleRepliesto"automatic"(regresses selective participation defaults for shared channels) or the runtime patch proposed below.Additional information
Proposed fix (Option 1 — recommended):
Hoist the
maybeApplyTtsToPayloadcall out of the per-pipeline runtimes (run-delivery.runtime,dispatch-acp-tts.runtime) into the shared outbound payload normalization layer that allsendMessage*provider paths invoke before handing off to the channel API. Reuse the existing gateshouldAttemptTtsPayload. Single landing point covers Slack, Telegram, Signal, iMessage, and any future provider.Proposed fix (Option 2 — lighter but additive):
Extend the
visibleRepliesenum 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
visibleRepliesto"automatic", which restores TTS but breaks intentional lurking — net regression for shared agent channels.Related (separate issue worth filing): During the same investigation,
fetchWithSsrFGuardwas observed to leak socket/agent slots when an in-flightmodel_callis classifiedstalled_agent_runwithout a terminalembedded run agent endevent 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.