diff --git a/CHANGELOG.md b/CHANGELOG.md index 393a5583a1b..94d52c0e37b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -480,6 +480,7 @@ Docs: https://docs.openclaw.ai - Doctor/plugins: discover doctor contracts from load-path channel plugins during `openclaw doctor --fix`, so plugin-owned legacy config repair runs before validation. (#77477) Thanks @jalehman. - Dependencies: bump transitive `basic-ftp` to 5.3.1 so the runtime lockfile no longer includes the vulnerable 5.3.0 build flagged by the production dependency audit. (#78637) Thanks @sallyom. - Agents/compaction: clamp compaction summary reserve tokens to each model's output limit so high-context compaction no longer requests invalid `max_tokens` values. (#54392) Thanks @adzendo. +- Agents/subagents: have completed session-mode subagent registry rows honor `agents.defaults.subagents.archiveAfterMinutes` (default 60 minutes; same knob run-mode already uses for `archiveAtMs`) instead of a hardcoded 5-minute TTL, so `subagents list` and other registry-backed surfaces still show recently-completed runs and operators have one consistent retention knob across spawn modes. (#78263) Thanks @arniesaha. ## 2026.5.3-1 diff --git a/src/agents/subagent-registry.test.ts b/src/agents/subagent-registry.test.ts index 5a263580717..654f8511c4a 100644 --- a/src/agents/subagent-registry.test.ts +++ b/src/agents/subagent-registry.test.ts @@ -893,6 +893,14 @@ describe("subagent registry seam flow", () => { it("passes stored agentDir through swept context-engine cleanup paths", async () => { const now = Date.parse("2026-03-24T12:00:00Z"); + // Session-mode reaping now honors agents.defaults.subagents.archiveAfterMinutes + // (same knob run-mode uses for archiveAtMs). The default-config mock above sets + // archiveAfterMinutes: 0, which disables session-mode reaping; opt this test + // into a real retention window so the swept-cleanup path still fires. + mocks.getRuntimeConfig.mockReturnValueOnce({ + agents: { defaults: { subagents: { archiveAfterMinutes: 1 } } }, + session: { mainKey: "main", scope: "per-sender" as const }, + }); mod.addSubagentRunForTests({ runId: "run-session-swept-context-engine", childSessionKey: "agent:alt:session:child-session", diff --git a/src/agents/subagent-registry.ts b/src/agents/subagent-registry.ts index e8dcec65420..a9eb2da494f 100644 --- a/src/agents/subagent-registry.ts +++ b/src/agents/subagent-registry.ts @@ -36,6 +36,7 @@ import { reconcileOrphanedRestoredRuns, reconcileOrphanedRun, resolveAnnounceRetryDelayMs, + resolveArchiveAfterMs, resolveSubagentRunOrphanReason, resolveSubagentSessionStatus, safeRemoveAttachmentsDir, @@ -196,8 +197,6 @@ const LIFECYCLE_ERROR_RETRY_GRACE_MS = 15_000; * `timed out` completion right before the eventual success. */ const LIFECYCLE_TIMEOUT_RETRY_GRACE_MS = 15_000; -/** Absolute TTL for session-mode runs after cleanup completes (no archiveAtMs). */ -const SESSION_RUN_TTL_MS = 5 * 60_000; // 5 minutes /** Absolute TTL for orphaned pendingLifecycleError / pendingLifecycleTimeout entries. */ const PENDING_LIFECYCLE_TERMINAL_TTL_MS = 5 * 60_000; // 5 minutes /** Grace period before treating a "running" subagent without a live run context as stale. */ @@ -751,6 +750,7 @@ async function sweepSubagentRuns() { try { const now = Date.now(); const storeCache = new Map>(); + const sessionRetentionMs = resolveArchiveAfterMs(subagentRegistryDeps.getRuntimeConfig()); let mutated = false; for (const [runId, entry] of subagentRuns.entries()) { if (typeof entry.endedAt !== "number") { @@ -813,12 +813,18 @@ async function sweepSubagentRuns() { } } - // Session-mode runs have no archiveAtMs — apply absolute TTL after cleanup completes. + // Session-mode runs have no archiveAtMs because the child session is retained + // independently — but the registry row itself still needs to be reaped after + // cleanup, otherwise `subagents list` and other registry-backed surfaces grow + // without bound. Honor the same `agents.defaults.subagents.archiveAfterMinutes` + // window run-mode uses for `archiveAtMs`, so operators get one consistent + // retention knob (default 60 minutes; 0 disables session-mode reaping). // Use cleanupCompletedAt (not endedAt) to avoid interrupting deferred cleanup flows. if (!entry.archiveAtMs) { if ( + typeof sessionRetentionMs === "number" && typeof entry.cleanupCompletedAt === "number" && - now - entry.cleanupCompletedAt > SESSION_RUN_TTL_MS + now - entry.cleanupCompletedAt > sessionRetentionMs ) { clearPendingLifecycleError(runId); void notifyContextEngineSubagentEnded({