mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 21:24:31 +00:00
fix(subagents): announce delivery with descendant gating, frozen result refresh, and cron retry (#35080)
Thanks @tyler6204
This commit is contained in:
@@ -15,6 +15,14 @@ let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfi
|
||||
scope: "per-sender",
|
||||
},
|
||||
};
|
||||
let requesterDepthResolver: (sessionKey?: string) => number = () => 0;
|
||||
let subagentSessionRunActive = true;
|
||||
let shouldIgnorePostCompletion = false;
|
||||
let pendingDescendantRuns = 0;
|
||||
let fallbackRequesterResolution: {
|
||||
requesterSessionKey: string;
|
||||
requesterOrigin?: { channel?: string; to?: string; accountId?: string };
|
||||
} | null = null;
|
||||
|
||||
vi.mock("../gateway/call.js", () => ({
|
||||
callGateway: vi.fn(async (request: GatewayCall) => {
|
||||
@@ -42,7 +50,7 @@ vi.mock("../config/sessions.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./subagent-depth.js", () => ({
|
||||
getSubagentDepthFromSessionStore: () => 0,
|
||||
getSubagentDepthFromSessionStore: (sessionKey?: string) => requesterDepthResolver(sessionKey),
|
||||
}));
|
||||
|
||||
vi.mock("./pi-embedded.js", () => ({
|
||||
@@ -53,9 +61,11 @@ vi.mock("./pi-embedded.js", () => ({
|
||||
|
||||
vi.mock("./subagent-registry.js", () => ({
|
||||
countActiveDescendantRuns: () => 0,
|
||||
countPendingDescendantRuns: () => 0,
|
||||
isSubagentSessionRunActive: () => true,
|
||||
resolveRequesterForChildSession: () => null,
|
||||
countPendingDescendantRuns: () => pendingDescendantRuns,
|
||||
listSubagentRunsForRequester: () => [],
|
||||
isSubagentSessionRunActive: () => subagentSessionRunActive,
|
||||
shouldIgnorePostCompletionAnnounceForSession: () => shouldIgnorePostCompletion,
|
||||
resolveRequesterForChildSession: () => fallbackRequesterResolution,
|
||||
}));
|
||||
|
||||
import { runSubagentAnnounceFlow } from "./subagent-announce.js";
|
||||
@@ -95,8 +105,8 @@ function setConfiguredAnnounceTimeout(timeoutMs: number): void {
|
||||
async function runAnnounceFlowForTest(
|
||||
childRunId: string,
|
||||
overrides: Partial<AnnounceFlowParams> = {},
|
||||
): Promise<void> {
|
||||
await runSubagentAnnounceFlow({
|
||||
): Promise<boolean> {
|
||||
return await runSubagentAnnounceFlow({
|
||||
...baseAnnounceFlowParams,
|
||||
childRunId,
|
||||
...overrides,
|
||||
@@ -114,6 +124,11 @@ describe("subagent announce timeout config", () => {
|
||||
configOverride = {
|
||||
session: defaultSessionConfig,
|
||||
};
|
||||
requesterDepthResolver = () => 0;
|
||||
subagentSessionRunActive = true;
|
||||
shouldIgnorePostCompletion = false;
|
||||
pendingDescendantRuns = 0;
|
||||
fallbackRequesterResolution = null;
|
||||
});
|
||||
|
||||
it("uses 60s timeout by default for direct announce agent call", async () => {
|
||||
@@ -135,7 +150,7 @@ describe("subagent announce timeout config", () => {
|
||||
expect(directAgentCall?.timeoutMs).toBe(90_000);
|
||||
});
|
||||
|
||||
it("honors configured announce timeout for completion direct send call", async () => {
|
||||
it("honors configured announce timeout for completion direct agent call", async () => {
|
||||
setConfiguredAnnounceTimeout(90_000);
|
||||
await runAnnounceFlowForTest("run-config-timeout-send", {
|
||||
requesterOrigin: {
|
||||
@@ -145,7 +160,93 @@ describe("subagent announce timeout config", () => {
|
||||
expectsCompletionMessage: true,
|
||||
});
|
||||
|
||||
const sendCall = findGatewayCall((call) => call.method === "send");
|
||||
expect(sendCall?.timeoutMs).toBe(90_000);
|
||||
const completionDirectAgentCall = findGatewayCall(
|
||||
(call) => call.method === "agent" && call.expectFinal === true,
|
||||
);
|
||||
expect(completionDirectAgentCall?.timeoutMs).toBe(90_000);
|
||||
});
|
||||
|
||||
it("regression, skips parent announce while descendants are still pending", async () => {
|
||||
requesterDepthResolver = () => 1;
|
||||
pendingDescendantRuns = 2;
|
||||
|
||||
const didAnnounce = await runAnnounceFlowForTest("run-pending-descendants", {
|
||||
requesterSessionKey: "agent:main:subagent:parent",
|
||||
requesterDisplayKey: "agent:main:subagent:parent",
|
||||
});
|
||||
|
||||
expect(didAnnounce).toBe(false);
|
||||
expect(
|
||||
findGatewayCall((call) => call.method === "agent" && call.expectFinal === true),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("regression, supports cron announceType without declaration order errors", async () => {
|
||||
const didAnnounce = await runAnnounceFlowForTest("run-announce-type", {
|
||||
announceType: "cron job",
|
||||
expectsCompletionMessage: true,
|
||||
requesterOrigin: { channel: "discord", to: "channel:cron" },
|
||||
});
|
||||
|
||||
expect(didAnnounce).toBe(true);
|
||||
const directAgentCall = findGatewayCall(
|
||||
(call) => call.method === "agent" && call.expectFinal === true,
|
||||
);
|
||||
const internalEvents =
|
||||
(directAgentCall?.params?.internalEvents as Array<{ announceType?: string }>) ?? [];
|
||||
expect(internalEvents[0]?.announceType).toBe("cron job");
|
||||
});
|
||||
|
||||
it("regression, routes child announce to parent session instead of grandparent when parent session still exists", async () => {
|
||||
const parentSessionKey = "agent:main:subagent:parent";
|
||||
requesterDepthResolver = (sessionKey?: string) =>
|
||||
sessionKey === parentSessionKey ? 1 : sessionKey?.includes(":subagent:") ? 1 : 0;
|
||||
subagentSessionRunActive = false;
|
||||
shouldIgnorePostCompletion = false;
|
||||
fallbackRequesterResolution = {
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterOrigin: { channel: "discord", to: "chan-main", accountId: "acct-main" },
|
||||
};
|
||||
// No sessionId on purpose: existence in store should still count as alive.
|
||||
sessionStore[parentSessionKey] = { updatedAt: Date.now() };
|
||||
|
||||
await runAnnounceFlowForTest("run-parent-route", {
|
||||
requesterSessionKey: parentSessionKey,
|
||||
requesterDisplayKey: parentSessionKey,
|
||||
childSessionKey: `${parentSessionKey}:subagent:child`,
|
||||
});
|
||||
|
||||
const directAgentCall = findGatewayCall(
|
||||
(call) => call.method === "agent" && call.expectFinal === true,
|
||||
);
|
||||
expect(directAgentCall?.params?.sessionKey).toBe(parentSessionKey);
|
||||
expect(directAgentCall?.params?.deliver).toBe(false);
|
||||
});
|
||||
|
||||
it("regression, falls back to grandparent only when parent subagent session is missing", async () => {
|
||||
const parentSessionKey = "agent:main:subagent:parent-missing";
|
||||
requesterDepthResolver = (sessionKey?: string) =>
|
||||
sessionKey === parentSessionKey ? 1 : sessionKey?.includes(":subagent:") ? 1 : 0;
|
||||
subagentSessionRunActive = false;
|
||||
shouldIgnorePostCompletion = false;
|
||||
fallbackRequesterResolution = {
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterOrigin: { channel: "discord", to: "chan-main", accountId: "acct-main" },
|
||||
};
|
||||
|
||||
await runAnnounceFlowForTest("run-parent-fallback", {
|
||||
requesterSessionKey: parentSessionKey,
|
||||
requesterDisplayKey: parentSessionKey,
|
||||
childSessionKey: `${parentSessionKey}:subagent:child`,
|
||||
});
|
||||
|
||||
const directAgentCall = findGatewayCall(
|
||||
(call) => call.method === "agent" && call.expectFinal === true,
|
||||
);
|
||||
expect(directAgentCall?.params?.sessionKey).toBe("agent:main:main");
|
||||
expect(directAgentCall?.params?.deliver).toBe(true);
|
||||
expect(directAgentCall?.params?.channel).toBe("discord");
|
||||
expect(directAgentCall?.params?.to).toBe("chan-main");
|
||||
expect(directAgentCall?.params?.accountId).toBe("acct-main");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user