fix(gateway): default non-finite recent transcript limits

This commit is contained in:
Peter Steinberger
2026-05-29 01:04:58 -04:00
parent 22e8cd2a1d
commit 0e86ca1352
2 changed files with 65 additions and 15 deletions

View File

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

View File

@@ -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<string, unknown>;
};
function normalizeRecentSessionReadOptions(opts?: Partial<ReadRecentSessionMessagesOptions>) {
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<string[]> {
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<unknown[]> {
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);
}