From f6b0281298dec3a260689fb1d1b8c63860027148 Mon Sep 17 00:00:00 2001 From: brokemac79 Date: Sat, 2 May 2026 00:50:24 +0100 Subject: [PATCH] [AI-assisted] fix(agents): initialize context engines before subagent spawn prep (#73904) Merged via squash. Prepared head SHA: a9f32b858a97c95a950a7e74f8ae70ac50f8a1c2 Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com> Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Reviewed-by: @jalehman --- CHANGELOG.md | 1 + src/agents/subagent-spawn.context.test.ts | 26 +++++++++++++++++++++++ src/agents/subagent-spawn.runtime.ts | 1 + src/agents/subagent-spawn.test-helpers.ts | 3 +++ src/agents/subagent-spawn.ts | 4 ++++ 5 files changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3bed1b826a..0133680b2f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai - Slack/setup: print the generated app manifest as plain JSON instead of embedding it inside the framed setup note, so it can be copied into Slack without deleting border characters. Fixes #65751. Thanks @theDanielJLewis. - Channels/WhatsApp: route CLI logout through the live Gateway and stop runtime-backed listeners before channel removal, so removing a WhatsApp account does not leave the old socket replying until restart. Fixes #67746. Thanks @123Mismail. - Voice Call/Twilio: honor TTS directive text and provider voice/model overrides during telephony synthesis, so `[[tts:...]]` tags are not spoken literally and voiceId overrides reach OpenAI/ElevenLabs calls. Fixes #58114. Thanks @legonhilltech-jpg. +- Agents/subagents: initialize built-in context engines before native `sessions_spawn` resolves spawn preparation, so cliBackend-only cold starts no longer fail with an unregistered `legacy` context engine. Fixes #73095. (#73904) Thanks @brokemac79. - Agents/Codex: stop prompting message-tool-only source turns to finish with `NO_REPLY`, so quiet turns are represented by not calling the visible message tool instead of conflicting final-text instructions. Thanks @pashpashpash. - Gateway/config: report failed backup restores as failed in logs and config observe audit records instead of marking them valid. (#70515) Thanks @davidangularme. - Compaction: use the active session model fallback chain for implicit summarization failures without persisting fallback model selection, so Azure content-filter 400s can recover. Fixes #64960. (#74470) Thanks @jalehman and @OpenCodeEngineer. diff --git a/src/agents/subagent-spawn.context.test.ts b/src/agents/subagent-spawn.context.test.ts index c9072fc1d50..57b350639f5 100644 --- a/src/agents/subagent-spawn.context.test.ts +++ b/src/agents/subagent-spawn.context.test.ts @@ -13,6 +13,7 @@ describe("sessions_spawn context modes", () => { const callGatewayMock = vi.fn(); const updateSessionStoreMock = vi.fn(); const forkSessionFromParentMock = vi.fn(); + const ensureContextEnginesInitializedMock = vi.fn(); const resolveContextEngineMock = vi.fn(); let spawnSubagentDirect: Awaited< ReturnType @@ -23,6 +24,7 @@ describe("sessions_spawn context modes", () => { callGatewayMock, updateSessionStoreMock, forkSessionFromParentMock, + ensureContextEnginesInitializedMock, resolveContextEngineMock, sessionStorePath: storePath, })); @@ -32,6 +34,7 @@ describe("sessions_spawn context modes", () => { callGatewayMock.mockReset(); updateSessionStoreMock.mockReset(); forkSessionFromParentMock.mockReset(); + ensureContextEnginesInitializedMock.mockReset(); resolveContextEngineMock.mockReset(); setupAcceptedSubagentGatewayMock(callGatewayMock); resolveContextEngineMock.mockResolvedValue({}); @@ -112,6 +115,29 @@ describe("sessions_spawn context modes", () => { ); }); + it("initializes built-in context engines before resolving spawn preparation", async () => { + let initialized = false; + ensureContextEnginesInitializedMock.mockImplementation(() => { + initialized = true; + }); + const prepareSubagentSpawn = vi.fn(async () => undefined); + resolveContextEngineMock.mockImplementation(async () => { + if (!initialized) { + throw new Error('Context engine "legacy" is not registered. Available engines: (none)'); + } + return { prepareSubagentSpawn }; + }); + + const result = await spawnSubagentDirect({ task: "clean worker" }, { agentSessionKey: "main" }); + + expect(result.status).toBe("accepted"); + expect(ensureContextEnginesInitializedMock).toHaveBeenCalledTimes(1); + expect(resolveContextEngineMock).toHaveBeenCalledTimes(1); + expect(ensureContextEnginesInitializedMock.mock.invocationCallOrder[0]).toBeLessThan( + resolveContextEngineMock.mock.invocationCallOrder[0], + ); + }); + it("rolls back context-engine preparation when agent start fails", async () => { const store: SessionStore = { main: { sessionId: "parent-session-id", updatedAt: 1 }, diff --git a/src/agents/subagent-spawn.runtime.ts b/src/agents/subagent-spawn.runtime.ts index 8f6b584e223..4c1364078c2 100644 --- a/src/agents/subagent-spawn.runtime.ts +++ b/src/agents/subagent-spawn.runtime.ts @@ -8,6 +8,7 @@ export { forkSessionFromParent, resolveParentForkMaxTokens, } from "../auto-reply/reply/session-fork.js"; +export { ensureContextEnginesInitialized } from "../context-engine/init.js"; export { resolveContextEngine } from "../context-engine/registry.js"; export { callGateway } from "../gateway/call.js"; export { ADMIN_SCOPE, isAdminOnlyMethod } from "../gateway/method-scopes.js"; diff --git a/src/agents/subagent-spawn.test-helpers.ts b/src/agents/subagent-spawn.test-helpers.ts index 5c357026a2b..df9854eae7d 100644 --- a/src/agents/subagent-spawn.test-helpers.ts +++ b/src/agents/subagent-spawn.test-helpers.ts @@ -117,6 +117,7 @@ export function expectPersistedRuntimeModel(params: { export async function loadSubagentSpawnModuleForTest(params: { callGatewayMock: MockFn; getRuntimeConfig?: () => Record; + ensureContextEnginesInitializedMock?: MockFn; updateSessionStoreMock?: MockFn; forkSessionFromParentMock?: MockFn; resolveContextEngineMock?: MockFn; @@ -178,6 +179,8 @@ export async function loadSubagentSpawnModuleForTest(params: { getRuntimeConfig: () => params.getRuntimeConfig?.() ?? createSubagentSpawnTestConfig(params.workspaceDir ?? os.tmpdir()), + ensureContextEnginesInitialized: + params.ensureContextEnginesInitializedMock ?? (() => undefined), resolveContextEngine: params.resolveContextEngineMock ?? (async () => ({})), resolveParentForkMaxTokens: params.resolveParentForkMaxTokensMock ?? (() => 100_000), mergeSessionEntry: ( diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index 5de497a9247..10c467921d7 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -54,6 +54,7 @@ import { mergeDeliveryContext, normalizeDeliveryContext, pruneLegacyStoreKeys, + ensureContextEnginesInitialized, resolveAgentConfig, resolveContextEngine, resolveDisplaySessionKey, @@ -92,6 +93,7 @@ type SubagentSpawnDeps = { forkSessionFromParent: typeof forkSessionFromParent; getGlobalHookRunner: () => SubagentLifecycleHookRunner | null; getRuntimeConfig: typeof getRuntimeConfig; + ensureContextEnginesInitialized: typeof ensureContextEnginesInitialized; resolveContextEngine: typeof resolveContextEngine; resolveParentForkMaxTokens: typeof resolveParentForkMaxTokens; updateSessionStore: typeof updateSessionStore; @@ -102,6 +104,7 @@ const defaultSubagentSpawnDeps: SubagentSpawnDeps = { forkSessionFromParent, getGlobalHookRunner, getRuntimeConfig, + ensureContextEnginesInitialized, resolveContextEngine, resolveParentForkMaxTokens, updateSessionStore, @@ -417,6 +420,7 @@ async function prepareContextEngineSubagentSpawn(params: { { status: "ok"; preparation?: SubagentSpawnPreparation } | { status: "error"; error: string } > { try { + subagentSpawnDeps.ensureContextEnginesInitialized(); const engine = await subagentSpawnDeps.resolveContextEngine(params.cfg); const preparation = await engine.prepareSubagentSpawn?.({ parentSessionKey: params.requesterInternalKey,