Compare commits

...

2 Commits

Author SHA1 Message Date
Peter Steinberger
b02c47043d fix: clarify talk mode simulator limits (#1358) (thanks @vignesh07) 2026-01-21 05:41:40 +00:00
Vignesh Natarajan
6f42c623b9 fix(ios): prevent Talk mode crash on simulator
- Disable Talk mode start on iOS simulator (no audio input)
- Validate audio input format before installing tap to avoid
  AVFAudio assertion crashes on misconfigured devices.

Tested:
- Launched app on iOS simulator and tapping Talk no longer crashes
  (shows error path instead).
2026-01-21 05:01:20 +00:00
8 changed files with 49 additions and 11 deletions

View File

@ -25,6 +25,7 @@ Docs: https://docs.clawd.bot
- UI: add copy-as-markdown with error feedback and drop legacy list view. (#1345) — thanks @bradleypriest.
- TUI: add input history (up/down) for submitted messages. (#1348) — thanks @vignesh07.
### Fixes
- iOS: explain Talk mode is unavailable on the simulator to avoid Speech live-audio crashes. (#1358) — thanks @vignesh07.
- Discovery: shorten Bonjour DNS-SD service type to `_clawdbot-gw._tcp` and update discovery clients/docs.
- Agents: preserve subagent announce thread/topic routing + queued replies across channels. (#1241) — thanks @gnarco.
- Agents: avoid treating timeout errors with "aborted" messages as user aborts, so model fallback still runs.

View File

@ -132,6 +132,13 @@ final class TalkModeManager: NSObject {
}
private func startRecognition() throws {
#if targetEnvironment(simulator)
// Apple Speech live-audio recognition is not supported on Simulator.
throw NSError(domain: "TalkMode", code: 2, userInfo: [
NSLocalizedDescriptionKey: "Talk mode is not supported on the iOS simulator (Speech live audio requires a device).",
])
#endif
self.stopRecognition()
self.speechRecognizer = SFSpeechRecognizer()
guard let recognizer = self.speechRecognizer else {
@ -146,6 +153,11 @@ final class TalkModeManager: NSObject {
let input = self.audioEngine.inputNode
let format = input.outputFormat(forBus: 0)
guard format.sampleRate > 0, format.channelCount > 0 else {
throw NSError(domain: "TalkMode", code: 3, userInfo: [
NSLocalizedDescriptionKey: "Invalid audio input format",
])
}
input.removeTap(onBus: 0)
let tapBlock = Self.makeAudioTapAppendCallback(request: request)
input.installTap(onBus: 0, bufferSize: 2048, format: format, block: tapBlock)

View File

@ -30,6 +30,13 @@ function buildRows(entries: Array<{ id: string; name?: string | undefined }>) {
}));
}
function formatEntry(entry: { id: string; name?: string; handle?: string }) {
const name = entry.name?.trim() ?? "";
const handle = entry.handle?.trim() ?? "";
const label = name || handle;
return label ? `${entry.id} - ${label}` : entry.id;
}
export function registerDirectoryCli(program: Command) {
const directory = program
.command("directory")

View File

@ -1,12 +1,15 @@
import { describe, expect, it, vi } from "vitest";
import { defaultRuntime } from "../runtime.js";
const { buildProgram } = await import("./program.js");
describe("dns cli", () => {
it("prints setup info (no apply)", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {});
const program = buildProgram();
await program.parseAsync(["dns", "setup"], { from: "user" });
expect(log).toHaveBeenCalledWith(expect.stringContaining("Domain:"));
const output = log.mock.calls.map((args) => args.join(" ")).join("\n");
expect(output).toContain("clawdbot.internal");
});
});

View File

@ -46,7 +46,16 @@ function formatNodeVersions(node: {
function parseSinceMs(raw: unknown, label: string): number | undefined {
if (raw === undefined || raw === null) return undefined;
const value = String(raw).trim();
let value = "";
if (typeof raw === "string") {
value = raw.trim();
} else if (typeof raw === "number") {
value = `${raw}`;
} else {
defaultRuntime.error(`${label}: expected a duration string`);
defaultRuntime.exit(1);
return undefined;
}
if (!value) return undefined;
try {
return parseDurationMs(value);

View File

@ -1,6 +1,8 @@
import { Command } from "commander";
import { describe, expect, it, vi } from "vitest";
import { defaultRuntime } from "../runtime.js";
const listChannelPairingRequests = vi.fn();
const approveChannelPairingCode = vi.fn();
const notifyPairingApproved = vi.fn();
@ -64,14 +66,16 @@ describe("pairing cli", () => {
},
]);
const log = vi.spyOn(console, "log").mockImplementation(() => {});
const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {});
const program = new Command();
program.name("test");
registerPairingCli(program);
await program.parseAsync(["pairing", "list", "--channel", "telegram"], {
from: "user",
});
expect(log).toHaveBeenCalledWith(expect.stringContaining("telegramUserId=123"));
const output = log.mock.calls.map((args) => args.join(" ")).join("\n");
expect(output).toContain("telegramUserId");
expect(output).toContain("123");
});
it("accepts channel as positional for list", async () => {
@ -124,14 +128,16 @@ describe("pairing cli", () => {
},
]);
const log = vi.spyOn(console, "log").mockImplementation(() => {});
const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {});
const program = new Command();
program.name("test");
registerPairingCli(program);
await program.parseAsync(["pairing", "list", "--channel", "discord"], {
from: "user",
});
expect(log).toHaveBeenCalledWith(expect.stringContaining("discordUserId=999"));
const output = log.mock.calls.map((args) => args.join(" ")).join("\n");
expect(output).toContain("discordUserId");
expect(output).toContain("999");
});
it("accepts channel as positional for approve (npm-run compatible)", async () => {
@ -146,7 +152,7 @@ describe("pairing cli", () => {
},
});
const log = vi.spyOn(console, "log").mockImplementation(() => {});
const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {});
const program = new Command();
program.name("test");
registerPairingCli(program);

View File

@ -13,7 +13,7 @@ type CommandOptions = Record<string, unknown>;
// --- Helpers ---
const SANDBOX_EXAMPLES = {
const SANDBOX_EXAMPLES: Record<string, [string, string][]> = {
main: [
["clawdbot sandbox list", "List all sandbox containers."],
["clawdbot sandbox list --browser", "List only browser containers."],
@ -40,7 +40,7 @@ const SANDBOX_EXAMPLES = {
["clawdbot sandbox explain --agent work", "Explain an agent sandbox."],
["clawdbot sandbox explain --json", "JSON output."],
],
} as const;
};
function createRunner(
commandFn: (opts: CommandOptions, runtime: typeof defaultRuntime) => Promise<void>,

View File

@ -187,7 +187,7 @@ export function onceMessage<T = unknown>(
// Full-suite runs can saturate the event loop (581+ files). Keep this high
// enough to avoid flaky RPC timeouts, but still fail fast when a response
// never arrives.
timeoutMs = 10_000,
timeoutMs = 15_000,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);