fix: let subagent kill cascade through ended parents

This commit is contained in:
Tak Hoffman
2026-03-24 15:13:07 -05:00
parent aaf2d6359e
commit f6a0cdc25a
3 changed files with 48 additions and 8 deletions

View File

@@ -515,7 +515,7 @@ export async function killControlledSubagentRun(params: {
};
}
const currentEntry = getSubagentRunByChildSessionKey(params.entry.childSessionKey);
if (!currentEntry || currentEntry.runId !== params.entry.runId || currentEntry.endedAt) {
if (!currentEntry || currentEntry.runId !== params.entry.runId) {
return {
status: "done" as const,
runId: params.entry.runId,
@@ -527,7 +527,7 @@ export async function killControlledSubagentRun(params: {
const killCache = new Map<string, Record<string, SessionEntry>>();
const stopResult = await killSubagentRun({
cfg: params.cfg,
entry: params.entry,
entry: currentEntry,
cache: killCache,
});
const seenChildSessionKeys = new Set<string>();

View File

@@ -3,7 +3,6 @@ import {
killControlledSubagentRun,
} from "../../../agents/subagent-control.js";
import type { CommandHandlerResult } from "../commands-types.js";
import { formatRunLabel } from "../subagents-utils.js";
import {
type SubagentsCommandContext,
COMMAND,
@@ -43,9 +42,6 @@ export async function handleSubagentsKillAction(
if ("reply" in targetResolution) {
return targetResolution.reply;
}
if (targetResolution.entry.endedAt) {
return stopWithText(`${formatRunLabel(targetResolution.entry)} is already finished.`);
}
const controller = resolveCommandSubagentController(params, requesterKey);
const result = await killControlledSubagentRun({

View File

@@ -107,8 +107,12 @@ vi.mock("./commands-context-report.js", () => ({
vi.resetModules();
const { addSubagentRunForTests, listSubagentRunsForRequester, resetSubagentRegistryForTests } =
await import("../../agents/subagent-registry.js");
const {
addSubagentRunForTests,
getSubagentRunByChildSessionKey,
listSubagentRunsForRequester,
resetSubagentRegistryForTests,
} = await import("../../agents/subagent-registry.js");
const { setDefaultChannelPluginRegistryForTests } =
await import("../../commands/channel-test-helpers.js");
const internalHooks = await import("../../hooks/internal-hooks.js");
@@ -1974,6 +1978,46 @@ describe("handleCommands subagents", () => {
expect(result.reply).toBeUndefined();
});
it("kills descendants when numeric target 1 is an ended orchestrator still waiting on children", async () => {
const now = Date.now();
const parentKey = "agent:main:subagent:orchestrator-ended";
const childKey = "agent:main:subagent:orchestrator-ended:subagent:worker";
addSubagentRunForTests({
runId: "run-orchestrator-ended",
childSessionKey: parentKey,
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
task: "orchestrate child workers",
cleanup: "keep",
createdAt: now - 120_000,
startedAt: now - 120_000,
endedAt: now - 110_000,
outcome: { status: "ok" },
});
addSubagentRunForTests({
runId: "run-orchestrator-child-active",
childSessionKey: childKey,
requesterSessionKey: parentKey,
requesterDisplayKey: "subagent:orchestrator-ended",
task: "child worker still running",
cleanup: "keep",
createdAt: now - 60_000,
startedAt: now - 60_000,
});
const cfg = {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig;
const params = buildParams("/kill 1", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply).toBeUndefined();
expect(getSubagentRunByChildSessionKey(childKey)?.endedAt).toBeTypeOf("number");
});
it("sends follow-up messages to finished subagents", async () => {
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: { runId?: string } };