From a722da3ed00538746ad4b07d3911bbaae6527bb8 Mon Sep 17 00:00:00 2001 From: James Reagan Date: Tue, 28 Apr 2026 00:34:58 -1000 Subject: [PATCH] fix(gateway): align session thinking defaults (#63418) Aligns Gateway history and session list thinking-default resolution so backend session state matches the Control UI default label: - `chat.history` now falls back through the shared Gateway session thinking-default resolver. - Explicit session overrides still win, then owning `agents.list[].thinkingDefault`, then global/model/catalog defaults. - `sessions.list` catalog-aware thinking defaults are covered by focused regressions. PR by @jpreagan. Validated in Blacksmith Testbox `tbx_01kq9t1aeqrz1mj598vvqv9dpg`: - `pnpm test:serial src/gateway/session-utils.test.ts src/gateway/server.sessions.gateway-server-sessions-a.test.ts src/gateway/server.chat.gateway-server-chat.test.ts` (141 passed) - `OPENCLAW_TESTBOX=1 pnpm check:changed` --- CHANGELOG.md | 1 + src/gateway/server-methods/chat.ts | 10 ++-- .../server.chat.gateway-server-chat.test.ts | 39 +++++++++++++ ...sessions.gateway-server-sessions-a.test.ts | 57 +++++++++++++++++++ src/gateway/session-utils.test.ts | 4 +- src/gateway/session-utils.ts | 2 +- 6 files changed, 107 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f677a5f5b15..2de70f4686d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/sessions: align `chat.history` and `sessions.list` thinking defaults with owning-agent and catalog-aware resolution so Control UI session defaults match backend runtime state. (#63418) Thanks @jpreagan. - Agents/media: register detached `video_generate` and `music_generate` tool run contexts until terminal status, so Discord-backed provider jobs stay live in `/tasks` instead of becoming `lost` when the parent chat run context disappears. Thanks @vincentkoc. - Agents/media: prefer OpenAI image and video providers when the default model uses the OpenAI Codex auth alias, so auto media generation no longer falls through to Fal before GPT Image or Sora. Thanks @vincentkoc. - Tasks/media: infer agent ownership for session-scoped task records so `/tasks` agent-local fallback includes session-backed `video_generate` and other async media jobs even when the current chat session has no linked rows. Thanks @vincentkoc. diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index bed55ff6be6..595a278d226 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -3,7 +3,6 @@ import path from "node:path"; import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent"; import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload"; import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "../../agents/agent-scope.js"; -import { resolveThinkingDefault } from "../../agents/model-selection.js"; import { rewriteTranscriptEntriesInSessionFile } from "../../agents/pi-embedded-runner/transcript-rewrite.js"; import { ensureSandboxWorkspaceForSession } from "../../agents/sandbox/context.js"; import { resolveAgentTimeoutMs } from "../../agents/timeout.js"; @@ -99,6 +98,7 @@ import { capArrayByJsonBytes, loadSessionEntry, resolveGatewayModelSupportsImages, + resolveGatewaySessionThinkingDefault, resolveDeletedAgentIdFromSessionKey, readSessionMessages, resolveSessionModelRef, @@ -1706,12 +1706,14 @@ export const chatHandlers: GatewayRequestHandlers = { } let thinkingLevel = entry?.thinkingLevel; if (!thinkingLevel) { - const catalog = await context.loadGatewayModelCatalog(); - thinkingLevel = resolveThinkingDefault({ + const loadedCatalog = await context.loadGatewayModelCatalog().catch(() => undefined); + const modelCatalog = Array.isArray(loadedCatalog) ? loadedCatalog : undefined; + thinkingLevel = resolveGatewaySessionThinkingDefault({ cfg, + agentId: sessionAgentId, provider: resolvedSessionModel.provider, model: resolvedSessionModel.model, - catalog, + modelCatalog, }); } const verboseLevel = entry?.verboseLevel ?? cfg.agents?.defaults?.verboseDefault; diff --git a/src/gateway/server.chat.gateway-server-chat.test.ts b/src/gateway/server.chat.gateway-server-chat.test.ts index d2dd6bb14a5..f91f178d2f0 100644 --- a/src/gateway/server.chat.gateway-server-chat.test.ts +++ b/src/gateway/server.chat.gateway-server-chat.test.ts @@ -918,6 +918,45 @@ describe("gateway server chat", () => { ]); }); + test("chat.history uses the owning agent thinkingDefault for non-default agent sessions", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); + try { + testState.sessionStorePath = path.join(dir, "sessions.json"); + testState.agentConfig = { + model: { primary: "openai/gpt-5" }, + thinkingDefault: "low", + }; + testState.agentsConfig = { + list: [ + { id: "main", default: true }, + { id: "alpha", thinkingDefault: "minimal" }, + ], + }; + await writeSessionStore({ + entries: { + "agent:alpha:main": { + sessionId: "sess-alpha", + updatedAt: Date.now(), + modelProvider: "openai", + model: "gpt-5", + }, + }, + }); + + const historyRes = await rpcReq<{ thinkingLevel?: string }>(ws, "chat.history", { + sessionKey: "agent:alpha:main", + }); + + expect(historyRes.ok).toBe(true); + expect(historyRes.payload?.thinkingLevel).toBe("minimal"); + } finally { + testState.agentConfig = undefined; + testState.agentsConfig = undefined; + testState.sessionStorePath = undefined; + await fs.rm(dir, { recursive: true, force: true }); + } + }); + test("chat.send does not persist verboseLevel for operator.write callers", async () => { await withGatewayServer(async ({ port }) => { await withMainSessionStore(async () => { diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts index 8a087d171b6..c32c3e6f463 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts @@ -775,6 +775,63 @@ describe("gateway server sessions", () => { ws.close(); }); + test("sessions.list uses the gateway model catalog for effective thinking defaults", async () => { + await createSessionStoreDir(); + testState.agentConfig = { + model: { primary: "test-provider/reasoner" }, + }; + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + modelProvider: "test-provider", + model: "reasoner", + }, + }, + }); + + const respond = vi.fn(); + const sessionsHandlers = await getSessionsHandlers(); + const { getRuntimeConfig } = await getGatewayConfigModule(); + await sessionsHandlers["sessions.list"]({ + req: { + type: "req", + id: "req-sessions-list-thinking-default", + method: "sessions.list", + params: {}, + }, + params: {}, + respond, + client: null, + isWebchatConnect: () => false, + context: { + getRuntimeConfig, + loadGatewayModelCatalog: async () => [ + { + provider: "test-provider", + id: "reasoner", + name: "Reasoner", + reasoning: true, + }, + ], + } as never, + }); + + expect(respond).toHaveBeenCalledWith( + true, + expect.objectContaining({ + sessions: expect.arrayContaining([ + expect.objectContaining({ + key: "agent:main:main", + thinkingDefault: "medium", + }), + ]), + }), + undefined, + ); + }); + test("sessions.changed mutation events include live usage metadata", async () => { const { dir } = await createSessionStoreDir(); await fs.writeFile( diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index c7d4b6e1d7a..018569c0bc3 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -154,7 +154,7 @@ describe("gateway session utils", () => { reasoning === true ? [{ id: "off" }, { id: "low" }, { id: "medium" }, { id: "high" }, { id: "max" }] : [{ id: "off" }], - defaultLevel: "off", + defaultLevel: reasoning === true ? "medium" : "off", }), }, }); @@ -193,6 +193,8 @@ describe("gateway session utils", () => { "high", "max", ]); + expect(defaults.thinkingDefault).toBe("medium"); + expect(row.thinkingDefault).toBe("medium"); }); test("session defaults use configured thinking default", () => { diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 52e7c114c82..8ae762ad93e 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -1042,7 +1042,7 @@ export function resolveGatewaySessionStoreTarget(params: { export { loadCombinedSessionStoreForGateway } from "../config/sessions/combined-store-gateway.js"; -function resolveGatewaySessionThinkingDefault(params: { +export function resolveGatewaySessionThinkingDefault(params: { cfg: OpenClawConfig; provider: string; model: string;