refactor: lazy load cron subagent followup runtime

This commit is contained in:
Shakker 2026-04-01 18:13:59 +01:00 committed by Peter Steinberger
parent 9919e978ca
commit cc57bcfe2f
7 changed files with 86 additions and 69 deletions

View File

@ -53,9 +53,12 @@ vi.mock("../../infra/system-events.js", () => ({
enqueueSystemEvent: vi.fn(),
}));
vi.mock("./subagent-followup.js", () => ({
vi.mock("./subagent-followup-hints.js", () => ({
expectsSubagentFollowup: vi.fn().mockReturnValue(false),
isLikelyInterimCronMessage: vi.fn().mockReturnValue(false),
}));
vi.mock("./subagent-followup.runtime.js", () => ({
readDescendantSubagentFallbackReply: vi.fn().mockResolvedValue(undefined),
waitForDescendantSubagentSummary: vi.fn().mockResolvedValue(undefined),
}));
@ -73,12 +76,11 @@ import {
} from "./delivery-dispatch.js";
import type { DeliveryTargetResolution } from "./delivery-target.js";
import type { RunCronAgentTurnResult } from "./run.js";
import { expectsSubagentFollowup, isLikelyInterimCronMessage } from "./subagent-followup-hints.js";
import {
expectsSubagentFollowup,
isLikelyInterimCronMessage,
readDescendantSubagentFallbackReply,
waitForDescendantSubagentSummary,
} from "./subagent-followup.js";
} from "./subagent-followup.runtime.js";
// ---------------------------------------------------------------------------
// Helpers

View File

@ -20,12 +20,7 @@ import type { CronJob, CronRunTelemetry } from "../types.js";
import type { DeliveryTargetResolution } from "./delivery-target.js";
import { pickSummaryFromOutput } from "./helpers.js";
import type { RunCronAgentTurnResult } from "./run.js";
import {
expectsSubagentFollowup,
isLikelyInterimCronMessage,
readDescendantSubagentFallbackReply,
waitForDescendantSubagentSummary,
} from "./subagent-followup.js";
import { expectsSubagentFollowup, isLikelyInterimCronMessage } from "./subagent-followup-hints.js";
function normalizeDeliveryTarget(channel: string, to: string): string {
const channelLower = channel.trim().toLowerCase();
@ -141,6 +136,9 @@ type CompletedDirectCronDelivery = {
};
let gatewayCallRuntimePromise: Promise<typeof import("../../gateway/call.runtime.js")> | undefined;
let subagentFollowupRuntimePromise:
| Promise<typeof import("./subagent-followup.runtime.js")>
| undefined;
const COMPLETED_DIRECT_CRON_DELIVERIES = new Map<string, CompletedDirectCronDelivery>();
@ -149,6 +147,13 @@ async function loadGatewayCallRuntime(): Promise<typeof import("../../gateway/ca
return await gatewayCallRuntimePromise;
}
async function loadSubagentFollowupRuntime(): Promise<
typeof import("./subagent-followup.runtime.js")
> {
subagentFollowupRuntimePromise ??= import("./subagent-followup.runtime.js");
return await subagentFollowupRuntimePromise;
}
function cloneDeliveryResults(
results: readonly OutboundDeliveryResult[],
): OutboundDeliveryResult[] {
@ -545,21 +550,27 @@ export async function dispatchCronDelivery(
const initialSynthesizedText = synthesizedText.trim();
let activeSubagentRuns = countActiveDescendantRuns(params.agentSessionKey);
const expectedSubagentFollowup = expectsSubagentFollowup(initialSynthesizedText);
const shouldCheckCompletedDescendants =
activeSubagentRuns === 0 && isLikelyInterimCronMessage(initialSynthesizedText);
const needsSubagentFollowupRuntime =
shouldCheckCompletedDescendants || activeSubagentRuns > 0 || expectedSubagentFollowup;
const subagentFollowupRuntime = needsSubagentFollowupRuntime
? await loadSubagentFollowupRuntime()
: undefined;
// Also check for already-completed descendants. If the subagent finished
// before delivery-dispatch runs, activeSubagentRuns is 0 and
// expectedSubagentFollowup may be false (e.g. cron said "on it" which
// doesn't match the narrow hint list). We still need to use the
// descendant's output instead of the interim cron text.
const completedDescendantReply =
activeSubagentRuns === 0 && isLikelyInterimCronMessage(initialSynthesizedText)
? await readDescendantSubagentFallbackReply({
sessionKey: params.agentSessionKey,
runStartedAt: params.runStartedAt,
})
: undefined;
const completedDescendantReply = shouldCheckCompletedDescendants
? await subagentFollowupRuntime?.readDescendantSubagentFallbackReply({
sessionKey: params.agentSessionKey,
runStartedAt: params.runStartedAt,
})
: undefined;
const hadDescendants = activeSubagentRuns > 0 || Boolean(completedDescendantReply);
if (activeSubagentRuns > 0 || expectedSubagentFollowup) {
let finalReply = await waitForDescendantSubagentSummary({
let finalReply = await subagentFollowupRuntime?.waitForDescendantSubagentSummary({
sessionKey: params.agentSessionKey,
initialReply: initialSynthesizedText,
timeoutMs: params.timeoutMs,
@ -567,7 +578,7 @@ export async function dispatchCronDelivery(
});
activeSubagentRuns = countActiveDescendantRuns(params.agentSessionKey);
if (!finalReply && activeSubagentRuns === 0) {
finalReply = await readDescendantSubagentFallbackReply({
finalReply = await subagentFollowupRuntime?.readDescendantSubagentFallbackReply({
sessionKey: params.agentSessionKey,
runStartedAt: params.runStartedAt,
});

View File

@ -61,7 +61,7 @@ import { buildCronAgentDefaultsConfig } from "./run-config.js";
import { resolveCronAgentSessionKey } from "./session-key.js";
import { resolveCronSession } from "./session.js";
import { resolveCronSkillsSnapshot } from "./skills-snapshot.js";
import { isLikelyInterimCronMessage } from "./subagent-followup.js";
import { isLikelyInterimCronMessage } from "./subagent-followup-hints.js";
let sessionStoreRuntimePromise:
| Promise<typeof import("../../config/sessions/store.runtime.js")>

View File

@ -0,0 +1,46 @@
const SUBAGENT_FOLLOWUP_HINTS = [
"subagent spawned",
"spawned a subagent",
"auto-announce when done",
"both subagents are running",
"wait for them to report back",
] as const;
const INTERIM_CRON_HINTS = [
"on it",
"pulling everything together",
"give me a few",
"give me a few min",
"few minutes",
"let me compile",
"i'll gather",
"i will gather",
"working on it",
"retrying now",
"should be about",
"should have your summary",
"it'll auto-announce when done",
"it will auto-announce when done",
...SUBAGENT_FOLLOWUP_HINTS,
] as const;
function normalizeHintText(value: string): string {
return value.trim().toLowerCase().replace(/\s+/g, " ");
}
export function isLikelyInterimCronMessage(value: string): boolean {
const normalized = normalizeHintText(value);
if (!normalized) {
// Empty text after payload filtering means the agent either returned
// NO_REPLY (deliberately silent) or produced no deliverable content.
// Do not treat this as an interim acknowledgement that needs a rerun.
return false;
}
const words = normalized.split(" ").filter(Boolean).length;
return words <= 45 && INTERIM_CRON_HINTS.some((hint) => normalized.includes(hint));
}
export function expectsSubagentFollowup(value: string): boolean {
const normalized = normalizeHintText(value);
return Boolean(normalized && SUBAGENT_FOLLOWUP_HINTS.some((hint) => normalized.includes(hint)));
}

View File

@ -0,0 +1,4 @@
export {
readDescendantSubagentFallbackReply,
waitForDescendantSubagentSummary,
} from "./subagent-followup.js";

View File

@ -5,9 +5,8 @@ vi.hoisted(() => {
process.env.OPENCLAW_TEST_FAST = "1";
});
import { expectsSubagentFollowup, isLikelyInterimCronMessage } from "./subagent-followup-hints.js";
import {
expectsSubagentFollowup,
isLikelyInterimCronMessage,
readDescendantSubagentFallbackReply,
waitForDescendantSubagentSummary,
} from "./subagent-followup.js";

View File

@ -2,6 +2,8 @@ import { listDescendantRunsForRequester } from "../../agents/subagent-registry-r
import { readLatestAssistantReply } from "../../agents/tools/agent-step.js";
import { SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
import { callGateway } from "../../gateway/call.js";
import { expectsSubagentFollowup, isLikelyInterimCronMessage } from "./subagent-followup-hints.js";
export { expectsSubagentFollowup, isLikelyInterimCronMessage } from "./subagent-followup-hints.js";
function resolveCronSubagentTimings() {
const fastTestMode = process.env.OPENCLAW_TEST_FAST === "1";
@ -12,53 +14,6 @@ function resolveCronSubagentTimings() {
};
}
const SUBAGENT_FOLLOWUP_HINTS = [
"subagent spawned",
"spawned a subagent",
"auto-announce when done",
"both subagents are running",
"wait for them to report back",
] as const;
const INTERIM_CRON_HINTS = [
"on it",
"pulling everything together",
"give me a few",
"give me a few min",
"few minutes",
"let me compile",
"i'll gather",
"i will gather",
"working on it",
"retrying now",
"should be about",
"should have your summary",
"it'll auto-announce when done",
"it will auto-announce when done",
...SUBAGENT_FOLLOWUP_HINTS,
] as const;
function normalizeHintText(value: string): string {
return value.trim().toLowerCase().replace(/\s+/g, " ");
}
export function isLikelyInterimCronMessage(value: string): boolean {
const normalized = normalizeHintText(value);
if (!normalized) {
// Empty text after payload filtering means the agent either returned
// NO_REPLY (deliberately silent) or produced no deliverable content.
// Do not treat this as an interim acknowledgement that needs a rerun.
return false;
}
const words = normalized.split(" ").filter(Boolean).length;
return words <= 45 && INTERIM_CRON_HINTS.some((hint) => normalized.includes(hint));
}
export function expectsSubagentFollowup(value: string): boolean {
const normalized = normalizeHintText(value);
return Boolean(normalized && SUBAGENT_FOLLOWUP_HINTS.some((hint) => normalized.includes(hint)));
}
export async function readDescendantSubagentFallbackReply(params: {
sessionKey: string;
runStartedAt: number;