From 569eb093d66ae0681b6774ee4bed4db6f546fae7 Mon Sep 17 00:00:00 2001 From: "C.L.A.I.R.E." Date: Fri, 13 Mar 2026 09:46:11 +0800 Subject: [PATCH] fix(session): preserve model override across daily freshness resets When a session becomes stale due to the daily freshness check (default 4 AM), the session is recreated but modelOverride and providerOverride are not carried over. This happens because the preservation logic only runs when `resetTriggered` is true (explicit /reset or /new commands), but daily freshness resets have `resetTriggered = false`. This causes users who set a per-session model via /model to lose their override every day, requiring them to re-execute the command. Move modelOverride/providerOverride preservation outside the `resetTriggered` guard so they survive all session reset scenarios. Fixes: daily reset clears session model override set via /model command --- src/auto-reply/reply/session.test.ts | 51 ++++++++++++++++++++++++++++ src/auto-reply/reply/session.ts | 8 +++-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/auto-reply/reply/session.test.ts b/src/auto-reply/reply/session.test.ts index db0870b704a..d14ae588347 100644 --- a/src/auto-reply/reply/session.test.ts +++ b/src/auto-reply/reply/session.test.ts @@ -1557,6 +1557,57 @@ describe("initSessionState preserves behavior overrides across /new and /reset", expect(result.sessionEntry.verboseLevel).toBeUndefined(); expect(result.sessionEntry.thinkingLevel).toBeUndefined(); }); + + it("preserves modelOverride and providerOverride across daily freshness resets", async () => { + // Daily freshness resets (e.g. 4am boundary) have resetTriggered=false, so + // modelOverride/providerOverride must be preserved outside the resetTriggered guard. + vi.useFakeTimers(); + try { + // Simulate: it is 5am, session was last active at 3am (before 4am daily boundary) + vi.setSystemTime(new Date(2026, 0, 18, 5, 0, 0)); + const storePath = await createStorePath("openclaw-freshness-model-override-"); + const sessionKey = "agent:main:telegram:dm:model-override-user"; + const existingSessionId = "stale-session-model-override"; + + await writeSessionStoreFast(storePath, { + [sessionKey]: { + sessionId: existingSessionId, + updatedAt: new Date(2026, 0, 18, 3, 0, 0).getTime(), + modelOverride: "claude-opus-4", + providerOverride: "anthropic", + }, + }); + + const cfg = { session: { store: storePath } } as OpenClawConfig; + const result = await initSessionState({ + ctx: { + Body: "hello", + RawBody: "hello", + CommandBody: "hello", + From: "model-override-user", + To: "bot", + ChatType: "direct", + SessionKey: sessionKey, + Provider: "telegram", + Surface: "telegram", + }, + cfg, + commandAuthorized: true, + }); + + expect(result.isNewSession).toBe(true); + expect(result.resetTriggered).toBe(false); + expect(result.sessionId).not.toBe(existingSessionId); + // Model/provider overrides must survive daily freshness resets + expect(result.sessionEntry.modelOverride).toBe("claude-opus-4"); + expect(result.sessionEntry.providerOverride).toBe("anthropic"); + // Behavior overrides are NOT carried over for non-resetTriggered resets + expect(result.sessionEntry.verboseLevel).toBeUndefined(); + expect(result.sessionEntry.thinkingLevel).toBeUndefined(); + } finally { + vi.useRealTimers(); + } + }); }); describe("drainFormattedSystemEvents", () => { diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 6db6b1708cb..68fb7861ac9 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -380,6 +380,12 @@ export async function initSessionState(params: { isNewSession = true; systemSent = false; abortedLastRun = false; + // Always preserve model/provider overrides across session resets + // (including daily freshness resets, not just explicit /reset or /new) + if (entry) { + persistedModelOverride = entry.modelOverride; + persistedProviderOverride = entry.providerOverride; + } // When a reset trigger (/new, /reset) starts a new session, carry over // user-set behavior overrides (verbose, thinking, reasoning, ttsAuto) // so the user doesn't have to re-enable them every time. @@ -388,8 +394,6 @@ export async function initSessionState(params: { persistedVerbose = entry.verboseLevel; persistedReasoning = entry.reasoningLevel; persistedTtsAuto = entry.ttsAuto; - persistedModelOverride = entry.modelOverride; - persistedProviderOverride = entry.providerOverride; persistedLabel = entry.label; } }