From 0e86ca1352250bdfe697d9d2bee0e020dc7ed8f8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 29 May 2026 01:04:58 -0400 Subject: [PATCH] fix(gateway): default non-finite recent transcript limits --- src/gateway/session-utils.fs.test.ts | 45 ++++++++++++++++++++++++++++ src/gateway/session-utils.fs.ts | 35 ++++++++++++---------- 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/src/gateway/session-utils.fs.test.ts b/src/gateway/session-utils.fs.test.ts index 0089efbe41b..477d1c57c09 100644 --- a/src/gateway/session-utils.fs.test.ts +++ b/src/gateway/session-utils.fs.test.ts @@ -461,6 +461,51 @@ describe("readSessionMessages", () => { expectMessageFields(out[1], { role: "assistant", content: "latest", openclaw: { seq: 4 } }); }); + test("returns no recent messages for non-finite maxMessages", async () => { + const sessionId = "test-session-recent-non-finite-max-messages"; + writeTranscript(tmpDir, sessionId, [ + { type: "session", version: 1, id: sessionId }, + { message: { role: "user", content: "old" } }, + { message: { role: "assistant", content: "latest" } }, + ]); + + expect( + readRecentSessionMessages(sessionId, storePath, undefined, { + maxMessages: Number.NaN, + maxBytes: 1024, + }), + ).toEqual([]); + await expect( + readRecentSessionMessagesAsync(sessionId, storePath, undefined, { + maxMessages: Number.POSITIVE_INFINITY, + maxBytes: 1024, + }), + ).resolves.toEqual([]); + }); + + test("uses the default recent byte cap for non-finite maxBytes", async () => { + const sessionId = "test-session-recent-non-finite-max-bytes"; + writeTranscript(tmpDir, sessionId, [ + { type: "session", version: 1, id: sessionId }, + { message: { role: "user", content: "old" } }, + { message: { role: "assistant", content: "latest" } }, + ]); + + const syncOut = readRecentSessionMessages(sessionId, storePath, undefined, { + maxMessages: 1, + maxBytes: Number.NaN, + }); + const asyncOut = await readRecentSessionMessagesAsync(sessionId, storePath, undefined, { + maxMessages: 1, + maxBytes: Number.POSITIVE_INFINITY, + }); + + expect(syncOut).toHaveLength(1); + expectMessageFields(syncOut[0], { role: "assistant", content: "latest" }); + expect(asyncOut).toHaveLength(1); + expectMessageFields(asyncOut[0], { role: "assistant", content: "latest" }); + }); + test("bounds recent-message reads for large append-only transcripts", () => { const sessionId = "test-session-recent-large"; const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`); diff --git a/src/gateway/session-utils.fs.ts b/src/gateway/session-utils.fs.ts index f0595256684..bd91aa05254 100644 --- a/src/gateway/session-utils.fs.ts +++ b/src/gateway/session-utils.fs.ts @@ -4,6 +4,10 @@ import { deriveSessionTotalTokens, hasNonzeroUsage, normalizeUsage } from "../ag import { jsonUtf8Bytes } from "../infra/json-utf8-bytes.js"; import { hasInterSessionUserProvenance } from "../sessions/input-provenance.js"; import { extractAssistantVisibleText } from "../shared/chat-message-content.js"; +import { + resolveIntegerOption, + resolveNonNegativeIntegerOption, +} from "../shared/number-coercion.js"; import { escapeRegExp } from "../shared/regexp.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { estimateStringChars, estimateTokensFromChars } from "../utils/cjk-chars.js"; @@ -181,13 +185,24 @@ type TailTranscriptRecord = { record: Record; }; +function normalizeRecentSessionReadOptions(opts?: Partial) { + const maxMessages = resolveNonNegativeIntegerOption(opts?.maxMessages, 0); + const maxBytes = resolveIntegerOption(opts?.maxBytes, RECENT_SESSION_MESSAGES_DEFAULT_MAX_BYTES, { + min: 1024, + }); + const maxLines = resolveIntegerOption(opts?.maxLines, maxMessages * 20 + 20, { + min: maxMessages, + }); + return { maxMessages, maxBytes, maxLines }; +} + export function readRecentSessionMessages( sessionId: string, storePath: string | undefined, sessionFile?: string, opts?: ReadRecentSessionMessagesOptions, ): unknown[] { - const maxMessages = Math.max(0, Math.floor(opts?.maxMessages ?? 0)); + const { maxMessages, maxBytes, maxLines } = normalizeRecentSessionReadOptions(opts); if (maxMessages === 0) { return []; } @@ -207,13 +222,8 @@ export function readRecentSessionMessages( return []; } - const maxBytes = Math.max( - 1024, - Math.floor(opts?.maxBytes ?? RECENT_SESSION_MESSAGES_DEFAULT_MAX_BYTES), - ); const readLen = Math.min(stat.size, maxBytes); const readStart = Math.max(0, stat.size - readLen); - const maxLines = Math.max(maxMessages, Math.floor(opts?.maxLines ?? maxMessages * 20 + 20)); return ( withOpenTranscriptFd(filePath, (fd) => { @@ -239,14 +249,9 @@ async function readRecentTranscriptTailLinesAsync( stat: fs.Stats, opts: ReadRecentSessionMessagesOptions, ): Promise { - const maxMessages = Math.max(0, Math.floor(opts.maxMessages)); - const maxBytes = Math.max( - 1024, - Math.floor(opts.maxBytes ?? RECENT_SESSION_MESSAGES_DEFAULT_MAX_BYTES), - ); + const { maxMessages, maxBytes, maxLines } = normalizeRecentSessionReadOptions(opts); const readLen = Math.min(stat.size, maxBytes); const readStart = Math.max(0, stat.size - readLen); - const maxLines = Math.max(maxMessages, Math.floor(opts.maxLines ?? maxMessages * 20 + 20)); const handle = await fs.promises.open(filePath, "r"); try { const buffer = Buffer.alloc(readLen); @@ -655,7 +660,8 @@ export async function readRecentSessionMessagesAsync( sessionFile?: string, opts?: ReadRecentSessionMessagesOptions, ): Promise { - const maxMessages = Math.max(0, Math.floor(opts?.maxMessages ?? 0)); + const normalized = normalizeRecentSessionReadOptions(opts); + const { maxMessages } = normalized; if (maxMessages === 0) { return []; } @@ -675,8 +681,7 @@ export async function readRecentSessionMessagesAsync( return []; } const lines = await readRecentTranscriptTailLinesAsync(filePath, stat, { - ...opts, - maxMessages, + ...normalized, }); return parseRecentTranscriptTailMessages(lines, maxMessages); }