diff --git a/CHANGELOG.md b/CHANGELOG.md index 04b4caa4d56..f09e6d2d8cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai - Security/Hooks: harden webhook and device token verification with shared constant-time secret comparison, and add per-client auth-failure throttling for hook endpoints (`429` + `Retry-After`). Thanks @akhmittra. - Security/Browser: require auth for loopback browser control HTTP routes, auto-generate `gateway.auth.token` when browser control starts without auth, and add a security-audit check for unauthenticated browser control. Thanks @tcusolle. - Sessions/Gateway: harden transcript path resolution and reject unsafe session IDs/file paths so session operations stay within agent sessions directories. Thanks @akhmittra. +- Sessions: preserve `verboseLevel`, `thinkingLevel`/`reasoningLevel`, and `ttsAuto` overrides across `/new` and `/reset` session resets. (#10787) Thanks @mcaxtr. - Gateway: raise WS payload/buffer limits so 5,000,000-byte image attachments work reliably. (#14486) Thanks @0xRaini. - Logging/CLI: use local timezone timestamps for console prefixing, and include `±HH:MM` offsets when using `openclaw logs --local-time` to avoid ambiguity. (#14771) Thanks @0xRaini. - Gateway: drain active turns before restart to prevent message loss. (#13931) Thanks @0xRaini. diff --git a/src/auto-reply/reply/session-resets.test.ts b/src/auto-reply/reply/session-resets.test.ts index 15d5e3275a7..691b7acd809 100644 --- a/src/auto-reply/reply/session-resets.test.ts +++ b/src/auto-reply/reply/session-resets.test.ts @@ -451,6 +451,168 @@ describe("applyResetModelOverride", () => { }); }); +describe("initSessionState preserves behavior overrides across /new and /reset", () => { + async function createStorePath(prefix: string): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); + return path.join(root, "sessions.json"); + } + + async function seedSessionStoreWithOverrides(params: { + storePath: string; + sessionKey: string; + sessionId: string; + overrides: Record; + }): Promise { + const { saveSessionStore } = await import("../../config/sessions.js"); + await saveSessionStore(params.storePath, { + [params.sessionKey]: { + sessionId: params.sessionId, + updatedAt: Date.now(), + ...params.overrides, + }, + }); + } + + it("/new preserves verboseLevel from previous session", async () => { + const storePath = await createStorePath("openclaw-reset-verbose-"); + const sessionKey = "agent:main:telegram:dm:user1"; + const existingSessionId = "existing-session-verbose"; + await seedSessionStoreWithOverrides({ + storePath, + sessionKey, + sessionId: existingSessionId, + overrides: { verboseLevel: "on" }, + }); + + const cfg = { + session: { store: storePath, idleMinutes: 999 }, + } as OpenClawConfig; + + const result = await initSessionState({ + ctx: { + Body: "/new", + RawBody: "/new", + CommandBody: "/new", + From: "user1", + To: "bot", + ChatType: "direct", + SessionKey: sessionKey, + Provider: "telegram", + Surface: "telegram", + }, + cfg, + commandAuthorized: true, + }); + + expect(result.isNewSession).toBe(true); + expect(result.resetTriggered).toBe(true); + expect(result.sessionId).not.toBe(existingSessionId); + expect(result.sessionEntry.verboseLevel).toBe("on"); + }); + + it("/reset preserves thinkingLevel and reasoningLevel from previous session", async () => { + const storePath = await createStorePath("openclaw-reset-thinking-"); + const sessionKey = "agent:main:telegram:dm:user2"; + const existingSessionId = "existing-session-thinking"; + await seedSessionStoreWithOverrides({ + storePath, + sessionKey, + sessionId: existingSessionId, + overrides: { thinkingLevel: "full", reasoningLevel: "high" }, + }); + + const cfg = { + session: { store: storePath, idleMinutes: 999 }, + } as OpenClawConfig; + + const result = await initSessionState({ + ctx: { + Body: "/reset", + RawBody: "/reset", + CommandBody: "/reset", + From: "user2", + To: "bot", + ChatType: "direct", + SessionKey: sessionKey, + Provider: "telegram", + Surface: "telegram", + }, + cfg, + commandAuthorized: true, + }); + + expect(result.isNewSession).toBe(true); + expect(result.resetTriggered).toBe(true); + expect(result.sessionEntry.thinkingLevel).toBe("full"); + expect(result.sessionEntry.reasoningLevel).toBe("high"); + }); + + it("/new preserves ttsAuto from previous session", async () => { + const storePath = await createStorePath("openclaw-reset-tts-"); + const sessionKey = "agent:main:telegram:dm:user3"; + const existingSessionId = "existing-session-tts"; + await seedSessionStoreWithOverrides({ + storePath, + sessionKey, + sessionId: existingSessionId, + overrides: { ttsAuto: "on" }, + }); + + const cfg = { + session: { store: storePath, idleMinutes: 999 }, + } as OpenClawConfig; + + const result = await initSessionState({ + ctx: { + Body: "/new", + RawBody: "/new", + CommandBody: "/new", + From: "user3", + To: "bot", + ChatType: "direct", + SessionKey: sessionKey, + Provider: "telegram", + Surface: "telegram", + }, + cfg, + commandAuthorized: true, + }); + + expect(result.isNewSession).toBe(true); + expect(result.sessionEntry.ttsAuto).toBe("on"); + }); + + it("idle-based new session does NOT preserve overrides (no entry to read)", async () => { + const storePath = await createStorePath("openclaw-idle-no-preserve-"); + const sessionKey = "agent:main:telegram:dm:new-user"; + + const cfg = { + session: { store: storePath, idleMinutes: 0 }, + } as OpenClawConfig; + + const result = await initSessionState({ + ctx: { + Body: "hello", + RawBody: "hello", + CommandBody: "hello", + From: "new-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.sessionEntry.verboseLevel).toBeUndefined(); + expect(result.sessionEntry.thinkingLevel).toBeUndefined(); + }); +}); + describe("prependSystemEvents", () => { it("adds a local timestamp to queued system events by default", async () => { vi.useFakeTimers(); diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 8a31e0119a0..5f561348bcb 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -240,6 +240,15 @@ export async function initSessionState(params: { isNewSession = true; systemSent = false; abortedLastRun = false; + // 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. + if (resetTriggered && entry) { + persistedThinking = entry.thinkingLevel; + persistedVerbose = entry.verboseLevel; + persistedReasoning = entry.reasoningLevel; + persistedTtsAuto = entry.ttsAuto; + } } const baseEntry = !isNewSession && freshEntry ? entry : undefined;