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
This commit is contained in:
zhang-guiping
2026-06-01 05:37:59 +08:00
committed by GitHub
parent 2ac2a8d210
commit cbdb59b255
3 changed files with 33 additions and 8 deletions

View File

@@ -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:
<ParamField path="prepareSubagentSpawn" type="method">
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.
</ParamField>
<ParamField path="onSubagentEnded" type="method">
Clean up when a subagent session completes or is swept.

View File

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

View File

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