fix: steer ended subagent orchestrators with live descendants

This commit is contained in:
Tak Hoffman
2026-03-24 15:27:19 -05:00
parent e99c270684
commit caa718a554
3 changed files with 66 additions and 5 deletions

View File

@@ -643,7 +643,8 @@ export async function steerControlledSubagentRun(params: {
error: "Leaf subagents cannot control other sessions.",
};
}
if (params.entry.endedAt) {
const targetHasPendingDescendants = countPendingDescendantRuns(params.entry.childSessionKey) > 0;
if (params.entry.endedAt && !targetHasPendingDescendants) {
return {
status: "done",
runId: params.entry.runId,
@@ -660,7 +661,13 @@ export async function steerControlledSubagentRun(params: {
};
}
const currentEntry = getSubagentRunByChildSessionKey(params.entry.childSessionKey);
if (!currentEntry || currentEntry.runId !== params.entry.runId || currentEntry.endedAt) {
const currentHasPendingDescendants =
currentEntry && countPendingDescendantRuns(currentEntry.childSessionKey) > 0;
if (
!currentEntry ||
currentEntry.runId !== params.entry.runId ||
(currentEntry.endedAt && !currentHasPendingDescendants)
) {
return {
status: "done",
runId: params.entry.runId,

View File

@@ -33,9 +33,6 @@ export async function handleSubagentsSendAction(
if ("reply" in targetResolution) {
return targetResolution.reply;
}
if (steerRequested && targetResolution.entry.endedAt) {
return stopWithText(`${formatRunLabel(targetResolution.entry)} is already finished.`);
}
const controller = resolveCommandSubagentController(params, ctx.requesterKey);

View File

@@ -2186,6 +2186,63 @@ describe("handleCommands subagents", () => {
expect(trackedRuns[0].endedAt).toBeUndefined();
});
it("steers ended orchestrators that are still waiting on active descendants", async () => {
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string };
if (request.method === "agent") {
return { runId: "run-steer-ended-parent" };
}
return {};
});
const parentKey = "agent:main:subagent:orchestrator-ended";
const childKey = "agent:main:subagent:orchestrator-ended:subagent:child";
const storePath = path.join(testWorkspaceDir, "sessions-subagents-steer-ended-parent.json");
await updateSessionStore(storePath, (store) => {
store[parentKey] = {
sessionId: "ended-parent-session",
updatedAt: Date.now(),
};
store[childKey] = {
sessionId: "active-child-session",
updatedAt: Date.now(),
};
});
addSubagentRunForTests({
runId: "run-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-active-child",
childSessionKey: childKey,
requesterSessionKey: parentKey,
requesterDisplayKey: "subagent:orchestrator-ended",
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("/steer 1 regroup around the remaining child work", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("steered");
const trackedRuns = listSubagentRunsForRequester("agent:main:main");
expect(trackedRuns[0].runId).toBe("run-steer-ended-parent");
});
it("restores announce behavior when /steer replacement dispatch fails", async () => {
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string };