From 79f340a410fc49d93d8ae3ac3ad9b4ae1098ed56 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 15 Jan 2026 07:46:51 +0000 Subject: [PATCH] chore: prep 2026.1.14 npm release --- CHANGELOG.md | 22 +- scripts/release-check.ts | 2 +- src/cli/browser-cli-extension.test.ts | 5 +- src/cli/cron-cli/register.wake.ts | 31 +- src/cli/hooks-cli.ts | 3 +- src/cli/memory-cli.ts | 3 +- src/cli/nodes-cli/register.ts | 3 +- src/cli/program/register.agent.ts | 3 +- src/cli/program/register.maintenance.ts | 6 +- src/cli/program/register.setup.ts | 3 +- .../register.status-health-sessions.ts | 6 +- src/commands/agent.ts | 529 +++++++++--------- src/commands/doctor-sandbox.ts | 1 - src/daemon/constants.ts | 4 +- src/daemon/schtasks.ts | 1 - src/entry.ts | 12 +- src/process/child-process-bridge.test.ts | 62 +- src/telegram/bot.ts | 5 +- 18 files changed, 353 insertions(+), 348 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b35f1dd87..6be86789a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2026.1.14 (unreleased) +## 2026.1.14 ### Changes - Docs: clarify per-agent auth stores, sandboxed skill binaries, and elevated semantics. @@ -23,6 +23,11 @@ - CLI/Docs: add per-command CLI doc pages and link them from `clawdbot --help`. - Browser: copy the installed Chrome extension path to clipboard after `clawdbot browser extension install/path`. - WhatsApp: add `channels.whatsapp.sendReadReceipts` to disable auto read receipts. (#882) — thanks @chrisrodz. +- Usage: add MiniMax coding plan usage tracking. +- Auth: label Claude Code CLI auth options. (#915) — thanks @SeanZoR. +- Docs: standardize Claude Code CLI naming across docs and prompts. (follow-up to #915) +- Telegram: add message delete action in the message tool. (#903) — thanks @sleontenko. +- Config: add `channels..configWrites` gating for channel-initiated config writes; migrate Slack channel IDs. ### Fixes - Gateway: forward termination signals to respawned CLI child processes to avoid orphaned systemd runs. (#933) — thanks @roshanasingh4. @@ -50,22 +55,11 @@ - Telegram: let control commands bypass per-chat sequentialization; always allow abort triggers. - Auto-reply: treat trailing `NO_REPLY` tokens as silent replies. - Config: prevent partial config writes from clobbering unrelated settings (base hash guard + merge patch for connection saves). - -## 2026.1.14 - -### Changes -- Usage: add MiniMax coding plan usage tracking. -- Auth: label Claude Code CLI auth options. (#915) — thanks @SeanZoR. -- Docs: standardize Claude Code CLI naming across docs and prompts. (follow-up to #915) -- Telegram: add message delete action in the message tool. (#903) — thanks @sleontenko. -- Config: add `channels..configWrites` gating for channel-initiated config writes; migrate Slack channel IDs. - -### Fixes - Sessions: return deep clones (`structuredClone`) so cached session entries can't be mutated. (#934) — thanks @ronak-guliani. - Heartbeat: keep `updatedAt` monotonic when restoring heartbeat sessions. (#934) — thanks @ronak-guliani. - Agent: clear run context after CLI runs (`clearAgentRunContext`) to avoid runaway contexts. (#934) — thanks @ronak-guliani. - - Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor. - - UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor. +- Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor. +- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor. - TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank. - TUI: add a bright spinner + elapsed time in the status line for send/stream/run states. - Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`). diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 9a22467d6..e6cf1a178 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -21,7 +21,7 @@ type PackageJson = { }; function runPackDry(): PackResult[] { - const raw = execSync("npm pack --dry-run --json", { + const raw = execSync("npm pack --dry-run --json --ignore-scripts", { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); diff --git a/src/cli/browser-cli-extension.test.ts b/src/cli/browser-cli-extension.test.ts index eab4b3f82..5ab8436f6 100644 --- a/src/cli/browser-cli-extension.test.ts +++ b/src/cli/browser-cli-extension.test.ts @@ -54,7 +54,10 @@ describe("browser extension install", () => { const program = new Command(); const browser = program.command("browser").option("--json", false); - registerBrowserExtensionCommands(browser, (cmd) => cmd.parent?.opts?.() as { json?: boolean }); + registerBrowserExtensionCommands( + browser, + (cmd) => cmd.parent?.opts?.() as { json?: boolean }, + ); await program.parseAsync(["browser", "extension", "path"], { from: "user" }); diff --git a/src/cli/cron-cli/register.wake.ts b/src/cli/cron-cli/register.wake.ts index 8ce750fb9..db9ce0600 100644 --- a/src/cli/cron-cli/register.wake.ts +++ b/src/cli/cron-cli/register.wake.ts @@ -18,19 +18,20 @@ export function registerWakeCommand(program: Command) { .addHelpText( "after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/wake", "docs.clawd.bot/cli/wake")}\n`, - ).action(async (opts: GatewayRpcOpts & { text?: string; mode?: string }) => { - try { - const result = await callGatewayFromCli( - "wake", - opts, - { mode: opts.mode, text: opts.text }, - { expectFinal: false }, - ); - if (opts.json) defaultRuntime.log(JSON.stringify(result, null, 2)); - else defaultRuntime.log("ok"); - } catch (err) { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - } - }); + ) + .action(async (opts: GatewayRpcOpts & { text?: string; mode?: string }) => { + try { + const result = await callGatewayFromCli( + "wake", + opts, + { mode: opts.mode, text: opts.text }, + { expectFinal: false }, + ); + if (opts.json) defaultRuntime.log(JSON.stringify(result, null, 2)); + else defaultRuntime.log("ok"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); } diff --git a/src/cli/hooks-cli.ts b/src/cli/hooks-cli.ts index 40c3a60b6..912c47359 100644 --- a/src/cli/hooks-cli.ts +++ b/src/cli/hooks-cli.ts @@ -27,7 +27,8 @@ export function registerHooksCli(program: Command) { .description("Webhook helpers and hook-based integrations") .addHelpText( "after", - () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/hooks", "docs.clawd.bot/cli/hooks")}\n`, + () => + `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/hooks", "docs.clawd.bot/cli/hooks")}\n`, ); const gmail = hooks.command("gmail").description("Gmail Pub/Sub hooks (via gogcli)"); diff --git a/src/cli/memory-cli.ts b/src/cli/memory-cli.ts index 977319d04..04cb32625 100644 --- a/src/cli/memory-cli.ts +++ b/src/cli/memory-cli.ts @@ -25,7 +25,8 @@ export function registerMemoryCli(program: Command) { .description("Memory search tools") .addHelpText( "after", - () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/memory", "docs.clawd.bot/cli/memory")}\n`, + () => + `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/memory", "docs.clawd.bot/cli/memory")}\n`, ); memory diff --git a/src/cli/nodes-cli/register.ts b/src/cli/nodes-cli/register.ts index 90fa1eee4..20dd61902 100644 --- a/src/cli/nodes-cli/register.ts +++ b/src/cli/nodes-cli/register.ts @@ -16,7 +16,8 @@ export function registerNodesCli(program: Command) { .description("Manage gateway-owned node pairing") .addHelpText( "after", - () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/nodes", "docs.clawd.bot/cli/nodes")}\n`, + () => + `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/nodes", "docs.clawd.bot/cli/nodes")}\n`, ); registerNodesStatusCommands(nodes); diff --git a/src/cli/program/register.agent.ts b/src/cli/program/register.agent.ts index b032a18c3..621debef1 100644 --- a/src/cli/program/register.agent.ts +++ b/src/cli/program/register.agent.ts @@ -68,7 +68,8 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.clawd.bot/cli/agent .description("Manage isolated agents (workspaces + auth + routing)") .addHelpText( "after", - () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/agents", "docs.clawd.bot/cli/agents")}\n`, + () => + `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/agents", "docs.clawd.bot/cli/agents")}\n`, ); agents diff --git a/src/cli/program/register.maintenance.ts b/src/cli/program/register.maintenance.ts index 8ff5b6649..ffa5ac46d 100644 --- a/src/cli/program/register.maintenance.ts +++ b/src/cli/program/register.maintenance.ts @@ -13,7 +13,8 @@ export function registerMaintenanceCommands(program: Command) { .description("Health checks + quick fixes for the gateway and channels") .addHelpText( "after", - () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/doctor", "docs.clawd.bot/cli/doctor")}\n`, + () => + `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/doctor", "docs.clawd.bot/cli/doctor")}\n`, ) .option("--no-workspace-suggestions", "Disable workspace memory system suggestions", false) .option("--yes", "Accept defaults without prompting", false) @@ -64,7 +65,8 @@ export function registerMaintenanceCommands(program: Command) { .description("Reset local config/state (keeps the CLI installed)") .addHelpText( "after", - () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/reset", "docs.clawd.bot/cli/reset")}\n`, + () => + `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/reset", "docs.clawd.bot/cli/reset")}\n`, ) .option("--scope ", "config|config+creds+sessions|full (default: interactive prompt)") .option("--yes", "Skip confirmation prompts", false) diff --git a/src/cli/program/register.setup.ts b/src/cli/program/register.setup.ts index 5c64a8414..189773d72 100644 --- a/src/cli/program/register.setup.ts +++ b/src/cli/program/register.setup.ts @@ -12,7 +12,8 @@ export function registerSetupCommand(program: Command) { .description("Initialize ~/.clawdbot/clawdbot.json and the agent workspace") .addHelpText( "after", - () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/setup", "docs.clawd.bot/cli/setup")}\n`, + () => + `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/setup", "docs.clawd.bot/cli/setup")}\n`, ) .option( "--workspace ", diff --git a/src/cli/program/register.status-health-sessions.ts b/src/cli/program/register.status-health-sessions.ts index b12b9242c..3db3dfdef 100644 --- a/src/cli/program/register.status-health-sessions.ts +++ b/src/cli/program/register.status-health-sessions.ts @@ -33,7 +33,8 @@ Examples: ) .addHelpText( "after", - () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/status", "docs.clawd.bot/cli/status")}\n`, + () => + `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/status", "docs.clawd.bot/cli/status")}\n`, ) .action(async (opts) => { const verbose = Boolean(opts.verbose || opts.debug); @@ -71,7 +72,8 @@ Examples: .option("--debug", "Alias for --verbose", false) .addHelpText( "after", - () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/health", "docs.clawd.bot/cli/health")}\n`, + () => + `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/health", "docs.clawd.bot/cli/health")}\n`, ) .action(async (opts) => { const verbose = Boolean(opts.verbose || opts.debug); diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 42cf3a6c8..afd3ea4c8 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -38,7 +38,11 @@ import { type SessionEntry, saveSessionStore, } from "../config/sessions.js"; -import { clearAgentRunContext, emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js"; +import { + clearAgentRunContext, + emitAgentEvent, + registerAgentRunContext, +} from "../infra/agent-events.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { applyVerboseOverride } from "../sessions/level-overrides.js"; import { resolveSendPolicy } from "../sessions/send-policy.js"; @@ -137,305 +141,308 @@ export async function agentCommand( } } - let resolvedThinkLevel = - thinkOnce ?? - thinkOverride ?? - persistedThinking ?? - (agentCfg?.thinkingDefault as ThinkLevel | undefined); - const resolvedVerboseLevel = - verboseOverride ?? persistedVerbose ?? (agentCfg?.verboseDefault as VerboseLevel | undefined); + let resolvedThinkLevel = + thinkOnce ?? + thinkOverride ?? + persistedThinking ?? + (agentCfg?.thinkingDefault as ThinkLevel | undefined); + const resolvedVerboseLevel = + verboseOverride ?? persistedVerbose ?? (agentCfg?.verboseDefault as VerboseLevel | undefined); - if (sessionKey) { - registerAgentRunContext(runId, { - sessionKey, - verboseLevel: resolvedVerboseLevel, - }); - } - - const needsSkillsSnapshot = isNewSession || !sessionEntry?.skillsSnapshot; - const skillsSnapshot = needsSkillsSnapshot - ? buildWorkspaceSkillSnapshot(workspaceDir, { config: cfg }) - : sessionEntry?.skillsSnapshot; - - if (skillsSnapshot && sessionStore && sessionKey && needsSkillsSnapshot) { - const current = sessionEntry ?? { - sessionId, - updatedAt: Date.now(), - }; - const next: SessionEntry = { - ...current, - sessionId, - updatedAt: Date.now(), - skillsSnapshot, - }; - sessionStore[sessionKey] = next; - await saveSessionStore(storePath, sessionStore); - sessionEntry = next; - } - - // Persist explicit /command overrides to the session store when we have a key. - if (sessionStore && sessionKey) { - const entry = sessionStore[sessionKey] ?? sessionEntry ?? { sessionId, updatedAt: Date.now() }; - const next: SessionEntry = { ...entry, sessionId, updatedAt: Date.now() }; - if (thinkOverride) { - if (thinkOverride === "off") delete next.thinkingLevel; - else next.thinkingLevel = thinkOverride; + if (sessionKey) { + registerAgentRunContext(runId, { + sessionKey, + verboseLevel: resolvedVerboseLevel, + }); } - applyVerboseOverride(next, verboseOverride); - sessionStore[sessionKey] = next; - await saveSessionStore(storePath, sessionStore); - } - const agentModelPrimary = resolveAgentModelPrimary(cfg, sessionAgentId); - const cfgForModelSelection = agentModelPrimary - ? { - ...cfg, - agents: { - ...cfg.agents, - defaults: { - ...cfg.agents?.defaults, - model: { - ...(typeof cfg.agents?.defaults?.model === "object" - ? cfg.agents.defaults.model - : undefined), - primary: agentModelPrimary, + const needsSkillsSnapshot = isNewSession || !sessionEntry?.skillsSnapshot; + const skillsSnapshot = needsSkillsSnapshot + ? buildWorkspaceSkillSnapshot(workspaceDir, { config: cfg }) + : sessionEntry?.skillsSnapshot; + + if (skillsSnapshot && sessionStore && sessionKey && needsSkillsSnapshot) { + const current = sessionEntry ?? { + sessionId, + updatedAt: Date.now(), + }; + const next: SessionEntry = { + ...current, + sessionId, + updatedAt: Date.now(), + skillsSnapshot, + }; + sessionStore[sessionKey] = next; + await saveSessionStore(storePath, sessionStore); + sessionEntry = next; + } + + // Persist explicit /command overrides to the session store when we have a key. + if (sessionStore && sessionKey) { + const entry = sessionStore[sessionKey] ?? + sessionEntry ?? { sessionId, updatedAt: Date.now() }; + const next: SessionEntry = { ...entry, sessionId, updatedAt: Date.now() }; + if (thinkOverride) { + if (thinkOverride === "off") delete next.thinkingLevel; + else next.thinkingLevel = thinkOverride; + } + applyVerboseOverride(next, verboseOverride); + sessionStore[sessionKey] = next; + await saveSessionStore(storePath, sessionStore); + } + + const agentModelPrimary = resolveAgentModelPrimary(cfg, sessionAgentId); + const cfgForModelSelection = agentModelPrimary + ? { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + model: { + ...(typeof cfg.agents?.defaults?.model === "object" + ? cfg.agents.defaults.model + : undefined), + primary: agentModelPrimary, + }, }, }, - }, - } - : cfg; + } + : cfg; - const { provider: defaultProvider, model: defaultModel } = resolveConfiguredModelRef({ - cfg: cfgForModelSelection, - defaultProvider: DEFAULT_PROVIDER, - defaultModel: DEFAULT_MODEL, - }); - let provider = defaultProvider; - let model = defaultModel; - const hasAllowlist = agentCfg?.models && Object.keys(agentCfg.models).length > 0; - const hasStoredOverride = Boolean(sessionEntry?.modelOverride || sessionEntry?.providerOverride); - const needsModelCatalog = hasAllowlist || hasStoredOverride; - let allowedModelKeys = new Set(); - let allowedModelCatalog: Awaited> = []; - let modelCatalog: Awaited> | null = null; - - if (needsModelCatalog) { - modelCatalog = await loadModelCatalog({ config: cfg }); - const allowed = buildAllowedModelSet({ - cfg, - catalog: modelCatalog, - defaultProvider, - defaultModel, + const { provider: defaultProvider, model: defaultModel } = resolveConfiguredModelRef({ + cfg: cfgForModelSelection, + defaultProvider: DEFAULT_PROVIDER, + defaultModel: DEFAULT_MODEL, }); - allowedModelKeys = allowed.allowedKeys; - allowedModelCatalog = allowed.allowedCatalog; - } + let provider = defaultProvider; + let model = defaultModel; + const hasAllowlist = agentCfg?.models && Object.keys(agentCfg.models).length > 0; + const hasStoredOverride = Boolean( + sessionEntry?.modelOverride || sessionEntry?.providerOverride, + ); + const needsModelCatalog = hasAllowlist || hasStoredOverride; + let allowedModelKeys = new Set(); + let allowedModelCatalog: Awaited> = []; + let modelCatalog: Awaited> | null = null; - if (sessionEntry && sessionStore && sessionKey && hasStoredOverride) { - const overrideProvider = sessionEntry.providerOverride?.trim() || defaultProvider; - const overrideModel = sessionEntry.modelOverride?.trim(); - if (overrideModel) { - const key = modelKey(overrideProvider, overrideModel); + if (needsModelCatalog) { + modelCatalog = await loadModelCatalog({ config: cfg }); + const allowed = buildAllowedModelSet({ + cfg, + catalog: modelCatalog, + defaultProvider, + defaultModel, + }); + allowedModelKeys = allowed.allowedKeys; + allowedModelCatalog = allowed.allowedCatalog; + } + + if (sessionEntry && sessionStore && sessionKey && hasStoredOverride) { + const overrideProvider = sessionEntry.providerOverride?.trim() || defaultProvider; + const overrideModel = sessionEntry.modelOverride?.trim(); + if (overrideModel) { + const key = modelKey(overrideProvider, overrideModel); + if ( + !isCliProvider(overrideProvider, cfg) && + allowedModelKeys.size > 0 && + !allowedModelKeys.has(key) + ) { + delete sessionEntry.providerOverride; + delete sessionEntry.modelOverride; + sessionEntry.updatedAt = Date.now(); + sessionStore[sessionKey] = sessionEntry; + await saveSessionStore(storePath, sessionStore); + } + } + } + + const storedProviderOverride = sessionEntry?.providerOverride?.trim(); + const storedModelOverride = sessionEntry?.modelOverride?.trim(); + if (storedModelOverride) { + const candidateProvider = storedProviderOverride || defaultProvider; + const key = modelKey(candidateProvider, storedModelOverride); if ( - !isCliProvider(overrideProvider, cfg) && - allowedModelKeys.size > 0 && - !allowedModelKeys.has(key) + isCliProvider(candidateProvider, cfg) || + allowedModelKeys.size === 0 || + allowedModelKeys.has(key) ) { - delete sessionEntry.providerOverride; - delete sessionEntry.modelOverride; + provider = candidateProvider; + model = storedModelOverride; + } + } + if (sessionEntry?.authProfileOverride) { + const store = ensureAuthProfileStore(); + const profile = store.profiles[sessionEntry.authProfileOverride]; + if (!profile || profile.provider !== provider) { + delete sessionEntry.authProfileOverride; + sessionEntry.updatedAt = Date.now(); + if (sessionStore && sessionKey) { + sessionStore[sessionKey] = sessionEntry; + await saveSessionStore(storePath, sessionStore); + } + } + } + + if (!resolvedThinkLevel) { + let catalogForThinking = modelCatalog ?? allowedModelCatalog; + if (!catalogForThinking || catalogForThinking.length === 0) { + modelCatalog = await loadModelCatalog({ config: cfg }); + catalogForThinking = modelCatalog; + } + resolvedThinkLevel = resolveThinkingDefault({ + cfg, + provider, + model, + catalog: catalogForThinking, + }); + } + if (resolvedThinkLevel === "xhigh" && !supportsXHighThinking(provider, model)) { + const explicitThink = Boolean(thinkOnce || thinkOverride); + if (explicitThink) { + throw new Error(`Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`); + } + resolvedThinkLevel = "high"; + if (sessionEntry && sessionStore && sessionKey && sessionEntry.thinkingLevel === "xhigh") { + sessionEntry.thinkingLevel = "high"; sessionEntry.updatedAt = Date.now(); sessionStore[sessionKey] = sessionEntry; await saveSessionStore(storePath, sessionStore); } } - } - - const storedProviderOverride = sessionEntry?.providerOverride?.trim(); - const storedModelOverride = sessionEntry?.modelOverride?.trim(); - if (storedModelOverride) { - const candidateProvider = storedProviderOverride || defaultProvider; - const key = modelKey(candidateProvider, storedModelOverride); - if ( - isCliProvider(candidateProvider, cfg) || - allowedModelKeys.size === 0 || - allowedModelKeys.has(key) - ) { - provider = candidateProvider; - model = storedModelOverride; - } - } - if (sessionEntry?.authProfileOverride) { - const store = ensureAuthProfileStore(); - const profile = store.profiles[sessionEntry.authProfileOverride]; - if (!profile || profile.provider !== provider) { - delete sessionEntry.authProfileOverride; - sessionEntry.updatedAt = Date.now(); - if (sessionStore && sessionKey) { - sessionStore[sessionKey] = sessionEntry; - await saveSessionStore(storePath, sessionStore); - } - } - } - - if (!resolvedThinkLevel) { - let catalogForThinking = modelCatalog ?? allowedModelCatalog; - if (!catalogForThinking || catalogForThinking.length === 0) { - modelCatalog = await loadModelCatalog({ config: cfg }); - catalogForThinking = modelCatalog; - } - resolvedThinkLevel = resolveThinkingDefault({ - cfg, - provider, - model, - catalog: catalogForThinking, + const sessionFile = resolveSessionFilePath(sessionId, sessionEntry, { + agentId: sessionAgentId, }); - } - if (resolvedThinkLevel === "xhigh" && !supportsXHighThinking(provider, model)) { - const explicitThink = Boolean(thinkOnce || thinkOverride); - if (explicitThink) { - throw new Error(`Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`); - } - resolvedThinkLevel = "high"; - if (sessionEntry && sessionStore && sessionKey && sessionEntry.thinkingLevel === "xhigh") { - sessionEntry.thinkingLevel = "high"; - sessionEntry.updatedAt = Date.now(); - sessionStore[sessionKey] = sessionEntry; - await saveSessionStore(storePath, sessionStore); - } - } - const sessionFile = resolveSessionFilePath(sessionId, sessionEntry, { - agentId: sessionAgentId, - }); - const startedAt = Date.now(); - let lifecycleEnded = false; + const startedAt = Date.now(); + let lifecycleEnded = false; - let result: Awaited>; - let fallbackProvider = provider; - let fallbackModel = model; - try { - const messageChannel = resolveMessageChannel(opts.messageChannel, opts.channel); - const fallbackResult = await runWithModelFallback({ - cfg, - provider, - model, - fallbacksOverride: resolveAgentModelFallbacksOverride(cfg, sessionAgentId), - run: (providerOverride, modelOverride) => { - if (isCliProvider(providerOverride, cfg)) { - const cliSessionId = getCliSessionId(sessionEntry, providerOverride); - return runCliAgent({ + let result: Awaited>; + let fallbackProvider = provider; + let fallbackModel = model; + try { + const messageChannel = resolveMessageChannel(opts.messageChannel, opts.channel); + const fallbackResult = await runWithModelFallback({ + cfg, + provider, + model, + fallbacksOverride: resolveAgentModelFallbacksOverride(cfg, sessionAgentId), + run: (providerOverride, modelOverride) => { + if (isCliProvider(providerOverride, cfg)) { + const cliSessionId = getCliSessionId(sessionEntry, providerOverride); + return runCliAgent({ + sessionId, + sessionKey, + sessionFile, + workspaceDir, + config: cfg, + prompt: body, + provider: providerOverride, + model: modelOverride, + thinkLevel: resolvedThinkLevel, + timeoutMs, + runId, + extraSystemPrompt: opts.extraSystemPrompt, + cliSessionId, + images: opts.images, + }); + } + return runEmbeddedPiAgent({ sessionId, sessionKey, + messageChannel, sessionFile, workspaceDir, config: cfg, + skillsSnapshot, prompt: body, + images: opts.images, provider: providerOverride, model: modelOverride, + authProfileId: sessionEntry?.authProfileOverride, thinkLevel: resolvedThinkLevel, + verboseLevel: resolvedVerboseLevel, timeoutMs, runId, + lane: opts.lane, + abortSignal: opts.abortSignal, extraSystemPrompt: opts.extraSystemPrompt, - cliSessionId, - images: opts.images, + agentDir, + onAgentEvent: (evt) => { + if ( + evt.stream === "lifecycle" && + typeof evt.data?.phase === "string" && + (evt.data.phase === "end" || evt.data.phase === "error") + ) { + lifecycleEnded = true; + } + emitAgentEvent({ + runId, + stream: evt.stream, + data: evt.data, + }); + }, }); - } - return runEmbeddedPiAgent({ - sessionId, - sessionKey, - messageChannel, - sessionFile, - workspaceDir, - config: cfg, - skillsSnapshot, - prompt: body, - images: opts.images, - provider: providerOverride, - model: modelOverride, - authProfileId: sessionEntry?.authProfileOverride, - thinkLevel: resolvedThinkLevel, - verboseLevel: resolvedVerboseLevel, - timeoutMs, + }, + }); + result = fallbackResult.result; + fallbackProvider = fallbackResult.provider; + fallbackModel = fallbackResult.model; + if (!lifecycleEnded) { + emitAgentEvent({ runId, - lane: opts.lane, - abortSignal: opts.abortSignal, - extraSystemPrompt: opts.extraSystemPrompt, - agentDir, - onAgentEvent: (evt) => { - if ( - evt.stream === "lifecycle" && - typeof evt.data?.phase === "string" && - (evt.data.phase === "end" || evt.data.phase === "error") - ) { - lifecycleEnded = true; - } - emitAgentEvent({ - runId, - stream: evt.stream, - data: evt.data, - }); + stream: "lifecycle", + data: { + phase: "end", + startedAt, + endedAt: Date.now(), + aborted: result.meta.aborted ?? false, }, }); - }, - }); - result = fallbackResult.result; - fallbackProvider = fallbackResult.provider; - fallbackModel = fallbackResult.model; - if (!lifecycleEnded) { - emitAgentEvent({ - runId, - stream: "lifecycle", - data: { - phase: "end", - startedAt, - endedAt: Date.now(), - aborted: result.meta.aborted ?? false, - }, - }); + } + } catch (err) { + if (!lifecycleEnded) { + emitAgentEvent({ + runId, + stream: "lifecycle", + data: { + phase: "error", + startedAt, + endedAt: Date.now(), + error: String(err), + }, + }); + } + throw err; } - } catch (err) { - if (!lifecycleEnded) { - emitAgentEvent({ - runId, - stream: "lifecycle", - data: { - phase: "error", - startedAt, - endedAt: Date.now(), - error: String(err), - }, - }); - } - throw err; - } - // Update token+model fields in the session store. - if (sessionStore && sessionKey) { - await updateSessionStoreAfterAgentRun({ + // Update token+model fields in the session store. + if (sessionStore && sessionKey) { + await updateSessionStoreAfterAgentRun({ + cfg, + contextTokensOverride: agentCfg?.contextTokens, + sessionId, + sessionKey, + storePath, + sessionStore, + defaultProvider: provider, + defaultModel: model, + fallbackProvider, + fallbackModel, + result, + }); + } + + const payloads = result.payloads ?? []; + return await deliverAgentCommandResult({ cfg, - contextTokensOverride: agentCfg?.contextTokens, - sessionId, - sessionKey, - storePath, - sessionStore, - defaultProvider: provider, - defaultModel: model, - fallbackProvider, - fallbackModel, + deps, + runtime, + opts, + sessionEntry, result, + payloads, }); - } - - const payloads = result.payloads ?? []; - return await deliverAgentCommandResult({ - cfg, - deps, - runtime, - opts, - sessionEntry, - result, - payloads, - }); } finally { clearAgentRunContext(runId); } diff --git a/src/commands/doctor-sandbox.ts b/src/commands/doctor-sandbox.ts index 515070b66..2b665c52c 100644 --- a/src/commands/doctor-sandbox.ts +++ b/src/commands/doctor-sandbox.ts @@ -163,7 +163,6 @@ async function handleMissingSandboxImage( } if (built) return; - } export async function maybeRepairSandboxImages( diff --git a/src/daemon/constants.ts b/src/daemon/constants.ts index c16f7657b..909d69d07 100644 --- a/src/daemon/constants.ts +++ b/src/daemon/constants.ts @@ -4,9 +4,7 @@ export const GATEWAY_SYSTEMD_SERVICE_NAME = "clawdbot-gateway"; export const GATEWAY_WINDOWS_TASK_NAME = "Clawdbot Gateway"; export const GATEWAY_SERVICE_MARKER = "clawdbot"; export const GATEWAY_SERVICE_KIND = "gateway"; -export const LEGACY_GATEWAY_LAUNCH_AGENT_LABELS = [ - "com.steipete.clawdbot.gateway", -]; +export const LEGACY_GATEWAY_LAUNCH_AGENT_LABELS = ["com.steipete.clawdbot.gateway"]; export const LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES: string[] = []; export const LEGACY_GATEWAY_WINDOWS_TASK_NAMES: string[] = []; diff --git a/src/daemon/schtasks.ts b/src/daemon/schtasks.ts index 6c986a271..7a4466972 100644 --- a/src/daemon/schtasks.ts +++ b/src/daemon/schtasks.ts @@ -28,7 +28,6 @@ function resolveTaskScriptPath(env: Record): string return path.join(home, `.clawdbot${suffix}`, "gateway.cmd"); } - function quoteCmdArg(value: string): string { if (!/[ \t"]/g.test(value)) return value; return `"${value.replace(/"/g, '\\"')}"`; diff --git a/src/entry.ts b/src/entry.ts index cac3ff4a8..b87af64b3 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -25,14 +25,10 @@ function ensureExperimentalWarningSuppressed(): boolean { process.env.CLAWDBOT_NODE_OPTIONS_READY = "1"; process.env.NODE_OPTIONS = `${nodeOptions} ${EXPERIMENTAL_WARNING_FLAG}`.trim(); - const child = spawn( - process.execPath, - [...process.execArgv, ...process.argv.slice(1)], - { - stdio: "inherit", - env: process.env, - }, - ); + const child = spawn(process.execPath, [...process.execArgv, ...process.argv.slice(1)], { + stdio: "inherit", + env: process.env, + }); attachChildProcessBridge(child); diff --git a/src/process/child-process-bridge.test.ts b/src/process/child-process-bridge.test.ts index 00d376c0f..128dae6de 100644 --- a/src/process/child-process-bridge.test.ts +++ b/src/process/child-process-bridge.test.ts @@ -76,40 +76,36 @@ describe("attachChildProcessBridge", () => { children.length = 0; }); - it( - "forwards SIGTERM to the wrapped child", - async () => { - const childPath = path.resolve(process.cwd(), "test/fixtures/child-process-bridge/child.js"); + it("forwards SIGTERM to the wrapped child", async () => { + const childPath = path.resolve(process.cwd(), "test/fixtures/child-process-bridge/child.js"); - const child = spawn(process.execPath, [childPath], { - stdio: ["ignore", "pipe", "inherit"], - env: process.env, + const child = spawn(process.execPath, [childPath], { + stdio: ["ignore", "pipe", "inherit"], + env: process.env, + }); + const { detach } = attachChildProcessBridge(child); + detachments.push(detach); + children.push(child); + + if (!child.stdout) throw new Error("expected stdout"); + const portLine = await waitForLine(child.stdout); + const port = Number(portLine); + expect(Number.isFinite(port)).toBe(true); + + expect(await canConnect(port)).toBe(true); + + // Simulate systemd sending SIGTERM to the parent process. + process.emit("SIGTERM"); + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error("timeout waiting for child exit")), 10_000); + child.once("exit", () => { + clearTimeout(timeout); + resolve(); }); - const { detach } = attachChildProcessBridge(child); - detachments.push(detach); - children.push(child); + }); - if (!child.stdout) throw new Error("expected stdout"); - const portLine = await waitForLine(child.stdout); - const port = Number(portLine); - expect(Number.isFinite(port)).toBe(true); - - expect(await canConnect(port)).toBe(true); - - // Simulate systemd sending SIGTERM to the parent process. - process.emit("SIGTERM"); - - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error("timeout waiting for child exit")), 10_000); - child.once("exit", () => { - clearTimeout(timeout); - resolve(); - }); - }); - - await new Promise((r) => setTimeout(r, 250)); - expect(await canConnect(port)).toBe(false); - }, - 20_000, - ); + await new Promise((r) => setTimeout(r, 250)); + expect(await canConnect(port)).toBe(false); + }, 20_000); }); diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index ecdb115de..31a345fcb 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -69,7 +69,10 @@ export function getTelegramSequentialKey(ctx: { const chatId = msg?.chat?.id ?? ctx.chat?.id; const rawText = msg?.text ?? msg?.caption; const botUsername = (ctx as { me?: { username?: string } }).me?.username; - if (rawText && isControlCommandMessage(rawText, undefined, botUsername ? { botUsername } : undefined)) { + if ( + rawText && + isControlCommandMessage(rawText, undefined, botUsername ? { botUsername } : undefined) + ) { if (typeof chatId === "number") return `telegram:${chatId}:control`; return "telegram:control"; }