Skip to content

Commit e24704d

Browse files
committed
fix: dedupe active child session counts
1 parent eb40f0b commit e24704d

3 files changed

Lines changed: 96 additions & 1 deletion

File tree

src/agents/openclaw-tools.subagents.sessions-spawn-depth-limits.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,62 @@ describe("sessions_spawn depth + child limits", () => {
227227
});
228228
});
229229

230+
it("does not double-count restarted child sessions toward maxChildrenPerAgent", async () => {
231+
configOverride = {
232+
session: createPerSenderSessionConfig({ store: storeTemplatePath }),
233+
agents: {
234+
defaults: {
235+
subagents: {
236+
maxSpawnDepth: 2,
237+
maxChildrenPerAgent: 2,
238+
},
239+
},
240+
},
241+
};
242+
243+
const childSessionKey = "agent:main:subagent:restarted-child";
244+
addSubagentRunForTests({
245+
runId: "existing-old-run",
246+
childSessionKey,
247+
requesterSessionKey: "agent:main:subagent:parent",
248+
requesterDisplayKey: "agent:main:subagent:parent",
249+
task: "old orchestration run",
250+
cleanup: "keep",
251+
createdAt: Date.now() - 30_000,
252+
startedAt: Date.now() - 30_000,
253+
endedAt: Date.now() - 20_000,
254+
cleanupCompletedAt: undefined,
255+
});
256+
addSubagentRunForTests({
257+
runId: "existing-current-run",
258+
childSessionKey,
259+
requesterSessionKey: "agent:main:subagent:parent",
260+
requesterDisplayKey: "agent:main:subagent:parent",
261+
task: "current orchestration run",
262+
cleanup: "keep",
263+
createdAt: Date.now() - 10_000,
264+
startedAt: Date.now() - 10_000,
265+
});
266+
addSubagentRunForTests({
267+
runId: "existing-descendant-run",
268+
childSessionKey: `${childSessionKey}:subagent:leaf`,
269+
requesterSessionKey: childSessionKey,
270+
requesterDisplayKey: childSessionKey,
271+
task: "descendant still running",
272+
cleanup: "keep",
273+
createdAt: Date.now() - 5_000,
274+
startedAt: Date.now() - 5_000,
275+
});
276+
277+
const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:subagent:parent" });
278+
const result = await tool.execute("call-max-children-dedupe", { task: "hello" });
279+
280+
expect(result.details).toMatchObject({
281+
status: "accepted",
282+
runId: "run-depth",
283+
});
284+
});
285+
230286
it("does not use subagent maxConcurrent as a per-parent spawn gate", async () => {
231287
configOverride = {
232288
session: createPerSenderSessionConfig({ store: storeTemplatePath }),

src/agents/subagent-registry-queries.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,37 @@ describe("subagent registry query regressions", () => {
175175
expect(countActiveRunsForSessionFromRuns(runs, "agent:main:main")).toBe(0);
176176
});
177177

178+
it("dedupes stale and current rows for the same child session when counting active runs", () => {
179+
const childSessionKey = "agent:main:subagent:orch-restarted";
180+
const runs = toRunMap([
181+
makeRun({
182+
runId: "run-old",
183+
childSessionKey,
184+
requesterSessionKey: "agent:main:main",
185+
createdAt: 100,
186+
startedAt: 100,
187+
endedAt: 150,
188+
cleanupCompletedAt: undefined,
189+
}),
190+
makeRun({
191+
runId: "run-current",
192+
childSessionKey,
193+
requesterSessionKey: "agent:main:main",
194+
createdAt: 200,
195+
startedAt: 200,
196+
}),
197+
makeRun({
198+
runId: "run-descendant-active",
199+
childSessionKey: `${childSessionKey}:subagent:child`,
200+
requesterSessionKey: childSessionKey,
201+
createdAt: 210,
202+
startedAt: 210,
203+
}),
204+
]);
205+
206+
expect(countActiveRunsForSessionFromRuns(runs, "agent:main:main")).toBe(1);
207+
});
208+
178209
it("scopes direct child listings to the requester run window when requesterRunId is provided", () => {
179210
const requesterSessionKey = "agent:main:subagent:orchestrator";
180211
const runs = toRunMap([

src/agents/subagent-registry-queries.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,19 @@ export function countActiveRunsForSessionFromRuns(
136136
return pending;
137137
};
138138

139-
let count = 0;
139+
const latestByChildSessionKey = new Map<string, SubagentRunRecord>();
140140
for (const entry of runs.values()) {
141141
if (resolveControllerSessionKey(entry) !== key) {
142142
continue;
143143
}
144+
const existing = latestByChildSessionKey.get(entry.childSessionKey);
145+
if (!existing || entry.createdAt > existing.createdAt) {
146+
latestByChildSessionKey.set(entry.childSessionKey, entry);
147+
}
148+
}
149+
150+
let count = 0;
151+
for (const entry of latestByChildSessionKey.values()) {
144152
if (typeof entry.endedAt !== "number") {
145153
count += 1;
146154
continue;

0 commit comments

Comments
 (0)