mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-24 07:31:44 +00:00
fix: keep active-descendant subagents visible in reply status
This commit is contained in:
73
src/auto-reply/reply/commands-status.test.ts
Normal file
73
src/auto-reply/reply/commands-status.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
addSubagentRunForTests,
|
||||
resetSubagentRegistryForTests,
|
||||
} from "../../agents/subagent-registry.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { buildStatusReply } from "./commands-status.js";
|
||||
import { buildCommandTestParams } from "./commands.test-harness.js";
|
||||
|
||||
describe("buildStatusReply subagent summary", () => {
|
||||
beforeEach(() => {
|
||||
resetSubagentRegistryForTests();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetSubagentRegistryForTests();
|
||||
});
|
||||
|
||||
it("counts ended orchestrators with active descendants as active", async () => {
|
||||
const parentKey = "agent:main:subagent:status-ended-parent";
|
||||
addSubagentRunForTests({
|
||||
runId: "run-status-ended-parent",
|
||||
childSessionKey: parentKey,
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "status orchestrator",
|
||||
cleanup: "keep",
|
||||
createdAt: Date.now() - 120_000,
|
||||
startedAt: Date.now() - 120_000,
|
||||
endedAt: Date.now() - 110_000,
|
||||
outcome: { status: "ok" },
|
||||
});
|
||||
addSubagentRunForTests({
|
||||
runId: "run-status-active-child",
|
||||
childSessionKey: "agent:main:subagent:status-ended-parent:subagent:child",
|
||||
requesterSessionKey: parentKey,
|
||||
requesterDisplayKey: "subagent:status-ended-parent",
|
||||
task: "status child still running",
|
||||
cleanup: "keep",
|
||||
createdAt: Date.now() - 60_000,
|
||||
startedAt: Date.now() - 60_000,
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
} as OpenClawConfig;
|
||||
const params = buildCommandTestParams("/status", cfg);
|
||||
const reply = await buildStatusReply({
|
||||
cfg,
|
||||
command: params.command,
|
||||
sessionEntry: params.sessionEntry,
|
||||
sessionKey: params.sessionKey,
|
||||
parentSessionKey: params.sessionKey,
|
||||
sessionScope: params.sessionScope,
|
||||
storePath: params.storePath,
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
contextTokens: 0,
|
||||
resolvedThinkLevel: params.resolvedThinkLevel,
|
||||
resolvedFastMode: false,
|
||||
resolvedVerboseLevel: params.resolvedVerboseLevel,
|
||||
resolvedReasoningLevel: params.resolvedReasoningLevel,
|
||||
resolvedElevatedLevel: params.resolvedElevatedLevel,
|
||||
resolveDefaultThinkingLevel: params.resolveDefaultThinkingLevel,
|
||||
isGroup: params.isGroup,
|
||||
defaultGroupActivation: params.defaultGroupActivation,
|
||||
});
|
||||
|
||||
expect(reply?.text).toContain("🤖 Subagents: 1 active");
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { resolveFastModeState } from "../../agents/fast-mode.js";
|
||||
import { resolveModelAuthLabel } from "../../agents/model-auth-label.js";
|
||||
import { listSubagentRunsForRequester } from "../../agents/subagent-registry.js";
|
||||
import {
|
||||
countPendingDescendantRuns,
|
||||
listSubagentRunsForRequester,
|
||||
} from "../../agents/subagent-registry.js";
|
||||
import {
|
||||
resolveInternalSessionKey,
|
||||
resolveMainSessionAlias,
|
||||
@@ -188,7 +191,9 @@ export async function buildStatusReply(params: {
|
||||
const runs = listSubagentRunsForRequester(requesterKey);
|
||||
const verboseEnabled = resolvedVerboseLevel && resolvedVerboseLevel !== "off";
|
||||
if (runs.length > 0) {
|
||||
const active = runs.filter((entry) => !entry.endedAt);
|
||||
const active = runs.filter(
|
||||
(entry) => !entry.endedAt || countPendingDescendantRuns(entry.childSessionKey) > 0,
|
||||
);
|
||||
const done = runs.length - active.length;
|
||||
if (verboseEnabled) {
|
||||
const labels = active
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { countPendingDescendantRuns } from "../../../agents/subagent-registry.js";
|
||||
import { getSessionBindingService } from "../../../infra/outbound/session-binding-service.js";
|
||||
import type { CommandHandlerResult } from "../commands-types.js";
|
||||
import { formatRunLabel, sortSubagentRuns } from "../subagents-utils.js";
|
||||
@@ -49,6 +50,9 @@ export function handleSubagentsAgentsAction(ctx: SubagentsCommandContext): Comma
|
||||
if (!entry.endedAt) {
|
||||
return true;
|
||||
}
|
||||
if (countPendingDescendantRuns(entry.childSessionKey) > 0) {
|
||||
return true;
|
||||
}
|
||||
return resolveSessionBindings(entry.childSessionKey).length > 0;
|
||||
});
|
||||
|
||||
|
||||
@@ -2243,6 +2243,99 @@ describe("handleCommands subagents", () => {
|
||||
expect(trackedRuns[0].runId).toBe("run-steer-ended-parent");
|
||||
});
|
||||
|
||||
it("lists ended orchestrators that are still waiting on active descendants in /agents", async () => {
|
||||
const parentKey = "agent:main:subagent:agents-ended-parent";
|
||||
const childKey = "agent:main:subagent:agents-ended-parent:subagent:child";
|
||||
const storePath = path.join(testWorkspaceDir, "sessions-subagents-agents-ended-parent.json");
|
||||
await updateSessionStore(storePath, (store) => {
|
||||
store[parentKey] = {
|
||||
sessionId: "agents-ended-parent-session",
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
store[childKey] = {
|
||||
sessionId: "agents-active-child-session",
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
});
|
||||
addSubagentRunForTests({
|
||||
runId: "run-agents-ended-parent",
|
||||
childSessionKey: parentKey,
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "orchestrate child workers",
|
||||
cleanup: "keep",
|
||||
createdAt: Date.now() - 120_000,
|
||||
startedAt: Date.now() - 120_000,
|
||||
endedAt: Date.now() - 110_000,
|
||||
outcome: { status: "ok" },
|
||||
});
|
||||
addSubagentRunForTests({
|
||||
runId: "run-agents-active-child",
|
||||
childSessionKey: childKey,
|
||||
requesterSessionKey: parentKey,
|
||||
requesterDisplayKey: "subagent:agents-ended-parent",
|
||||
task: "child worker still running",
|
||||
cleanup: "keep",
|
||||
createdAt: Date.now() - 60_000,
|
||||
startedAt: Date.now() - 60_000,
|
||||
});
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
} as OpenClawConfig;
|
||||
const params = buildParams("/agents", cfg);
|
||||
const result = await handleCommands(params);
|
||||
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("agents:");
|
||||
expect(result.reply?.text).toContain("orchestrate child workers");
|
||||
});
|
||||
|
||||
it("dedupes stale rows for the same child session in /agents", async () => {
|
||||
const childKey = "agent:main:subagent:agents-dedupe";
|
||||
const storePath = path.join(testWorkspaceDir, "sessions-subagents-agents-dedupe.json");
|
||||
await updateSessionStore(storePath, (store) => {
|
||||
store[childKey] = {
|
||||
sessionId: "agents-dedupe-session",
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
});
|
||||
addSubagentRunForTests({
|
||||
runId: "run-agents-dedupe-new",
|
||||
childSessionKey: childKey,
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "current worker label",
|
||||
cleanup: "keep",
|
||||
createdAt: Date.now() - 10_000,
|
||||
startedAt: Date.now() - 10_000,
|
||||
});
|
||||
addSubagentRunForTests({
|
||||
runId: "run-agents-dedupe-old",
|
||||
childSessionKey: childKey,
|
||||
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" },
|
||||
});
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
session: { store: storePath },
|
||||
} as OpenClawConfig;
|
||||
const params = buildParams("/agents", cfg);
|
||||
const result = await handleCommands(params);
|
||||
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("current worker label");
|
||||
expect(result.reply?.text).not.toContain("stale worker label");
|
||||
});
|
||||
|
||||
it("restores announce behavior when /steer replacement dispatch fails", async () => {
|
||||
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
||||
const request = opts as { method?: string };
|
||||
|
||||
Reference in New Issue
Block a user