fix: honor audio_as_voice streaming + parse tests (#490) (thanks @jarvis-medmatic)

This commit is contained in:
Peter Steinberger
2026-01-10 01:50:33 +01:00
parent 5fedfd8d15
commit c56b2f4bc1
7 changed files with 32 additions and 4 deletions

View File

@@ -42,6 +42,7 @@
- Commands: harden slash command registry and list text-only commands in `/commands`. - Commands: harden slash command registry and list text-only commands in `/commands`.
- Models/Auth: show per-agent auth candidates in `/model status`, and add `clawdbot models auth order {get,set,clear}` (per-agent auth rotation overrides). — thanks @steipete - Models/Auth: show per-agent auth candidates in `/model status`, and add `clawdbot models auth order {get,set,clear}` (per-agent auth rotation overrides). — thanks @steipete
- Telegram: keep streamMode draft-only; avoid forcing block streaming. (#619) — thanks @rubyrunsstuff - Telegram: keep streamMode draft-only; avoid forcing block streaming. (#619) — thanks @rubyrunsstuff
- Telegram: add `[[audio_as_voice]]` tag support for voice notes with streaming-safe delivery. (#490) — thanks @jarvis-medmatic
- Debugging: add raw model stream logging flags and document gateway watch mode. - Debugging: add raw model stream logging flags and document gateway watch mode.
- Gateway: decode dns-sd escaped UTF-8 in discovery output and show scan progress immediately. — thanks @steipete - Gateway: decode dns-sd escaped UTF-8 in discovery output and show scan progress immediately. — thanks @steipete
- Agent: add claude-cli/opus-4.5 runner via Claude CLI with resume support (tools disabled). - Agent: add claude-cli/opus-4.5 runner via Claude CLI with resume support (tools disabled).

View File

@@ -776,6 +776,7 @@ export async function compactEmbeddedPiSession(params: {
const enqueueGlobal = const enqueueGlobal =
params.enqueue ?? params.enqueue ??
((task, opts) => enqueueCommandInLane(globalLane, task, opts)); ((task, opts) => enqueueCommandInLane(globalLane, task, opts));
const runAbortController = new AbortController();
return enqueueCommandInLane(sessionLane, () => return enqueueCommandInLane(sessionLane, () =>
enqueueGlobal(async () => { enqueueGlobal(async () => {
const resolvedWorkspace = resolveUserPath(params.workspaceDir); const resolvedWorkspace = resolveUserPath(params.workspaceDir);

View File

@@ -651,7 +651,9 @@ export function createClawdbotCodingTools(options?: {
// Without this, some providers (notably OpenAI) will reject root-level union schemas. // Without this, some providers (notably OpenAI) will reject root-level union schemas.
const normalized = subagentFiltered.map(normalizeToolParameters); const normalized = subagentFiltered.map(normalizeToolParameters);
const withAbort = options?.abortSignal const withAbort = options?.abortSignal
? normalized.map((tool) => wrapToolWithAbortSignal(tool, options.abortSignal)) ? normalized.map((tool) =>
wrapToolWithAbortSignal(tool, options.abortSignal),
)
: normalized; : normalized;
// Anthropic blocks specific lowercase tool names (bash, read, write, edit) with OAuth tokens. // Anthropic blocks specific lowercase tool names (bash, read, write, edit) with OAuth tokens.

View File

@@ -3,6 +3,7 @@ import type { ClawdbotConfig } from "../../config/config.js";
import { import {
loadSessionStore, loadSessionStore,
resolveStorePath, resolveStorePath,
type SessionEntry,
saveSessionStore, saveSessionStore,
} from "../../config/sessions.js"; } from "../../config/sessions.js";
import { import {
@@ -35,7 +36,7 @@ export function setAbortMemory(key: string, value: boolean): void {
} }
function resolveSessionEntryForKey( function resolveSessionEntryForKey(
store: Record<string, { sessionId: string; updatedAt: number }> | undefined, store: Record<string, SessionEntry> | undefined,
sessionKey: string | undefined, sessionKey: string | undefined,
) { ) {
if (!store || !sessionKey) return {}; if (!store || !sessionKey) return {};

View File

@@ -7,7 +7,10 @@ import type { ReplyDispatcher } from "./reply-dispatcher.js";
const mocks = vi.hoisted(() => ({ const mocks = vi.hoisted(() => ({
routeReply: vi.fn(async () => ({ ok: true, messageId: "mock" })), routeReply: vi.fn(async () => ({ ok: true, messageId: "mock" })),
tryFastAbortFromMessage: vi.fn(async () => ({ handled: false, aborted: false })), tryFastAbortFromMessage: vi.fn(async () => ({
handled: false,
aborted: false,
})),
})); }));
vi.mock("./route-reply.js", () => ({ vi.mock("./route-reply.js", () => ({

19
src/media/parse.test.ts Normal file
View File

@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { splitMediaFromOutput } from "./parse.js";
describe("splitMediaFromOutput", () => {
it("detects audio_as_voice tag and strips it", () => {
const result = splitMediaFromOutput("Hello [[audio_as_voice]] world");
expect(result.audioAsVoice).toBe(true);
expect(result.text).toBe("Hello world");
});
it("keeps audio_as_voice detection stable across calls", () => {
const input = "Hello [[audio_as_voice]]";
const first = splitMediaFromOutput(input);
const second = splitMediaFromOutput(input);
expect(first.audioAsVoice).toBe(true);
expect(second.audioAsVoice).toBe(true);
});
});

View File

@@ -34,6 +34,7 @@ function isInsideFence(
// Regex to detect [[audio_as_voice]] tag // Regex to detect [[audio_as_voice]] tag
const AUDIO_AS_VOICE_RE = /\[\[audio_as_voice\]\]/gi; const AUDIO_AS_VOICE_RE = /\[\[audio_as_voice\]\]/gi;
const AUDIO_AS_VOICE_TEST_RE = /\[\[audio_as_voice\]\]/i;
export function splitMediaFromOutput(raw: string): { export function splitMediaFromOutput(raw: string): {
text: string; text: string;
@@ -123,7 +124,7 @@ export function splitMediaFromOutput(raw: string): {
.trim(); .trim();
// Detect and strip [[audio_as_voice]] tag // Detect and strip [[audio_as_voice]] tag
const hasAudioAsVoice = AUDIO_AS_VOICE_RE.test(cleanedText); const hasAudioAsVoice = AUDIO_AS_VOICE_TEST_RE.test(cleanedText);
if (hasAudioAsVoice) { if (hasAudioAsVoice) {
cleanedText = cleanedText cleanedText = cleanedText
.replace(AUDIO_AS_VOICE_RE, "") .replace(AUDIO_AS_VOICE_RE, "")