mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(agents): pin compaction to prepared generation (#125688)
This commit is contained in:
committed by
GitHub
parent
5db8368f35
commit
0f18bd7fa2
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user