diff --git a/CHANGELOG.md b/CHANGELOG.md index 21bcd9a93f5..6cb4779559a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Docs: https://docs.openclaw.ai +## Unreleased + +### Changes + +- Agents/compaction: resolve `agents.defaults.compaction.model` consistently for manual `/compact` and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg + ## 2026.3.31 ### Breaking diff --git a/src/agents/pi-embedded-runner/compact.hooks.test.ts b/src/agents/pi-embedded-runner/compact.hooks.test.ts index 3c7a40206a3..ea67c68048d 100644 --- a/src/agents/pi-embedded-runner/compact.hooks.test.ts +++ b/src/agents/pi-embedded-runner/compact.hooks.test.ts @@ -712,6 +712,41 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { expect(typeof runtimeContext?.rewriteTranscriptEntries).toBe("function"); }); + it("resolves the effective compaction model before manual engine-owned compaction", async () => { + await compactEmbeddedPiSession( + wrappedCompactionArgs({ + config: { + agents: { + defaults: { + compaction: { + model: "anthropic/claude-opus-4-6", + }, + }, + }, + }, + provider: "openai-codex", + model: "gpt-5.4", + authProfileId: "openai:p1", + }), + ); + + expect(resolveModelMock).toHaveBeenCalledWith( + "anthropic", + "claude-opus-4-6", + expect.any(String), + expect.anything(), + ); + expect(contextEngineCompactMock).toHaveBeenCalledWith( + expect.objectContaining({ + runtimeContext: expect.objectContaining({ + provider: "anthropic", + model: "claude-opus-4-6", + authProfileId: undefined, + }), + }), + ); + }); + it("does not fire after_compaction when compaction fails", async () => { hookRunner.hasHooks.mockReturnValue(true); const sync = vi.fn(async () => {}); diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 0a7590e0bbe..cb26bbd13dd 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -91,6 +91,10 @@ import { runBeforeCompactionHooks, runPostCompactionSideEffects, } from "./compaction-hooks.js"; +import { + buildEmbeddedCompactionRuntimeContext, + resolveEmbeddedCompactionTarget, +} from "./compaction-runtime-context.js"; import { compactWithSafetyTimeout, resolveCompactionTimeoutMs, @@ -286,31 +290,17 @@ export async function compactEmbeddedPiSessionDirect( workspaceDir: resolvedWorkspace, allowGatewaySubagentBinding: params.allowGatewaySubagentBinding, }); - // Resolve compaction model: prefer config override, then fall back to caller-supplied model - const compactionModelOverride = params.config?.agents?.defaults?.compaction?.model?.trim(); - let provider: string; - let modelId: string; - // When switching provider via override, drop the primary auth profile to avoid - // sending the wrong credentials (e.g. OpenAI profile token to OpenRouter). - let authProfileId: string | undefined = params.authProfileId; - if (compactionModelOverride) { - const slashIdx = compactionModelOverride.indexOf("/"); - if (slashIdx > 0) { - provider = compactionModelOverride.slice(0, slashIdx).trim(); - modelId = compactionModelOverride.slice(slashIdx + 1).trim() || DEFAULT_MODEL; - // Provider changed — drop primary auth profile so getApiKeyForModel - // falls back to provider-based key resolution for the override model. - if (provider !== (params.provider ?? "").trim()) { - authProfileId = undefined; - } - } else { - provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER; - modelId = compactionModelOverride; - } - } else { - provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER; - modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL; - } + const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({ + config: params.config, + provider: params.provider, + modelId: params.model, + authProfileId: params.authProfileId, + defaultProvider: DEFAULT_PROVIDER, + defaultModel: DEFAULT_MODEL, + }); + const provider = resolvedCompactionTarget.provider ?? DEFAULT_PROVIDER; + const modelId = resolvedCompactionTarget.model ?? DEFAULT_MODEL; + const authProfileId = resolvedCompactionTarget.authProfileId; const fail = (reason: string): EmbeddedPiCompactResult => { log.warn( `[compaction-diag] end runId=${runId} sessionKey=${params.sessionKey ?? params.sessionId} ` + @@ -955,12 +945,19 @@ export async function compactEmbeddedPiSession( ensureContextEnginesInitialized(); const contextEngine = await resolveContextEngine(params.config); try { - // Resolve token budget from model context window so the context engine - // knows the compaction target. The runner's afterTurn path passes this - // automatically, but the /compact command path needs to compute it here. - const ceProvider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER; - const ceModelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL; const agentDir = params.agentDir ?? resolveOpenClawAgentDir(); + const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({ + config: params.config, + provider: params.provider, + modelId: params.model, + authProfileId: params.authProfileId, + defaultProvider: DEFAULT_PROVIDER, + defaultModel: DEFAULT_MODEL, + }); + // Resolve token budget from the effective compaction model so engine- + // owned /compact implementations see the same target as the runtime. + const ceProvider = resolvedCompactionTarget.provider ?? DEFAULT_PROVIDER; + const ceModelId = resolvedCompactionTarget.model ?? DEFAULT_MODEL; const { model: ceModel } = await resolveModelAsync( ceProvider, ceModelId, @@ -995,6 +992,32 @@ export async function compactEmbeddedPiSession( workspaceDir: resolveUserPath(params.workspaceDir), messageProvider: resolvedMessageProvider, }; + const runtimeContext = { + ...params, + ...buildEmbeddedCompactionRuntimeContext({ + sessionKey: params.sessionKey, + messageChannel: params.messageChannel, + messageProvider: params.messageProvider, + agentAccountId: params.agentAccountId, + currentChannelId: params.currentChannelId, + currentThreadTs: params.currentThreadTs, + currentMessageId: params.currentMessageId, + authProfileId: params.authProfileId, + workspaceDir: params.workspaceDir, + agentDir, + config: params.config, + skillsSnapshot: params.skillsSnapshot, + senderIsOwner: params.senderIsOwner, + senderId: params.senderId, + provider: params.provider, + modelId: params.model, + thinkLevel: params.thinkLevel, + reasoningLevel: params.reasoningLevel, + bashElevated: params.bashElevated, + extraSystemPrompt: params.extraSystemPrompt, + ownerNumbers: params.ownerNumbers, + }), + }; // Engine-owned compaction doesn't load the transcript at this level, so // message counts are unavailable. We pass sessionFile so hook subscribers // can read the transcript themselves if they need exact counts. @@ -1022,7 +1045,7 @@ export async function compactEmbeddedPiSession( compactionTarget: params.trigger === "manual" ? "threshold" : "budget", customInstructions: params.customInstructions, force: params.trigger === "manual", - runtimeContext: params as Record, + runtimeContext, }); if (result.ok && result.compacted) { await runContextEngineMaintenance({ @@ -1031,7 +1054,7 @@ export async function compactEmbeddedPiSession( sessionKey: params.sessionKey, sessionFile: params.sessionFile, reason: "compaction", - runtimeContext: params as Record, + runtimeContext, }); } if (engineOwnsCompaction && result.ok && result.compacted) { diff --git a/src/agents/pi-embedded-runner/compaction-runtime-context.test.ts b/src/agents/pi-embedded-runner/compaction-runtime-context.test.ts index 286a49cf903..0926e99db82 100644 --- a/src/agents/pi-embedded-runner/compaction-runtime-context.test.ts +++ b/src/agents/pi-embedded-runner/compaction-runtime-context.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; -import { buildEmbeddedCompactionRuntimeContext } from "./compaction-runtime-context.js"; +import { + buildEmbeddedCompactionRuntimeContext, + resolveEmbeddedCompactionTarget, +} from "./compaction-runtime-context.js"; describe("buildEmbeddedCompactionRuntimeContext", () => { it("preserves sender and current message routing for compaction", () => { @@ -74,4 +77,71 @@ describe("buildEmbeddedCompactionRuntimeContext", () => { model: undefined, }); }); + + it("applies compaction.model override with provider/model format", () => { + const result = buildEmbeddedCompactionRuntimeContext({ + workspaceDir: "/tmp/workspace", + agentDir: "/tmp/agent", + config: { + agents: { defaults: { compaction: { model: "anthropic/claude-opus-4-6" } } }, + } as OpenClawConfig, + provider: "ollama", + modelId: "minimax-m2.7:cloud", + authProfileId: "ollama:default", + }); + expect(result.provider).toBe("anthropic"); + expect(result.model).toBe("claude-opus-4-6"); + // Auth profile dropped because provider changed + expect(result.authProfileId).toBeUndefined(); + }); + + it("applies compaction.model override with model-only format", () => { + const result = buildEmbeddedCompactionRuntimeContext({ + workspaceDir: "/tmp/workspace", + agentDir: "/tmp/agent", + config: { + agents: { defaults: { compaction: { model: "gpt-4o" } } }, + } as OpenClawConfig, + provider: "openai", + modelId: "gpt-3.5-turbo", + authProfileId: "openai:p1", + }); + expect(result.provider).toBe("openai"); + expect(result.model).toBe("gpt-4o"); + // Auth profile preserved because provider didn't change + expect(result.authProfileId).toBe("openai:p1"); + }); + + it("uses session model when no compaction.model override configured", () => { + const result = buildEmbeddedCompactionRuntimeContext({ + workspaceDir: "/tmp/workspace", + agentDir: "/tmp/agent", + config: {} as OpenClawConfig, + provider: "ollama", + modelId: "minimax-m2.7:cloud", + authProfileId: "ollama:default", + }); + expect(result.provider).toBe("ollama"); + expect(result.model).toBe("minimax-m2.7:cloud"); + expect(result.authProfileId).toBe("ollama:default"); + }); + + it("applies runtime defaults when resolving the effective compaction target", () => { + expect( + resolveEmbeddedCompactionTarget({ + config: { + agents: { defaults: { compaction: { model: "anthropic/" } } }, + } as OpenClawConfig, + provider: "openai-codex", + modelId: "gpt-5.4", + authProfileId: "openai:p1", + defaultProvider: "openai-codex", + defaultModel: "gpt-5.4", + }), + ).toEqual({ + provider: "anthropic", + model: "gpt-5.4", + authProfileId: undefined, + }); + }); }); diff --git a/src/agents/pi-embedded-runner/compaction-runtime-context.ts b/src/agents/pi-embedded-runner/compaction-runtime-context.ts index 5f64089f63b..86b9722e18c 100644 --- a/src/agents/pi-embedded-runner/compaction-runtime-context.ts +++ b/src/agents/pi-embedded-runner/compaction-runtime-context.ts @@ -27,6 +27,47 @@ export type EmbeddedCompactionRuntimeContext = { ownerNumbers?: string[]; }; +/** + * Resolve the effective compaction target from config, falling back to the + * caller-supplied provider/model and optionally applying runtime defaults. + */ +export function resolveEmbeddedCompactionTarget(params: { + config?: OpenClawConfig; + provider?: string | null; + modelId?: string | null; + authProfileId?: string | null; + defaultProvider?: string; + defaultModel?: string; +}): { provider: string | undefined; 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(); + if (!override) { + return { + provider, + model, + authProfileId: params.authProfileId ?? undefined, + }; + } + const slashIdx = override.indexOf("/"); + if (slashIdx > 0) { + const overrideProvider = override.slice(0, slashIdx).trim(); + const overrideModel = override.slice(slashIdx + 1).trim() || params.defaultModel; + // When switching provider via override, drop the primary auth profile to + // avoid sending the wrong credentials. + const authProfileId = + overrideProvider !== (params.provider ?? "")?.trim() + ? undefined + : (params.authProfileId ?? undefined); + return { provider: overrideProvider, model: overrideModel, authProfileId }; + } + return { + provider, + model: override, + authProfileId: params.authProfileId ?? undefined, + }; +} + export function buildEmbeddedCompactionRuntimeContext(params: { sessionKey?: string | null; messageChannel?: string | null; @@ -50,6 +91,12 @@ export function buildEmbeddedCompactionRuntimeContext(params: { extraSystemPrompt?: string; ownerNumbers?: string[]; }): EmbeddedCompactionRuntimeContext { + const resolved = resolveEmbeddedCompactionTarget({ + config: params.config, + provider: params.provider, + modelId: params.modelId, + authProfileId: params.authProfileId, + }); return { sessionKey: params.sessionKey ?? undefined, messageChannel: params.messageChannel ?? undefined, @@ -58,15 +105,15 @@ export function buildEmbeddedCompactionRuntimeContext(params: { currentChannelId: params.currentChannelId ?? undefined, currentThreadTs: params.currentThreadTs ?? undefined, currentMessageId: params.currentMessageId ?? undefined, - authProfileId: params.authProfileId ?? undefined, + authProfileId: resolved.authProfileId, workspaceDir: params.workspaceDir, agentDir: params.agentDir, config: params.config, skillsSnapshot: params.skillsSnapshot, senderIsOwner: params.senderIsOwner, senderId: params.senderId ?? undefined, - provider: params.provider ?? undefined, - model: params.modelId ?? undefined, + provider: resolved.provider, + model: resolved.model, thinkLevel: params.thinkLevel, reasoningLevel: params.reasoningLevel, bashElevated: params.bashElevated, diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.test.ts index ea757f3cd28..f6dac1d6d5a 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.test.ts @@ -1,34 +1,26 @@ import { describe, expect, it, vi } from "vitest"; -import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js"; import type { OpenClawConfig } from "../../../config/config.js"; import { isOllamaCompatProvider, + resolveOllamaBaseUrlForRun, resolveOllamaCompatNumCtxEnabled, shouldInjectOllamaCompatNumCtx, wrapOllamaCompatNumCtx, } from "../../../plugin-sdk/ollama.js"; import { appendBootstrapPromptWarning } from "../../bootstrap-budget.js"; import { buildAgentSystemPrompt } from "../../system-prompt.js"; -import { buildEmbeddedSystemPrompt } from "../system-prompt.js"; import { buildAfterTurnRuntimeContext, - buildSessionsYieldContextMessage, composeSystemPromptWithHookContext, - persistSessionsYieldContextMessage, prependSystemPromptAddition, - queueSessionsYieldInterruptMessage, resolveAttemptFsWorkspaceOnly, resolvePromptBuildHookResult, resolvePromptModeForSession, - stripSessionsYieldArtifacts, - shouldInjectHeartbeatPrompt, decodeHtmlEntitiesInObject, wrapStreamFnRepairMalformedToolCallArguments, wrapStreamFnSanitizeMalformedToolCalls, wrapStreamFnTrimToolCallNames, - resolveEmbeddedAgentStreamFn, } from "./attempt.js"; -import { shouldInjectHeartbeatPromptForTrigger } from "./trigger-policy.js"; type FakeWrappedStream = { result: () => Promise; @@ -152,98 +144,6 @@ describe("resolvePromptBuildHookResult", () => { }); }); -describe("sessions_yield helpers", () => { - it("builds a hidden follow-up context note", () => { - expect(buildSessionsYieldContextMessage("Waiting for subagent")).toContain( - "Waiting for subagent", - ); - expect(buildSessionsYieldContextMessage("Waiting for subagent")).toContain( - "ended intentionally via sessions_yield", - ); - }); - - it("queues a hidden interrupt steering message", () => { - const steer = vi.fn(); - queueSessionsYieldInterruptMessage({ agent: { steer } }); - expect(steer).toHaveBeenCalledWith( - expect.objectContaining({ - role: "custom", - customType: "openclaw.sessions_yield_interrupt", - display: false, - details: { source: "sessions_yield" }, - }), - ); - }); - - it("persists a hidden yield context message without triggering a turn", async () => { - const sendCustomMessage = vi.fn(async () => {}); - await persistSessionsYieldContextMessage( - { - sendCustomMessage, - }, - "Waiting for subagent", - ); - expect(sendCustomMessage).toHaveBeenCalledWith( - expect.objectContaining({ - customType: "openclaw.sessions_yield", - display: false, - details: { source: "sessions_yield", message: "Waiting for subagent" }, - content: expect.stringContaining("Waiting for subagent"), - }), - { triggerTurn: false }, - ); - }); - - it("strips trailing yield interrupt artifacts from memory and transcript state", () => { - const replaceMessages = vi.fn(); - const rewriteFile = vi.fn(); - const activeSession = { - messages: [ - { role: "user", content: [{ type: "text", text: "hi" }] }, - { role: "custom", customType: "openclaw.sessions_yield_interrupt" }, - { role: "assistant", stopReason: "aborted" }, - ], - agent: { replaceMessages }, - sessionManager: { - fileEntries: [ - { type: "session", id: "session-root" }, - { - type: "custom_message", - id: "interrupt", - parentId: "session-root", - customType: "openclaw.sessions_yield_interrupt", - }, - { - type: "message", - id: "aborted", - parentId: "interrupt", - message: { role: "assistant", stopReason: "aborted" }, - }, - ], - byId: new Map([ - ["interrupt", { id: "interrupt" }], - ["aborted", { id: "aborted" }], - ]), - leafId: "aborted", - _rewriteFile: rewriteFile, - }, - }; - - stripSessionsYieldArtifacts(activeSession as never); - - expect(replaceMessages).toHaveBeenCalledWith([ - { role: "user", content: [{ type: "text", text: "hi" }] }, - ]); - expect(activeSession.sessionManager.fileEntries).toEqual([ - { type: "session", id: "session-root" }, - ]); - expect(activeSession.sessionManager.byId.has("interrupt")).toBe(false); - expect(activeSession.sessionManager.byId.has("aborted")).toBe(false); - expect(activeSession.sessionManager.leafId).toBe("session-root"); - expect(rewriteFile).toHaveBeenCalledTimes(1); - }); -}); - describe("composeSystemPromptWithHookContext", () => { it("returns undefined when no hook system context is provided", () => { expect(composeSystemPromptWithHookContext({ baseSystemPrompt: "base" })).toBeUndefined(); @@ -322,71 +222,6 @@ describe("resolvePromptModeForSession", () => { }); }); -describe("shouldInjectHeartbeatPrompt", () => { - it("uses trigger policy defaults for non-cron triggers", () => { - expect(shouldInjectHeartbeatPromptForTrigger("user")).toBe(true); - expect(shouldInjectHeartbeatPromptForTrigger("heartbeat")).toBe(true); - expect(shouldInjectHeartbeatPromptForTrigger("memory")).toBe(true); - expect(shouldInjectHeartbeatPromptForTrigger(undefined)).toBe(true); - }); - - it("uses trigger policy overrides for cron", () => { - expect(shouldInjectHeartbeatPromptForTrigger("cron")).toBe(false); - }); - - it("injects the heartbeat prompt for default-agent non-cron runs", () => { - expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: true, trigger: "user" })).toBe(true); - expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: true, trigger: "heartbeat" })).toBe(true); - expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: true, trigger: "memory" })).toBe(true); - expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: true, trigger: undefined })).toBe(true); - }); - - it("suppresses the heartbeat prompt for cron-triggered runs", () => { - expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: true, trigger: "cron" })).toBe(false); - }); - - it("suppresses the heartbeat prompt for non-default agents", () => { - expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: false, trigger: "user" })).toBe(false); - }); - - it("omits heartbeat prompt content for cron-triggered full-mode runs on non-cron session keys", () => { - const sessionKey = "agent:main:kos:thread:abc"; - expect(resolvePromptModeForSession(sessionKey)).toBe("full"); - - const heartbeatPrompt = shouldInjectHeartbeatPrompt({ - isDefaultAgent: true, - trigger: "cron", - }) - ? resolveHeartbeatPrompt(undefined) - : undefined; - - const prompt = buildEmbeddedSystemPrompt({ - workspaceDir: "/tmp/openclaw", - defaultThinkLevel: "off", - reasoningLevel: "off", - reasoningTagHint: false, - heartbeatPrompt, - promptMode: resolvePromptModeForSession(sessionKey), - runtimeInfo: { - host: "host", - os: "Darwin", - arch: "arm64", - node: "v22.0.0", - model: "openai/gpt-5.4", - }, - tools: [], - modelAliasLines: [], - userTimezone: "UTC", - userTime: "00:00", - userTimeFormat: "24", - }); - - expect(prompt).not.toContain("## Heartbeats"); - expect(prompt).not.toContain("HEARTBEAT_OK"); - expect(prompt).not.toContain("Read HEARTBEAT.md"); - }); -}); - describe("resolveAttemptFsWorkspaceOnly", () => { it("uses global tools.fs.workspaceOnly when agent has no override", () => { const cfg: OpenClawConfig = { @@ -1948,50 +1783,6 @@ describe("shouldInjectOllamaCompatNumCtx", () => { }); }); -describe("resolveEmbeddedAgentStreamFn", () => { - it("keeps the session-managed HTTP stream when no override applies", () => { - const currentStreamFn = vi.fn(); - - const resolved = resolveEmbeddedAgentStreamFn({ - currentStreamFn: currentStreamFn as never, - shouldUseWebSocketTransport: false, - sessionId: "session-1", - model: { provider: "xai" } as never, - }); - - expect(resolved).toBe(currentStreamFn); - }); - - it("keeps the session-managed HTTP stream when websocket auth is unavailable", () => { - const currentStreamFn = vi.fn(); - - const resolved = resolveEmbeddedAgentStreamFn({ - currentStreamFn: currentStreamFn as never, - shouldUseWebSocketTransport: true, - wsApiKey: undefined, - sessionId: "session-1", - model: { provider: "xai" } as never, - }); - - expect(resolved).toBe(currentStreamFn); - }); - - it("prefers a provider-owned stream override when present", () => { - const currentStreamFn = vi.fn(); - const providerStreamFn = vi.fn(); - - const resolved = resolveEmbeddedAgentStreamFn({ - currentStreamFn: currentStreamFn as never, - providerStreamFn: providerStreamFn as never, - shouldUseWebSocketTransport: false, - sessionId: "session-1", - model: { provider: "xai" } as never, - }); - - expect(resolved).toBe(providerStreamFn); - }); -}); - describe("decodeHtmlEntitiesInObject", () => { it("decodes HTML entities in string values", () => { const result = decodeHtmlEntitiesInObject( @@ -2078,7 +1869,7 @@ describe("buildAfterTurnRuntimeContext", () => { }); }); - it("passes primary model through even when compaction.model is set (override resolved in compactDirect)", () => { + it("resolves compaction.model override in runtime context so all context engines use the correct model", () => { const legacy = buildAfterTurnRuntimeContext({ attempt: { sessionKey: "agent:main:session:abc", @@ -2108,11 +1899,14 @@ describe("buildAfterTurnRuntimeContext", () => { agentDir: "/tmp/agent", }); - // buildAfterTurnLegacyCompactionParams no longer resolves the override; - // compactEmbeddedPiSessionDirect does it centrally for both auto + manual paths. + // buildEmbeddedCompactionRuntimeContext now resolves the override eagerly + // so that context engines (including third-party ones) receive the correct + // compaction model in the runtime context. expect(legacy).toMatchObject({ - provider: "openai-codex", - model: "gpt-5.4", + provider: "openrouter", + model: "anthropic/claude-sonnet-4-5", + // Auth profile dropped because provider changed from openai-codex to openrouter + authProfileId: undefined, }); }); it("includes resolved auth profile fields for context-engine afterTurn compaction", () => {