Skip to content

Commit c541cde

Browse files
committed
fix: dedupe restarted descendant session counts
1 parent e24704d commit c541cde

3 files changed

Lines changed: 107 additions & 0 deletions

File tree

src/agents/openclaw-tools.sessions.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,76 @@ describe("sessions tools", () => {
998998
expect(details.text).toContain("active (waiting on 1 child)");
999999
});
10001000

1001+
it("subagents list does not double-count restarted descendants on one child session", async () => {
1002+
resetSubagentRegistryForTests();
1003+
const now = Date.now();
1004+
const parentKey = "agent:main:subagent:orchestrator-restarted-child";
1005+
const childKey = `${parentKey}:subagent:worker`;
1006+
addSubagentRunForTests({
1007+
runId: "run-orchestrator-ended-restarted",
1008+
childSessionKey: parentKey,
1009+
requesterSessionKey: "agent:main:main",
1010+
requesterDisplayKey: "main",
1011+
task: "orchestrate restarted child worker",
1012+
cleanup: "keep",
1013+
createdAt: now - 5 * 60_000,
1014+
startedAt: now - 5 * 60_000,
1015+
endedAt: now - 4 * 60_000,
1016+
outcome: { status: "ok" },
1017+
});
1018+
addSubagentRunForTests({
1019+
runId: "run-restarted-child-stale",
1020+
childSessionKey: childKey,
1021+
requesterSessionKey: parentKey,
1022+
requesterDisplayKey: parentKey,
1023+
task: "stale child run",
1024+
cleanup: "keep",
1025+
createdAt: now - 90_000,
1026+
startedAt: now - 90_000,
1027+
endedAt: now - 70_000,
1028+
cleanupCompletedAt: undefined,
1029+
outcome: { status: "ok" },
1030+
});
1031+
addSubagentRunForTests({
1032+
runId: "run-restarted-child-current",
1033+
childSessionKey: childKey,
1034+
requesterSessionKey: parentKey,
1035+
requesterDisplayKey: parentKey,
1036+
task: "current child run",
1037+
cleanup: "keep",
1038+
createdAt: now - 60_000,
1039+
startedAt: now - 60_000,
1040+
});
1041+
1042+
const tool = createOpenClawTools({
1043+
agentSessionKey: "agent:main:main",
1044+
}).find((candidate) => candidate.name === "subagents");
1045+
expect(tool).toBeDefined();
1046+
if (!tool) {
1047+
throw new Error("missing subagents tool");
1048+
}
1049+
1050+
const result = await tool.execute("call-subagents-list-restarted-child", { action: "list" });
1051+
const details = result.details as {
1052+
status?: string;
1053+
active?: Array<{ runId?: string; status?: string; pendingDescendants?: number }>;
1054+
text?: string;
1055+
};
1056+
1057+
expect(details.status).toBe("ok");
1058+
expect(details.active).toEqual(
1059+
expect.arrayContaining([
1060+
expect.objectContaining({
1061+
runId: "run-orchestrator-ended-restarted",
1062+
status: "active (waiting on 1 child)",
1063+
pendingDescendants: 1,
1064+
}),
1065+
]),
1066+
);
1067+
expect(details.text).toContain("active (waiting on 1 child)");
1068+
expect(details.text).not.toContain("active (waiting on 2 children)");
1069+
});
1070+
10011071
it("subagents list dedupes stale rows for the same child session", async () => {
10021072
resetSubagentRegistryForTests();
10031073
const now = Date.now();

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,35 @@ describe("subagent registry query regressions", () => {
115115
expect(countPendingDescendantRunsFromRuns(runs, middleSessionKey)).toBe(1);
116116
});
117117

118+
it("dedupes restarted descendant rows for the same child session when counting pending work", () => {
119+
const parentSessionKey = "agent:main:subagent:parent-dedupe";
120+
const childSessionKey = `${parentSessionKey}:subagent:worker`;
121+
const runs = toRunMap([
122+
makeRun({
123+
runId: "run-child-stale",
124+
childSessionKey,
125+
requesterSessionKey: parentSessionKey,
126+
createdAt: 100,
127+
endedAt: 150,
128+
cleanupCompletedAt: undefined,
129+
}),
130+
makeRun({
131+
runId: "run-child-current",
132+
childSessionKey,
133+
requesterSessionKey: parentSessionKey,
134+
createdAt: 200,
135+
}),
136+
makeRun({
137+
runId: "run-grandchild-current",
138+
childSessionKey: `${childSessionKey}:subagent:leaf`,
139+
requesterSessionKey: childSessionKey,
140+
createdAt: 210,
141+
}),
142+
]);
143+
144+
expect(countPendingDescendantRunsFromRuns(runs, parentSessionKey)).toBe(2);
145+
});
146+
118147
it("regression excluding current run, countPendingDescendantRunsExcludingRun keeps sibling gating intact", () => {
119148
// Regression guard: excluding the currently announcing run must not hide sibling pending work.
120149
const runs = toRunMap([

src/agents/subagent-registry-queries.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,18 @@ function forEachDescendantRun(
176176
if (!requester) {
177177
continue;
178178
}
179+
const latestByChildSessionKey = new Map<string, [string, SubagentRunRecord]>();
179180
for (const [runId, entry] of runs.entries()) {
180181
if (entry.requesterSessionKey !== requester) {
181182
continue;
182183
}
184+
const childKey = entry.childSessionKey.trim();
185+
const existing = latestByChildSessionKey.get(childKey);
186+
if (!existing || entry.createdAt > existing[1].createdAt) {
187+
latestByChildSessionKey.set(childKey, [runId, entry]);
188+
}
189+
}
190+
for (const [runId, entry] of latestByChildSessionKey.values()) {
183191
visitor(runId, entry);
184192
const childKey = entry.childSessionKey.trim();
185193
if (!childKey || visited.has(childKey)) {

0 commit comments

Comments
 (0)