diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cb770d256a..3532c5f51dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai - Exec approvals: allow bare command-name allowlist patterns to match PATH-resolved executable basenames without trusting `./tool` or absolute path-selected binaries. Fixes #71315. Thanks @chen-zhang-cs-code and @dengluozhang. - Config/recovery: skip whole-file last-known-good rollback when invalidity is scoped to `plugins.entries.*`, preserving unrelated user settings during plugin schema or host-version skew. Fixes #71289. Thanks @jalehman. - Agents/tools: keep resolved reply-run configs from being overwritten by stale runtime snapshots, and let empty web runtime metadata fall back to configured provider auto-detection so standard and queued turns expose the same tool set. Fixes #71355. Thanks @c-g14. +- Agents/TTS: preserve voice media when a tool-generated reply is paired with an exact `NO_REPLY` sentinel, stripping the sentinel text instead of dropping the audio payload. Fixes #66092. - Compaction: honor explicit `agents.defaults.compaction.keepRecentTokens` for manual `/compact`, re-distill safeguard summaries instead of snowballing previous summaries, and enable safeguard summary quality checks by default. Fixes #71357. Thanks @WhiteGiverMa. - Sessions: honor configured `session.maintenance` settings during load-time maintenance instead of falling back to default entry caps. Fixes #71356. Thanks @comolago. - Browser/sandbox: pass the resolved `browser.ssrfPolicy` into sandbox browser bridges and refresh cached bridges when the effective policy changes, so sandboxed browser navigation honors private-network opt-ins. Fixes #45153 and #57055. Thanks @jzakirov, @zuoanCo, and @kybrcore. diff --git a/src/agents/pi-embedded-runner/run/payloads.test.ts b/src/agents/pi-embedded-runner/run/payloads.test.ts index 8faf106189d..0674e7e5116 100644 --- a/src/agents/pi-embedded-runner/run/payloads.test.ts +++ b/src/agents/pi-embedded-runner/run/payloads.test.ts @@ -200,6 +200,20 @@ describe("buildEmbeddedRunPayloads tool-error warnings", () => { }); }); + it("strips NO_REPLY text but keeps voice media directives", () => { + const payloads = buildPayloads({ + assistantTexts: ["NO_REPLY\nMEDIA:/tmp/openclaw/tts-a/voice-a.opus\n[[audio_as_voice]]"], + }); + + expect(payloads).toHaveLength(1); + expect(payloads[0]).toMatchObject({ + mediaUrl: "/tmp/openclaw/tts-a/voice-a.opus", + mediaUrls: ["/tmp/openclaw/tts-a/voice-a.opus"], + audioAsVoice: true, + }); + expect(payloads[0]?.text).toBeUndefined(); + }); + it("preserves media directives when stored assistant text was reduced to visible text only", () => { const payloads = buildPayloads({ assistantTexts: ["Attached image"], diff --git a/src/agents/pi-embedded-runner/run/payloads.ts b/src/agents/pi-embedded-runner/run/payloads.ts index 0a94c58cf43..d0ecf81ae90 100644 --- a/src/agents/pi-embedded-runner/run/payloads.ts +++ b/src/agents/pi-embedded-runner/run/payloads.ts @@ -381,16 +381,27 @@ export function buildEmbeddedRunPayloads(params: { const hasAudioAsVoiceTag = replyItems.some((item) => item.audioAsVoice); return replyItems - .map((item) => ({ - text: normalizeOptionalString(item.text), - mediaUrls: item.media?.length ? item.media : undefined, - mediaUrl: item.media?.[0], - isError: item.isError, - replyToId: item.replyToId, - replyToTag: item.replyToTag, - replyToCurrent: item.replyToCurrent, - audioAsVoice: item.audioAsVoice || Boolean(hasAudioAsVoiceTag && item.media?.length), - })) + .map((item) => { + const payload = { + text: normalizeOptionalString(item.text), + mediaUrls: item.media?.length ? item.media : undefined, + mediaUrl: item.media?.[0], + isError: item.isError, + replyToId: item.replyToId, + replyToTag: item.replyToTag, + replyToCurrent: item.replyToCurrent, + audioAsVoice: item.audioAsVoice || Boolean(hasAudioAsVoiceTag && item.media?.length), + }; + if (payload.text && isSilentReplyPayloadText(payload.text, SILENT_REPLY_TOKEN)) { + const silentText = payload.text; + payload.text = undefined; + if (hasOutboundReplyContent(payload)) { + return payload; + } + payload.text = silentText; + } + return payload; + }) .filter((p) => { if (!hasOutboundReplyContent(p)) { return false;