fix(subagents): honor archiveAfterMinutes for session-mode reaping (#78263)

Merged via squash.

Prepared head SHA: b415467008
Co-authored-by: arniesaha <3646287+arniesaha@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
Arnab Saha
2026-05-06 19:24:09 -07:00
committed by GitHub
parent f2458d8828
commit 1c331a814a
3 changed files with 19 additions and 4 deletions

View File

@@ -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

View File

@@ -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",

View File

@@ -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<string, Record<string, SessionEntry>>();
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({