From cbdb59b255b0437665b43531d9df239bc535bfcb Mon Sep 17 00:00:00 2001 From: zhang-guiping Date: Mon, 1 Jun 2026 05:37:59 +0800 Subject: [PATCH] fix(agents): keep light isolated subagents lightweight Keep native subagent spawns with `lightContext=true` and resolved isolated context out of context-engine pre-spawn preparation so they remain lightweight. The normal isolated and forked context-engine lifecycle stays intact, and docs now call out the lightweight isolated exception. Fixes #81214 --- docs/concepts/context-engine.md | 2 +- src/agents/subagent-spawn.context.test.ts | 22 ++++++++++++++++++++++ src/agents/subagent-spawn.ts | 17 ++++++++++------- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/docs/concepts/context-engine.md b/docs/concepts/context-engine.md index f251db9cef7..21c07af5e00 100644 --- a/docs/concepts/context-engine.md +++ b/docs/concepts/context-engine.md @@ -91,7 +91,7 @@ For the bundled non-ACP Codex harness, OpenClaw applies the same lifecycle by pr OpenClaw calls two optional subagent lifecycle hooks: - Prepare shared context state before a child run starts. The hook receives parent/child session keys, `contextMode` (`isolated` or `fork`), available transcript ids/files, and optional TTL. If it returns a rollback handle, OpenClaw calls it when spawn fails after preparation succeeds. + Prepare shared context state before a child run starts. The hook receives parent/child session keys, `contextMode` (`isolated` or `fork`), available transcript ids/files, and optional TTL. If it returns a rollback handle, OpenClaw calls it when spawn fails after preparation succeeds. Native subagent spawns that request `lightContext` and resolve to `contextMode="isolated"` intentionally skip this hook so the child starts from the lightweight bootstrap context without context-engine-managed pre-spawn state. Clean up when a subagent session completes or is swept. diff --git a/src/agents/subagent-spawn.context.test.ts b/src/agents/subagent-spawn.context.test.ts index 373083cf7fa..78b79d8f285 100644 --- a/src/agents/subagent-spawn.context.test.ts +++ b/src/agents/subagent-spawn.context.test.ts @@ -154,6 +154,28 @@ describe("sessions_spawn context modes", () => { expect(prepareContext.contextMode).toBe("isolated"); }); + it("keeps lightContext isolated spawns out of context-engine preparation", async () => { + const store: SessionStore = { + main: { sessionId: "parent-session-id", updatedAt: 1 }, + }; + usePersistentStoreMock(store); + const prepareSubagentSpawn = vi.fn(async () => undefined); + resolveContextEngineMock.mockResolvedValue({ prepareSubagentSpawn }); + + const result = await spawnSubagentDirect( + { task: "clean worker", context: "isolated", lightContext: true }, + { agentSessionKey: "main" }, + ); + + expect(result.status).toBe("accepted"); + expect(forkSessionFromParentMock).not.toHaveBeenCalled(); + expect(ensureContextEnginesInitializedMock).not.toHaveBeenCalled(); + expect(resolveContextEngineMock).not.toHaveBeenCalled(); + expect(prepareSubagentSpawn).not.toHaveBeenCalled(); + const agentRequest = requireGatewayRequest("agent"); + expect(agentRequest.params?.bootstrapContextMode).toBe("lightweight"); + }); + it("caps oversized context engine subagent TTLs at the timer-safe ceiling", async () => { const store: SessionStore = { main: { sessionId: "parent-session-id", updatedAt: 1 }, diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index c5a58b20e0c..72ee94184b6 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -1485,13 +1485,16 @@ export async function spawnSubagentDirect( childSessionKey, }; } - const contextEnginePrepareResult = await prepareContextEngineSubagentSpawn({ - cfg, - context: preparedSpawnContext, - requesterInternalKey, - childSessionKey, - runTimeoutSeconds, - }); + const contextEnginePrepareResult = + params.lightContext && preparedSpawnContext.mode === "isolated" + ? ({ status: "ok", preparation: undefined } as const) + : await prepareContextEngineSubagentSpawn({ + cfg, + context: preparedSpawnContext, + requesterInternalKey, + childSessionKey, + runTimeoutSeconds, + }); if (contextEnginePrepareResult.status === "error") { await cleanupFailedSpawnBeforeAgentStart({ childSessionKey,