From aada44fca5ab0415acad2fcc591c8240bcdf7497 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 02:26:00 +0200 Subject: [PATCH] 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 ac6f93de4ab77ccb05a552a500986a9812b82c7e --- .../compact.hooks.test.ts | 386 +++++++++++++++++- .../embedded-agent-runner/compact.queued.ts | 98 ++++- src/agents/embedded-agent-runner/compact.ts | 84 ++-- .../compaction-runtime-context.test.ts | 131 +++++- .../compaction-runtime-context.ts | 51 ++- src/agents/embedded-agent-runner/run.ts | 3 + .../run/attempt.prompt-helpers.ts | 2 + src/agents/harness/selection.test.ts | 118 ++++++ src/agents/harness/selection.ts | 37 +- src/agents/openai-codex-routing.test.ts | 17 + src/agents/openai-codex-routing.ts | 7 +- src/auto-reply/reply/agent-runner-memory.ts | 1 + src/auto-reply/reply/commands-compact.ts | 1 + .../reply/directive-handling.persist.ts | 1 + src/context-engine/types.ts | 2 + 15 files changed, 853 insertions(+), 86 deletions(-) diff --git a/src/agents/embedded-agent-runner/compact.hooks.test.ts b/src/agents/embedded-agent-runner/compact.hooks.test.ts index f1e6b80a0b1..cf32bee4495 100644 --- a/src/agents/embedded-agent-runner/compact.hooks.test.ts +++ b/src/agents/embedded-agent-runner/compact.hooks.test.ts @@ -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; - return policyArg.provider === "openai" && policyArg.modelId === "gpt-primary"; - }); - findMockCall(resolveAgentHarnessPolicyMock, ([arg]) => { - const policyArg = arg as Record; - 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; - expect(harnessArg.contextTokenBudget).toBe(32_000); + expect(maybeCompactAgentHarnessSessionMock).not.toHaveBeenCalled(); const compactArg = mockCallArg(contextEngineCompactMock) as { tokenBudget?: number; runtimeContext?: Record; @@ -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; + 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; + 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; + }; + 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; + 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; + }; + 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; + }; + 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 () => ({ diff --git a/src/agents/embedded-agent-runner/compact.queued.ts b/src/agents/embedded-agent-runner/compact.queued.ts index 339b42edf3d..0e6387b9716 100644 --- a/src/agents/embedded-agent-runner/compact.queued.ts +++ b/src/agents/embedded-agent-runner/compact.queued.ts @@ -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, diff --git a/src/agents/embedded-agent-runner/compact.ts b/src/agents/embedded-agent-runner/compact.ts index b213d0fd28c..9ce08670f1a 100644 --- a/src/agents/embedded-agent-runner/compact.ts +++ b/src/agents/embedded-agent-runner/compact.ts @@ -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(); 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, diff --git a/src/agents/embedded-agent-runner/compaction-runtime-context.test.ts b/src/agents/embedded-agent-runner/compaction-runtime-context.test.ts index c9217bf436e..7051e06ceba 100644 --- a/src/agents/embedded-agent-runner/compaction-runtime-context.test.ts +++ b/src/agents/embedded-agent-runner/compaction-runtime-context.test.ts @@ -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", diff --git a/src/agents/embedded-agent-runner/compaction-runtime-context.ts b/src/agents/embedded-agent-runner/compaction-runtime-context.ts index c5de4ba0dc2..db43b9e8591 100644 --- a/src/agents/embedded-agent-runner/compaction-runtime-context.ts +++ b/src/agents/embedded-agent-runner/compaction-runtime-context.ts @@ -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, diff --git a/src/agents/embedded-agent-runner/run.ts b/src/agents/embedded-agent-runner/run.ts index 4b09aaf1889..952cf2f6e4a 100644 --- a/src/agents/embedded-agent-runner/run.ts +++ b/src/agents/embedded-agent-runner/run.ts @@ -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, diff --git a/src/agents/embedded-agent-runner/run/attempt.prompt-helpers.ts b/src/agents/embedded-agent-runner/run/attempt.prompt-helpers.ts index ef0deaa34d0..c7c2d7d3657 100644 --- a/src/agents/embedded-agent-runner/run/attempt.prompt-helpers.ts +++ b/src/agents/embedded-agent-runner/run/attempt.prompt-helpers.ts @@ -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, diff --git a/src/agents/harness/selection.test.ts b/src/agents/harness/selection.test.ts index b54d0d944e0..f826236ff57 100644 --- a/src/agents/harness/selection.test.ts +++ b/src/agents/harness/selection.test.ts @@ -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" }, diff --git a/src/agents/harness/selection.ts b/src/agents/harness/selection.ts index 55b1a80b258..09b0a4a003f 100644 --- a/src/agents/harness/selection.ts +++ b/src/agents/harness/selection.ts @@ -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 { diff --git a/src/agents/openai-codex-routing.test.ts b/src/agents/openai-codex-routing.test.ts index 43330ba0a4e..cc6cea3559f 100644 --- a/src/agents/openai-codex-routing.test.ts +++ b/src/agents/openai-codex-routing.test.ts @@ -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", () => { diff --git a/src/agents/openai-codex-routing.ts b/src/agents/openai-codex-routing.ts index dd91cc2ff94..8f0a41c9a2a 100644 --- a/src/agents/openai-codex-routing.ts +++ b/src/agents/openai-codex-routing.ts @@ -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; diff --git a/src/auto-reply/reply/agent-runner-memory.ts b/src/auto-reply/reply/agent-runner-memory.ts index 651409811d0..dda908bc65c 100644 --- a/src/auto-reply/reply/agent-runner-memory.ts +++ b/src/auto-reply/reply/agent-runner-memory.ts @@ -247,6 +247,7 @@ function resolveFollowupContextConfigProvider(params: { return resolveContextConfigProviderForRuntime({ provider, runtimeId: resolveFollowupAgentRuntimeId(params), + config: params.cfg, }); } diff --git a/src/auto-reply/reply/commands-compact.ts b/src/auto-reply/reply/commands-compact.ts index c98cab3c12b..4ba877e65b7 100644 --- a/src/auto-reply/reply/commands-compact.ts +++ b/src/auto-reply/reply/commands-compact.ts @@ -134,6 +134,7 @@ function resolveManualCompactContextTokenBudget(params: { const contextConfigProvider = resolveContextConfigProviderForRuntime({ provider, runtimeId: harnessPolicy.runtime, + config: params.cfg, }); const configuredContextTokens = resolveContextTokensForModel({ cfg: params.cfg, diff --git a/src/auto-reply/reply/directive-handling.persist.ts b/src/auto-reply/reply/directive-handling.persist.ts index 006802695c2..d4b7dcd762c 100644 --- a/src/auto-reply/reply/directive-handling.persist.ts +++ b/src/auto-reply/reply/directive-handling.persist.ts @@ -396,6 +396,7 @@ export async function persistInlineDirectives(params: { agentId: activeAgentId, sessionKey, }).runtime, + config: cfg, }), model, }), diff --git a/src/context-engine/types.ts b/src/context-engine/types.ts index 6b0b7c7eeab..917a6135aa9 100644 --- a/src/context-engine/types.ts +++ b/src/context-engine/types.ts @@ -205,6 +205,8 @@ export type ContextEngineRuntimeContext = Record & { 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. */