From 0eb6f5d8bc99667a866463918225979885f495d7 Mon Sep 17 00:00:00 2001 From: Sk7n4k3d Date: Mon, 20 Apr 2026 19:45:51 +0200 Subject: [PATCH] session: clear auto-sourced model/auth overrides on /new and /reset --- src/auto-reply/reply/session.test.ts | 60 +++++++++++++++++++ src/auto-reply/reply/session.ts | 22 +++++-- .../sessions/reset-preserved-selection.ts | 51 ++++++++++++++++ src/gateway/session-reset-service.ts | 43 +------------ 4 files changed, 129 insertions(+), 47 deletions(-) create mode 100644 src/config/sessions/reset-preserved-selection.ts diff --git a/src/auto-reply/reply/session.test.ts b/src/auto-reply/reply/session.test.ts index c6eaa7cc6c5..05d300b6410 100644 --- a/src/auto-reply/reply/session.test.ts +++ b/src/auto-reply/reply/session.test.ts @@ -1827,6 +1827,66 @@ describe("initSessionState preserves behavior overrides across /new and /reset", } }); + it("clears auto-sourced model/provider/auth overrides on /new and /reset (#69301)", async () => { + const storePath = await createStorePath("openclaw-reset-auto-overrides-"); + const sessionKey = "agent:main:telegram:direct:6761477233"; + const existingSessionId = "existing-session-auto-overrides"; + const autoOverrides = { + providerOverride: "openai-codex", + modelOverride: "gpt-5.4", + modelOverrideSource: "auto", + authProfileOverride: "openai-codex:default", + authProfileOverrideSource: "auto", + authProfileOverrideCompactionCount: 1, + verboseLevel: "on", + } as const; + const cases = [ + { name: "new clears auto-sourced overrides", body: "/new" }, + { name: "reset clears auto-sourced overrides", 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.authProfileOverride, testCase.name).toBeUndefined(); + expect(result.sessionEntry.authProfileOverrideSource, testCase.name).toBeUndefined(); + expect(result.sessionEntry.authProfileOverrideCompactionCount, testCase.name).toBeUndefined(); + // Unrelated behavior overrides still carry across the reset. + 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"; diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 7a0cdaca4f8..29cc29c22b4 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -9,6 +9,7 @@ import { resolveGroupSessionKey } from "../../config/sessions/group.js"; import { canonicalizeMainSessionAlias } from "../../config/sessions/main-session.js"; import { deriveSessionMetaPatch } from "../../config/sessions/metadata.js"; import { resolveSessionTranscriptPath, resolveStorePath } from "../../config/sessions/paths.js"; +import { resolveResetPreservedSelection } from "../../config/sessions/reset-preserved-selection.js"; import { evaluateSessionFreshness, resolveChannelResetConfig, @@ -317,6 +318,7 @@ export async function initSessionState(params: { let persistedTtsAuto: TtsAutoMode | undefined; let persistedModelOverride: string | undefined; let persistedProviderOverride: string | undefined; + let persistedModelOverrideSource: SessionEntry["modelOverrideSource"]; let persistedAuthProfileOverride: string | undefined; let persistedAuthProfileOverrideSource: SessionEntry["authProfileOverrideSource"]; let persistedAuthProfileOverrideCompactionCount: number | undefined; @@ -472,6 +474,7 @@ export async function initSessionState(params: { persistedTtsAuto = entry.ttsAuto; persistedModelOverride = entry.modelOverride; persistedProviderOverride = entry.providerOverride; + persistedModelOverrideSource = entry.modelOverrideSource; persistedAuthProfileOverride = entry.authProfileOverride; persistedAuthProfileOverrideSource = entry.authProfileOverrideSource; persistedAuthProfileOverrideCompactionCount = entry.authProfileOverrideCompactionCount; @@ -490,11 +493,19 @@ export async function initSessionState(params: { persistedTrace = entry.traceLevel; persistedReasoning = entry.reasoningLevel; persistedTtsAuto = entry.ttsAuto; - persistedModelOverride = entry.modelOverride; - persistedProviderOverride = entry.providerOverride; - persistedAuthProfileOverride = entry.authProfileOverride; - persistedAuthProfileOverrideSource = entry.authProfileOverrideSource; - persistedAuthProfileOverrideCompactionCount = entry.authProfileOverrideCompactionCount; + // Only carry over user-driven overrides on reset. Auto-created + // fallback overrides (e.g. rate-limit auth rotation, model auto-pin) + // must be cleared so /new and /reset actually return the session to + // the configured default instead of staying pinned to the auto pick + // (#69301). + const preservedSelection = resolveResetPreservedSelection({ entry }); + persistedModelOverride = preservedSelection.modelOverride; + persistedProviderOverride = preservedSelection.providerOverride; + persistedModelOverrideSource = preservedSelection.modelOverrideSource; + persistedAuthProfileOverride = preservedSelection.authProfileOverride; + persistedAuthProfileOverrideSource = preservedSelection.authProfileOverrideSource; + persistedAuthProfileOverrideCompactionCount = + preservedSelection.authProfileOverrideCompactionCount; // Explicit /new and /reset should rotate the underlying CLI conversation too. // Keep the model/auth choice, but force the next turn to mint a fresh CLI binding. persistedLabel = entry.label; @@ -590,6 +601,7 @@ export async function initSessionState(params: { responseUsage: baseEntry?.responseUsage, modelOverride: persistedModelOverride ?? baseEntry?.modelOverride, providerOverride: persistedProviderOverride ?? baseEntry?.providerOverride, + modelOverrideSource: persistedModelOverrideSource ?? baseEntry?.modelOverrideSource, authProfileOverride: persistedAuthProfileOverride ?? baseEntry?.authProfileOverride, authProfileOverrideSource: persistedAuthProfileOverrideSource ?? baseEntry?.authProfileOverrideSource, diff --git a/src/config/sessions/reset-preserved-selection.ts b/src/config/sessions/reset-preserved-selection.ts new file mode 100644 index 00000000000..9aa879e7526 --- /dev/null +++ b/src/config/sessions/reset-preserved-selection.ts @@ -0,0 +1,51 @@ +import type { SessionEntry } from "./types.js"; + +export type ResetPreservedSelectionState = Pick< + SessionEntry, + | "providerOverride" + | "modelOverride" + | "modelOverrideSource" + | "authProfileOverride" + | "authProfileOverrideSource" + | "authProfileOverrideCompactionCount" +>; + +/** + * Decide which model/provider/auth overrides survive a `/new` or `/reset`. + * + * Only user-driven overrides (explicit `/model`, `sessions.patch`, etc.) are + * preserved. Auto-created overrides (runtime fallbacks, rate-limit rotations) + * are cleared so resets actually return the session to the configured default. + * + * Legacy entries persisted before `modelOverrideSource` was tracked are + * treated as user-driven, matching the prior reset behavior so explicit + * selections made before the source field existed are not silently dropped. + */ +export function resolveResetPreservedSelection(params: { + entry?: SessionEntry; +}): Partial { + const { entry } = params; + if (!entry) { + return {}; + } + + const preserved: Partial = {}; + const preserveLegacyUserModelOverride = + entry.modelOverrideSource === "user" || + (entry.modelOverrideSource === undefined && Boolean(entry.modelOverride)); + if (preserveLegacyUserModelOverride && entry.modelOverride) { + preserved.providerOverride = entry.providerOverride; + preserved.modelOverride = entry.modelOverride; + preserved.modelOverrideSource = "user"; + } + + if (entry.authProfileOverrideSource === "user" && entry.authProfileOverride) { + preserved.authProfileOverride = entry.authProfileOverride; + preserved.authProfileOverrideSource = entry.authProfileOverrideSource; + if (entry.authProfileOverrideCompactionCount !== undefined) { + preserved.authProfileOverrideCompactionCount = entry.authProfileOverrideCompactionCount; + } + } + + return preserved; +} diff --git a/src/gateway/session-reset-service.ts b/src/gateway/session-reset-service.ts index c7bdbff9625..6e485973b6b 100644 --- a/src/gateway/session-reset-service.ts +++ b/src/gateway/session-reset-service.ts @@ -21,6 +21,7 @@ import { updateSessionStore, } from "../config/sessions.js"; import { resolveSessionFilePath, resolveSessionFilePathOptions } from "../config/sessions/paths.js"; +import { resolveResetPreservedSelection } from "../config/sessions/reset-preserved-selection.js"; import type { SessionAcpMeta } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { logVerbose } from "../globals.js"; @@ -62,48 +63,6 @@ function stripRuntimeModelState(entry?: SessionEntry): SessionEntry | undefined }; } -type ResetPreservedSelectionState = Pick< - SessionEntry, - | "providerOverride" - | "modelOverride" - | "modelOverrideSource" - | "authProfileOverride" - | "authProfileOverrideSource" - | "authProfileOverrideCompactionCount" ->; - -function resolveResetPreservedSelection(params: { - entry?: SessionEntry; -}): Partial { - const { entry } = params; - if (!entry) { - return {}; - } - - const preserved: Partial = {}; - // `modelOverrideSource` is new. Older persisted sessions can still carry - // user-selected overrides without the source field, so treat an absent - // source as legacy user state during reset and backfill it forward. - const preserveLegacyUserModelOverride = - entry.modelOverrideSource === "user" || - (entry.modelOverrideSource === undefined && Boolean(entry.modelOverride)); - if (preserveLegacyUserModelOverride && entry.modelOverride) { - preserved.providerOverride = entry.providerOverride; - preserved.modelOverride = entry.modelOverride; - preserved.modelOverrideSource = "user"; - } - - if (entry.authProfileOverrideSource === "user" && entry.authProfileOverride) { - preserved.authProfileOverride = entry.authProfileOverride; - preserved.authProfileOverrideSource = entry.authProfileOverrideSource; - if (entry.authProfileOverrideCompactionCount !== undefined) { - preserved.authProfileOverrideCompactionCount = entry.authProfileOverrideCompactionCount; - } - } - - return preserved; -} - export function archiveSessionTranscriptsForSession(params: { sessionId: string | undefined; storePath: string;