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;