diff --git a/CHANGELOG.md b/CHANGELOG.md index 2074cec49..dede88abb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ### Fixes - CLI/Daemon: add `clawdbot logs` tailing and improve restart/service hints across platforms. - Gateway/CLI/Doctor: tighten LAN bind auth checks, warn/migrate mis-keyed gateway tokens, and surface last gateway error when daemon looks running but the port is closed. +- CLI/Daemon: add `clawdbot daemon install --force` and expand daemon status guidance for config mismatches and probe targets. - Auto-reply: keep typing indicators alive during tool execution without changing typing-mode semantics. Thanks @thesash for PR #452. - macOS: harden Voice Wake tester/runtime (pause trigger, mic persistence, local-only tester) and keep transcript logs private. Thanks @xadenryan for PR #438. - macOS: preserve node bridge tunnel port override so remote nodes connect on the bridge port. Thanks @sircrumpet for PR #364. @@ -82,6 +83,7 @@ - Sub-agents: allow `sessions_spawn` model overrides and error on invalid models. Thanks @azade-c for PR #298. - Sub-agents: skip invalid model overrides with a warning and keep the run alive; tool exceptions now return tool errors instead of crashing the agent. - Sub-agents: allow `sessions_spawn` to target other agents via per-agent allowlists (`routing.agents..subagents.allowAgents`). +- Sub-agents: treat `sessions_spawn` allowlists as case-insensitive. - Tools: add `agents_list` to reveal allowed `sessions_spawn` targets for the current agent. - Sessions: forward explicit sessionKey through gateway/chat/node bridge to avoid sub-agent sessionId mixups. - Heartbeat: default interval 30m; clarified default prompt usage and HEARTBEAT.md template behavior. @@ -107,8 +109,7 @@ - Telegram: honor `/activation` session mode for group mention gating and clarify group activation docs. Thanks @julianengel for PR #377. - Telegram: isolate forum topic transcripts per thread and validate Gemini turn ordering in multi-topic sessions. Thanks @hsrvc for PR #407. - Telegram: render Telegram-safe HTML for outbound formatting and fall back to plain text on parse errors. Thanks @RandyVentures for PR #435. -- Telegram: force grammY to use native fetch under Bun for BAN compatibility (avoids TLS chain errors). -- Telegram: keep grammY default fetch on Node; only override under Bun to avoid Node 24 regressions. +- Telegram: prefer native fetch when available (Node 18+ + Bun) for BAN compatibility; still respects proxy override. - iMessage: ignore disconnect errors during shutdown (avoid unhandled promise rejections). Thanks @antons for PR #359. - Messages: stop defaulting ack reactions to 👀 when identity emoji is missing. - Auto-reply: require slash for control commands to avoid false triggers in normal text. diff --git a/src/agents/tools/sessions-spawn-tool.ts b/src/agents/tools/sessions-spawn-tool.ts index 1ad1235af..2379bfafd 100644 --- a/src/agents/tools/sessions-spawn-tool.ts +++ b/src/agents/tools/sessions-spawn-tool.ts @@ -106,15 +106,14 @@ export function createSessionsSpawnTool(opts?: { const allowAgents = resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ?? []; - const allowAny = allowAgents.some( - (value) => value.trim() === "*", - ); + const allowAny = allowAgents.some((value) => value.trim() === "*"); + const normalizedTargetId = targetAgentId.toLowerCase(); const allowSet = new Set( allowAgents .filter((value) => value.trim() && value.trim() !== "*") - .map((value) => normalizeAgentId(value)), + .map((value) => normalizeAgentId(value).toLowerCase()), ); - if (!allowAny && !allowSet.has(targetAgentId)) { + if (!allowAny && !allowSet.has(normalizedTargetId)) { const allowedText = allowAny ? "*" : allowSet.size > 0 diff --git a/src/cli/daemon-cli.ts b/src/cli/daemon-cli.ts index 552161684..e73a62ed0 100644 --- a/src/cli/daemon-cli.ts +++ b/src/cli/daemon-cli.ts @@ -130,6 +130,7 @@ export type DaemonInstallOptions = { port?: string | number; runtime?: string; token?: string; + force?: boolean; }; function parsePort(raw: unknown): number | null { @@ -192,6 +193,14 @@ function safeDaemonEnv(env: Record | undefined): string[] { return lines; } +function normalizeListenerAddress(raw: string): string { + let value = raw.trim(); + if (!value) return value; + value = value.replace(/^TCP\s+/i, ""); + value = value.replace(/\s+\(LISTEN\)\s*$/i, ""); + return value.trim(); +} + async function probeGatewayStatus(opts: { url: string; token?: string; @@ -330,7 +339,7 @@ async function gatherDaemonStatus(opts: { const serviceEnv = command?.environment ?? undefined; const mergedDaemonEnv = { ...(process.env as Record), - ...(serviceEnv ?? {}), + ...(serviceEnv ?? undefined), } satisfies Record; const cliConfigPath = resolveConfigPath( @@ -389,7 +398,7 @@ async function gatherDaemonStatus(opts: { const probeUrl = probeUrlOverride ?? `ws://${probeHost}:${daemonPort}`; const probeNote = !probeUrlOverride && bindMode === "lan" - ? "Local probe uses loopback (127.0.0.1); gateway bind=lan listens on 0.0.0.0." + ? "Local probe uses loopback (127.0.0.1). bind=lan listens on 0.0.0.0 (all interfaces); use a LAN IP for remote clients." : !probeUrlOverride && bindMode === "loopback" ? "Loopback-only gateway; only local clients can connect." : undefined; @@ -539,6 +548,9 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) { defaultRuntime.error( "Root cause: CLI and daemon are using different config paths (likely a profile/state-dir mismatch).", ); + defaultRuntime.error( + "Fix: rerun `clawdbot daemon install --force` from the same --profile / CLAWDBOT_STATE_DIR you expect.", + ); } } if (status.gateway) { @@ -610,7 +622,7 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) { const addrs = Array.from( new Set( status.port.listeners - .map((l) => l.address?.trim()) + .map((l) => (l.address ? normalizeListenerAddress(l.address) : "")) .filter((v): v is string => Boolean(v)), ), ); @@ -729,8 +741,11 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) { return; } if (loaded) { - defaultRuntime.log(`Gateway service already ${service.loadedText}.`); - return; + if (!opts.force) { + defaultRuntime.log(`Gateway service already ${service.loadedText}.`); + defaultRuntime.log("Reinstall with: clawdbot daemon install --force"); + return; + } } const devMode = @@ -896,6 +911,7 @@ export function registerDaemonCli(program: Command) { .option("--port ", "Gateway port") .option("--runtime ", "Daemon runtime (node|bun). Default: node") .option("--token ", "Gateway token (token auth)") + .option("--force", "Reinstall/overwrite if already installed", false) .action(async (opts) => { await runDaemonInstall(opts); }); diff --git a/src/infra/restart.ts b/src/infra/restart.ts index 1663d1f9c..3276a6923 100644 --- a/src/infra/restart.ts +++ b/src/infra/restart.ts @@ -19,14 +19,18 @@ function formatSpawnDetail(result: { }): string { const clean = (value: string | Buffer | null | undefined) => { const text = - typeof value === "string" - ? value - : value - ? value.toString() - : ""; + typeof value === "string" ? value : value ? value.toString() : ""; return text.replace(/\s+/g, " ").trim(); }; - if (result.error) return String(result.error); + if (result.error) { + if (result.error instanceof Error) return result.error.message; + if (typeof result.error === "string") return result.error; + try { + return JSON.stringify(result.error); + } catch { + return "unknown error"; + } + } const stderr = clean(result.stderr); if (stderr) return stderr; const stdout = clean(result.stdout); @@ -42,6 +46,9 @@ function normalizeSystemdUnit(raw?: string): string { } export function triggerClawdbotRestart(): RestartAttempt { + if (process.env.VITEST || process.env.NODE_ENV === "test") { + return { ok: true, method: "supervisor", detail: "test mode" }; + } const tried: string[] = []; if (process.platform !== "darwin") { if (process.platform === "linux") { diff --git a/src/telegram/fetch.ts b/src/telegram/fetch.ts index 6b3d6977f..fa94a4f93 100644 --- a/src/telegram/fetch.ts +++ b/src/telegram/fetch.ts @@ -1,13 +1,15 @@ -// Bun compatibility: force native fetch under Bun; keep grammY defaults on Node. +// Ensure native fetch is used when available (Bun + Node 18+). export function resolveTelegramFetch( proxyFetch?: typeof fetch, ): typeof fetch | undefined { if (proxyFetch) return proxyFetch; - const isBun = "Bun" in globalThis || Boolean(process?.versions?.bun); - if (!isBun) return undefined; const fetchImpl = globalThis.fetch; + const isBun = "Bun" in globalThis || Boolean(process?.versions?.bun); if (!fetchImpl) { - throw new Error("fetch is not available; set telegram.proxy in config"); + if (isBun) { + throw new Error("fetch is not available; set telegram.proxy in config"); + } + return undefined; } return fetchImpl; }