mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-23 15:11:42 +00:00
fix: dedupe stale subagent rows in reply views
This commit is contained in:
@@ -998,6 +998,60 @@ describe("sessions tools", () => {
|
||||
expect(details.text).toContain("active (waiting on 1 child)");
|
||||
});
|
||||
|
||||
it("subagents list dedupes stale rows for the same child session", async () => {
|
||||
resetSubagentRegistryForTests();
|
||||
const now = Date.now();
|
||||
const childSessionKey = "agent:main:subagent:list-dedupe-worker";
|
||||
addSubagentRunForTests({
|
||||
runId: "run-list-current",
|
||||
childSessionKey,
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "current worker label",
|
||||
cleanup: "keep",
|
||||
createdAt: now - 60_000,
|
||||
startedAt: now - 60_000,
|
||||
});
|
||||
addSubagentRunForTests({
|
||||
runId: "run-list-stale",
|
||||
childSessionKey,
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "stale worker label",
|
||||
cleanup: "keep",
|
||||
createdAt: now - 120_000,
|
||||
startedAt: now - 120_000,
|
||||
endedAt: now - 90_000,
|
||||
outcome: { status: "ok" },
|
||||
});
|
||||
|
||||
const tool = createOpenClawTools({
|
||||
agentSessionKey: "agent:main:main",
|
||||
}).find((candidate) => candidate.name === "subagents");
|
||||
expect(tool).toBeDefined();
|
||||
if (!tool) {
|
||||
throw new Error("missing subagents tool");
|
||||
}
|
||||
|
||||
const result = await tool.execute("call-subagents-list-dedupe", { action: "list" });
|
||||
const details = result.details as {
|
||||
status?: string;
|
||||
active?: Array<{ runId?: string }>;
|
||||
recent?: Array<{ runId?: string }>;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
expect(details.status).toBe("ok");
|
||||
expect(details.active).toEqual([
|
||||
expect.objectContaining({
|
||||
runId: "run-list-current",
|
||||
}),
|
||||
]);
|
||||
expect(details.recent?.find((entry) => entry.runId === "run-list-stale")).toBeFalsy();
|
||||
expect(details.text).toContain("current worker label");
|
||||
expect(details.text).not.toContain("stale worker label");
|
||||
});
|
||||
|
||||
it("subagents list usage separates io tokens from prompt/cache", async () => {
|
||||
resetSubagentRegistryForTests();
|
||||
const now = Date.now();
|
||||
|
||||
@@ -272,6 +272,15 @@ export function buildSubagentList(params: {
|
||||
}): BuiltSubagentList {
|
||||
const now = Date.now();
|
||||
const recentCutoff = now - params.recentMinutes * 60_000;
|
||||
const dedupedRuns: SubagentRunRecord[] = [];
|
||||
const seenChildSessionKeys = new Set<string>();
|
||||
for (const entry of sortSubagentRuns(params.runs)) {
|
||||
if (seenChildSessionKeys.has(entry.childSessionKey)) {
|
||||
continue;
|
||||
}
|
||||
seenChildSessionKeys.add(entry.childSessionKey);
|
||||
dedupedRuns.push(entry);
|
||||
}
|
||||
const cache = new Map<string, Record<string, SessionEntry>>();
|
||||
const pendingDescendantCount = createPendingDescendantCounter();
|
||||
let index = 1;
|
||||
@@ -316,10 +325,10 @@ export function buildSubagentList(params: {
|
||||
index += 1;
|
||||
return view;
|
||||
};
|
||||
const active = params.runs
|
||||
const active = dedupedRuns
|
||||
.filter((entry) => isActiveSubagentRun(entry, pendingDescendantCount))
|
||||
.map((entry) => buildListEntry(entry, getSubagentSessionRuntimeMs(entry, now) ?? 0));
|
||||
const recent = params.runs
|
||||
const recent = dedupedRuns
|
||||
.filter(
|
||||
(entry) =>
|
||||
!isActiveSubagentRun(entry, pendingDescendantCount) &&
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { listBySessionMock } = vi.hoisted(() => ({
|
||||
listBySessionMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../infra/outbound/session-binding-service.js", () => ({
|
||||
getSessionBindingService: () => ({
|
||||
listBySession: listBySessionMock,
|
||||
}),
|
||||
}));
|
||||
|
||||
import { handleSubagentsAgentsAction } from "./action-agents.js";
|
||||
|
||||
describe("handleSubagentsAgentsAction", () => {
|
||||
it("dedupes stale bound rows for the same child session", () => {
|
||||
const childSessionKey = "agent:main:subagent:worker";
|
||||
listBySessionMock.mockImplementation((sessionKey: string) =>
|
||||
sessionKey === childSessionKey
|
||||
? [
|
||||
{
|
||||
bindingId: "binding-1",
|
||||
targetSessionKey: childSessionKey,
|
||||
targetKind: "subagent",
|
||||
conversation: {
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
conversationId: "thread-1",
|
||||
},
|
||||
status: "active",
|
||||
boundAt: Date.now() - 20_000,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
|
||||
const result = handleSubagentsAgentsAction({
|
||||
params: {
|
||||
ctx: {
|
||||
Provider: "discord",
|
||||
Surface: "discord",
|
||||
},
|
||||
command: {
|
||||
channel: "discord",
|
||||
},
|
||||
},
|
||||
requesterKey: "agent:main:main",
|
||||
runs: [
|
||||
{
|
||||
runId: "run-current",
|
||||
childSessionKey,
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "current worker label",
|
||||
cleanup: "keep",
|
||||
createdAt: Date.now() - 10_000,
|
||||
startedAt: Date.now() - 10_000,
|
||||
},
|
||||
{
|
||||
runId: "run-stale",
|
||||
childSessionKey,
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "stale worker label",
|
||||
cleanup: "keep",
|
||||
createdAt: Date.now() - 20_000,
|
||||
startedAt: Date.now() - 20_000,
|
||||
endedAt: Date.now() - 15_000,
|
||||
outcome: { status: "ok" },
|
||||
},
|
||||
],
|
||||
restTokens: [],
|
||||
} as never);
|
||||
|
||||
expect(result.reply?.text).toContain("current worker label");
|
||||
expect(result.reply?.text).not.toContain("stale worker label");
|
||||
});
|
||||
});
|
||||
@@ -46,15 +46,22 @@ export function handleSubagentsAgentsAction(ctx: SubagentsCommandContext): Comma
|
||||
return resolved;
|
||||
};
|
||||
|
||||
const visibleRuns = sortSubagentRuns(runs).filter((entry) => {
|
||||
if (!entry.endedAt) {
|
||||
return true;
|
||||
const visibleRuns: typeof runs = [];
|
||||
const seenChildSessionKeys = new Set<string>();
|
||||
for (const entry of sortSubagentRuns(runs)) {
|
||||
if (seenChildSessionKeys.has(entry.childSessionKey)) {
|
||||
continue;
|
||||
}
|
||||
if (countPendingDescendantRuns(entry.childSessionKey) > 0) {
|
||||
return true;
|
||||
const visible =
|
||||
!entry.endedAt ||
|
||||
countPendingDescendantRuns(entry.childSessionKey) > 0 ||
|
||||
resolveSessionBindings(entry.childSessionKey).length > 0;
|
||||
if (!visible) {
|
||||
continue;
|
||||
}
|
||||
return resolveSessionBindings(entry.childSessionKey).length > 0;
|
||||
});
|
||||
seenChildSessionKeys.add(entry.childSessionKey);
|
||||
visibleRuns.push(entry);
|
||||
}
|
||||
|
||||
const lines = ["agents:", "-----"];
|
||||
if (visibleRuns.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user