fix(agents): keep voice media with no-reply sentinel

This commit is contained in:
Peter Steinberger
2026-04-25 05:09:55 +01:00
parent 85db7af8d9
commit 14eab13ab4
3 changed files with 36 additions and 10 deletions

View File

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

View File

@@ -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"],

View File

@@ -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;