mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
fix: guarantee manual subagent spawn sends completion message
This commit is contained in:
@@ -360,7 +360,11 @@ function buildAnnounceReplyInstruction(params: {
|
||||
remainingActiveSubagentRuns: number;
|
||||
requesterIsSubagent: boolean;
|
||||
announceType: SubagentAnnounceType;
|
||||
expectsCompletionMessage?: boolean;
|
||||
}): string {
|
||||
if (params.expectsCompletionMessage) {
|
||||
return `A completed ${params.announceType} is ready for user delivery. Convert the result above into your normal assistant voice and send that user-facing update now. Keep this internal context private (don't mention system/log/stats/session details or announce type).`;
|
||||
}
|
||||
if (params.remainingActiveSubagentRuns > 0) {
|
||||
const activeRunsLabel = params.remainingActiveSubagentRuns === 1 ? "run" : "runs";
|
||||
return `There are still ${params.remainingActiveSubagentRuns} active subagent ${activeRunsLabel} for this session. If they are part of the same workflow, wait for the remaining results before sending a user update. If they are unrelated, respond normally using only the result above.`;
|
||||
@@ -387,8 +391,10 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
label?: string;
|
||||
outcome?: SubagentRunOutcome;
|
||||
announceType?: SubagentAnnounceType;
|
||||
expectsCompletionMessage?: boolean;
|
||||
}): Promise<boolean> {
|
||||
let didAnnounce = false;
|
||||
const expectsCompletionMessage = params.expectsCompletionMessage === true;
|
||||
let shouldDeleteChildSession = params.cleanup === "delete";
|
||||
try {
|
||||
let targetRequesterSessionKey = params.requesterSessionKey;
|
||||
@@ -506,7 +512,7 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
let triggerMessage = "";
|
||||
|
||||
let requesterDepth = getSubagentDepthFromSessionStore(targetRequesterSessionKey);
|
||||
let requesterIsSubagent = requesterDepth >= 1;
|
||||
let requesterIsSubagent = !expectsCompletionMessage && requesterDepth >= 1;
|
||||
// If the requester subagent has already finished, bubble the announce to its
|
||||
// requester (typically main) so descendant completion is not silently lost.
|
||||
// BUT: only fallback if the parent SESSION is deleted, not just if the current
|
||||
@@ -559,6 +565,7 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
remainingActiveSubagentRuns,
|
||||
requesterIsSubagent,
|
||||
announceType,
|
||||
expectsCompletionMessage,
|
||||
});
|
||||
const statsLine = await buildCompactAnnounceStatsLine({
|
||||
sessionKey: params.childSessionKey,
|
||||
@@ -580,20 +587,22 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
childSessionKey: params.childSessionKey,
|
||||
childRunId: params.childRunId,
|
||||
});
|
||||
const queued = await maybeQueueSubagentAnnounce({
|
||||
requesterSessionKey: targetRequesterSessionKey,
|
||||
announceId,
|
||||
triggerMessage,
|
||||
summaryLine: taskLabel,
|
||||
requesterOrigin: targetRequesterOrigin,
|
||||
});
|
||||
if (queued === "steered") {
|
||||
didAnnounce = true;
|
||||
return true;
|
||||
}
|
||||
if (queued === "queued") {
|
||||
didAnnounce = true;
|
||||
return true;
|
||||
if (!expectsCompletionMessage) {
|
||||
const queued = await maybeQueueSubagentAnnounce({
|
||||
requesterSessionKey: targetRequesterSessionKey,
|
||||
announceId,
|
||||
triggerMessage,
|
||||
summaryLine: taskLabel,
|
||||
requesterOrigin: targetRequesterOrigin,
|
||||
});
|
||||
if (queued === "steered") {
|
||||
didAnnounce = true;
|
||||
return true;
|
||||
}
|
||||
if (queued === "queued") {
|
||||
didAnnounce = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Send to the requester session. For nested subagents this is an internal
|
||||
|
||||
@@ -30,6 +30,7 @@ export type SubagentRunRecord = {
|
||||
cleanupCompletedAt?: number;
|
||||
cleanupHandled?: boolean;
|
||||
suppressAnnounceReason?: "steer-restart" | "killed";
|
||||
expectsCompletionMessage?: boolean;
|
||||
/** Number of times announce delivery has been attempted and returned false (deferred). */
|
||||
announceRetryCount?: number;
|
||||
/** Timestamp of the last announce retry attempt (for backoff). */
|
||||
@@ -91,6 +92,7 @@ function startSubagentAnnounceCleanupFlow(runId: string, entry: SubagentRunRecor
|
||||
requesterOrigin,
|
||||
requesterDisplayKey: entry.requesterDisplayKey,
|
||||
task: entry.task,
|
||||
expectsCompletionMessage: entry.expectsCompletionMessage,
|
||||
timeoutMs: SUBAGENT_ANNOUNCE_TIMEOUT_MS,
|
||||
cleanup: entry.cleanup,
|
||||
waitForCompletion: false,
|
||||
@@ -478,6 +480,7 @@ export function registerSubagentRun(params: {
|
||||
label?: string;
|
||||
model?: string;
|
||||
runTimeoutSeconds?: number;
|
||||
expectsCompletionMessage?: boolean;
|
||||
}) {
|
||||
const now = Date.now();
|
||||
const cfg = loadConfig();
|
||||
@@ -494,6 +497,7 @@ export function registerSubagentRun(params: {
|
||||
requesterDisplayKey: params.requesterDisplayKey,
|
||||
task: params.task,
|
||||
cleanup: params.cleanup,
|
||||
expectsCompletionMessage: params.expectsCompletionMessage,
|
||||
label: params.label,
|
||||
model: params.model,
|
||||
runTimeoutSeconds,
|
||||
|
||||
@@ -25,6 +25,7 @@ export type SpawnSubagentParams = {
|
||||
thinking?: string;
|
||||
runTimeoutSeconds?: number;
|
||||
cleanup?: "delete" | "keep";
|
||||
expectsCompletionMessage?: boolean;
|
||||
};
|
||||
|
||||
export type SpawnSubagentContext = {
|
||||
@@ -318,6 +319,7 @@ export async function spawnSubagentDirect(
|
||||
label: label || undefined,
|
||||
model: resolvedModel,
|
||||
runTimeoutSeconds,
|
||||
expectsCompletionMessage: params.expectsCompletionMessage === true,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resetSubagentRegistryForTests } from "../../agents/subagent-registry.js";
|
||||
import type { SpawnSubagentResult } from "../../agents/subagent-spawn.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resetSubagentRegistryForTests } from "../../agents/subagent-registry.js";
|
||||
|
||||
const hoisted = vi.hoisted(() => {
|
||||
const spawnSubagentDirectMock = vi.fn();
|
||||
@@ -94,6 +94,7 @@ describe("/subagents spawn command", () => {
|
||||
expect(spawnParams.task).toBe("do the thing");
|
||||
expect(spawnParams.agentId).toBe("beta");
|
||||
expect(spawnParams.cleanup).toBe("keep");
|
||||
expect(spawnParams.expectsCompletionMessage).toBe(true);
|
||||
expect(spawnCtx.agentSessionKey).toBeDefined();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import crypto from "node:crypto";
|
||||
import type { SubagentRunRecord } from "../../agents/subagent-registry.js";
|
||||
import type { CommandHandler } from "./commands-types.js";
|
||||
import { AGENT_LANE_SUBAGENT } from "../../agents/lanes.js";
|
||||
import { abortEmbeddedPiRun } from "../../agents/pi-embedded.js";
|
||||
import type { SubagentRunRecord } from "../../agents/subagent-registry.js";
|
||||
import {
|
||||
clearSubagentRunSteerRestart,
|
||||
listSubagentRunsForRequester,
|
||||
@@ -35,7 +36,6 @@ import {
|
||||
} from "../../shared/subagents-format.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
||||
import { stopSubagentsForRequester } from "./abort.js";
|
||||
import type { CommandHandler } from "./commands-types.js";
|
||||
import { clearSessionQueues } from "./queue.js";
|
||||
import { formatRunLabel, formatRunStatus, sortSubagentRuns } from "./subagents-utils.js";
|
||||
|
||||
@@ -674,7 +674,7 @@ export const handleSubagentsCommand: CommandHandler = async (params, allowTextCo
|
||||
}
|
||||
|
||||
const result = await spawnSubagentDirect(
|
||||
{ task, agentId, model, thinking, cleanup: "keep" },
|
||||
{ task, agentId, model, thinking, cleanup: "keep", expectsCompletionMessage: true },
|
||||
{
|
||||
agentSessionKey: requesterKey,
|
||||
agentChannel: params.command.channel,
|
||||
|
||||
Reference in New Issue
Block a user