mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Fix subagent wake loops and native /subagents targeting
This commit is contained in:
@@ -2093,6 +2093,65 @@ describe("subagent announce formatting", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not re-wake an already woken run id", async () => {
|
||||
sessionStore = {
|
||||
"agent:main:subagent:parent": {
|
||||
sessionId: "session-parent",
|
||||
},
|
||||
};
|
||||
|
||||
subagentRegistryMock.countPendingDescendantRuns.mockReturnValue(0);
|
||||
subagentRegistryMock.listSubagentRunsForRequester.mockImplementation(
|
||||
(sessionKey: string, scope?: { requesterRunId?: string }) => {
|
||||
if (sessionKey !== "agent:main:subagent:parent") {
|
||||
return [];
|
||||
}
|
||||
if (scope?.requesterRunId !== "run-parent-phase-2:wake") {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
runId: "run-child-a",
|
||||
childSessionKey: "agent:main:subagent:parent:subagent:a",
|
||||
requesterSessionKey: "agent:main:subagent:parent",
|
||||
requesterDisplayKey: "parent",
|
||||
task: "child task a",
|
||||
label: "child-a",
|
||||
cleanup: "keep",
|
||||
createdAt: 10,
|
||||
endedAt: 20,
|
||||
cleanupCompletedAt: 21,
|
||||
frozenResultText: "result from child a",
|
||||
outcome: { status: "ok" },
|
||||
},
|
||||
];
|
||||
},
|
||||
);
|
||||
|
||||
const didAnnounce = await runSubagentAnnounceFlow({
|
||||
childSessionKey: "agent:main:subagent:parent",
|
||||
childRunId: "run-parent-phase-2:wake",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
...defaultOutcomeAnnounce,
|
||||
expectsCompletionMessage: true,
|
||||
wakeOnDescendantSettle: true,
|
||||
roundOneReply: "waiting for children",
|
||||
});
|
||||
|
||||
expect(didAnnounce).toBe(true);
|
||||
expect(subagentRegistryMock.replaceSubagentRunAfterSteer).not.toHaveBeenCalled();
|
||||
expect(agentSpy).toHaveBeenCalledTimes(1);
|
||||
const call = agentSpy.mock.calls[0]?.[0] as {
|
||||
params?: { sessionKey?: string; message?: string };
|
||||
};
|
||||
expect(call?.params?.sessionKey).toBe("agent:main:main");
|
||||
const message = call?.params?.message ?? "";
|
||||
expect(message).toContain("Child completion results:");
|
||||
expect(message).toContain("result from child a");
|
||||
expect(message).not.toContain("All pending descendants for that run have now settled");
|
||||
});
|
||||
|
||||
it("nested completion chains re-check child then parent deterministically", async () => {
|
||||
const parentSessionKey = "agent:main:subagent:parent";
|
||||
const childSessionKey = "agent:main:subagent:parent:subagent:child";
|
||||
|
||||
@@ -1098,6 +1098,24 @@ function buildDescendantWakeMessage(params: { findings: string; taskLabel: strin
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
const WAKE_RUN_SUFFIX = ":wake";
|
||||
|
||||
function stripWakeRunSuffixes(runId: string): string {
|
||||
let next = runId.trim();
|
||||
while (next.endsWith(WAKE_RUN_SUFFIX)) {
|
||||
next = next.slice(0, -WAKE_RUN_SUFFIX.length);
|
||||
}
|
||||
return next || runId.trim();
|
||||
}
|
||||
|
||||
function isWakeContinuationRun(runId: string): boolean {
|
||||
const trimmed = runId.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
return stripWakeRunSuffixes(trimmed) !== trimmed;
|
||||
}
|
||||
|
||||
async function wakeSubagentRunAfterDescendants(params: {
|
||||
runId: string;
|
||||
childSessionKey: string;
|
||||
@@ -1311,13 +1329,22 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
childRunId: params.childRunId,
|
||||
});
|
||||
|
||||
if (params.wakeOnDescendantSettle === true && childCompletionFindings?.trim()) {
|
||||
const childRunAlreadyWoken = isWakeContinuationRun(params.childRunId);
|
||||
if (
|
||||
params.wakeOnDescendantSettle === true &&
|
||||
childCompletionFindings?.trim() &&
|
||||
!childRunAlreadyWoken
|
||||
) {
|
||||
const wakeAnnounceId = buildAnnounceIdFromChildRun({
|
||||
childSessionKey: params.childSessionKey,
|
||||
childRunId: stripWakeRunSuffixes(params.childRunId),
|
||||
});
|
||||
const woke = await wakeSubagentRunAfterDescendants({
|
||||
runId: params.childRunId,
|
||||
childSessionKey: params.childSessionKey,
|
||||
taskLabel: params.label || params.task || "task",
|
||||
findings: childCompletionFindings,
|
||||
announceId,
|
||||
announceId: wakeAnnounceId,
|
||||
signal: params.signal,
|
||||
});
|
||||
if (woke) {
|
||||
|
||||
@@ -47,9 +47,12 @@ export const handleSubagentsCommand: CommandHandler = async (params, allowTextCo
|
||||
return handleSubagentsHelpAction();
|
||||
}
|
||||
|
||||
const requesterKey = resolveRequesterSessionKey(params, {
|
||||
preferCommandTarget: action === "spawn",
|
||||
});
|
||||
const requesterKey =
|
||||
action === "spawn"
|
||||
? resolveRequesterSessionKey(params, {
|
||||
preferCommandTarget: true,
|
||||
})
|
||||
: resolveRequesterSessionKey(params);
|
||||
if (!requesterKey) {
|
||||
return stopWithText("⚠️ Missing session key.");
|
||||
}
|
||||
|
||||
@@ -206,7 +206,9 @@ export function resolveRequesterSessionKey(
|
||||
): string | undefined {
|
||||
const commandTarget = params.ctx.CommandTargetSessionKey?.trim();
|
||||
const commandSession = params.sessionKey?.trim();
|
||||
const raw = opts?.preferCommandTarget
|
||||
const shouldPreferCommandTarget =
|
||||
opts?.preferCommandTarget ?? params.ctx.CommandSource === "native";
|
||||
const raw = shouldPreferCommandTarget
|
||||
? commandTarget || commandSession
|
||||
: commandSession || commandTarget;
|
||||
if (!raw) {
|
||||
|
||||
@@ -1050,23 +1050,23 @@ describe("handleCommands subagents", () => {
|
||||
expect(result.reply?.text).not.toContain("after a short hard cutoff.");
|
||||
});
|
||||
|
||||
it("lists subagents for the current command session over the target session", async () => {
|
||||
it("lists subagents for the command target session for native /subagents", async () => {
|
||||
addSubagentRunForTests({
|
||||
runId: "run-1",
|
||||
childSessionKey: "agent:main:subagent:abc",
|
||||
requesterSessionKey: "agent:main:slack:slash:u1",
|
||||
requesterDisplayKey: "agent:main:slack:slash:u1",
|
||||
task: "do thing",
|
||||
runId: "run-target",
|
||||
childSessionKey: "agent:main:subagent:target",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "agent:main:main",
|
||||
task: "target run",
|
||||
cleanup: "keep",
|
||||
createdAt: 1000,
|
||||
startedAt: 1000,
|
||||
});
|
||||
addSubagentRunForTests({
|
||||
runId: "run-2",
|
||||
childSessionKey: "agent:main:subagent:def",
|
||||
runId: "run-slash",
|
||||
childSessionKey: "agent:main:subagent:slash",
|
||||
requesterSessionKey: "agent:main:slack:slash:u1",
|
||||
requesterDisplayKey: "agent:main:slack:slash:u1",
|
||||
task: "another thing",
|
||||
task: "slash run",
|
||||
cleanup: "keep",
|
||||
createdAt: 2000,
|
||||
startedAt: 2000,
|
||||
@@ -1083,8 +1083,8 @@ describe("handleCommands subagents", () => {
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("active subagents:");
|
||||
expect(result.reply?.text).toContain("do thing");
|
||||
expect(result.reply?.text).not.toContain("\n\n2.");
|
||||
expect(result.reply?.text).toContain("target run");
|
||||
expect(result.reply?.text).not.toContain("slash run");
|
||||
});
|
||||
|
||||
it("formats subagent usage with io and prompt/cache breakdown", async () => {
|
||||
|
||||
Reference in New Issue
Block a user