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 () => {
|
it("nested completion chains re-check child then parent deterministically", async () => {
|
||||||
const parentSessionKey = "agent:main:subagent:parent";
|
const parentSessionKey = "agent:main:subagent:parent";
|
||||||
const childSessionKey = "agent:main:subagent:parent:subagent:child";
|
const childSessionKey = "agent:main:subagent:parent:subagent:child";
|
||||||
|
|||||||
@@ -1098,6 +1098,24 @@ function buildDescendantWakeMessage(params: { findings: string; taskLabel: strin
|
|||||||
].join("\n");
|
].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: {
|
async function wakeSubagentRunAfterDescendants(params: {
|
||||||
runId: string;
|
runId: string;
|
||||||
childSessionKey: string;
|
childSessionKey: string;
|
||||||
@@ -1311,13 +1329,22 @@ export async function runSubagentAnnounceFlow(params: {
|
|||||||
childRunId: params.childRunId,
|
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({
|
const woke = await wakeSubagentRunAfterDescendants({
|
||||||
runId: params.childRunId,
|
runId: params.childRunId,
|
||||||
childSessionKey: params.childSessionKey,
|
childSessionKey: params.childSessionKey,
|
||||||
taskLabel: params.label || params.task || "task",
|
taskLabel: params.label || params.task || "task",
|
||||||
findings: childCompletionFindings,
|
findings: childCompletionFindings,
|
||||||
announceId,
|
announceId: wakeAnnounceId,
|
||||||
signal: params.signal,
|
signal: params.signal,
|
||||||
});
|
});
|
||||||
if (woke) {
|
if (woke) {
|
||||||
|
|||||||
@@ -47,9 +47,12 @@ export const handleSubagentsCommand: CommandHandler = async (params, allowTextCo
|
|||||||
return handleSubagentsHelpAction();
|
return handleSubagentsHelpAction();
|
||||||
}
|
}
|
||||||
|
|
||||||
const requesterKey = resolveRequesterSessionKey(params, {
|
const requesterKey =
|
||||||
preferCommandTarget: action === "spawn",
|
action === "spawn"
|
||||||
});
|
? resolveRequesterSessionKey(params, {
|
||||||
|
preferCommandTarget: true,
|
||||||
|
})
|
||||||
|
: resolveRequesterSessionKey(params);
|
||||||
if (!requesterKey) {
|
if (!requesterKey) {
|
||||||
return stopWithText("⚠️ Missing session key.");
|
return stopWithText("⚠️ Missing session key.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,7 +206,9 @@ export function resolveRequesterSessionKey(
|
|||||||
): string | undefined {
|
): string | undefined {
|
||||||
const commandTarget = params.ctx.CommandTargetSessionKey?.trim();
|
const commandTarget = params.ctx.CommandTargetSessionKey?.trim();
|
||||||
const commandSession = params.sessionKey?.trim();
|
const commandSession = params.sessionKey?.trim();
|
||||||
const raw = opts?.preferCommandTarget
|
const shouldPreferCommandTarget =
|
||||||
|
opts?.preferCommandTarget ?? params.ctx.CommandSource === "native";
|
||||||
|
const raw = shouldPreferCommandTarget
|
||||||
? commandTarget || commandSession
|
? commandTarget || commandSession
|
||||||
: commandSession || commandTarget;
|
: commandSession || commandTarget;
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
|
|||||||
@@ -1050,23 +1050,23 @@ describe("handleCommands subagents", () => {
|
|||||||
expect(result.reply?.text).not.toContain("after a short hard cutoff.");
|
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({
|
addSubagentRunForTests({
|
||||||
runId: "run-1",
|
runId: "run-target",
|
||||||
childSessionKey: "agent:main:subagent:abc",
|
childSessionKey: "agent:main:subagent:target",
|
||||||
requesterSessionKey: "agent:main:slack:slash:u1",
|
requesterSessionKey: "agent:main:main",
|
||||||
requesterDisplayKey: "agent:main:slack:slash:u1",
|
requesterDisplayKey: "agent:main:main",
|
||||||
task: "do thing",
|
task: "target run",
|
||||||
cleanup: "keep",
|
cleanup: "keep",
|
||||||
createdAt: 1000,
|
createdAt: 1000,
|
||||||
startedAt: 1000,
|
startedAt: 1000,
|
||||||
});
|
});
|
||||||
addSubagentRunForTests({
|
addSubagentRunForTests({
|
||||||
runId: "run-2",
|
runId: "run-slash",
|
||||||
childSessionKey: "agent:main:subagent:def",
|
childSessionKey: "agent:main:subagent:slash",
|
||||||
requesterSessionKey: "agent:main:slack:slash:u1",
|
requesterSessionKey: "agent:main:slack:slash:u1",
|
||||||
requesterDisplayKey: "agent:main:slack:slash:u1",
|
requesterDisplayKey: "agent:main:slack:slash:u1",
|
||||||
task: "another thing",
|
task: "slash run",
|
||||||
cleanup: "keep",
|
cleanup: "keep",
|
||||||
createdAt: 2000,
|
createdAt: 2000,
|
||||||
startedAt: 2000,
|
startedAt: 2000,
|
||||||
@@ -1083,8 +1083,8 @@ describe("handleCommands subagents", () => {
|
|||||||
const result = await handleCommands(params);
|
const result = await handleCommands(params);
|
||||||
expect(result.shouldContinue).toBe(false);
|
expect(result.shouldContinue).toBe(false);
|
||||||
expect(result.reply?.text).toContain("active subagents:");
|
expect(result.reply?.text).toContain("active subagents:");
|
||||||
expect(result.reply?.text).toContain("do thing");
|
expect(result.reply?.text).toContain("target run");
|
||||||
expect(result.reply?.text).not.toContain("\n\n2.");
|
expect(result.reply?.text).not.toContain("slash run");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("formats subagent usage with io and prompt/cache breakdown", async () => {
|
it("formats subagent usage with io and prompt/cache breakdown", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user