fix fallback provenance across reloads (#82363)

This commit is contained in:
Josh Avant
2026-05-15 19:12:34 -05:00
committed by GitHub
parent b08e0da25b
commit 1dac68c0bb
20 changed files with 318 additions and 24 deletions

View File

@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
- Trajectory export: skip and report malformed session/runtime JSONL rows in `manifest.json` instead of letting wrong-shaped session rows crash support bundle export.
- Config/doctor: copy fallback-enabled channel `allowFrom` entries into explicit `groupAllowFrom` allowlists during `openclaw doctor --fix`, preserving current group access without adding runtime fallback-transition flags.
- Configure: show one OpenAI provider entry with ChatGPT/Codex sign-in and API key choices, and keep browsed Codex models in the saved `/model` picker allowlist.
- Agents/model fallback: preserve auto fallback chains across deferred config reloads when session fallback provenance survives but `modelOverrideSource` is missing. Fixes #81982. Thanks @joshavant.
- Hooks: raise bounded gateway lifecycle hook wait budgets to 5 seconds for shutdown and 10 seconds for pre-restart, giving short restart notification handlers time to finish before shutdown continues. (#82273) Thanks @bryanbaer.
- Plugin releases: require external package compatibility metadata in the npm plugin publish plan, matching the ClawHub package contract before packages ship.
- Agents/OpenAI-compatible: honor per-model `max_completion_tokens`/`max_tokens` params in embedded OpenAI-completions runs so high-token Kimi-style routes keep their configured completion cap. Fixes #82230. Thanks @albert-zen.

View File

@@ -266,6 +266,7 @@ vi.mock("../utils/message-channel.js", () => ({
}));
vi.mock("./agent-scope.js", () => ({
hasSessionAutoModelFallbackProvenance: () => false,
listAgentEntries: () => [],
listAgentIds: () => ["default"],
resolveAgentConfig: () => undefined,

View File

@@ -39,6 +39,7 @@ import { createTrajectoryRuntimeRecorder } from "../trajectory/runtime.js";
import { resolveMessageChannel } from "../utils/message-channel.js";
import { resolveAgentRuntimeConfig } from "./agent-runtime-config.js";
import {
hasSessionAutoModelFallbackProvenance,
listAgentIds,
resolveAgentDir,
resolveEffectiveModelFallbacks,
@@ -748,6 +749,8 @@ async function agentCommandInternal(
let storedModelOverrideSource = hasStoredOverride
? sessionEntry?.modelOverrideSource
: undefined;
const hasStoredAutoFallbackProvenance =
hasStoredOverride && hasSessionAutoModelFallbackProvenance(sessionEntry);
const explicitProviderOverride =
typeof opts.provider === "string"
? normalizeExplicitOverrideInput(opts.provider, "provider")
@@ -1032,6 +1035,9 @@ async function agentCommandInternal(
hasSessionModelOverride:
hasExplicitRunOverride || Boolean(storedProviderOverride || storedModelOverride),
modelOverrideSource: hasExplicitRunOverride ? "user" : storedModelOverrideSource,
hasAutoFallbackProvenance: hasExplicitRunOverride
? false
: hasStoredAutoFallbackProvenance,
});
let fallbackAttemptIndex = 0;

View File

@@ -324,6 +324,23 @@ describe("resolveAgentConfig", () => {
hasSessionModelOverride: true,
}),
).toStrictEqual([]);
expect(
resolveEffectiveModelFallbacks({
cfg,
agentId: "linus",
hasSessionModelOverride: true,
hasAutoFallbackProvenance: true,
}),
).toEqual(["openai/gpt-5.4"]);
expect(
resolveEffectiveModelFallbacks({
cfg,
agentId: "linus",
hasSessionModelOverride: true,
modelOverrideSource: "user",
hasAutoFallbackProvenance: true,
}),
).toStrictEqual([]);
expect(
resolveEffectiveModelFallbacks({
cfg: cfgNoOverride,

View File

@@ -1,6 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { resolveAgentModelFallbackValues } from "../config/model-input.js";
export { hasSessionAutoModelFallbackProvenance } from "../config/sessions/model-override-provenance.js";
import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js";
import type { AgentModelConfig } from "../config/types.agents-shared.js";
import type { AgentConfig } from "../config/types.agents.js";
@@ -271,12 +272,16 @@ export function resolveEffectiveModelFallbacks(params: {
agentId: string;
hasSessionModelOverride: boolean;
modelOverrideSource?: "auto" | "user";
hasAutoFallbackProvenance?: boolean;
}): string[] | undefined {
const agentFallbacksOverride = resolveAgentModelFallbacksOverride(params.cfg, params.agentId);
if (!params.hasSessionModelOverride) {
return agentFallbacksOverride;
}
if (params.modelOverrideSource !== "auto") {
const canUseConfiguredFallbacks =
params.modelOverrideSource === "auto" ||
(params.modelOverrideSource === undefined && params.hasAutoFallbackProvenance === true);
if (!canUseConfiguredFallbacks) {
return [];
}
const defaultFallbacks = resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.model);

View File

@@ -3959,8 +3959,8 @@ describe("runAgentTurnWithFallback", () => {
});
const followupRun = createFollowupRun();
followupRun.run.provider = "anthropic";
followupRun.run.model = "claude-opus-4-6";
followupRun.run.provider = "bailian";
followupRun.run.model = "qwen3.6-plus";
const sessionEntry: SessionEntry = {
sessionId: "session",
@@ -4007,6 +4007,71 @@ describe("runAgentTurnWithFallback", () => {
expect(sessionEntry.modelOverrideSource).toBeUndefined();
});
it("persists fallback selection for recovered auto overrides without modelOverrideSource", async () => {
state.runWithModelFallbackMock.mockImplementation(
async (params: { run: (provider: string, model: string) => Promise<unknown> }) => ({
result: await params.run("openai-codex", "gpt-5.4"),
provider: "openai-codex",
model: "gpt-5.4",
attempts: [],
}),
);
state.runEmbeddedPiAgentMock.mockResolvedValue({
payloads: [{ text: "ok" }],
meta: {},
});
const followupRun = createFollowupRun();
followupRun.run.provider = "anthropic";
followupRun.run.model = "claude-opus-4-6";
const sessionEntry: SessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
totalTokens: 1,
compactionCount: 0,
providerOverride: "bailian",
modelOverride: "qwen3.6-plus",
modelOverrideFallbackOriginProvider: "minimax",
modelOverrideFallbackOriginModel: "MiniMax-M2.7",
// modelOverrideSource intentionally absent
};
const sessionStore = { main: sessionEntry };
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
const result = await runAgentTurnWithFallback({
commandBody: "hello",
followupRun,
sessionCtx: {
Provider: "telegram",
MessageSid: "msg",
} as unknown as TemplateContext,
opts: {},
typingSignals: createMockTypingSignaler(),
blockReplyPipeline: null,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
applyReplyToMode: (payload) => payload,
shouldEmitToolResult: () => true,
shouldEmitToolOutput: () => false,
pendingToolTasks: new Set(),
resetSessionAfterCompactionFailure: async () => false,
resetSessionAfterRoleOrderingConflict: async () => false,
isHeartbeat: false,
sessionKey: "main",
getActiveSessionEntry: () => sessionEntry,
activeSessionStore: sessionStore,
resolvedVerboseLevel: "off",
});
expect(result.kind).toBe("success");
expect(sessionEntry.providerOverride).toBe("openai-codex");
expect(sessionEntry.modelOverride).toBe("gpt-5.4");
expect(sessionEntry.modelOverrideSource).toBe("auto");
expect(sessionEntry.modelOverrideFallbackOriginProvider).toBe("minimax");
expect(sessionEntry.modelOverrideFallbackOriginModel).toBe("MiniMax-M2.7");
});
it("does not persist fallback selection when modelOverrideSource is user", async () => {
// Regression: fallback persistence overwrote user-initiated /models
// selections. When the user explicitly picked a model, the fallback

View File

@@ -4,6 +4,7 @@ import {
hasOutboundReplyContent,
resolveSendableOutboundReplyParts,
} from "openclaw/plugin-sdk/reply-payload";
import { hasSessionAutoModelFallbackProvenance } from "../../agents/agent-scope.js";
import {
buildOAuthRefreshFailureLoginCommand,
classifyOAuthRefreshFailure,
@@ -252,7 +253,11 @@ function resolveFallbackSelectionOrigin(params: { entry: SessionEntry; run: Foll
provider: string;
model: string;
} {
if (params.entry.modelOverrideSource === "auto") {
if (
params.entry.modelOverrideSource === "auto" ||
(params.entry.modelOverrideSource === undefined &&
hasSessionAutoModelFallbackProvenance(params.entry))
) {
const persistedOriginProvider = normalizeOptionalString(
params.entry.modelOverrideFallbackOriginProvider,
);
@@ -1301,7 +1306,8 @@ export async function runAgentTurnWithFallback(params: {
const isUserModelOverride =
activeSessionEntry.modelOverrideSource === "user" ||
(activeSessionEntry.modelOverrideSource === undefined &&
Boolean(normalizeOptionalString(activeSessionEntry.modelOverride)));
Boolean(normalizeOptionalString(activeSessionEntry.modelOverride)) &&
!hasSessionAutoModelFallbackProvenance(activeSessionEntry));
if (isUserModelOverride) {
return undefined;
}

View File

@@ -31,17 +31,19 @@ export function resolveModelFallbackOptions(
configOverride: FollowupRun["run"]["config"] = run.config,
) {
const config = configOverride;
const fallbacksOverride = resolveEffectiveModelFallbacks({
cfg: config,
agentId: run.agentId,
hasSessionModelOverride: run.hasSessionModelOverride === true,
modelOverrideSource: run.modelOverrideSource,
hasAutoFallbackProvenance: run.hasAutoFallbackProvenance === true,
});
return {
cfg: config,
provider: run.provider,
model: run.model,
agentDir: run.agentDir,
fallbacksOverride: resolveEffectiveModelFallbacks({
cfg: config,
agentId: run.agentId,
hasSessionModelOverride: run.hasSessionModelOverride === true,
modelOverrideSource: run.modelOverrideSource,
}),
fallbacksOverride,
};
}
@@ -60,6 +62,7 @@ export function buildEmbeddedRunBaseParams(params: {
agentId: params.run.agentId,
hasSessionModelOverride: params.run.hasSessionModelOverride === true,
modelOverrideSource: params.run.modelOverrideSource,
hasAutoFallbackProvenance: params.run.hasAutoFallbackProvenance === true,
});
return {
sessionFile: params.run.sessionFile,

View File

@@ -73,6 +73,7 @@ describe("agent-runner-utils", () => {
agentId: run.agentId,
hasSessionModelOverride: true,
modelOverrideSource: "user",
hasAutoFallbackProvenance: false,
});
expect(resolved).toEqual({
cfg: run.config,
@@ -83,6 +84,25 @@ describe("agent-runner-utils", () => {
});
});
it("passes through recovered auto fallback provenance for model fallback options", () => {
hoisted.resolveEffectiveModelFallbacksMock.mockReturnValue(["fallback-model"]);
const run = makeRun({
hasSessionModelOverride: true,
hasAutoFallbackProvenance: true,
});
const resolved = resolveModelFallbackOptions(run);
expect(hoisted.resolveEffectiveModelFallbacksMock).toHaveBeenCalledWith({
cfg: run.config,
agentId: run.agentId,
hasSessionModelOverride: true,
modelOverrideSource: undefined,
hasAutoFallbackProvenance: true,
});
expect(resolved.fallbacksOverride).toEqual(["fallback-model"]);
});
it("passes through missing agentId for helper-based fallback resolution", () => {
hoisted.resolveEffectiveModelFallbacksMock.mockReturnValue(["fallback-model"]);
const run = makeRun({ agentId: undefined });
@@ -94,6 +114,7 @@ describe("agent-runner-utils", () => {
agentId: undefined,
hasSessionModelOverride: false,
modelOverrideSource: undefined,
hasAutoFallbackProvenance: false,
});
expect(resolved.fallbacksOverride).toEqual(["fallback-model"]);
});
@@ -135,6 +156,35 @@ describe("agent-runner-utils", () => {
expect(resolved.runId).toBe("run-1");
});
it("passes through recovered auto fallback provenance for embedded run params", () => {
hoisted.resolveEffectiveModelFallbacksMock.mockReturnValue(["fallback-model"]);
const run = makeRun({
hasSessionModelOverride: true,
hasAutoFallbackProvenance: true,
});
const authProfile = resolveProviderScopedAuthProfile({
provider: "openai",
primaryProvider: "openai",
});
const resolved = buildEmbeddedRunBaseParams({
run,
provider: "openai",
model: "gpt-4.1-mini",
runId: "run-1",
authProfile,
});
expect(hoisted.resolveEffectiveModelFallbacksMock).toHaveBeenCalledWith({
cfg: run.config,
agentId: run.agentId,
hasSessionModelOverride: true,
modelOverrideSource: undefined,
hasAutoFallbackProvenance: true,
});
expect(resolved.modelFallbacksOverride).toEqual(["fallback-model"]);
});
it("does not force final-tag enforcement for minimax providers", () => {
const run = makeRun();

View File

@@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import {
hasSessionAutoModelFallbackProvenance,
hasConfiguredModelFallbacks,
resolveAgentConfig,
resolveSessionAgentId,
@@ -186,7 +187,12 @@ function resolveConfiguredFallbackModel(params: {
fallbackStateEntry?: SessionEntry;
}): { provider: string; model: string; persistedAutoFallback: boolean } {
const entry = params.fallbackStateEntry;
if (entry?.modelOverrideSource === "auto") {
const isAutoFallbackOverride =
entry?.modelOverrideSource === "auto" ||
(entry !== undefined &&
entry.modelOverrideSource === undefined &&
hasSessionAutoModelFallbackProvenance(entry));
if (isAutoFallbackOverride && entry !== undefined) {
const originProvider = normalizeOptionalString(entry.modelOverrideFallbackOriginProvider);
const originModel = normalizeOptionalString(entry.modelOverrideFallbackOriginModel);
if (originProvider && originModel) {

View File

@@ -1,4 +1,5 @@
import crypto from "node:crypto";
import { hasSessionAutoModelFallbackProvenance } from "../../agents/agent-scope.js";
import { resolveSessionAuthProfileOverride } from "../../agents/auth-profiles/session-override.js";
import type { ExecToolDefaults } from "../../agents/bash-tools.js";
import { resolveFastModeState } from "../../agents/fast-mode.js";
@@ -965,6 +966,12 @@ export async function runPreparedReply(
normalizeOptionalString(preparedSessionState.sessionEntry?.modelOverride) ||
normalizeOptionalString(preparedSessionState.sessionEntry?.providerOverride),
);
const runModelOverrideSource = runHasSessionModelOverride
? preparedSessionState.sessionEntry?.modelOverrideSource
: undefined;
const runHasAutoFallbackProvenance =
runHasSessionModelOverride &&
hasSessionAutoModelFallbackProvenance(preparedSessionState.sessionEntry);
const followupRun = {
prompt: queuedBody,
transcriptPrompt: transcriptCommandBody,
@@ -1018,9 +1025,8 @@ export async function runPreparedReply(
provider,
model,
hasSessionModelOverride: runHasSessionModelOverride,
modelOverrideSource: runHasSessionModelOverride
? preparedSessionState.sessionEntry?.modelOverrideSource
: undefined,
modelOverrideSource: runModelOverrideSource,
hasAutoFallbackProvenance: runHasAutoFallbackProvenance || undefined,
authProfileId,
authProfileIdSource,
thinkLevel: resolvedThinkLevel,

View File

@@ -1068,6 +1068,28 @@ describe("createModelSelectionState auto-failover overrides", () => {
expect(sessionStore[sessionKey]?.modelOverride).toBe("minimax/minimax-m2.7");
});
it("keeps recovered heartbeat auto-failover override without modelOverrideSource", async () => {
const { state, sessionStore } = await resolveStateWithOverride({
providerOverride: "openrouter",
modelOverride: "minimax/minimax-m2.7",
modelOverrideSource: undefined,
modelOverrideFallbackOriginProvider: "openai",
modelOverrideFallbackOriginModel: "gpt-4o",
primaryProvider: "openai",
primaryModel: "gpt-4o",
provider: "openrouter",
model: "minimax/minimax-m2.7",
isHeartbeat: true,
});
expect(state.provider).toBe("openrouter");
expect(state.model).toBe("minimax/minimax-m2.7");
expect(state.resetModelOverride).toBe(false);
expect(sessionStore[sessionKey]?.providerOverride).toBe("openrouter");
expect(sessionStore[sessionKey]?.modelOverride).toBe("minimax/minimax-m2.7");
expect(sessionStore[sessionKey]?.modelOverrideSource).toBeUndefined();
});
it("clears legacy heartbeat auto-failover override when no origin metadata exists", async () => {
const { state, sessionStore } = await resolveStateWithOverride({
providerOverride: "openrouter",

View File

@@ -67,7 +67,7 @@ describe("refreshQueuedFollowupSession", () => {
const queuedRun: FollowupRun = {
prompt: "queued message",
enqueuedAt: Date.now(),
run: makeRun(),
run: { ...makeRun(), hasAutoFallbackProvenance: true },
};
queue.items.push(queuedRun);

View File

@@ -125,10 +125,12 @@ export function refreshQueuedFollowupSession(params: {
Boolean(params.previousSessionId) &&
Boolean(params.nextSessionId) &&
params.previousSessionId !== params.nextSessionId;
const shouldRewriteSelection =
const shouldRewriteModelSelection =
typeof params.nextProvider === "string" ||
typeof params.nextModel === "string" ||
Object.hasOwn(params, "nextModelOverrideSource") ||
Object.hasOwn(params, "nextModelOverrideSource");
const shouldRewriteSelection =
shouldRewriteModelSelection ||
Object.hasOwn(params, "nextAuthProfileId") ||
Object.hasOwn(params, "nextAuthProfileIdSource");
if (!shouldRewriteSession && !shouldRewriteSelection) {
@@ -153,6 +155,9 @@ export function refreshQueuedFollowupSession(params: {
if (typeof params.nextModel === "string") {
run.model = params.nextModel;
}
if (shouldRewriteModelSelection) {
delete run.hasAutoFallbackProvenance;
}
if (Object.hasOwn(params, "nextModelOverrideSource")) {
run.hasSessionModelOverride = Boolean(run.provider || run.model);
run.modelOverrideSource = params.nextModelOverrideSource;

View File

@@ -87,6 +87,7 @@ export type FollowupRun = {
model: string;
hasSessionModelOverride?: boolean;
modelOverrideSource?: "auto" | "user";
hasAutoFallbackProvenance?: boolean;
authProfileId?: string;
authProfileIdSource?: "auto" | "user";
thinkLevel?: ThinkLevel;

View File

@@ -2407,6 +2407,65 @@ describe("initSessionState preserves behavior overrides across /new and /reset",
}
});
it("clears recovered auto fallback model overrides without modelOverrideSource on /new and /reset", async () => {
const storePath = await createStorePath("openclaw-reset-recovered-auto-fallback-");
const sessionKey = "agent:main:telegram:direct:6761477233";
const existingSessionId = "existing-session-recovered-auto-fallback";
const autoOverrides = {
providerOverride: "openai-codex",
modelOverride: "gpt-5.4",
modelOverrideFallbackOriginProvider: "anthropic",
modelOverrideFallbackOriginModel: "claude-opus-4-6",
verboseLevel: "on",
} as const;
const cases = [
{ name: "new clears recovered auto fallback override", body: "/new" },
{ name: "reset clears recovered auto fallback override", body: "/reset" },
] as const;
for (const testCase of cases) {
await seedSessionStoreWithOverrides({
storePath,
sessionKey,
sessionId: existingSessionId,
overrides: { ...autoOverrides },
});
const cfg = {
session: { store: storePath, idleMinutes: 999 },
} as OpenClawConfig;
const result = await initSessionState({
ctx: {
Body: testCase.body,
RawBody: testCase.body,
CommandBody: testCase.body,
From: "6761477233",
To: "bot",
ChatType: "direct",
SessionKey: sessionKey,
Provider: "telegram",
Surface: "telegram",
},
cfg,
commandAuthorized: true,
});
expect(result.isNewSession, testCase.name).toBe(true);
expect(result.resetTriggered, testCase.name).toBe(true);
expect(result.sessionId, testCase.name).not.toBe(existingSessionId);
expect(result.sessionEntry.modelOverride, testCase.name).toBeUndefined();
expect(result.sessionEntry.providerOverride, testCase.name).toBeUndefined();
expect(result.sessionEntry.modelOverrideSource, testCase.name).toBeUndefined();
expect(
result.sessionEntry.modelOverrideFallbackOriginProvider,
testCase.name,
).toBeUndefined();
expect(result.sessionEntry.modelOverrideFallbackOriginModel, testCase.name).toBeUndefined();
expect(result.sessionEntry.verboseLevel, testCase.name).toBe(autoOverrides.verboseLevel);
}
});
it("preserves spawned session ownership metadata across /new and /reset", async () => {
const storePath = await createStorePath("openclaw-reset-spawned-metadata-");
const sessionKey = "subagent:owned-child";

View File

@@ -1,3 +1,4 @@
import { hasSessionAutoModelFallbackProvenance } from "../../agents/agent-scope.js";
import {
modelKey,
normalizeModelRef,
@@ -91,7 +92,15 @@ export function isStaleHeartbeatAutoFallbackOverride(params: {
if (params.storedOverride?.source !== "session") {
return false;
}
if (params.sessionEntry?.modelOverrideSource !== "auto") {
const entry = params.sessionEntry;
const recoveredAutoFallbackOverride =
entry !== undefined &&
entry.modelOverrideSource === undefined &&
hasSessionAutoModelFallbackProvenance(entry);
if (entry?.modelOverrideSource !== "auto" && !recoveredAutoFallbackOverride) {
return false;
}
if (!entry) {
return false;
}
@@ -106,8 +115,8 @@ export function isStaleHeartbeatAutoFallbackOverride(params: {
const originKey = resolveModelRefKey({
defaultProvider: params.defaultProvider,
overrideProvider: params.sessionEntry.modelOverrideFallbackOriginProvider,
overrideModel: params.sessionEntry.modelOverrideFallbackOriginModel,
overrideProvider: entry.modelOverrideFallbackOriginProvider,
overrideModel: entry.modelOverrideFallbackOriginModel,
});
if (originKey) {
return originKey !== primaryKey;
@@ -115,7 +124,7 @@ export function isStaleHeartbeatAutoFallbackOverride(params: {
const noticeSelectedKey = resolveModelRefKey({
defaultProvider: params.defaultProvider,
overrideModel: normalizeOptionalString(params.sessionEntry.fallbackNoticeSelectedModel),
overrideModel: normalizeOptionalString(entry.fallbackNoticeSelectedModel),
});
if (noticeSelectedKey) {
return noticeSelectedKey !== primaryKey;

View File

@@ -0,0 +1,24 @@
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import type { SessionEntry } from "./types.js";
export function hasSessionAutoModelFallbackProvenance(
entry:
| Pick<
SessionEntry,
| "providerOverride"
| "modelOverride"
| "modelOverrideFallbackOriginProvider"
| "modelOverrideFallbackOriginModel"
>
| undefined,
): boolean {
const hasActiveOverride = Boolean(
normalizeOptionalString(entry?.providerOverride) ||
normalizeOptionalString(entry?.modelOverride),
);
return Boolean(
hasActiveOverride &&
normalizeOptionalString(entry?.modelOverrideFallbackOriginProvider) &&
normalizeOptionalString(entry?.modelOverrideFallbackOriginModel),
);
}

View File

@@ -1,3 +1,4 @@
import { hasSessionAutoModelFallbackProvenance } from "./model-override-provenance.js";
import type { SessionEntry } from "./types.js";
export type ResetPreservedSelectionState = Pick<
@@ -30,9 +31,13 @@ export function resolveResetPreservedSelection(params: {
}
const preserved: Partial<ResetPreservedSelectionState> = {};
const recoveredAutoFallbackOverride =
entry.modelOverrideSource === undefined && hasSessionAutoModelFallbackProvenance(entry);
const preserveLegacyUserModelOverride =
entry.modelOverrideSource === "user" ||
(entry.modelOverrideSource === undefined && Boolean(entry.modelOverride));
(entry.modelOverrideSource === undefined &&
Boolean(entry.modelOverride) &&
!recoveredAutoFallbackOverride);
if (preserveLegacyUserModelOverride && entry.modelOverride) {
preserved.providerOverride = entry.providerOverride;
preserved.modelOverride = entry.modelOverride;

View File

@@ -1,6 +1,7 @@
import crypto from "node:crypto";
import { clearBootstrapSnapshotOnSessionRollover } from "../../agents/bootstrap-cache.js";
import { resolveSessionLifecycleTimestamps } from "../../config/sessions/lifecycle.js";
import { hasSessionAutoModelFallbackProvenance } from "../../config/sessions/model-override-provenance.js";
import { resolveStorePath } from "../../config/sessions/paths.js";
import {
evaluateSessionFreshness,
@@ -59,7 +60,9 @@ function copySessionFields(
}
function preserveNonAutoModelOverride(target: SessionEntry, entry: SessionEntry): void {
if (entry.modelOverrideSource !== "auto") {
const recoveredAutoFallbackOverride =
entry.modelOverrideSource === undefined && hasSessionAutoModelFallbackProvenance(entry);
if (entry.modelOverrideSource !== "auto" && !recoveredAutoFallbackOverride) {
if (entry.modelOverride !== undefined) {
target.modelOverride = entry.modelOverride;
}