diff --git a/src/config/agent-limits.ts b/src/config/agent-limits.ts index 0a1733c038..5a0e4a42a2 100644 --- a/src/config/agent-limits.ts +++ b/src/config/agent-limits.ts @@ -1,2 +1,20 @@ +import type { ClawdbotConfig } from "./types.js"; + export const DEFAULT_AGENT_MAX_CONCURRENT = 4; export const DEFAULT_SUBAGENT_MAX_CONCURRENT = 8; + +export function resolveAgentMaxConcurrent(cfg?: ClawdbotConfig): number { + const raw = cfg?.agents?.defaults?.maxConcurrent; + if (typeof raw === "number" && Number.isFinite(raw)) { + return Math.max(1, Math.floor(raw)); + } + return DEFAULT_AGENT_MAX_CONCURRENT; +} + +export function resolveSubagentMaxConcurrent(cfg?: ClawdbotConfig): number { + const raw = cfg?.agents?.defaults?.subagents?.maxConcurrent; + if (typeof raw === "number" && Number.isFinite(raw)) { + return Math.max(1, Math.floor(raw)); + } + return DEFAULT_SUBAGENT_MAX_CONCURRENT; +} diff --git a/src/config/config.agent-concurrency-defaults.test.ts b/src/config/config.agent-concurrency-defaults.test.ts new file mode 100644 index 0000000000..2f53bc6056 --- /dev/null +++ b/src/config/config.agent-concurrency-defaults.test.ts @@ -0,0 +1,62 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { describe, expect, it, vi } from "vitest"; +import { + DEFAULT_AGENT_MAX_CONCURRENT, + DEFAULT_SUBAGENT_MAX_CONCURRENT, + resolveAgentMaxConcurrent, + resolveSubagentMaxConcurrent, +} from "./agent-limits.js"; +import { withTempHome } from "./test-helpers.js"; + +describe("agent concurrency defaults", () => { + it("resolves defaults when unset", () => { + expect(resolveAgentMaxConcurrent({})).toBe(DEFAULT_AGENT_MAX_CONCURRENT); + expect(resolveSubagentMaxConcurrent({})).toBe(DEFAULT_SUBAGENT_MAX_CONCURRENT); + }); + + it("resolves configured values", () => { + const cfg = { + agents: { + defaults: { + maxConcurrent: 6, + subagents: { maxConcurrent: 9 }, + }, + }, + }; + expect(resolveAgentMaxConcurrent(cfg)).toBe(6); + expect(resolveSubagentMaxConcurrent(cfg)).toBe(9); + }); + + it("clamps invalid values to at least 1", () => { + const cfg = { + agents: { + defaults: { + maxConcurrent: 0, + subagents: { maxConcurrent: -3 }, + }, + }, + }; + expect(resolveAgentMaxConcurrent(cfg)).toBe(1); + expect(resolveSubagentMaxConcurrent(cfg)).toBe(1); + }); + + it("injects defaults on load", async () => { + await withTempHome(async (home) => { + const configDir = path.join(home, ".clawdbot"); + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile( + path.join(configDir, "clawdbot.json"), + JSON.stringify({}, null, 2), + "utf-8", + ); + + vi.resetModules(); + const { loadConfig } = await import("./config.js"); + const cfg = loadConfig(); + + expect(cfg.agents?.defaults?.maxConcurrent).toBe(DEFAULT_AGENT_MAX_CONCURRENT); + expect(cfg.agents?.defaults?.subagents?.maxConcurrent).toBe(DEFAULT_SUBAGENT_MAX_CONCURRENT); + }); + }); +}); diff --git a/src/gateway/server-lanes.ts b/src/gateway/server-lanes.ts index 95de0586f6..9e05edd226 100644 --- a/src/gateway/server-lanes.ts +++ b/src/gateway/server-lanes.ts @@ -1,18 +1,9 @@ import type { loadConfig } from "../config/config.js"; -import { - DEFAULT_AGENT_MAX_CONCURRENT, - DEFAULT_SUBAGENT_MAX_CONCURRENT, -} from "../config/agent-limits.js"; +import { resolveAgentMaxConcurrent, resolveSubagentMaxConcurrent } from "../config/agent-limits.js"; import { setCommandLaneConcurrency } from "../process/command-queue.js"; export function applyGatewayLaneConcurrency(cfg: ReturnType) { setCommandLaneConcurrency("cron", cfg.cron?.maxConcurrentRuns ?? 1); - setCommandLaneConcurrency( - "main", - cfg.agents?.defaults?.maxConcurrent ?? DEFAULT_AGENT_MAX_CONCURRENT, - ); - setCommandLaneConcurrency( - "subagent", - cfg.agents?.defaults?.subagents?.maxConcurrent ?? DEFAULT_SUBAGENT_MAX_CONCURRENT, - ); + setCommandLaneConcurrency("main", resolveAgentMaxConcurrent(cfg)); + setCommandLaneConcurrency("subagent", resolveSubagentMaxConcurrent(cfg)); } diff --git a/src/gateway/server-reload-handlers.ts b/src/gateway/server-reload-handlers.ts index 31c643db88..6728228dbe 100644 --- a/src/gateway/server-reload-handlers.ts +++ b/src/gateway/server-reload-handlers.ts @@ -8,10 +8,7 @@ import { setGatewaySigusr1RestartPolicy, } from "../infra/restart.js"; import { setCommandLaneConcurrency } from "../process/command-queue.js"; -import { - DEFAULT_AGENT_MAX_CONCURRENT, - DEFAULT_SUBAGENT_MAX_CONCURRENT, -} from "../config/agent-limits.js"; +import { resolveAgentMaxConcurrent, resolveSubagentMaxConcurrent } from "../config/agent-limits.js"; import { isTruthyEnvValue } from "../infra/env.js"; import type { ChannelKind, GatewayReloadPlan } from "./config-reload.js"; import { resolveHooksConfig } from "./hooks.js"; @@ -131,14 +128,8 @@ export function createGatewayReloadHandlers(params: { } setCommandLaneConcurrency("cron", nextConfig.cron?.maxConcurrentRuns ?? 1); - setCommandLaneConcurrency( - "main", - nextConfig.agents?.defaults?.maxConcurrent ?? DEFAULT_AGENT_MAX_CONCURRENT, - ); - setCommandLaneConcurrency( - "subagent", - nextConfig.agents?.defaults?.subagents?.maxConcurrent ?? DEFAULT_SUBAGENT_MAX_CONCURRENT, - ); + setCommandLaneConcurrency("main", resolveAgentMaxConcurrent(nextConfig)); + setCommandLaneConcurrency("subagent", resolveSubagentMaxConcurrent(nextConfig)); if (plan.hotReasons.length > 0) { params.logReload.info(`config hot reload applied (${plan.hotReasons.join(", ")})`); diff --git a/src/telegram/monitor.ts b/src/telegram/monitor.ts index be70c6a337..24c8743df5 100644 --- a/src/telegram/monitor.ts +++ b/src/telegram/monitor.ts @@ -1,7 +1,7 @@ import { type RunOptions, run } from "@grammyjs/runner"; import type { ClawdbotConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js"; -import { DEFAULT_AGENT_MAX_CONCURRENT } from "../config/agent-limits.js"; +import { resolveAgentMaxConcurrent } from "../config/agent-limits.js"; import { computeBackoff, sleepWithAbort } from "../infra/backoff.js"; import { formatDurationMs } from "../infra/format-duration.js"; import type { RuntimeEnv } from "../runtime.js"; @@ -29,7 +29,7 @@ export type MonitorTelegramOpts = { export function createTelegramRunnerOptions(cfg: ClawdbotConfig): RunOptions { return { sink: { - concurrency: cfg.agents?.defaults?.maxConcurrent ?? DEFAULT_AGENT_MAX_CONCURRENT, + concurrency: resolveAgentMaxConcurrent(cfg), }, runner: { fetch: {