diff --git a/src/agents/command/cli-compaction.test.ts b/src/agents/command/cli-compaction.test.ts index 2dc37a40072e..2645c2b69e73 100644 --- a/src/agents/command/cli-compaction.test.ts +++ b/src/agents/command/cli-compaction.test.ts @@ -8,7 +8,7 @@ import { replaceSessionEntry } from "../../config/sessions/session-accessor.js"; import { SESSION_TOTAL_TOKENS_VERSION, type SessionEntry } from "../../config/sessions/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { ContextEngine } from "../../context-engine/types.js"; -import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js"; +import { createModelGenerationFixture } from "../embedded-agent-runner/model.generation-scope.test-support.js"; import { SessionManager } from "../sessions/session-manager.js"; import { resetCliCompactionTestDeps, @@ -112,6 +112,24 @@ const defaultPreemptiveCompaction = () => ({ effectiveReserveTokens: 200, }); +function createPreparedRuntimeLease(input: { + config: OpenClawConfig; + agentDir: string; + agentId?: string; + workspaceDir?: string; +}) { + const prepared = createModelGenerationFixture({ config: input.config, label: "cli" }); + return { + snapshot: { + ...prepared.preparedModelRuntime, + ...(input.agentId ? { agentId: input.agentId } : {}), + agentDir: input.agentDir, + ...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}), + }, + release: vi.fn(), + }; +} + async function prepareCompactionScenario(params: { tmpDir: string; suffix: string; @@ -231,7 +249,7 @@ describe("runCliTurnCompactionLifecycle", () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-compaction-")); setCliCompactionTestDeps({ resolveCliBackendConfig: () => null, - loadAgentRuntimePluginRegistryHandle: () => createEmptyPluginRegistry(), + acquirePreparedModelRuntime: async (input) => createPreparedRuntimeLease(input), }); }); @@ -586,8 +604,13 @@ describe("runCliTurnCompactionLifecycle", () => { const compactCalls: CompactParams[] = []; const contextEngine = buildContextEngine({ compactCalls }); const resolveContextEngine = vi.fn(async () => contextEngine); - const pluginRegistry = createEmptyPluginRegistry(); - const loadAgentRuntimePluginRegistryHandle = vi.fn(() => pluginRegistry); + const preparedRuntimeLease = createPreparedRuntimeLease({ + config: {}, + agentId: "main", + agentDir: tmpDir, + workspaceDir: tmpDir, + }); + const acquirePreparedModelRuntime = vi.fn(async () => preparedRuntimeLease); const ensureSelectedAgentHarnessPlugin = vi.fn(async () => undefined); const compactAgentHarnessSession = vi.fn(async () => ({ ok: true, @@ -617,7 +640,7 @@ describe("runCliTurnCompactionLifecycle", () => { recordCliCompactionInStore, deps: { resolveContextEngine, - loadAgentRuntimePluginRegistryHandle, + acquirePreparedModelRuntime, ensureSelectedAgentHarnessPlugin, maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, applyAgentAutoCompactionGuard, @@ -638,21 +661,25 @@ describe("runCliTurnCompactionLifecycle", () => { modelId: "gpt-5.5", sessionKey, agentHarnessRuntimeOverride: "codex", - pluginRegistry, + pluginRegistry: preparedRuntimeLease.snapshot.pluginRegistry, }), ); - expect(loadAgentRuntimePluginRegistryHandle).toHaveBeenCalledWith({ + expect(acquirePreparedModelRuntime).toHaveBeenCalledWith({ config: {}, + agentId: "main", + agentDir: tmpDir, workspaceDir: tmpDir, allowGatewaySubagentBinding: true, - selections: [{ agentId: "main", modelId: "gpt-5.5", provider: "openai", runtime: "codex" }], + runtimePluginSelections: [ + { agentId: "main", modelId: "gpt-5.5", provider: "openai", runtime: "codex" }, + ], }); expect(applyAgentAutoCompactionGuard.mock.invocationCallOrder[0] ?? 0).toBeLessThan( compactAgentHarnessSession.mock.invocationCallOrder[0] ?? 0, ); expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); const compactAgentHarnessSessionCalls = compactAgentHarnessSession.mock - .calls as unknown as Array<[Record]>; + .calls as unknown as Array<[Record, { preparedModelRuntime?: unknown }]>; expect(compactAgentHarnessSessionCalls[0]?.[0]).toMatchObject({ sessionId, sessionKey, @@ -673,6 +700,10 @@ describe("runCliTurnCompactionLifecycle", () => { agentHarnessId: "codex", modelSelectionLocked: true, }); + expect(compactAgentHarnessSessionCalls[0]?.[1]?.preparedModelRuntime).toBe( + preparedRuntimeLease.snapshot, + ); + expect(preparedRuntimeLease.release).toHaveBeenCalledOnce(); expect(compactCalls).toHaveLength(0); expect(recordCliCompactionInStore).toHaveBeenCalledTimes(1); expect(recordCliCompactionInStore).toHaveBeenCalledWith( diff --git a/src/agents/command/cli-compaction.ts b/src/agents/command/cli-compaction.ts index 932ec7ea9f1c..80a2b3d32d28 100644 --- a/src/agents/command/cli-compaction.ts +++ b/src/agents/command/cli-compaction.ts @@ -14,7 +14,7 @@ import { resolveContextEngine as resolveContextEngineImpl } from "../../context- import { buildContextEngineRuntimeSettings } from "../../context-engine/runtime-settings.js"; import type { ContextEngine } from "../../context-engine/types.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; -import { withPluginRuntimeRegistryScope } from "../../plugins/runtime/gateway-request-scope.js"; +import { withPluginRuntimeGenerationScope } from "../../plugins/runtime/generation-scope.js"; import type { SkillSnapshot } from "../../skills/types.js"; import { createPreparedEmbeddedAgentSettingsManager as createPreparedEmbeddedAgentSettingsManagerImpl } from "../agent-project-settings.js"; import { OPENCLAW_AGENT_RUNTIME_ID, normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js"; @@ -41,7 +41,7 @@ import type { EmbeddedAgentCompactResult } from "../embedded-agent-runner/types. import { isRecoverableNativeHarnessBindingFailure } from "../harness/compaction-recovery.js"; import { maybeCompactAgentHarnessSession as maybeCompactAgentHarnessSessionImpl } from "../harness/compaction.js"; import { ensureSelectedAgentHarnessPlugin as ensureSelectedAgentHarnessPluginImpl } from "../harness/runtime-plugin.js"; -import { loadAgentRuntimePluginRegistryHandle } from "../runtime-plugins.js"; +import { acquireAgentRunPreparedModelRuntime } from "../prepared-model-runtime.js"; import { SessionManager } from "../sessions/session-manager.js"; import { clearCliSessionInStore as clearCliSessionInStoreImpl, @@ -80,7 +80,7 @@ type CliCompactionDeps = { shouldPreemptivelyCompactBeforePrompt: typeof shouldPreemptivelyCompactBeforePromptImpl; resolveLiveToolResultMaxChars: typeof resolveLiveToolResultMaxCharsImpl; runContextEngineMaintenance: typeof runContextEngineMaintenanceImpl; - loadAgentRuntimePluginRegistryHandle: typeof loadAgentRuntimePluginRegistryHandle; + acquirePreparedModelRuntime: typeof acquireAgentRunPreparedModelRuntime; ensureSelectedAgentHarnessPlugin: typeof ensureSelectedAgentHarnessPluginImpl; maybeCompactAgentHarnessSession: typeof maybeCompactAgentHarnessSessionImpl; clearCliSessionInStore: typeof clearCliSessionInStoreImpl; @@ -135,7 +135,7 @@ const cliCompactionDeps: CliCompactionDeps = { shouldPreemptivelyCompactBeforePrompt: shouldPreemptivelyCompactBeforePromptImpl, resolveLiveToolResultMaxChars: resolveLiveToolResultMaxCharsImpl, runContextEngineMaintenance: runContextEngineMaintenanceImpl, - loadAgentRuntimePluginRegistryHandle, + acquirePreparedModelRuntime: acquireAgentRunPreparedModelRuntime, ensureSelectedAgentHarnessPlugin: ensureSelectedAgentHarnessPluginImpl, maybeCompactAgentHarnessSession: maybeCompactAgentHarnessSessionImpl, clearCliSessionInStore: clearCliSessionInStoreImpl, @@ -159,7 +159,7 @@ export function resetCliCompactionTestDeps(): void { shouldPreemptivelyCompactBeforePrompt: shouldPreemptivelyCompactBeforePromptImpl, resolveLiveToolResultMaxChars: resolveLiveToolResultMaxCharsImpl, runContextEngineMaintenance: runContextEngineMaintenanceImpl, - loadAgentRuntimePluginRegistryHandle, + acquirePreparedModelRuntime: acquireAgentRunPreparedModelRuntime, ensureSelectedAgentHarnessPlugin: ensureSelectedAgentHarnessPluginImpl, maybeCompactAgentHarnessSession: maybeCompactAgentHarnessSessionImpl, clearCliSessionInStore: clearCliSessionInStoreImpl, @@ -427,11 +427,13 @@ async function compactNativeHarnessCliTranscript(params: { const nativeHarnessId = params.sessionEntry.agentHarnessId?.trim(); const modelSelectionLocked = params.sessionEntry.modelSelectionLocked === true; const authProfileId = params.sessionEntry.authProfileOverride?.trim() || undefined; - const pluginRegistry = cliCompactionDeps.loadAgentRuntimePluginRegistryHandle({ + const preparedRuntimeLease = await cliCompactionDeps.acquirePreparedModelRuntime({ config: params.cfg, + ...(sessionAgentId ? { agentId: sessionAgentId } : {}), + agentDir: params.agentDir, workspaceDir: params.workspaceDir, allowGatewaySubagentBinding: true, - selections: [ + runtimePluginSelections: [ { provider: params.provider, modelId: params.model, @@ -440,74 +442,82 @@ async function compactNativeHarnessCliTranscript(params: { }, ], }); - result = await withPluginRuntimeRegistryScope(pluginRegistry, async () => { - await cliCompactionDeps.ensureSelectedAgentHarnessPlugin({ - provider: params.provider, - modelId: params.model, - config: params.cfg, - sessionKey: params.sessionKey, - workspaceDir: params.workspaceDir, - ...(sessionAgentId ? { agentId: sessionAgentId } : {}), - ...(nativeHarnessId ? { agentHarnessRuntimeOverride: nativeHarnessId } : {}), - pluginRegistry, + try { + const preparedModelRuntime = preparedRuntimeLease.snapshot; + result = await withPluginRuntimeGenerationScope(preparedModelRuntime, async () => { + await cliCompactionDeps.ensureSelectedAgentHarnessPlugin({ + provider: params.provider, + modelId: params.model, + config: params.cfg, + sessionKey: params.sessionKey, + workspaceDir: params.workspaceDir, + ...(sessionAgentId ? { agentId: sessionAgentId } : {}), + ...(nativeHarnessId ? { agentHarnessRuntimeOverride: nativeHarnessId } : {}), + pluginRegistry: preparedModelRuntime.pluginRegistry, + }); + return await compactWithSafetyTimeout( + (abortSignal) => + cliCompactionDeps.maybeCompactAgentHarnessSession( + { + sessionId: params.sessionId, + sessionKey: params.sessionKey, + sessionFile: params.sessionFile, + workspaceDir: params.workspaceDir, + cwd: params.cwd, + agentDir: params.agentDir, + config: params.cfg, + skillsSnapshot: params.skillsSnapshot, + provider: params.provider, + model: params.model, + authProfileId, + contextTokenBudget: params.contextTokenBudget, + currentTokenCount: params.currentTokenCount, + trigger: "budget", + force: true, + messageChannel: params.messageChannel, + agentAccountId: params.agentAccountId, + senderIsOwner: params.senderIsOwner, + thinkLevel: params.thinkLevel, + extraSystemPrompt: params.extraSystemPrompt, + modelSelectionLocked, + allowGatewaySubagentBinding: true, + ...(params.contextEngine + ? { + contextEngine: params.contextEngine, + contextEngineRuntimeContext: buildCliCompactionRuntimeContext({ + sessionKey: params.sessionKey, + messageChannel: params.messageChannel, + agentAccountId: params.agentAccountId, + authProfileId, + workspaceDir: params.workspaceDir, + cwd: params.cwd, + agentDir: params.agentDir, + cfg: params.cfg, + skillsSnapshot: params.skillsSnapshot, + senderIsOwner: params.senderIsOwner, + provider: params.provider, + model: params.model, + harnessRuntime: nativeHarnessId, + modelSelectionLocked, + thinkLevel: params.thinkLevel, + extraSystemPrompt: params.extraSystemPrompt, + currentTokenCount: params.currentTokenCount, + contextTokenBudget: params.contextTokenBudget, + trigger: "cli_native_budget", + }), + } + : {}), + ...(nativeHarnessId ? { agentHarnessId: nativeHarnessId } : {}), + ...(abortSignal ? { abortSignal } : {}), + }, + { preparedModelRuntime }, + ), + resolveCompactionTimeoutMs(params.cfg), + ); }); - return await compactWithSafetyTimeout( - (abortSignal) => - cliCompactionDeps.maybeCompactAgentHarnessSession({ - sessionId: params.sessionId, - sessionKey: params.sessionKey, - sessionFile: params.sessionFile, - workspaceDir: params.workspaceDir, - cwd: params.cwd, - agentDir: params.agentDir, - config: params.cfg, - skillsSnapshot: params.skillsSnapshot, - provider: params.provider, - model: params.model, - authProfileId, - contextTokenBudget: params.contextTokenBudget, - currentTokenCount: params.currentTokenCount, - trigger: "budget", - force: true, - messageChannel: params.messageChannel, - agentAccountId: params.agentAccountId, - senderIsOwner: params.senderIsOwner, - thinkLevel: params.thinkLevel, - extraSystemPrompt: params.extraSystemPrompt, - modelSelectionLocked, - allowGatewaySubagentBinding: true, - ...(params.contextEngine - ? { - contextEngine: params.contextEngine, - contextEngineRuntimeContext: buildCliCompactionRuntimeContext({ - sessionKey: params.sessionKey, - messageChannel: params.messageChannel, - agentAccountId: params.agentAccountId, - authProfileId, - workspaceDir: params.workspaceDir, - cwd: params.cwd, - agentDir: params.agentDir, - cfg: params.cfg, - skillsSnapshot: params.skillsSnapshot, - senderIsOwner: params.senderIsOwner, - provider: params.provider, - model: params.model, - harnessRuntime: nativeHarnessId, - modelSelectionLocked, - thinkLevel: params.thinkLevel, - extraSystemPrompt: params.extraSystemPrompt, - currentTokenCount: params.currentTokenCount, - contextTokenBudget: params.contextTokenBudget, - trigger: "cli_native_budget", - }), - } - : {}), - ...(nativeHarnessId ? { agentHarnessId: nativeHarnessId } : {}), - ...(abortSignal ? { abortSignal } : {}), - }), - resolveCompactionTimeoutMs(params.cfg), - ); - }); + } finally { + preparedRuntimeLease.release(); + } } catch (error) { log.warn( `CLI native harness compaction failed for ${params.provider}/${params.model}: ${error instanceof Error ? error.message : String(error)}`, diff --git a/src/agents/embedded-agent-runner/compact.hooks.test.ts b/src/agents/embedded-agent-runner/compact.hooks.test.ts index 37de2ccbf8d5..649bbec60e6d 100644 --- a/src/agents/embedded-agent-runner/compact.hooks.test.ts +++ b/src/agents/embedded-agent-runner/compact.hooks.test.ts @@ -2677,6 +2677,20 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => { }); describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { + async function acquiredPreparedModelRuntime() { + const pendingLease = acquireAgentRunPreparedModelRuntimeMock.mock.results[0]?.value; + if (!pendingLease) { + throw new Error("expected prepared model runtime acquisition"); + } + return (await pendingLease).snapshot; + } + + function expectedNativeCompactionOptions( + nativeCompactionRequest: "after_context_engine" | "required_preflight", + ) { + return { nativeCompactionRequest, preparedModelRuntime: expect.any(Object) }; + } + function mockQueuedRouteAwareModel( defaultApi: "openai-responses" | "openai-chatgpt-responses" = "openai-responses", ) { @@ -2783,10 +2797,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { }), ); - const snapshot = acquireAgentRunPreparedModelRuntimeMock.mock.results[0]?.value - ? (await acquireAgentRunPreparedModelRuntimeMock.mock.results[0].value).snapshot - : undefined; - expect(snapshot).toBeDefined(); + const snapshot = await acquiredPreparedModelRuntime(); expect(mockCallArg(resolveModelAsyncMock, 0, 4)).toMatchObject({ preparedModelRuntime: snapshot, skipAgentDiscovery: true, @@ -2871,6 +2882,11 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { }), ), ).rejects.toThrow("route materialization failed"); + const snapshot = await acquiredPreparedModelRuntime(); + expect( + (mockCallArg(resolveModelAsyncMock, 1, 4) as { preparedModelRuntime?: unknown }) + .preparedModelRuntime, + ).toBe(snapshot); expect(dispose).toHaveBeenCalledTimes(1); expect(enqueueCommandInLaneMock).not.toHaveBeenCalled(); }); @@ -3316,13 +3332,14 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { expect(result.ok).toBe(true); expect(contextEngineCompactMock).toHaveBeenCalledTimes(1); expect(maybeCompactAgentHarnessSessionMock).toHaveBeenCalledTimes(1); + const snapshot = await acquiredPreparedModelRuntime(); expect(maybeCompactAgentHarnessSessionMock).toHaveBeenCalledWith( expect.objectContaining({ provider: "openai", model: "gpt-5.5", agentHarnessId: "codex", }), - { nativeCompactionRequest: "after_context_engine" }, + { nativeCompactionRequest: "after_context_engine", preparedModelRuntime: snapshot }, ); const compactArg = mockCallArg(contextEngineCompactMock) as { runtimeContext?: Record; @@ -3371,6 +3388,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { expect(result.compacted).toBe(true); expect(result.result?.summary).toBe("engine-summary"); expect(maybeCompactAgentHarnessSessionMock).toHaveBeenCalledTimes(1); + const snapshot = await acquiredPreparedModelRuntime(); expect(maybeCompactAgentHarnessSessionMock).toHaveBeenCalledWith( expect.objectContaining({ provider: "openai", @@ -3378,7 +3396,10 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { agentHarnessId: "codex", preflightRequired: true, }), - expect.objectContaining({ nativeCompactionRequest: "required_preflight" }), + expect.objectContaining({ + nativeCompactionRequest: "required_preflight", + preparedModelRuntime: snapshot, + }), ); expect(contextEngineCompactMock).toHaveBeenCalledTimes(1); }); @@ -3579,7 +3600,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { provider: "openai", model: "gpt-5.5", }), - { nativeCompactionRequest: "after_context_engine" }, + expectedNativeCompactionOptions("after_context_engine"), ); const compactArg = mockCallArg(contextEngineCompactMock) as { runtimeContext?: Record; @@ -3683,7 +3704,10 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { baseUrl: "https://api.openai.com/v1", }), }), - { nativeCompactionRequest: "after_context_engine" }, + { + nativeCompactionRequest: "after_context_engine", + preparedModelRuntime: expect.any(Object), + }, ); } finally { closeOpenClawAgentDatabasesForTest(); @@ -3772,7 +3796,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { model: "gpt-5.5", agentHarnessId: "codex", }), - { nativeCompactionRequest: "after_context_engine" }, + expectedNativeCompactionOptions("after_context_engine"), ); const compactArg = mockCallArg(contextEngineCompactMock) as { runtimeContext?: Record; @@ -3861,7 +3885,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { }), runtimeAuthPlan: undefined, }), - { nativeCompactionRequest: "after_context_engine" }, + expectedNativeCompactionOptions("after_context_engine"), ); }); @@ -3899,7 +3923,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { ); expect(maybeCompactAgentHarnessSessionMock).toHaveBeenCalledWith( expect.objectContaining({ runtimeAuthPlan: undefined }), - { nativeCompactionRequest: "after_context_engine" }, + expectedNativeCompactionOptions("after_context_engine"), ); }); @@ -4141,7 +4165,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { }), runtimeAuthPlan: expect.objectContaining({ modelRoute }), }), - { nativeCompactionRequest: "after_context_engine" }, + expectedNativeCompactionOptions("after_context_engine"), ); const compactArg = mockCallArg(contextEngineCompactMock) as { runtimeContext?: Record; @@ -4252,7 +4276,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { model: "gpt-5.4", agentHarnessId: "codex", }), - { nativeCompactionRequest: "after_context_engine" }, + expectedNativeCompactionOptions("after_context_engine"), ); const details = result.result?.details as | { codexNativeCompaction?: Record } @@ -4650,7 +4674,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { sessionFile: TEST_SESSION_KEY, trigger: "budget", }), - { nativeCompactionRequest: "after_context_engine" }, + expectedNativeCompactionOptions("after_context_engine"), ); expect(contextEngineCompactMock.mock.invocationCallOrder[0]).toBeLessThan( expectDefined( @@ -4758,7 +4782,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { expect.objectContaining({ trigger: "budget", }), - { nativeCompactionRequest: "after_context_engine" }, + expectedNativeCompactionOptions("after_context_engine"), ); const details = result.result?.details as | { codexNativeCompaction?: Record } diff --git a/src/agents/embedded-agent-runner/compact.queued.ts b/src/agents/embedded-agent-runner/compact.queued.ts index d1831bd36b57..4ac49dbedef5 100644 --- a/src/agents/embedded-agent-runner/compact.queued.ts +++ b/src/agents/embedded-agent-runner/compact.queued.ts @@ -541,6 +541,7 @@ async function compactResolvedContextEngine( const resolved = await resolveModelAsync(ceRuntimeProvider, ceModelId, agentDir, config, { authStorage, modelRegistry, + preparedModelRuntime, skipAgentDiscovery: true, allowBundledStaticCatalogFallback: true, preferBundledStaticCatalogTransport: true, @@ -616,14 +617,17 @@ async function compactResolvedContextEngine( contextTokenBudget, contextEngineRuntimeContext, }, - preparedParams.preflightRequired === true - ? { - nativeCompactionRequest: "required_preflight", - onNativeCompactionCapabilityUsed: () => { - requiredPreflightNativeCapabilityUsed = true; - }, - } - : undefined, + { + preparedModelRuntime, + ...(preparedParams.preflightRequired === true + ? { + nativeCompactionRequest: "required_preflight", + onNativeCompactionCapabilityUsed: () => { + requiredPreflightNativeCapabilityUsed = true; + }, + } + : {}), + }, ) : undefined; // A model lock normally makes the native harness result terminal: the @@ -883,7 +887,7 @@ async function compactResolvedContextEngine( contextTokenBudget, contextEngineRuntimeContext, }, - { nativeCompactionRequest: "after_context_engine" }, + { nativeCompactionRequest: "after_context_engine", preparedModelRuntime }, ); if (secondaryNativeHarnessCompaction && !secondaryNativeHarnessCompaction.ok) { log.warn( diff --git a/src/agents/harness/compaction.ts b/src/agents/harness/compaction.ts index 3f313c2ffc92..1dfabaf87f06 100644 --- a/src/agents/harness/compaction.ts +++ b/src/agents/harness/compaction.ts @@ -17,6 +17,7 @@ import { } from "../model-auth.js"; import { isCliRuntimeAliasForProvider, isCliRuntimeProvider } from "../model-runtime-aliases.js"; import { isOpenAIProvider } from "../openai-routing.js"; +import type { PreparedModelRuntimeSnapshot } from "../prepared-model-runtime.js"; import { unwrapModelHeaderSentinelsForProviderEgress, unwrapSecretSentinelsForProviderEgress, @@ -54,6 +55,7 @@ import type { AgentHarness, AgentHarnessNativeCompactionRequest } from "./types. * can opt in through their `compact` hook. */ type InternalAgentHarnessCompactionOptions = { + preparedModelRuntime: PreparedModelRuntimeSnapshot; nativeCompactionRequest?: AgentHarnessNativeCompactionRequest; onNativeCompactionCapabilityUsed?: () => void; }; @@ -117,6 +119,7 @@ async function resolveHarnessCompactApiKey(params: { agentId: string; sessionKey?: string; pinnedHarnessId?: string; + preparedModelRuntime: PreparedModelRuntimeSnapshot; }): Promise<{ harness: AgentHarness; apiKey?: string; @@ -137,6 +140,7 @@ async function resolveHarnessCompactApiKey(params: { ? providedRuntimeAuthPlan : undefined; const workspaceDir = resolveUserPath(compactParams.workspaceDir); + const preparedStores = params.preparedModelRuntime.createStores(); const callerRuntimeModel = compactParams.runtimeModel; const fallbackResolution = ( harness: AgentHarness, @@ -194,6 +198,8 @@ async function resolveHarnessCompactApiKey(params: { authProfileMode, }: Parameters>[0]["resolveModel"]>[0]) => resolveModelAsync(provider, modelId, agentDir, config, { + ...preparedStores, + preparedModelRuntime: params.preparedModelRuntime, authProfileId: profileId, authProfileMode, skipAgentDiscovery: true, @@ -206,6 +212,8 @@ async function resolveHarnessCompactApiKey(params: { try { model = ( await resolveModelAsync(provider, modelId, agentDir, compactParams.config, { + ...preparedStores, + preparedModelRuntime: params.preparedModelRuntime, authProfileId: reusableRuntimeAuthPlan?.forwardedAuthProfileId ?? compactParams.authProfileId?.trim() ?? @@ -364,7 +372,7 @@ async function resolveHarnessCompactApiKey(params: { /** Runs harness-provided compaction when the selected runtime supports it. */ export async function maybeCompactAgentHarnessSession( params: CompactEmbeddedAgentSessionParams, - options: InternalAgentHarnessCompactionOptions = {}, + options: InternalAgentHarnessCompactionOptions, ): Promise { const selectedRuntime = normalizeOptionalAgentRuntimeId(params.agentHarnessId); const pinnedHarnessId = @@ -479,6 +487,7 @@ export async function maybeCompactAgentHarnessSession( agentId: compactIdentity.agentId, sessionKey: runtimePolicySessionKey, pinnedHarnessId, + preparedModelRuntime: options.preparedModelRuntime, }); harness = resolved.harness; const nativeToolPolicyRestricted = resolveNativeToolPolicyRestricted(harness); diff --git a/src/agents/harness/selection.test.ts b/src/agents/harness/selection.test.ts index efec0fb40698..a95aa5c2c20a 100644 --- a/src/agents/harness/selection.test.ts +++ b/src/agents/harness/selection.test.ts @@ -7,6 +7,7 @@ import { OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST } from "../../context-engine/host import type { ContextEngine } from "../../context-engine/types.js"; import { resetAgentRunRegistryForTest } from "../../infra/agent-run-registry.js"; import { createOpenClawCodingTools } from "../../plugin-sdk/agent-harness.js"; +import { getActivePluginRegistry } from "../../plugins/runtime.js"; import { mintSecretSentinel } from "../../secrets/sentinel.js"; import type { UserTurnTranscriptRecorder } from "../../sessions/user-turn-transcript.types.js"; import { @@ -17,6 +18,11 @@ import { } from "../admitted-run-context.js"; import { isHostScopedAgentToolActive } from "../agent-tools.ring-zero-context.js"; import { testing as cliBackendsTesting } from "../cli-backends.test-support.js"; +import { + createModelGenerationFixture, + publishCurrentModelGeneration, + resetModelGenerationFixtureState, +} from "../embedded-agent-runner/model.generation-scope.test-support.js"; import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult, @@ -24,7 +30,7 @@ import type { import { getGatewayToolCallerIdentity } from "../tools/gateway-caller-context.js"; import { callGatewayTool } from "../tools/gateway.js"; import type { SystemAgentToolOptions } from "../tools/system-agent-tool.js"; -import { maybeCompactAgentHarnessSession } from "./compaction.js"; +import { maybeCompactAgentHarnessSession as maybeCompactAgentHarnessSessionImpl } from "./compaction.js"; import type { ContextEngineLogicalTurnLease } from "./context-engine-logical-turn.js"; import { clearAgentHarnesses, registerAgentHarness } from "./registry.js"; import { @@ -156,6 +162,7 @@ let selectionAdmittedRunContext: AdmittedRunContext; beforeEach(async () => { resetAgentRunRegistryForTest(); + resetModelGenerationFixtureState(); selectionAdmission = prepareAgentRunAdmission({ cfg: {}, facts: { @@ -214,6 +221,7 @@ afterEach(() => { selectionAdmission.close(); resetAgentRunRegistryForTest(); clearAgentHarnesses(); + resetModelGenerationFixtureState(); cliBackendsTesting.resetDepsForTest(); agentRunAttempt.mockClear(); compactAuthMocks.prepareAgentRuntimeAuth.mockClear(); @@ -435,7 +443,21 @@ function agentModelRuntimeConfig( } as OpenClawConfig; } -type CompactSessionParams = Parameters[0]; +function maybeCompactAgentHarnessSession( + params: Parameters[0], + options: Partial[1]> = {}, +) { + const preparedModelRuntime = + options.preparedModelRuntime ?? + createModelGenerationFixture({ + config: params.config ?? {}, + createStores: () => ({ authStorage: {} as never, modelRegistry: {} as never }), + label: "harness-test", + }).preparedModelRuntime; + return maybeCompactAgentHarnessSessionImpl(params, { ...options, preparedModelRuntime }); +} + +type CompactSessionParams = Parameters[0]; const OPENAI_PLATFORM_ROUTE = { provider: "openai", @@ -3354,6 +3376,111 @@ describe("selectAgentHarness", () => { ); }); + it("keeps auth-route rematerialization on the caller-owned prepared generation", async () => { + const cfg = {} as OpenClawConfig; + const createStores = () => ({ authStorage: {} as never, modelRegistry: {} as never }); + const generationA = createModelGenerationFixture({ + config: cfg, + createStores, + label: "compact-a", + provider: "local-proxy", + requestProvider: "local-proxy", + modelId: "proxy-model", + runtimeApi: "openai-responses", + }); + const generationB = createModelGenerationFixture({ + config: cfg, + createStores, + label: "compact-b", + provider: "local-proxy", + requestProvider: "local-proxy", + modelId: "proxy-model", + runtimeApi: "openai-responses", + }); + publishCurrentModelGeneration(generationA); + compactAuthMocks.resolveModelAsync.mockImplementation( + async (_provider, _modelId, _agentDir, _config, options) => { + const registry = options?.preparedModelRuntime?.pluginRegistry ?? getActivePluginRegistry(); + const label = registry === generationA.pluginRegistry ? "A" : "B"; + return { + model: { + provider: "local-proxy", + id: "proxy-model", + name: `Runtime ${label}`, + api: "openai-responses", + baseUrl: `https://generation-${label.toLowerCase()}.example.test/v1`, + }, + }; + }, + ); + compactAuthMocks.ensureAuthProfileStoreWithoutExternalProfiles.mockReturnValue({ + version: 1, + profiles: { + "local-proxy:stale": { + type: "api_key", + provider: "local-proxy", + key: "stale-key", + }, + }, + }); + const profilePlan = { + providerForAuth: "local-proxy", + authProfileProviderForAuth: "local-proxy", + forwardedAuthProfileId: "local-proxy:stale", + forwardedAuthProfileSource: "auto" as const, + selectedAuthMode: "api_key" as const, + }; + const directPlan = { + providerForAuth: "local-proxy", + authProfileProviderForAuth: "local-proxy", + selectedAuthMode: "api_key" as const, + }; + compactAuthMocks.prepareAgentRuntimeAuth.mockReturnValueOnce({ + plan: profilePlan, + attempts: [ + { + kind: "profile" as const, + profileId: "local-proxy:stale", + plan: profilePlan, + allowAuthProfileFallback: false, + }, + { kind: "direct" as const, plan: directPlan, requiresPriorProfileAttempt: true }, + ], + }); + compactAuthMocks.getApiKeyForModelCore.mockImplementation(async (params) => { + if (params.profileId === "local-proxy:stale") { + publishCurrentModelGeneration(generationB); + throw new Error("stale profile"); + } + return { apiKey: "direct-key", source: "direct", mode: "api-key" }; + }); + const compact = registerTestCompactor({ id: "copilot", provider: "local-proxy" }); + const options = { preparedModelRuntime: generationA.preparedModelRuntime }; + + await expect( + maybeCompactAgentHarnessSession( + createCompactionParams({ + config: cfg, + provider: "local-proxy", + model: "proxy-model", + agentHarnessId: "copilot", + }), + options, + ), + ).resolves.toEqual({ ok: true, compacted: false }); + + expect(compactAuthMocks.resolveModelAsync).toHaveBeenCalledTimes(2); + expect(compact).toHaveBeenCalledWith( + expect.objectContaining({ + runtimeModel: expect.objectContaining({ + name: "Runtime A", + api: "openai-responses", + baseUrl: "https://generation-a.example.test/v1", + }), + }), + ); + }); + it("does not compact a selected plugin harness through OpenClaw when the plugin has no compactor", async () => { registerFailingCodexHarness();