fix(session): preserve verbose/thinking/tts overrides across /new and /reset (openclaw#10881) thanks @mcaxtr

Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test

Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Marcus Castro
2026-02-12 23:27:12 -03:00
committed by GitHub
parent cd50b5ded2
commit 585c9a7265
3 changed files with 172 additions and 0 deletions

View File

@@ -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.

View File

@@ -451,6 +451,168 @@ describe("applyResetModelOverride", () => {
});
});
describe("initSessionState preserves behavior overrides across /new and /reset", () => {
async function createStorePath(prefix: string): Promise<string> {
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<string, unknown>;
}): Promise<void> {
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();

View File

@@ -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;