diff --git a/src/agents/subagent-registry-helpers.ts b/src/agents/subagent-registry-helpers.ts index f8adb4e27b0..870ebc466e5 100644 --- a/src/agents/subagent-registry-helpers.ts +++ b/src/agents/subagent-registry-helpers.ts @@ -1,5 +1,6 @@ import fsSync, { promises as fs } from "node:fs"; import path from "node:path"; +import { DEFAULT_SUBAGENT_ARCHIVE_AFTER_MINUTES } from "../config/agent-limits.js"; import { getRuntimeConfig } from "../config/config.js"; import { loadSessionStore, @@ -345,7 +346,9 @@ export function reconcileOrphanedRestoredRuns(params: { export function resolveArchiveAfterMs(cfg?: OpenClawConfig) { const config = cfg ?? getRuntimeConfig(); - const minutes = config.agents?.defaults?.subagents?.archiveAfterMinutes ?? 60; + const minutes = + config.agents?.defaults?.subagents?.archiveAfterMinutes ?? + DEFAULT_SUBAGENT_ARCHIVE_AFTER_MINUTES; if (!Number.isFinite(minutes) || minutes < 0) { return undefined; } diff --git a/src/cli/config-cli.test.ts b/src/cli/config-cli.test.ts index 41556f10027..de55d65f5c9 100644 --- a/src/cli/config-cli.test.ts +++ b/src/cli/config-cli.test.ts @@ -950,6 +950,26 @@ describe("config cli", () => { expect(mockLog).toHaveBeenCalledWith("__OPENCLAW_REDACTED__"); }); + + it("prints materialized subagent archive default", async () => { + const resolved: OpenClawConfig = {}; + const config: OpenClawConfig = { + agents: { + defaults: { + maxConcurrent: 4, + subagents: { + maxConcurrent: 8, + archiveAfterMinutes: 60, + }, + }, + }, + }; + setSnapshot(resolved, config); + + await runConfigCommand(["config", "get", "agents.defaults.subagents.archiveAfterMinutes"]); + + expect(mockLog).toHaveBeenCalledWith("60"); + }); }); describe("config validate", () => { diff --git a/src/config/agent-limits.ts b/src/config/agent-limits.ts index 0e3d4bcd896..14b1b66aa45 100644 --- a/src/config/agent-limits.ts +++ b/src/config/agent-limits.ts @@ -3,6 +3,7 @@ import type { OpenClawConfig } from "./types.js"; export const DEFAULT_AGENT_MAX_CONCURRENT = 4; export const DEFAULT_SUBAGENT_MAX_CONCURRENT = 8; export const DEFAULT_SUBAGENT_MAX_CHILDREN_PER_AGENT = 5; +export const DEFAULT_SUBAGENT_ARCHIVE_AFTER_MINUTES = 60; // Keep depth-1 subagents as leaves unless config explicitly opts into nesting. export const DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH = 1; diff --git a/src/config/config.agent-concurrency-defaults.test.ts b/src/config/config.agent-concurrency-defaults.test.ts index f5bda63b0ed..3abaf1cc945 100644 --- a/src/config/config.agent-concurrency-defaults.test.ts +++ b/src/config/config.agent-concurrency-defaults.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { DEFAULT_AGENT_MAX_CONCURRENT, + DEFAULT_SUBAGENT_ARCHIVE_AFTER_MINUTES, DEFAULT_SUBAGENT_MAX_CONCURRENT, resolveAgentMaxConcurrent, resolveSubagentMaxConcurrent, @@ -48,5 +49,8 @@ describe("agent concurrency defaults", () => { expect(cfg.agents?.defaults?.maxConcurrent).toBe(DEFAULT_AGENT_MAX_CONCURRENT); expect(cfg.agents?.defaults?.subagents?.maxConcurrent).toBe(DEFAULT_SUBAGENT_MAX_CONCURRENT); + expect(cfg.agents?.defaults?.subagents?.archiveAfterMinutes).toBe( + DEFAULT_SUBAGENT_ARCHIVE_AFTER_MINUTES, + ); }); }); diff --git a/src/config/defaults.test.ts b/src/config/defaults.test.ts index 40a4a69c165..98a8fb21bfd 100644 --- a/src/config/defaults.test.ts +++ b/src/config/defaults.test.ts @@ -1,5 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js"; +import { + DEFAULT_AGENT_MAX_CONCURRENT, + DEFAULT_SUBAGENT_ARCHIVE_AFTER_MINUTES, + DEFAULT_SUBAGENT_MAX_CONCURRENT, +} from "./agent-limits.js"; import { applyAgentDefaults, applyContextPruningDefaults, @@ -112,5 +116,17 @@ describe("config defaults", () => { expect(next.agents?.defaults?.maxConcurrent).toBe(DEFAULT_AGENT_MAX_CONCURRENT); expect(next.agents?.defaults?.subagents?.maxConcurrent).toBe(DEFAULT_SUBAGENT_MAX_CONCURRENT); + expect(next.agents?.defaults?.subagents?.archiveAfterMinutes).toBe( + DEFAULT_SUBAGENT_ARCHIVE_AFTER_MINUTES, + ); + }); + + it("preserves explicit subagent archive default", () => { + const next = applyAgentDefaults({ + agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } }, + } as never); + + expect(next.agents?.defaults?.subagents?.archiveAfterMinutes).toBe(0); + expect(next.agents?.defaults?.subagents?.maxConcurrent).toBe(DEFAULT_SUBAGENT_MAX_CONCURRENT); }); }); diff --git a/src/config/defaults.ts b/src/config/defaults.ts index fe74df12282..9ffa5736c78 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -2,7 +2,11 @@ import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js"; import { normalizeConfiguredProviderCatalogModelId } from "../agents/model-ref-shared.js"; import { normalizeProviderId } from "../agents/provider-id.js"; import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; -import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js"; +import { + DEFAULT_AGENT_MAX_CONCURRENT, + DEFAULT_SUBAGENT_ARCHIVE_AFTER_MINUTES, + DEFAULT_SUBAGENT_MAX_CONCURRENT, +} from "./agent-limits.js"; import { normalizeAgentModelMapForConfig, normalizeAgentModelRefForConfig } from "./model-input.js"; import { applyProviderConfigDefaultsForConfig, @@ -394,7 +398,10 @@ export function applyAgentDefaults(cfg: OpenClawConfig): OpenClawConfig { const hasSubMax = typeof defaults?.subagents?.maxConcurrent === "number" && Number.isFinite(defaults.subagents.maxConcurrent); - if (hasMax && hasSubMax) { + const hasSubArchive = + typeof defaults?.subagents?.archiveAfterMinutes === "number" && + Number.isFinite(defaults.subagents.archiveAfterMinutes); + if (hasMax && hasSubMax && hasSubArchive) { return cfg; } @@ -410,6 +417,10 @@ export function applyAgentDefaults(cfg: OpenClawConfig): OpenClawConfig { nextSubagents.maxConcurrent = DEFAULT_SUBAGENT_MAX_CONCURRENT; mutated = true; } + if (!hasSubArchive) { + nextSubagents.archiveAfterMinutes = DEFAULT_SUBAGENT_ARCHIVE_AFTER_MINUTES; + mutated = true; + } if (!mutated) { return cfg;