[AI-assisted] fix(agents): initialize context engines before subagent spawn prep (#73904)

Merged via squash.

Prepared head SHA: a9f32b858a
Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
brokemac79
2026-05-02 00:50:24 +01:00
committed by GitHub
parent 5b38005a4c
commit f6b0281298
5 changed files with 35 additions and 0 deletions

View File

@@ -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.

View File

@@ -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<typeof loadSubagentSpawnModuleForTest>
@@ -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 },

View File

@@ -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";

View File

@@ -117,6 +117,7 @@ export function expectPersistedRuntimeModel(params: {
export async function loadSubagentSpawnModuleForTest(params: {
callGatewayMock: MockFn;
getRuntimeConfig?: () => Record<string, unknown>;
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: (

View File

@@ -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,