fix(agents): pin compaction to prepared generation (#125688)

This commit is contained in:
Peter Steinberger
2026-08-18 00:45:38 -07:00
committed by GitHub
parent 5db8368f35
commit 0f18bd7fa2
6 changed files with 315 additions and 110 deletions
+40 -9
View File
@@ -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<string, unknown>]>;
.calls as unknown as Array<[Record<string, unknown>, { 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(
+84 -74
View File
@@ -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)}`,
@@ -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<string, unknown>;
@@ -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<string, unknown>;
@@ -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<string, unknown>;
@@ -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<string, unknown>;
@@ -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<string, unknown> }
@@ -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<string, unknown> }
@@ -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(
+10 -1
View File
@@ -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<Parameters<typeof materializePreparedRuntimeModel<Model>>[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<EmbeddedAgentCompactResult | undefined> {
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);
+129 -2
View File
@@ -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<typeof maybeCompactAgentHarnessSession>[0];
function maybeCompactAgentHarnessSession(
params: Parameters<typeof maybeCompactAgentHarnessSessionImpl>[0],
options: Partial<Parameters<typeof maybeCompactAgentHarnessSessionImpl>[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<typeof maybeCompactAgentHarnessSessionImpl>[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();