fix(agents): preserve Codex auth for compaction fallback

Fixes #86820.

Preserve Codex OAuth-backed compaction by selecting and loading the Codex harness before resolving direct or queued compaction models, while keeping OpenAI-compatible custom base URLs on the OpenAI context config path. Also preserves persisted concrete harness pins so compaction does not hot-switch existing sessions just because an explicit Codex fallback exists.

Verification:
- node scripts/run-vitest.mjs src/agents/embedded-agent-runner/compact.hooks.test.ts src/agents/harness/selection.test.ts src/agents/harness/runtime-plugin.test.ts
- pnpm tsgo:prod
- pnpm check:test-types
- pnpm lint --threads=8
- git diff --check origin/main...HEAD
- git diff --check
- autoreview clean: no accepted/actionable findings reported; overall patch is correct (0.82)
- GitHub PR checks green on ac6f93de4a
This commit is contained in:
Peter Steinberger
2026-05-30 02:26:00 +02:00
committed by GitHub
parent 43658872d9
commit aada44fca5
15 changed files with 853 additions and 86 deletions

View File

@@ -618,14 +618,6 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
expect(result.ok).toBe(true);
expect(result.result?.summary).toBe("oauth fallback summary");
findMockCall(resolveAgentHarnessPolicyMock, ([arg]) => {
const policyArg = arg as Record<string, unknown>;
return policyArg.provider === "openai" && policyArg.modelId === "gpt-primary";
});
findMockCall(resolveAgentHarnessPolicyMock, ([arg]) => {
const policyArg = arg as Record<string, unknown>;
return policyArg.provider === "openai" && policyArg.modelId === "gpt-fallback";
});
findMockCall(
resolveModelMock,
([provider, modelId]) => provider === "openai-codex" && modelId === "gpt-primary",
@@ -639,7 +631,7 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
});
});
it("uses the selected Codex runtime provider for OpenAI compaction context windows", async () => {
it("uses the selected Codex runtime provider for OpenAI compaction", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
model: { provider, api: "responses", id: modelId, input: [] },
@@ -655,6 +647,7 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: {
models: {
providers: {
@@ -672,7 +665,7 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
});
expect(result.ok).toBe(true);
expect(mockCallArg(resolveModelMock)).toBe("openai");
expect(mockCallArg(resolveModelMock)).toBe("openai-codex");
expect(mockCallArg(resolveModelMock, 0, 1)).toBe("gpt-5.5");
expectRecordFields(mockCallArg(resolveContextWindowInfoMock), {
provider: "openai-codex",
@@ -680,8 +673,86 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
});
});
it("preserves direct OpenAI API-key compaction when no Codex auth is configured", async () => {
it("uses explicit Codex runtime policy for direct OpenAI compaction", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({
runtime: "codex",
runtimeSource: "model",
} as never);
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
model: { provider, api: "responses", id: modelId, input: [] },
error: null,
authStorage: { setRuntimeApiKey: vi.fn() },
modelRegistry: {},
}));
const result = await compactEmbeddedAgentSessionDirect({
sessionId: "session-1",
sessionKey: TEST_SESSION_KEY,
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
config: {
models: {
providers: {
openai: { models: [{ id: "fake-model", contextWindow: 1_000_000 }] },
"openai-codex": { models: [{ id: "fake-model", contextWindow: 350_000 }] },
},
},
} as never,
});
expect(result.ok).toBe(true);
expect(resolveAgentHarnessPolicyMock).toHaveBeenCalledWith(
expect.objectContaining({ provider: "openai", modelId: "fake-model" }),
);
expect(mockCallArg(resolveModelMock)).toBe("openai-codex");
expectRecordFields(mockCallArg(resolveContextWindowInfoMock), {
provider: "openai-codex",
modelId: "fake-model",
});
});
it("keeps custom OpenAI-compatible compaction on OpenAI context config", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
model: { provider, api: "responses", id: modelId, input: [], contextWindow: 1_000_000 },
error: null,
authStorage: { setRuntimeApiKey: vi.fn() },
modelRegistry: {},
}));
const result = await compactEmbeddedAgentSessionDirect({
sessionId: "session-1",
sessionKey: TEST_SESSION_KEY,
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: {
models: {
providers: {
openai: {
baseUrl: "https://openai-compatible.example/v1",
models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }],
},
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
agents: { defaults: { embeddedHarness: { runtime: "codex" } } },
} as never,
});
expect(result.ok).toBe(true);
expect(mockCallArg(resolveModelMock)).toBe("openai");
expect(mockCallArg(resolveModelMock, 0, 1)).toBe("gpt-5.5");
expectRecordFields(mockCallArg(resolveContextWindowInfoMock), {
provider: "openai",
modelId: "gpt-5.5",
});
});
it("preserves direct OpenAI API-key compaction when OpenClaw runtime is active", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "openclaw" });
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
model: { provider, api: "responses", id: modelId, input: [] },
error: null,
@@ -702,7 +773,7 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
openai: { models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }] },
},
},
agents: { defaults: { embeddedHarness: { runtime: "codex" } } },
agents: { defaults: { embeddedHarness: { runtime: "openclaw" } } },
} as never,
});
@@ -711,6 +782,52 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
expect(mockCallArg(resolveModelMock, 0, 1)).toBe("gpt-5.5");
});
it("routes OpenAI compaction model overrides through Codex OAuth when Codex runtime is active", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
model: { provider, api: "responses", id: modelId, input: [] },
error: null,
authStorage: { setRuntimeApiKey: vi.fn() },
modelRegistry: {},
}));
const result = await compactEmbeddedAgentSessionDirect({
sessionId: "session-1",
sessionKey: TEST_SESSION_KEY,
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: {
models: {
providers: {
openai: {
models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }, { id: "gpt-5.4-mini" }],
},
"openai-codex": {
models: [{ id: "gpt-5.5" }, { id: "gpt-5.4-mini", contextWindow: 350_000 }],
},
},
},
agents: {
defaults: {
embeddedHarness: { runtime: "codex" },
compaction: { model: "openai/gpt-5.4-mini" },
},
},
} as never,
});
expect(result.ok).toBe(true);
expect(mockCallArg(resolveModelMock)).toBe("openai-codex");
expect(mockCallArg(resolveModelMock, 0, 1)).toBe("gpt-5.4-mini");
expectRecordFields(mockCallArg(resolveContextWindowInfoMock), {
provider: "openai-codex",
modelId: "gpt-5.4-mini",
});
});
it("uses Codex auth for runtime model loading while preserving OpenAI context config", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "openclaw" });
resolveModelMock.mockImplementation((provider = "openai", modelId = "fake") => ({
@@ -1671,8 +1788,7 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => {
}),
);
const harnessArg = mockCallArg(maybeCompactAgentHarnessSessionMock) as Record<string, unknown>;
expect(harnessArg.contextTokenBudget).toBe(32_000);
expect(maybeCompactAgentHarnessSessionMock).not.toHaveBeenCalled();
const compactArg = mockCallArg(contextEngineCompactMock) as {
tokenBudget?: number;
runtimeContext?: Record<string, unknown>;
@@ -1720,6 +1836,248 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => {
});
});
it("passes selected Codex runtime to queued context-engine runtime context", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({
ok: true,
compacted: true,
result: {
summary: "harness",
firstKeptEntryId: "entry-1",
tokensBefore: 100,
},
});
const result = await compactEmbeddedAgentSession(
wrappedCompactionArgs({
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: {
models: {
providers: {
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
},
}),
);
expect(result.ok).toBe(true);
const harnessArg = mockCallArg(maybeCompactAgentHarnessSessionMock) as Record<string, unknown>;
expectRecordFields(harnessArg.contextEngineRuntimeContext, {
provider: "openai",
runtimeProvider: "openai-codex",
model: "gpt-5.5",
});
});
it("uses explicit Codex runtime policy for queued native compaction", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({
runtime: "codex",
runtimeSource: "model",
} as never);
maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({
ok: true,
compacted: true,
result: {
summary: "harness",
firstKeptEntryId: "entry-1",
tokensBefore: 100,
},
});
const result = await compactEmbeddedAgentSession(
wrappedCompactionArgs({
provider: "openai",
model: "gpt-5.5",
config: {
models: {
providers: {
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
},
}),
);
expect(result.ok).toBe(true);
const harnessArg = mockCallArg(maybeCompactAgentHarnessSessionMock) as Record<string, unknown>;
expectRecordFields(harnessArg.contextEngineRuntimeContext, {
provider: "openai",
runtimeProvider: "openai-codex",
model: "gpt-5.5",
});
});
it("preserves concrete OpenClaw pins over explicit Codex policy for queued compaction", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({
runtime: "codex",
runtimeSource: "model",
} as never);
maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({
ok: true,
compacted: true,
result: {
summary: "harness",
firstKeptEntryId: "entry-1",
tokensBefore: 100,
},
});
const result = await compactEmbeddedAgentSession(
wrappedCompactionArgs({
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "openclaw",
config: {
models: {
providers: {
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
},
}),
);
expect(result.ok).toBe(true);
expect(maybeCompactAgentHarnessSessionMock).not.toHaveBeenCalled();
const compactArg = mockCallArg(contextEngineCompactMock) as {
runtimeContext?: Record<string, unknown>;
};
expectRecordFields(compactArg.runtimeContext, {
provider: "openai",
runtimeProvider: undefined,
model: "gpt-5.5",
});
});
it("keeps concrete Codex pins when explicit policy is auto for queued compaction", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({
runtime: "auto",
runtimeSource: "model",
} as never);
maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({
ok: true,
compacted: true,
result: {
summary: "harness",
firstKeptEntryId: "entry-1",
tokensBefore: 100,
},
});
const result = await compactEmbeddedAgentSession(
wrappedCompactionArgs({
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: {
models: {
providers: {
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
},
}),
);
expect(result.ok).toBe(true);
const harnessArg = mockCallArg(maybeCompactAgentHarnessSessionMock) as Record<string, unknown>;
expectRecordFields(harnessArg.contextEngineRuntimeContext, {
provider: "openai",
runtimeProvider: "openai-codex",
model: "gpt-5.5",
});
});
it("does not route queued compaction through implicit Codex policy alone", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({
ok: true,
compacted: true,
result: {
summary: "harness",
firstKeptEntryId: "entry-1",
tokensBefore: 100,
},
});
const result = await compactEmbeddedAgentSession(
wrappedCompactionArgs({
provider: "openai",
model: "gpt-5.5",
config: {
models: {
providers: {
openai: { models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }] },
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
},
}),
);
expect(result.ok).toBe(true);
expect(maybeCompactAgentHarnessSessionMock).not.toHaveBeenCalled();
const compactArg = mockCallArg(contextEngineCompactMock) as {
runtimeContext?: Record<string, unknown>;
};
expectRecordFields(compactArg.runtimeContext, {
provider: "openai",
runtimeProvider: undefined,
model: "gpt-5.5",
});
});
it("keeps queued custom OpenAI-compatible compaction on OpenAI context config", async () => {
resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "codex" });
maybeCompactAgentHarnessSessionMock.mockResolvedValueOnce({
ok: true,
compacted: true,
result: {
summary: "harness",
firstKeptEntryId: "entry-1",
tokensBefore: 100,
},
});
const result = await compactEmbeddedAgentSession(
wrappedCompactionArgs({
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: {
models: {
providers: {
openai: {
baseUrl: "https://openai-compatible.example/v1",
models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }],
},
"openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] },
},
},
},
}),
);
expect(result.ok).toBe(true);
expect(mockCallArg(resolveModelMock)).toBe("openai");
expectRecordFields(mockCallArg(resolveContextWindowInfoMock), {
provider: "openai",
modelId: "gpt-5.5",
});
expect(maybeCompactAgentHarnessSessionMock).not.toHaveBeenCalled();
const compactArg = mockCallArg(contextEngineCompactMock) as {
runtimeContext?: Record<string, unknown>;
};
expectRecordFields(compactArg.runtimeContext, {
provider: "openai",
runtimeProvider: undefined,
model: "gpt-5.5",
});
});
it("fails deferred budget compaction when background maintenance is not scheduled", async () => {
const dispose = vi.fn(async () => {});
const maintain = vi.fn(async () => ({

View File

@@ -16,16 +16,19 @@ import { formatErrorMessage } from "../../infra/errors.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
import { enqueueCommandInLane } from "../../process/command-queue.js";
import { parseAgentSessionKey } from "../../routing/session-key.js";
import { resolveUserPath } from "../../utils.js";
import { isDefaultAgentRuntimeId, normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
import { resolveAgentDir, resolveSessionAgentIds } from "../agent-scope.js";
import { resolveContextWindowInfo } from "../context-window-guard.js";
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
import { isRecoverableNativeHarnessBindingFailure } from "../harness/compaction-recovery.js";
import { ensureSelectedAgentHarnessPlugin } from "../harness/runtime-plugin.js";
import {
maybeCompactAgentHarnessSession,
resolveAgentHarnessPolicy,
} from "../harness/selection.js";
import { resolveContextConfigProviderForRuntime } from "../openai-codex-routing.js";
import { isOpenAICodexProvider, isOpenAIProvider } from "../openai-codex-routing.js";
import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js";
import { DEFERRED_CONTEXT_ENGINE_COMPACTION_REASON } from "./compact-reasons.js";
import type { CompactEmbeddedAgentSessionParams } from "./compact.types.js";
@@ -169,7 +172,12 @@ export async function compactEmbeddedAgentSession(
agentDir,
workspaceDir: resolvedWorkspaceDir,
});
const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({
const runtimePolicySessionKey = params.sandboxSessionKey ?? params.sessionKey;
const runtimePolicyAgentId =
params.sandboxSessionKey && parseAgentSessionKey(params.sandboxSessionKey)
? undefined
: params.agentId;
const policyCompactionTarget = resolveEmbeddedCompactionTarget({
config: params.config,
provider: params.provider,
modelId: params.model,
@@ -177,9 +185,51 @@ export async function compactEmbeddedAgentSession(
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const configuredHarnessPolicy = resolveAgentHarnessPolicy({
provider: policyCompactionTarget.provider ?? DEFAULT_PROVIDER,
modelId: policyCompactionTarget.model ?? DEFAULT_MODEL,
config: params.config,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
});
const configuredHarnessRuntime =
configuredHarnessPolicy.runtimeSource &&
configuredHarnessPolicy.runtimeSource !== "implicit" &&
!isDefaultAgentRuntimeId(configuredHarnessPolicy.runtime)
? configuredHarnessPolicy.runtime
: undefined;
// The persisted harness id is the runtime contract for this session; config
// changes can supply a runtime only when the session has no concrete pin.
const selectedHarnessRuntime = params.agentHarnessId ?? configuredHarnessRuntime;
const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({
config: params.config,
provider: params.provider,
modelId: params.model,
authProfileId: params.authProfileId,
harnessRuntime: selectedHarnessRuntime,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const ceProvider = resolvedCompactionTarget.provider ?? DEFAULT_PROVIDER;
const ceRuntimeProvider = resolvedCompactionTarget.runtimeProvider ?? ceProvider;
const ceContextConfigProvider = resolvedCompactionTarget.contextProvider ?? ceProvider;
const ceModelId = resolvedCompactionTarget.model ?? DEFAULT_MODEL;
const attemptNativeHarnessCompaction = shouldAttemptNativeHarnessCompaction({
provider: ceProvider,
contextProvider: resolvedCompactionTarget.contextProvider,
selectedHarnessRuntime,
});
if (attemptNativeHarnessCompaction) {
await ensureSelectedAgentHarnessPlugin({
config: params.config,
provider: ceProvider,
modelId: ceModelId,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
agentHarnessRuntimeOverride: selectedHarnessRuntime,
workspaceDir: resolvedWorkspaceDir,
});
}
const { model: ceModel } = await resolveModelAsync(
ceRuntimeProvider,
ceModelId,
@@ -187,21 +237,11 @@ export async function compactEmbeddedAgentSession(
params.config,
);
const ceRuntimeModel = ceModel as ProviderRuntimeModel | undefined;
const ceHarnessPolicy = resolveAgentHarnessPolicy({
provider: ceProvider,
modelId: ceModelId,
config: params.config,
agentId: agentIds.sessionAgentId,
sessionKey: params.sessionKey,
});
const resolvedContextTokenBudget =
normalizeContextTokenBudget(
resolveContextWindowInfo({
cfg: params.config,
provider: resolveContextConfigProviderForRuntime({
provider: ceProvider,
runtimeId: params.agentHarnessId ?? ceHarnessPolicy.runtime,
}),
provider: ceContextConfigProvider,
modelId: ceModelId,
modelContextTokens: readAgentModelContextTokens(ceModel),
modelContextWindow: ceRuntimeModel?.contextWindow,
@@ -216,15 +256,18 @@ export async function compactEmbeddedAgentSession(
const contextEngineRuntimeContext = buildCompactionContextEngineRuntimeContext({
params,
agentDir,
harnessRuntime: selectedHarnessRuntime,
contextTokenBudget,
contextEnginePluginId: resolveContextEngineOwnerPluginId(contextEngine),
});
const harnessResult = await maybeCompactAgentHarnessSession({
...params,
contextEngine,
contextTokenBudget,
contextEngineRuntimeContext,
});
const harnessResult = attemptNativeHarnessCompaction
? await maybeCompactAgentHarnessSession({
...params,
contextEngine,
contextTokenBudget,
contextEngineRuntimeContext,
})
: undefined;
if (harnessResult) {
if (!shouldFallbackAfterHarnessCompaction(harnessResult)) {
await contextEngine.dispose?.();
@@ -468,9 +511,25 @@ export async function compactEmbeddedAgentSession(
);
}
function shouldAttemptNativeHarnessCompaction(params: {
provider: string;
contextProvider?: string;
selectedHarnessRuntime?: string | null;
}): boolean {
if (isOpenAICodexProvider(params.provider)) {
return true;
}
const selectedRuntime = normalizeOptionalAgentRuntimeId(params.selectedHarnessRuntime);
if (!selectedRuntime || selectedRuntime === "auto" || selectedRuntime === "openclaw") {
return false;
}
return isOpenAIProvider(params.provider) ? params.contextProvider !== undefined : true;
}
function buildCompactionContextEngineRuntimeContext(params: {
params: CompactEmbeddedAgentSessionParams;
agentDir: string;
harnessRuntime?: string;
contextEnginePluginId?: string;
contextTokenBudget?: number;
}): ContextEngineRuntimeContext {
@@ -499,6 +558,7 @@ function buildCompactionContextEngineRuntimeContext(params: {
senderId: params.params.senderId,
provider: params.params.provider,
modelId: params.params.model,
harnessRuntime: params.harnessRuntime,
modelFallbacksOverride: params.params.modelFallbacksOverride,
thinkLevel: params.params.thinkLevel,
reasoningLevel: params.params.reasoningLevel,

View File

@@ -24,7 +24,11 @@ import {
resolveProviderTextTransforms,
transformProviderSystemPrompt,
} from "../../plugins/provider-runtime.js";
import { isCronSessionKey, isSubagentSessionKey } from "../../routing/session-key.js";
import {
isCronSessionKey,
isSubagentSessionKey,
parseAgentSessionKey,
} from "../../routing/session-key.js";
import { resolveSkillsPromptForRun } from "../../skills/loading/workspace.js";
import { resolveEmbeddedRunSkillEntries } from "../../skills/runtime/embedded-run-entries.js";
import {
@@ -41,6 +45,7 @@ import {
setCompactionSafeguardCancelReason,
} from "../agent-hooks/compaction-safeguard-runtime.js";
import { createPreparedEmbeddedAgentSettingsManager } from "../agent-project-settings.js";
import { isDefaultAgentRuntimeId } from "../agent-runtime-id.js";
import {
resolveAgentDir,
resolveRunModelFallbacksOverride,
@@ -87,7 +92,6 @@ import {
import { isFallbackSummaryError, runWithModelFallback } from "../model-fallback.js";
import { supportsModelTools } from "../model-tool-support.js";
import { ensureOpenClawModelsJson } from "../models-config.js";
import { resolveContextConfigProviderForRuntime } from "../openai-codex-routing.js";
import { wrapStreamFnTextTransforms } from "../plugin-text-transforms.js";
import { resolveAgentPromptSurfaceForSessionKey } from "../prompt-surface.js";
import { registerProviderStreamForModel } from "../provider-stream.js";
@@ -489,7 +493,19 @@ async function compactEmbeddedAgentSessionDirectOnce(
workspaceDir: resolvedWorkspace,
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
});
const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({
const earlyAgentIds = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
const agentDir =
params.agentDir ?? resolveAgentDir(params.config ?? {}, earlyAgentIds.sessionAgentId);
const runtimePolicySessionKey = params.sandboxSessionKey ?? params.sessionKey;
const runtimePolicyAgentId =
params.sandboxSessionKey && parseAgentSessionKey(params.sandboxSessionKey)
? undefined
: params.agentId;
const policyCompactionTarget = resolveEmbeddedCompactionTarget({
config: params.config,
provider: params.provider,
modelId: params.model,
@@ -497,12 +513,47 @@ async function compactEmbeddedAgentSessionDirectOnce(
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const configuredHarnessPolicy = resolveAgentHarnessPolicy({
provider: policyCompactionTarget.provider ?? DEFAULT_PROVIDER,
modelId: policyCompactionTarget.model ?? DEFAULT_MODEL,
config: params.config,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
});
const configuredHarnessRuntime =
configuredHarnessPolicy.runtimeSource &&
configuredHarnessPolicy.runtimeSource !== "implicit" &&
!isDefaultAgentRuntimeId(configuredHarnessPolicy.runtime)
? configuredHarnessPolicy.runtime
: undefined;
const selectedHarnessRuntime = params.agentHarnessId ?? configuredHarnessRuntime;
const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({
config: params.config,
provider: params.provider,
modelId: params.model,
authProfileId: params.authProfileId,
harnessRuntime: selectedHarnessRuntime,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
// Keep the configured provider for harness policy, while auth/model loading below can
// route OpenAI compaction through Codex OAuth when that runtime owns the session credentials.
const provider = resolvedCompactionTarget.provider ?? DEFAULT_PROVIDER;
const runtimeProvider = resolvedCompactionTarget.runtimeProvider ?? provider;
const contextConfigProvider = resolvedCompactionTarget.contextProvider ?? provider;
const modelId = resolvedCompactionTarget.model ?? DEFAULT_MODEL;
const authProfileId = resolvedCompactionTarget.authProfileId;
if (runtimeProvider !== provider || selectedHarnessRuntime) {
await ensureSelectedAgentHarnessPlugin({
config: params.config,
provider,
modelId,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
agentHarnessRuntimeOverride: selectedHarnessRuntime,
workspaceDir: resolvedWorkspace,
});
}
let thinkLevel: ThinkLevel = params.thinkLevel ?? "off";
const attemptedThinking = new Set<ThinkLevel>();
const fail = (reason: string, err?: unknown): EmbeddedAgentCompactResult => {
@@ -531,13 +582,6 @@ async function compactEmbeddedAgentSessionDirectOnce(
: undefined,
};
};
const earlyAgentIds = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
const agentDir =
params.agentDir ?? resolveAgentDir(params.config ?? {}, earlyAgentIds.sessionAgentId);
await ensureOpenClawModelsJson(params.config, agentDir, {
workspaceDir: resolvedWorkspace,
});
@@ -627,11 +671,7 @@ async function compactEmbeddedAgentSessionDirectOnce(
sessionId: params.sessionId,
cwd: effectiveCwd,
});
const { sessionAgentId: effectiveSkillAgentId } = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
const { sessionAgentId: effectiveSkillAgentId } = earlyAgentIds;
let restoreSkillEnv: (() => void) | undefined;
let compactionSessionManager: unknown = null;
@@ -683,19 +723,9 @@ async function compactEmbeddedAgentSessionDirectOnce(
// Apply contextTokens cap to model so session runtime's auto-compaction
// threshold uses the effective limit, not the native context window.
const runtimeModelWithContext = runtimeModel as ProviderRuntimeModel;
const runtimeHarnessPolicy = resolveAgentHarnessPolicy({
provider,
modelId,
config: params.config,
agentId: effectiveSkillAgentId,
sessionKey: params.sessionKey,
});
const ctxInfo = resolveContextWindowInfo({
cfg: params.config,
provider: resolveContextConfigProviderForRuntime({
provider,
runtimeId: params.agentHarnessId ?? runtimeHarnessPolicy.runtime,
}),
provider: contextConfigProvider,
modelId,
modelContextTokens: readAgentModelContextTokens(runtimeModel),
modelContextWindow: runtimeModelWithContext.contextWindow,
@@ -731,7 +761,7 @@ async function compactEmbeddedAgentSessionDirectOnce(
model: effectiveModel,
modelApi: effectiveModel.api,
harnessId: params.agentHarnessId,
harnessRuntime: runtimeHarnessPolicy.runtime,
harnessRuntime: selectedHarnessRuntime,
authProfileProvider: authProfileId?.split(":", 1)[0],
sessionAuthProfileId: authProfileId,
config: params.config,

View File

@@ -25,7 +25,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
workspaceDir: "/tmp/workspace",
cwd: "/tmp/task-repo",
agentDir: "/tmp/agent",
config: {} as OpenClawConfig,
config: {} as unknown as OpenClawConfig,
senderIsOwner: true,
senderId: "user-123",
provider: "openai-codex",
@@ -87,7 +87,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
agentDir: "/tmp/agent",
config: {
agents: { defaults: { compaction: { model: "anthropic/claude-opus-4-6" } } },
} as OpenClawConfig,
} as unknown as OpenClawConfig,
provider: "ollama",
modelId: "minimax-m2.7:cloud",
authProfileId: "ollama:default",
@@ -104,7 +104,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
agentDir: "/tmp/agent",
config: {
agents: { defaults: { compaction: { model: "gpt-4o" } } },
} as OpenClawConfig,
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-3.5-turbo",
authProfileId: "openai:p1",
@@ -119,7 +119,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
const result = buildEmbeddedCompactionRuntimeContext({
workspaceDir: "/tmp/workspace",
agentDir: "/tmp/agent",
config: {} as OpenClawConfig,
config: {} as unknown as OpenClawConfig,
provider: "ollama",
modelId: "minimax-m2.7:cloud",
authProfileId: "ollama:default",
@@ -153,7 +153,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
sessionKey: "agent:main:thread:1",
workspaceDir: "/tmp/workspace",
agentDir: "/tmp/agent",
config: {} as OpenClawConfig,
config: {} as unknown as OpenClawConfig,
});
try {
@@ -188,7 +188,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
const result = buildEmbeddedCompactionRuntimeContext({
workspaceDir: "/tmp/workspace",
agentDir: "/tmp/agent",
config: {} as OpenClawConfig,
config: {} as unknown as OpenClawConfig,
});
expect(result.activeProcessSessions).toBeUndefined();
@@ -199,7 +199,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
resolveEmbeddedCompactionTarget({
config: {
agents: { defaults: { compaction: { model: "anthropic/" } } },
} as OpenClawConfig,
} as unknown as OpenClawConfig,
provider: "openai-codex",
modelId: "gpt-5.4",
authProfileId: "openai:p1",
@@ -223,6 +223,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBe("openai-codex");
expect(result.contextProvider).toBeUndefined();
expect(result.model).toBe("gpt-5.4");
expect(result.authProfileId).toBe("openai-codex:default");
});
@@ -231,7 +232,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
auth: { order: { openai: ["openai-codex:default"] } },
} as OpenClawConfig,
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
defaultProvider: "openai",
@@ -239,6 +240,90 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBe("openai-codex");
expect(result.contextProvider).toBeUndefined();
expect(result.model).toBe("gpt-5.5");
expect(result.authProfileId).toBeUndefined();
});
it("routes Codex-runtime OpenAI compaction through the plugin-backed Codex provider", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
models: {
providers: {
openai: { models: [{ id: "gpt-5.5" }] },
},
},
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
harnessRuntime: "codex",
defaultProvider: "openai",
defaultModel: "gpt-5.5",
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBe("openai-codex");
expect(result.contextProvider).toBe("openai-codex");
expect(result.model).toBe("gpt-5.5");
expect(result.authProfileId).toBeUndefined();
});
it("carries the selected harness id for delegated runtime compaction", () => {
const result = buildEmbeddedCompactionRuntimeContext({
workspaceDir: "/tmp/workspace",
agentDir: "/tmp/agent",
config: {} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
harnessRuntime: "codex",
});
expect(result.agentHarnessId).toBe("codex");
expect(result.runtimeProvider).toBe("openai-codex");
});
it("preserves direct OpenAI compaction for the OpenClaw runtime", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
models: {
providers: {
openai: { models: [{ id: "gpt-5.5" }] },
},
},
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
harnessRuntime: "openclaw",
defaultProvider: "openai",
defaultModel: "gpt-5.5",
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBeUndefined();
expect(result.contextProvider).toBeUndefined();
expect(result.model).toBe("gpt-5.5");
expect(result.authProfileId).toBeUndefined();
});
it("preserves custom OpenAI-compatible compaction providers", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
models: {
providers: {
openai: {
baseUrl: "https://openai-compatible.example/v1",
models: [{ id: "gpt-5.5" }],
},
"openai-codex": { models: [{ id: "gpt-5.5" }] },
},
},
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
harnessRuntime: "codex",
defaultProvider: "openai",
defaultModel: "gpt-5.5",
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBeUndefined();
expect(result.contextProvider).toBeUndefined();
expect(result.model).toBe("gpt-5.5");
expect(result.authProfileId).toBeUndefined();
});
@@ -247,7 +332,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
agents: { defaults: { compaction: { model: "gpt-5.4" } } },
} as OpenClawConfig,
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
authProfileId: "openai-codex:default",
@@ -256,6 +341,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBe("openai-codex");
expect(result.contextProvider).toBeUndefined();
expect(result.model).toBe("gpt-5.4");
expect(result.authProfileId).toBe("openai-codex:default");
});
@@ -264,7 +350,7 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
agents: { defaults: { compaction: { model: "openai/gpt-5.4" } } },
} as OpenClawConfig,
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
authProfileId: "openai-codex:default",
@@ -273,10 +359,35 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBe("openai-codex");
expect(result.contextProvider).toBeUndefined();
expect(result.model).toBe("gpt-5.4");
expect(result.authProfileId).toBe("openai-codex:default");
});
it("routes OpenAI compaction model overrides through Codex runtime auth", () => {
const result = resolveEmbeddedCompactionTarget({
config: {
models: {
providers: {
openai: { models: [{ id: "gpt-5.5" }, { id: "gpt-5.4-mini" }] },
"openai-codex": { models: [{ id: "gpt-5.5" }, { id: "gpt-5.4-mini" }] },
},
},
agents: { defaults: { compaction: { model: "openai/gpt-5.4-mini" } } },
} as unknown as OpenClawConfig,
provider: "openai",
modelId: "gpt-5.5",
harnessRuntime: "codex",
defaultProvider: "openai",
defaultModel: "gpt-5.5",
});
expect(result.provider).toBe("openai");
expect(result.runtimeProvider).toBe("openai-codex");
expect(result.contextProvider).toBe("openai-codex");
expect(result.model).toBe("gpt-5.4-mini");
expect(result.authProfileId).toBeUndefined();
});
it("leaves non-openai providers unchanged", () => {
const result = resolveEmbeddedCompactionTarget({
provider: "anthropic",

View File

@@ -2,12 +2,16 @@ import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options
import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { SkillSnapshot } from "../../skills/types.js";
import { normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
import {
listActiveProcessSessionReferences,
type ActiveProcessSessionReference,
} from "../bash-process-references.js";
import type { ExecElevatedDefaults } from "../bash-tools.js";
import { resolveSelectedOpenAIRuntimeProvider } from "../openai-codex-routing.js";
import {
openAIProviderUsesCodexRuntimeByDefault,
resolveSelectedOpenAIRuntimeProvider,
} from "../openai-codex-routing.js";
export type EmbeddedCompactionRuntimeContext = {
sessionKey?: string;
@@ -18,6 +22,7 @@ export type EmbeddedCompactionRuntimeContext = {
currentThreadTs?: string;
currentMessageId?: string | number;
authProfileId?: string;
agentHarnessId?: string;
workspaceDir: string;
cwd?: string;
agentDir: string;
@@ -47,37 +52,49 @@ export function resolveEmbeddedCompactionTarget(params: {
provider?: string | null;
modelId?: string | null;
authProfileId?: string | null;
harnessRuntime?: string | null;
defaultProvider?: string;
defaultModel?: string;
}): {
provider: string | undefined;
runtimeProvider?: string;
contextProvider?: string;
model: string | undefined;
authProfileId: string | undefined;
} {
const provider = params.provider?.trim() || params.defaultProvider;
const model = params.modelId?.trim() || params.defaultModel;
const override = params.config?.agents?.defaults?.compaction?.model?.trim();
const resolveRuntimeProvider = (
const resolveTargetProviders = (
targetProvider: string | undefined,
authProfileId: string | undefined,
) => {
if (!targetProvider) {
return undefined;
return {};
}
const useCodexHarnessRuntime = shouldUseCodexRuntimeProviderForCompaction({
config: params.config,
provider: targetProvider,
harnessRuntime: params.harnessRuntime,
});
const harnessRuntime = useCodexHarnessRuntime ? params.harnessRuntime : "openclaw";
const runtimeProvider = resolveSelectedOpenAIRuntimeProvider({
provider: targetProvider,
harnessRuntime: "openclaw",
harnessRuntime: harnessRuntime ?? undefined,
authProfileId,
config: params.config,
});
return runtimeProvider === targetProvider ? undefined : runtimeProvider;
const routedRuntimeProvider = runtimeProvider === targetProvider ? undefined : runtimeProvider;
return {
runtimeProvider: routedRuntimeProvider,
contextProvider: useCodexHarnessRuntime ? routedRuntimeProvider : undefined,
};
};
if (!override) {
const authProfileId = params.authProfileId ?? undefined;
return {
provider,
runtimeProvider: resolveRuntimeProvider(provider, authProfileId),
...resolveTargetProviders(provider, authProfileId),
model,
authProfileId,
};
@@ -94,7 +111,7 @@ export function resolveEmbeddedCompactionTarget(params: {
: (params.authProfileId ?? undefined);
return {
provider: overrideProvider,
runtimeProvider: resolveRuntimeProvider(overrideProvider, authProfileId),
...resolveTargetProviders(overrideProvider, authProfileId),
model: overrideModel,
authProfileId,
};
@@ -102,12 +119,26 @@ export function resolveEmbeddedCompactionTarget(params: {
const authProfileId = params.authProfileId ?? undefined;
return {
provider,
runtimeProvider: resolveRuntimeProvider(provider, authProfileId),
...resolveTargetProviders(provider, authProfileId),
model: override,
authProfileId,
};
}
function shouldUseCodexRuntimeProviderForCompaction(params: {
config?: OpenClawConfig;
provider: string;
harnessRuntime?: string | null;
}): boolean {
if (normalizeOptionalAgentRuntimeId(params.harnessRuntime) !== "codex") {
return false;
}
if (!openAIProviderUsesCodexRuntimeByDefault(params)) {
return false;
}
return true;
}
export function buildEmbeddedCompactionRuntimeContext(params: {
sessionKey?: string | null;
messageChannel?: string | null;
@@ -126,6 +157,7 @@ export function buildEmbeddedCompactionRuntimeContext(params: {
senderId?: string | null;
provider?: string | null;
modelId?: string | null;
harnessRuntime?: string | null;
modelFallbacksOverride?: string[];
thinkLevel?: ThinkLevel;
reasoningLevel?: ReasoningLevel;
@@ -140,7 +172,9 @@ export function buildEmbeddedCompactionRuntimeContext(params: {
provider: params.provider,
modelId: params.modelId,
authProfileId: params.authProfileId,
harnessRuntime: params.harnessRuntime,
});
const agentHarnessId = params.harnessRuntime?.trim() || undefined;
const processScopeKey = params.sessionKey?.trim();
const activeProcessSessions =
params.activeProcessSessions ??
@@ -156,6 +190,7 @@ export function buildEmbeddedCompactionRuntimeContext(params: {
currentThreadTs: params.currentThreadTs ?? undefined,
currentMessageId: params.currentMessageId ?? undefined,
authProfileId: resolved.authProfileId,
agentHarnessId,
workspaceDir: params.workspaceDir,
cwd: params.cwd ?? undefined,
agentDir: params.agentDir,

View File

@@ -768,6 +768,7 @@ export async function runEmbeddedAgent(
contextConfigProvider: resolveContextConfigProviderForRuntime({
provider: modelConfigProvider,
runtimeId: agentHarness.id,
config: params.config,
}),
modelId,
runtimeModel,
@@ -1905,6 +1906,7 @@ export async function runEmbeddedAgent(
senderId: params.senderId,
provider,
modelId,
harnessRuntime: agentHarness.id,
modelFallbacksOverride: params.modelFallbacksOverride,
thinkLevel,
reasoningLevel: params.reasoningLevel,
@@ -2096,6 +2098,7 @@ export async function runEmbeddedAgent(
senderId: params.senderId,
provider,
modelId,
harnessRuntime: agentHarness.id,
thinkLevel,
reasoningLevel: params.reasoningLevel,
bashElevated: params.bashElevated,

View File

@@ -534,6 +534,7 @@ type AfterTurnRuntimeContextAttempt = Pick<
| "senderId"
| "provider"
| "modelId"
| "agentHarnessId"
| "thinkLevel"
| "reasoningLevel"
| "bashElevated"
@@ -574,6 +575,7 @@ export function buildAfterTurnRuntimeContext(params: {
senderId: params.attempt.senderId,
provider: params.attempt.provider,
modelId: params.attempt.modelId,
harnessRuntime: params.attempt.agentHarnessId,
thinkLevel: params.attempt.thinkLevel,
reasoningLevel: params.attempt.reasoningLevel,
bashElevated: params.attempt.bashElevated,

View File

@@ -756,6 +756,35 @@ describe("selectAgentHarness", () => {
).resolves.toBeUndefined();
});
it("honors selected plugin harness pins during compaction preflight", async () => {
const compact = vi.fn(async () => ({ ok: true, compacted: false }));
registerAgentHarness(
{
id: "codex",
label: "Codex",
supports: (ctx) =>
ctx.provider === "openai" ? { supported: true, priority: 100 } : { supported: false },
runAttempt: vi.fn(async () => createAttemptResult("codex")),
compact,
},
{ ownerPluginId: "codex" },
);
await expect(
maybeCompactAgentHarnessSession({
sessionId: "session-1",
sessionKey: "agent:main:main",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentHarnessId: "codex",
config: agentModelRuntimeConfig("openai/gpt-5.5", "openclaw"),
}),
).resolves.toEqual({ ok: true, compacted: false });
expect(compact).toHaveBeenCalledTimes(1);
});
it("does not compact a selected plugin harness through OpenClaw when the plugin has no compactor", async () => {
registerFailingCodexHarness();
@@ -777,6 +806,95 @@ describe("selectAgentHarness", () => {
});
});
it("uses agent-scoped runtime policy during compaction preflight", async () => {
const compact = vi.fn(async () => ({ ok: true, compacted: false }));
registerAgentHarness(
{
id: "codex",
label: "Codex",
supports: (ctx) =>
ctx.provider === "openai" ? { supported: true, priority: 100 } : { supported: false },
runAttempt: vi.fn(async () => createAttemptResult("codex")),
compact,
},
{ ownerPluginId: "codex" },
);
await expect(
maybeCompactAgentHarnessSession({
sessionId: "session-1",
sessionKey: "agent:strict:main",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentId: "strict",
config: agentModelRuntimeConfig("openai/gpt-5.5", "codex", "strict"),
}),
).resolves.toEqual({ ok: true, compacted: false });
expect(compact).toHaveBeenCalledTimes(1);
});
it("uses sandbox session key for compaction preflight runtime policy", async () => {
const compact = vi.fn(async () => ({ ok: true, compacted: false }));
registerAgentHarness(
{
id: "codex",
label: "Codex",
supports: (ctx) =>
ctx.provider === "openai" ? { supported: true, priority: 100 } : { supported: false },
runAttempt: vi.fn(async () => createAttemptResult("codex")),
compact,
},
{ ownerPluginId: "codex" },
);
await expect(
maybeCompactAgentHarnessSession({
sessionId: "session-1",
sessionKey: "agent:main:main",
sandboxSessionKey: "agent:strict:main",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentId: "main",
config: agentModelRuntimeConfig("openai/gpt-5.5", "codex", "strict"),
}),
).resolves.toEqual({ ok: true, compacted: false });
expect(compact).toHaveBeenCalledTimes(1);
});
it("keeps explicit agent id for non-agent sandbox policy keys during compaction preflight", async () => {
const compact = vi.fn(async () => ({ ok: true, compacted: false }));
registerAgentHarness(
{
id: "codex",
label: "Codex",
supports: (ctx) =>
ctx.provider === "openai" ? { supported: true, priority: 100 } : { supported: false },
runAttempt: vi.fn(async () => createAttemptResult("codex")),
compact,
},
{ ownerPluginId: "codex" },
);
await expect(
maybeCompactAgentHarnessSession({
sessionId: "session-1",
sessionKey: "agent:main:main",
sandboxSessionKey: "global",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp/workspace",
provider: "openai",
model: "gpt-5.5",
agentId: "strict",
config: agentModelRuntimeConfig("openai/gpt-5.5", "codex", "strict"),
}),
).resolves.toEqual({ ok: true, compacted: false });
expect(compact).toHaveBeenCalledTimes(1);
});
it.each([
{ provider: "anthropic", modelId: "sonnet-4.6", alias: "claude-cli" },
{ provider: "google", modelId: "gemini-3-pro-preview", alias: "google-gemini-cli" },

View File

@@ -1,6 +1,7 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { parseAgentSessionKey } from "../../routing/session-key.js";
import { isDefaultAgentRuntimeId, normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
import {
resolveEffectiveToolPolicy,
@@ -490,21 +491,43 @@ export async function maybeCompactAgentHarnessSession(
if (params.provider && isCliRuntimeProvider(params.provider, { config: params.config })) {
return undefined;
}
const runtimePolicySessionKey = params.sandboxSessionKey ?? params.sessionKey;
const runtimePolicyAgentId =
params.sandboxSessionKey && parseAgentSessionKey(params.sandboxSessionKey)
? undefined
: params.agentId;
const runtime = resolveConfiguredAgentHarnessPolicy({
provider: params.provider,
modelId: params.model,
config: params.config,
sessionKey: params.sessionKey,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
}).runtime;
if (isCliRuntimeAliasForProvider({ runtime, provider: params.provider, cfg: params.config })) {
return undefined;
}
const harness = selectAgentHarness({
provider: params.provider ?? "",
modelId: params.model,
config: params.config,
sessionKey: params.sessionKey,
});
const selectedRuntime = normalizeOptionalAgentRuntimeId(params.agentHarnessId);
const agentHarnessRuntimeOverride =
selectedRuntime && !isDefaultAgentRuntimeId(selectedRuntime) ? selectedRuntime : undefined;
let harness: AgentHarness;
try {
harness = selectAgentHarness({
provider: params.provider ?? "",
modelId: params.model,
config: params.config,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
agentHarnessRuntimeOverride,
});
} catch (err) {
if (agentHarnessRuntimeOverride) {
const message = formatErrorMessage(err);
if (message.includes("does not support")) {
return undefined;
}
}
throw err;
}
if (!harness.compact) {
if (harness.id !== "openclaw") {
return {

View File

@@ -4,6 +4,7 @@ import {
listOpenAIAuthProfileProvidersForAgentRuntime,
modelSelectionShouldEnsureCodexPlugin,
openAIProviderUsesCodexRuntimeByDefault,
resolveContextConfigProviderForRuntime,
resolveOpenAIRuntimeProvider,
resolveSelectedOpenAIRuntimeProvider,
} from "./openai-codex-routing.js";
@@ -33,6 +34,22 @@ describe("OpenAI Codex routing policy", () => {
expect(openAIProviderUsesCodexRuntimeByDefault({ provider: "openai", config })).toBe(false);
expect(modelSelectionShouldEnsureCodexPlugin({ model: "openai/gpt-5.5", config })).toBe(false);
expect(
resolveContextConfigProviderForRuntime({
provider: "openai",
runtimeId: "codex",
config,
}),
).toBe("openai");
});
it("uses Codex context config for official OpenAI under the Codex runtime", () => {
expect(
resolveContextConfigProviderForRuntime({
provider: "openai",
runtimeId: "codex",
}),
).toBe("openai-codex");
});
it("maps explicit OpenClaw plus Codex auth profile to the OpenClaw Codex-auth transport", () => {

View File

@@ -200,10 +200,15 @@ export function resolveSelectedOpenAIRuntimeProvider(params: {
export function resolveContextConfigProviderForRuntime(params: {
provider: string;
runtimeId?: string;
config?: OpenClawConfig;
}): string {
const provider = normalizeProviderId(params.provider);
const runtimeId = normalizeOptionalAgentRuntimeId(params.runtimeId) ?? OPENCLAW_AGENT_RUNTIME_ID;
if (provider === OPENAI_PROVIDER_ID && runtimeId === "codex") {
if (
provider === OPENAI_PROVIDER_ID &&
runtimeId === "codex" &&
openAIProviderUsesCodexRuntimeByDefault({ provider, config: params.config })
) {
return OPENAI_CODEX_PROVIDER_ID;
}
return params.provider;

View File

@@ -247,6 +247,7 @@ function resolveFollowupContextConfigProvider(params: {
return resolveContextConfigProviderForRuntime({
provider,
runtimeId: resolveFollowupAgentRuntimeId(params),
config: params.cfg,
});
}

View File

@@ -134,6 +134,7 @@ function resolveManualCompactContextTokenBudget(params: {
const contextConfigProvider = resolveContextConfigProviderForRuntime({
provider,
runtimeId: harnessPolicy.runtime,
config: params.cfg,
});
const configuredContextTokens = resolveContextTokensForModel({
cfg: params.cfg,

View File

@@ -396,6 +396,7 @@ export async function persistInlineDirectives(params: {
agentId: activeAgentId,
sessionKey,
}).runtime,
config: cfg,
}),
model,
}),

View File

@@ -205,6 +205,8 @@ export type ContextEngineRuntimeContext = Record<string, unknown> & {
allowDeferredCompactionExecution?: boolean;
/** Runtime-resolved context window budget for the active model call. */
tokenBudget?: number;
/** Selected agent harness id when compaction delegates back to the runtime. */
agentHarnessId?: string;
/** Best-effort current prompt/context token estimate for this turn. */
currentTokenCount?: number;
/** Optional prompt-cache telemetry for cache-aware engines. */