diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fdb94ec9..17d774c8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ - 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 - 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. - 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). diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index e36b58e63..b65e4cd3f 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -776,6 +776,7 @@ export async function compactEmbeddedPiSession(params: { const enqueueGlobal = params.enqueue ?? ((task, opts) => enqueueCommandInLane(globalLane, task, opts)); + const runAbortController = new AbortController(); return enqueueCommandInLane(sessionLane, () => enqueueGlobal(async () => { const resolvedWorkspace = resolveUserPath(params.workspaceDir); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index b330506dc..e68b530d7 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -651,7 +651,9 @@ export function createClawdbotCodingTools(options?: { // Without this, some providers (notably OpenAI) will reject root-level union schemas. const normalized = subagentFiltered.map(normalizeToolParameters); const withAbort = options?.abortSignal - ? normalized.map((tool) => wrapToolWithAbortSignal(tool, options.abortSignal)) + ? normalized.map((tool) => + wrapToolWithAbortSignal(tool, options.abortSignal), + ) : normalized; // Anthropic blocks specific lowercase tool names (bash, read, write, edit) with OAuth tokens. diff --git a/src/auto-reply/reply/abort.ts b/src/auto-reply/reply/abort.ts index 7f9fefa05..690b266d9 100644 --- a/src/auto-reply/reply/abort.ts +++ b/src/auto-reply/reply/abort.ts @@ -3,6 +3,7 @@ import type { ClawdbotConfig } from "../../config/config.js"; import { loadSessionStore, resolveStorePath, + type SessionEntry, saveSessionStore, } from "../../config/sessions.js"; import { @@ -35,7 +36,7 @@ export function setAbortMemory(key: string, value: boolean): void { } function resolveSessionEntryForKey( - store: Record | undefined, + store: Record | undefined, sessionKey: string | undefined, ) { if (!store || !sessionKey) return {}; diff --git a/src/auto-reply/reply/dispatch-from-config.test.ts b/src/auto-reply/reply/dispatch-from-config.test.ts index 557e804d1..26bb863cb 100644 --- a/src/auto-reply/reply/dispatch-from-config.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.test.ts @@ -7,7 +7,10 @@ import type { ReplyDispatcher } from "./reply-dispatcher.js"; const mocks = vi.hoisted(() => ({ 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", () => ({ diff --git a/src/media/parse.test.ts b/src/media/parse.test.ts new file mode 100644 index 000000000..f910d851c --- /dev/null +++ b/src/media/parse.test.ts @@ -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); + }); +}); diff --git a/src/media/parse.ts b/src/media/parse.ts index 77b4bd9f9..38f751992 100644 --- a/src/media/parse.ts +++ b/src/media/parse.ts @@ -34,6 +34,7 @@ function isInsideFence( // Regex to detect [[audio_as_voice]] tag const AUDIO_AS_VOICE_RE = /\[\[audio_as_voice\]\]/gi; +const AUDIO_AS_VOICE_TEST_RE = /\[\[audio_as_voice\]\]/i; export function splitMediaFromOutput(raw: string): { text: string; @@ -123,7 +124,7 @@ export function splitMediaFromOutput(raw: string): { .trim(); // 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) { cleanedText = cleanedText .replace(AUDIO_AS_VOICE_RE, "")