From 694598822f19aad0a456beb6d94dcb0c95592ec0 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 1 May 2026 00:56:04 -0700 Subject: [PATCH] fix(gateway): cap sessions list preview hydration --- CHANGELOG.md | 2 +- src/gateway/session-utils.test.ts | 42 +++++++++++++++++++++++++++++++ src/gateway/session-utils.ts | 14 ++++++----- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeec5340289..b8b75741c4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ Docs: https://docs.openclaw.ai - Google Meet: interrupt Realtime provider output when local barge-in clears playback, so command-pair audio stops model speech instead of only restarting Chrome playback. Fixes #73850. (#73834) Thanks @shhtheonlyperson. - Gateway/config: cap oversized plugin-owned schemas in the full `config.schema` response so large installed plugin sets cannot balloon Gateway RSS or crash schema clients. Thanks @vincentkoc. -- Gateway/sessions: use bounded tail reads for sessions-list transcript usage fallbacks, keeping large session stores responsive when rows request derived previews. Thanks @vincentkoc. +- Gateway/sessions: use bounded tail reads for sessions-list transcript usage fallbacks and cap bulk title/last-message hydration, keeping large session stores responsive when rows request derived previews. Thanks @vincentkoc. - Gateway/chat: bound chat-history transcript reads to the requested display window so large session logs no longer OOM the Gateway when clients ask for a small history page. Thanks @vincentkoc. - Voice Call/Twilio: honor stored pre-connect TwiML before realtime webhook shortcuts and reject DTMF sequences outside conversation mode, so Meet PIN entry cannot be skipped or silently dropped. Thanks @donkeykong91 and @PfanP. - Google Meet/Voice Call: play Twilio Meet DTMF before opening the realtime media stream and carry the intro as the initial Voice Call message, so the greeting is generated after Meet admits the phone participant instead of racing a live-call TwiML update. Thanks @donkeykong91 and @PfanP. diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index 7408faec29d..9614fb62aa9 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -1110,6 +1110,48 @@ describe("resolveSessionModelRef", () => { }); describe("listSessionsFromStore selected model display", () => { + test("caps transcript title and last-message hydration for bulk list responses", () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sessions-list-cap-")); + try { + const storePath = path.join(tmpDir, "sessions.json"); + const store: Record = {}; + const now = Date.now(); + for (let i = 0; i < 105; i += 1) { + const sessionId = `sess-${i}`; + store[`agent:main:${sessionId}`] = { + sessionId, + updatedAt: now - i, + } as SessionEntry; + fs.writeFileSync( + path.join(tmpDir, `${sessionId}.jsonl`), + [ + JSON.stringify({ type: "session", version: 1, id: sessionId }), + JSON.stringify({ message: { role: "user", content: `title ${i}` } }), + JSON.stringify({ message: { role: "assistant", content: `last ${i}` } }), + ].join("\n"), + "utf-8", + ); + } + + const result = listSessionsFromStore({ + cfg: createModelDefaultsConfig({ primary: "openai/gpt-5.4" }), + storePath, + store, + opts: { includeDerivedTitles: true, includeLastMessage: true, limit: 105 }, + }); + + expect(result.sessions).toHaveLength(105); + expect(result.sessions[0]?.derivedTitle).toBe("title 0"); + expect(result.sessions[0]?.lastMessagePreview).toBe("last 0"); + expect(result.sessions[99]?.derivedTitle).toBe("title 99"); + expect(result.sessions[99]?.lastMessagePreview).toBe("last 99"); + expect(result.sessions[100]?.derivedTitle).toBeUndefined(); + expect(result.sessions[100]?.lastMessagePreview).toBeUndefined(); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + test("shows the selected override model even when a fallback runtime model exists", () => { const cfg = createModelDefaultsConfig({ primary: "anthropic/claude-opus-4-6", diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 269c938643a..9e937c98cbc 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -1629,6 +1629,7 @@ export function listSessionsFromStore(params: { const { cfg, storePath, store, opts } = params; const now = Date.now(); const sessionListTranscriptUsageMaxBytes = 64 * 1024; + const sessionListTranscriptFieldRows = 100; const includeGlobal = opts.includeGlobal === true; const includeUnknown = opts.includeUnknown === true; @@ -1724,8 +1725,9 @@ export function listSessionsFromStore(params: { entries = entries.slice(0, limit); } - const sessions = entries.map(([key, entry]) => - buildGatewaySessionRow({ + const sessions = entries.map(([key, entry], index) => { + const includeTranscriptFields = index < sessionListTranscriptFieldRows; + return buildGatewaySessionRow({ cfg, storePath, store, @@ -1733,11 +1735,11 @@ export function listSessionsFromStore(params: { entry, modelCatalog: params.modelCatalog, now, - includeDerivedTitles, - includeLastMessage, + includeDerivedTitles: includeTranscriptFields && includeDerivedTitles, + includeLastMessage: includeTranscriptFields && includeLastMessage, transcriptUsageMaxBytes: sessionListTranscriptUsageMaxBytes, - }), - ); + }); + }); return { ts: now,