mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 07:32:44 +00:00
fix(subagents): announce delivery with descendant gating, frozen result refresh, and cron retry (#35080)
Thanks @tyler6204
This commit is contained in:
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { countPendingDescendantRuns } from "../../../agents/subagent-registry.js";
|
||||
import { loadSessionStore, resolveStorePath } from "../../../config/sessions.js";
|
||||
import { formatDurationCompact } from "../../../shared/subagents-format.js";
|
||||
import type { CommandHandlerResult } from "../commands-types.js";
|
||||
@@ -38,7 +39,7 @@ export function handleSubagentsInfoAction(ctx: SubagentsCommandContext): Command
|
||||
|
||||
const lines = [
|
||||
"ℹ️ Subagent info",
|
||||
`Status: ${resolveDisplayStatus(run)}`,
|
||||
`Status: ${resolveDisplayStatus(run, { pendingDescendants: countPendingDescendantRuns(run.childSessionKey) })}`,
|
||||
`Label: ${formatRunLabel(run)}`,
|
||||
`Task: ${run.task}`,
|
||||
`Run: ${run.runId}`,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { countPendingDescendantRuns } from "../../../agents/subagent-registry.js";
|
||||
import { loadSessionStore, resolveStorePath } from "../../../config/sessions.js";
|
||||
import type { CommandHandlerResult } from "../commands-types.js";
|
||||
import { sortSubagentRuns } from "../subagents-utils.js";
|
||||
@@ -16,6 +17,18 @@ export function handleSubagentsListAction(ctx: SubagentsCommandContext): Command
|
||||
const now = Date.now();
|
||||
const recentCutoff = now - RECENT_WINDOW_MINUTES * 60_000;
|
||||
const storeCache: SessionStoreCache = new Map();
|
||||
const pendingDescendantCache = new Map<string, number>();
|
||||
const pendingDescendantCount = (sessionKey: string) => {
|
||||
if (pendingDescendantCache.has(sessionKey)) {
|
||||
return pendingDescendantCache.get(sessionKey) ?? 0;
|
||||
}
|
||||
const pending = Math.max(0, countPendingDescendantRuns(sessionKey));
|
||||
pendingDescendantCache.set(sessionKey, pending);
|
||||
return pending;
|
||||
};
|
||||
const isActiveRun = (entry: (typeof runs)[number]) =>
|
||||
!entry.endedAt || pendingDescendantCount(entry.childSessionKey) > 0;
|
||||
|
||||
let index = 1;
|
||||
|
||||
const mapRuns = (entries: typeof runs, runtimeMs: (entry: (typeof runs)[number]) => number) =>
|
||||
@@ -34,15 +47,16 @@ export function handleSubagentsListAction(ctx: SubagentsCommandContext): Command
|
||||
index,
|
||||
runtimeMs: runtimeMs(entry),
|
||||
sessionEntry,
|
||||
pendingDescendants: pendingDescendantCount(entry.childSessionKey),
|
||||
});
|
||||
index += 1;
|
||||
return line;
|
||||
});
|
||||
|
||||
const activeEntries = sorted.filter((entry) => !entry.endedAt);
|
||||
const activeEntries = sorted.filter((entry) => isActiveRun(entry));
|
||||
const activeLines = mapRuns(activeEntries, (entry) => now - (entry.startedAt ?? entry.createdAt));
|
||||
const recentEntries = sorted.filter(
|
||||
(entry) => !!entry.endedAt && (entry.endedAt ?? 0) >= recentCutoff,
|
||||
(entry) => !isActiveRun(entry) && !!entry.endedAt && (entry.endedAt ?? 0) >= recentCutoff,
|
||||
);
|
||||
const recentLines = mapRuns(
|
||||
recentEntries,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { SubagentRunRecord } from "../../../agents/subagent-registry.js";
|
||||
import {
|
||||
countPendingDescendantRuns,
|
||||
type SubagentRunRecord,
|
||||
} from "../../../agents/subagent-registry.js";
|
||||
import {
|
||||
extractAssistantText,
|
||||
resolveInternalSessionKey,
|
||||
@@ -118,7 +121,15 @@ function resolveModelDisplay(
|
||||
return combined;
|
||||
}
|
||||
|
||||
export function resolveDisplayStatus(entry: SubagentRunRecord) {
|
||||
export function resolveDisplayStatus(
|
||||
entry: SubagentRunRecord,
|
||||
options?: { pendingDescendants?: number },
|
||||
) {
|
||||
const pendingDescendants = Math.max(0, options?.pendingDescendants ?? 0);
|
||||
if (pendingDescendants > 0) {
|
||||
const childLabel = pendingDescendants === 1 ? "child" : "children";
|
||||
return `active (waiting on ${pendingDescendants} ${childLabel})`;
|
||||
}
|
||||
const status = formatRunStatus(entry);
|
||||
return status === "error" ? "failed" : status;
|
||||
}
|
||||
@@ -128,12 +139,15 @@ export function formatSubagentListLine(params: {
|
||||
index: number;
|
||||
runtimeMs: number;
|
||||
sessionEntry?: SessionEntry;
|
||||
pendingDescendants?: number;
|
||||
}) {
|
||||
const usageText = formatTokenUsageDisplay(params.sessionEntry);
|
||||
const label = truncateLine(formatRunLabel(params.entry, { maxLength: 48 }), 48);
|
||||
const task = formatTaskPreview(params.entry.task);
|
||||
const runtime = formatDurationCompact(params.runtimeMs);
|
||||
const status = resolveDisplayStatus(params.entry);
|
||||
const status = resolveDisplayStatus(params.entry, {
|
||||
pendingDescendants: params.pendingDescendants,
|
||||
});
|
||||
return `${params.index}. ${label} (${resolveModelDisplay(params.sessionEntry, params.entry.model)}, ${runtime}${usageText ? `, ${usageText}` : ""}) ${status}${task.toLowerCase() !== label.toLowerCase() ? ` - ${task}` : ""}`;
|
||||
}
|
||||
|
||||
@@ -191,6 +205,8 @@ export function resolveSubagentTarget(
|
||||
token,
|
||||
recentWindowMinutes: RECENT_WINDOW_MINUTES,
|
||||
label: (entry) => formatRunLabel(entry),
|
||||
isActive: (entry) =>
|
||||
!entry.endedAt || Math.max(0, countPendingDescendantRuns(entry.childSessionKey)) > 0,
|
||||
errors: {
|
||||
missingTarget: "Missing subagent id.",
|
||||
invalidIndex: (value) => `Invalid subagent index: ${value}`,
|
||||
@@ -220,7 +236,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) {
|
||||
|
||||
@@ -1296,23 +1296,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,
|
||||
@@ -1329,8 +1329,47 @@ 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("keeps ended orchestrators in active list while descendants are pending", async () => {
|
||||
const now = Date.now();
|
||||
addSubagentRunForTests({
|
||||
runId: "run-orchestrator-ended",
|
||||
childSessionKey: "agent:main:subagent:orchestrator-ended",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "orchestrate child workers",
|
||||
cleanup: "keep",
|
||||
createdAt: now - 120_000,
|
||||
startedAt: now - 120_000,
|
||||
endedAt: now - 60_000,
|
||||
outcome: { status: "ok" },
|
||||
});
|
||||
addSubagentRunForTests({
|
||||
runId: "run-orchestrator-child-active",
|
||||
childSessionKey: "agent:main:subagent:orchestrator-ended:subagent:child",
|
||||
requesterSessionKey: "agent:main:subagent:orchestrator-ended",
|
||||
requesterDisplayKey: "subagent:orchestrator-ended",
|
||||
task: "child worker still running",
|
||||
cleanup: "keep",
|
||||
createdAt: now - 30_000,
|
||||
startedAt: now - 30_000,
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
} as OpenClawConfig;
|
||||
const params = buildParams("/subagents list", cfg);
|
||||
const result = await handleCommands(params);
|
||||
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("active (waiting on 1 child)");
|
||||
expect(result.reply?.text).not.toContain(
|
||||
"recent subagents (last 30m):\n-----\n1. orchestrate child workers",
|
||||
);
|
||||
});
|
||||
|
||||
it("formats subagent usage with io and prompt/cache breakdown", async () => {
|
||||
|
||||
@@ -41,6 +41,7 @@ export function resolveSubagentTargetFromRuns(params: {
|
||||
token: string | undefined;
|
||||
recentWindowMinutes: number;
|
||||
label: (entry: SubagentRunRecord) => string;
|
||||
isActive?: (entry: SubagentRunRecord) => boolean;
|
||||
errors: {
|
||||
missingTarget: string;
|
||||
invalidIndex: (value: string) => string;
|
||||
@@ -59,10 +60,13 @@ export function resolveSubagentTargetFromRuns(params: {
|
||||
if (trimmed === "last") {
|
||||
return { entry: sorted[0] };
|
||||
}
|
||||
const isActive = params.isActive ?? ((entry: SubagentRunRecord) => !entry.endedAt);
|
||||
const recentCutoff = Date.now() - params.recentWindowMinutes * 60_000;
|
||||
const numericOrder = [
|
||||
...sorted.filter((entry) => !entry.endedAt),
|
||||
...sorted.filter((entry) => !!entry.endedAt && (entry.endedAt ?? 0) >= recentCutoff),
|
||||
...sorted.filter((entry) => isActive(entry)),
|
||||
...sorted.filter(
|
||||
(entry) => !isActive(entry) && !!entry.endedAt && (entry.endedAt ?? 0) >= recentCutoff,
|
||||
),
|
||||
];
|
||||
if (/^\d+$/.test(trimmed)) {
|
||||
const idx = Number.parseInt(trimmed, 10);
|
||||
|
||||
Reference in New Issue
Block a user