fix: harden unhandled rejection handling and tts menus (#2451) (thanks @Glucksberg)
This commit is contained in:
parent
4754eddd96
commit
c6db48b346
@ -55,6 +55,8 @@ Status: unreleased.
|
|||||||
- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
|
- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
- Gateway: suppress AbortError and transient network errors in unhandled rejections. (#2451) Thanks @Glucksberg.
|
||||||
|
- TTS: keep /tts status replies on text-only commands and avoid duplicate block-stream audio. (#2451) Thanks @Glucksberg.
|
||||||
- Security: pin npm overrides to keep tar@7.5.4 for install toolchains.
|
- Security: pin npm overrides to keep tar@7.5.4 for install toolchains.
|
||||||
- Security: properly test Windows ACL audit for config includes. (#2403) Thanks @dominicnunez.
|
- Security: properly test Windows ACL audit for config includes. (#2403) Thanks @dominicnunez.
|
||||||
- CLI: recognize versioned Node executables when parsing argv. (#2490) Thanks @David-Marsh-Photo.
|
- CLI: recognize versioned Node executables when parsing argv. (#2490) Thanks @David-Marsh-Photo.
|
||||||
|
|||||||
@ -25,11 +25,11 @@ type ParsedTtsCommand = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function parseTtsCommand(normalized: string): ParsedTtsCommand | null {
|
function parseTtsCommand(normalized: string): ParsedTtsCommand | null {
|
||||||
// Accept `/tts <action> [args]` - return null for `/tts` alone to trigger inline menu.
|
// Accept `/tts` and `/tts <action> [args]` as a single control surface.
|
||||||
if (normalized === "/tts") return null;
|
if (normalized === "/tts") return { action: "status", args: "" };
|
||||||
if (!normalized.startsWith("/tts ")) return null;
|
if (!normalized.startsWith("/tts ")) return null;
|
||||||
const rest = normalized.slice(5).trim();
|
const rest = normalized.slice(5).trim();
|
||||||
if (!rest) return null;
|
if (!rest) return { action: "status", args: "" };
|
||||||
const [action, ...tail] = rest.split(/\s+/);
|
const [action, ...tail] = rest.split(/\s+/);
|
||||||
return { action: action.toLowerCase(), args: tail.join(" ").trim() };
|
return { action: action.toLowerCase(), args: tail.join(" ").trim() };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -420,3 +420,17 @@ describe("handleCommands subagents", () => {
|
|||||||
expect(result.reply?.text).toContain("Status: done");
|
expect(result.reply?.text).toContain("Status: done");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("handleCommands /tts", () => {
|
||||||
|
it("returns status for bare /tts on text command surfaces", async () => {
|
||||||
|
const cfg = {
|
||||||
|
commands: { text: true },
|
||||||
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||||
|
messages: { tts: { prefsPath: path.join(testWorkspaceDir, "tts.json") } },
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
const params = buildParams("/tts", cfg);
|
||||||
|
const result = await handleCommands(params);
|
||||||
|
expect(result.shouldContinue).toBe(false);
|
||||||
|
expect(result.reply?.text).toContain("TTS status");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js";
|
|||||||
import { shouldSkipDuplicateInbound } from "./inbound-dedupe.js";
|
import { shouldSkipDuplicateInbound } from "./inbound-dedupe.js";
|
||||||
import type { ReplyDispatcher, ReplyDispatchKind } from "./reply-dispatcher.js";
|
import type { ReplyDispatcher, ReplyDispatchKind } from "./reply-dispatcher.js";
|
||||||
import { isRoutableChannel, routeReply } from "./route-reply.js";
|
import { isRoutableChannel, routeReply } from "./route-reply.js";
|
||||||
import { maybeApplyTtsToPayload, normalizeTtsAutoMode } from "../../tts/tts.js";
|
import { maybeApplyTtsToPayload, normalizeTtsAutoMode, resolveTtsConfig } from "../../tts/tts.js";
|
||||||
|
|
||||||
const AUDIO_PLACEHOLDER_RE = /^<media:audio>(\s*\([^)]*\))?$/i;
|
const AUDIO_PLACEHOLDER_RE = /^<media:audio>(\s*\([^)]*\))?$/i;
|
||||||
const AUDIO_HEADER_RE = /^\[Audio\b/i;
|
const AUDIO_HEADER_RE = /^\[Audio\b/i;
|
||||||
@ -342,10 +342,16 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ttsMode = resolveTtsConfig(cfg).mode ?? "final";
|
||||||
// Generate TTS-only reply after block streaming completes (when there's no final reply).
|
// Generate TTS-only reply after block streaming completes (when there's no final reply).
|
||||||
// This handles the case where block streaming succeeds and drops final payloads,
|
// This handles the case where block streaming succeeds and drops final payloads,
|
||||||
// but we still want TTS audio to be generated from the accumulated block content.
|
// but we still want TTS audio to be generated from the accumulated block content.
|
||||||
if (replies.length === 0 && blockCount > 0 && accumulatedBlockText.trim()) {
|
if (
|
||||||
|
ttsMode === "final" &&
|
||||||
|
replies.length === 0 &&
|
||||||
|
blockCount > 0 &&
|
||||||
|
accumulatedBlockText.trim()
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const ttsSyntheticReply = await maybeApplyTtsToPayload({
|
const ttsSyntheticReply = await maybeApplyTtsToPayload({
|
||||||
payload: { text: accumulatedBlockText },
|
payload: { text: accumulatedBlockText },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user