From e24704d5eb8a0a68220677f603c418afe5ffe479 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:45:41 -0500 Subject: [PATCH] fix: dedupe active child session counts --- ...agents.sessions-spawn-depth-limits.test.ts | 56 +++++++++++++++++++ src/agents/subagent-registry-queries.test.ts | 31 ++++++++++ src/agents/subagent-registry-queries.ts | 10 +++- 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn-depth-limits.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn-depth-limits.test.ts index f41a43dd1a4..bdcfbbd2fc0 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn-depth-limits.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn-depth-limits.test.ts @@ -227,6 +227,62 @@ describe("sessions_spawn depth + child limits", () => { }); }); + it("does not double-count restarted child sessions toward maxChildrenPerAgent", async () => { + configOverride = { + session: createPerSenderSessionConfig({ store: storeTemplatePath }), + agents: { + defaults: { + subagents: { + maxSpawnDepth: 2, + maxChildrenPerAgent: 2, + }, + }, + }, + }; + + const childSessionKey = "agent:main:subagent:restarted-child"; + addSubagentRunForTests({ + runId: "existing-old-run", + childSessionKey, + requesterSessionKey: "agent:main:subagent:parent", + requesterDisplayKey: "agent:main:subagent:parent", + task: "old orchestration run", + cleanup: "keep", + createdAt: Date.now() - 30_000, + startedAt: Date.now() - 30_000, + endedAt: Date.now() - 20_000, + cleanupCompletedAt: undefined, + }); + addSubagentRunForTests({ + runId: "existing-current-run", + childSessionKey, + requesterSessionKey: "agent:main:subagent:parent", + requesterDisplayKey: "agent:main:subagent:parent", + task: "current orchestration run", + cleanup: "keep", + createdAt: Date.now() - 10_000, + startedAt: Date.now() - 10_000, + }); + addSubagentRunForTests({ + runId: "existing-descendant-run", + childSessionKey: `${childSessionKey}:subagent:leaf`, + requesterSessionKey: childSessionKey, + requesterDisplayKey: childSessionKey, + task: "descendant still running", + cleanup: "keep", + createdAt: Date.now() - 5_000, + startedAt: Date.now() - 5_000, + }); + + const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:subagent:parent" }); + const result = await tool.execute("call-max-children-dedupe", { task: "hello" }); + + expect(result.details).toMatchObject({ + status: "accepted", + runId: "run-depth", + }); + }); + it("does not use subagent maxConcurrent as a per-parent spawn gate", async () => { configOverride = { session: createPerSenderSessionConfig({ store: storeTemplatePath }), diff --git a/src/agents/subagent-registry-queries.test.ts b/src/agents/subagent-registry-queries.test.ts index 52e6b5c7c3e..ab1675bed68 100644 --- a/src/agents/subagent-registry-queries.test.ts +++ b/src/agents/subagent-registry-queries.test.ts @@ -175,6 +175,37 @@ describe("subagent registry query regressions", () => { expect(countActiveRunsForSessionFromRuns(runs, "agent:main:main")).toBe(0); }); + it("dedupes stale and current rows for the same child session when counting active runs", () => { + const childSessionKey = "agent:main:subagent:orch-restarted"; + const runs = toRunMap([ + makeRun({ + runId: "run-old", + childSessionKey, + requesterSessionKey: "agent:main:main", + createdAt: 100, + startedAt: 100, + endedAt: 150, + cleanupCompletedAt: undefined, + }), + makeRun({ + runId: "run-current", + childSessionKey, + requesterSessionKey: "agent:main:main", + createdAt: 200, + startedAt: 200, + }), + makeRun({ + runId: "run-descendant-active", + childSessionKey: `${childSessionKey}:subagent:child`, + requesterSessionKey: childSessionKey, + createdAt: 210, + startedAt: 210, + }), + ]); + + expect(countActiveRunsForSessionFromRuns(runs, "agent:main:main")).toBe(1); + }); + it("scopes direct child listings to the requester run window when requesterRunId is provided", () => { const requesterSessionKey = "agent:main:subagent:orchestrator"; const runs = toRunMap([ diff --git a/src/agents/subagent-registry-queries.ts b/src/agents/subagent-registry-queries.ts index 4ddf23bf2db..e5c38ac5ce5 100644 --- a/src/agents/subagent-registry-queries.ts +++ b/src/agents/subagent-registry-queries.ts @@ -136,11 +136,19 @@ export function countActiveRunsForSessionFromRuns( return pending; }; - let count = 0; + const latestByChildSessionKey = new Map(); for (const entry of runs.values()) { if (resolveControllerSessionKey(entry) !== key) { continue; } + const existing = latestByChildSessionKey.get(entry.childSessionKey); + if (!existing || entry.createdAt > existing.createdAt) { + latestByChildSessionKey.set(entry.childSessionKey, entry); + } + } + + let count = 0; + for (const entry of latestByChildSessionKey.values()) { if (typeof entry.endedAt !== "number") { count += 1; continue;