Skip to content

Commit ecf6cbf

Browse files
committed
fix(gateway): bound sessions list transcript usage
1 parent aec83af commit ecf6cbf

4 files changed

Lines changed: 99 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
1616
### Fixes
1717

1818
- 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.
19+
- Gateway/sessions: use bounded tail reads for sessions-list transcript usage fallbacks, keeping large session stores responsive when rows request derived previews. Thanks @vincentkoc.
1920
- 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.
2021
- 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.
2122
- 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.

src/gateway/session-utils.fs.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
readFirstUserMessageFromTranscript,
99
readLastMessagePreviewFromTranscript,
1010
readLatestSessionUsageFromTranscript,
11+
readRecentSessionUsageFromTranscript,
1112
readRecentSessionMessages,
1213
readSessionMessages,
1314
readSessionTitleFieldsFromTranscript,
@@ -947,6 +948,48 @@ describe("readLatestSessionUsageFromTranscript", () => {
947948
expect(snapshot?.costUsd).toBeCloseTo(0.0063, 8);
948949
});
949950

951+
test("bounds recent usage reads for bulk session listing", () => {
952+
const sessionId = "usage-recent-large";
953+
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
954+
const lines = [
955+
JSON.stringify({ type: "session", version: 1, id: sessionId }),
956+
...Array.from({ length: 2500 }, (_, index) =>
957+
JSON.stringify({
958+
message: { role: "user", content: `filler ${index} ${"x".repeat(700)}` },
959+
}),
960+
),
961+
JSON.stringify({
962+
message: {
963+
role: "assistant",
964+
provider: "openai",
965+
model: "gpt-5.4",
966+
usage: {
967+
input: 900,
968+
output: 100,
969+
cost: { total: 0.003 },
970+
},
971+
},
972+
}),
973+
];
974+
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
975+
const readFileSpy = vi.spyOn(fs, "readFileSync");
976+
977+
try {
978+
expect(
979+
readRecentSessionUsageFromTranscript(sessionId, storePath, undefined, undefined, 64 * 1024),
980+
).toMatchObject({
981+
modelProvider: "openai",
982+
model: "gpt-5.4",
983+
inputTokens: 900,
984+
outputTokens: 100,
985+
totalTokens: 900,
986+
});
987+
expect(readFileSpy).not.toHaveBeenCalled();
988+
} finally {
989+
readFileSpy.mockRestore();
990+
}
991+
});
992+
950993
test("returns null when the transcript has no assistant usage snapshot", () => {
951994
const sessionId = "usage-empty";
952995
writeTranscript(tmpDir, sessionId, [

src/gateway/session-utils.fs.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,39 @@ export function readLatestSessionUsageFromTranscript(
730730
});
731731
}
732732

733+
export function readRecentSessionUsageFromTranscript(
734+
sessionId: string,
735+
storePath: string | undefined,
736+
sessionFile: string | undefined,
737+
agentId: string | undefined,
738+
maxBytes: number,
739+
): SessionTranscriptUsageSnapshot | null {
740+
const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile, agentId);
741+
if (!filePath) {
742+
return null;
743+
}
744+
745+
return withOpenTranscriptFd(filePath, (fd) => {
746+
const stat = fs.fstatSync(fd);
747+
if (stat.size === 0) {
748+
return null;
749+
}
750+
const readLen = Math.min(stat.size, Math.max(1024, Math.floor(maxBytes)));
751+
const readStart = Math.max(0, stat.size - readLen);
752+
const buf = Buffer.alloc(readLen);
753+
const bytesRead = fs.readSync(fd, buf, 0, readLen, readStart);
754+
if (bytesRead <= 0) {
755+
return null;
756+
}
757+
const chunk = buf
758+
.toString("utf-8", 0, bytesRead)
759+
.split(/\r?\n/)
760+
.slice(readStart > 0 ? 1 : 0)
761+
.join("\n");
762+
return extractLatestUsageFromTranscriptChunk(chunk);
763+
});
764+
}
765+
733766
const PREVIEW_READ_SIZES = [64 * 1024, 256 * 1024, 1024 * 1024];
734767
const PREVIEW_MAX_LINES = 200;
735768

src/gateway/session-utils.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ import {
8787
} from "./session-store-key.js";
8888
import {
8989
readLatestSessionUsageFromTranscript,
90+
readRecentSessionUsageFromTranscript,
9091
readSessionTitleFieldsFromTranscript,
9192
} from "./session-utils.fs.js";
9293
import type {
@@ -105,6 +106,7 @@ export {
105106
readFirstUserMessageFromTranscript,
106107
readLastMessagePreviewFromTranscript,
107108
readLatestSessionUsageFromTranscript,
109+
readRecentSessionUsageFromTranscript,
108110
readRecentSessionMessages,
109111
readSessionTitleFieldsFromTranscript,
110112
readSessionPreviewItemsFromTranscript,
@@ -403,6 +405,7 @@ function resolveTranscriptUsageFallback(params: {
403405
storePath: string;
404406
fallbackProvider?: string;
405407
fallbackModel?: string;
408+
maxTranscriptBytes?: number;
406409
}): {
407410
estimatedCostUsd?: number;
408411
totalTokens?: number;
@@ -419,12 +422,21 @@ function resolveTranscriptUsageFallback(params: {
419422
const agentId = parsed?.agentId
420423
? normalizeAgentId(parsed.agentId)
421424
: resolveDefaultAgentId(params.cfg);
422-
const snapshot = readLatestSessionUsageFromTranscript(
423-
entry.sessionId,
424-
params.storePath,
425-
entry.sessionFile,
426-
agentId,
427-
);
425+
const snapshot =
426+
typeof params.maxTranscriptBytes === "number"
427+
? readRecentSessionUsageFromTranscript(
428+
entry.sessionId,
429+
params.storePath,
430+
entry.sessionFile,
431+
agentId,
432+
params.maxTranscriptBytes,
433+
)
434+
: readLatestSessionUsageFromTranscript(
435+
entry.sessionId,
436+
params.storePath,
437+
entry.sessionFile,
438+
agentId,
439+
);
428440
if (!snapshot) {
429441
return null;
430442
}
@@ -1300,6 +1312,7 @@ export function buildGatewaySessionRow(params: {
13001312
now?: number;
13011313
includeDerivedTitles?: boolean;
13021314
includeLastMessage?: boolean;
1315+
transcriptUsageMaxBytes?: number;
13031316
}): GatewaySessionRow {
13041317
const { cfg, storePath, store, key, entry } = params;
13051318
const now = params.now ?? Date.now();
@@ -1408,6 +1421,7 @@ export function buildGatewaySessionRow(params: {
14081421
storePath,
14091422
fallbackProvider: resolvedModel.provider,
14101423
fallbackModel: resolvedModel.model ?? DEFAULT_MODEL,
1424+
maxTranscriptBytes: params.transcriptUsageMaxBytes,
14111425
})
14121426
: null;
14131427
const preferLiveSubagentModelIdentity =
@@ -1614,6 +1628,7 @@ export function listSessionsFromStore(params: {
16141628
}): SessionsListResult {
16151629
const { cfg, storePath, store, opts } = params;
16161630
const now = Date.now();
1631+
const sessionListTranscriptUsageMaxBytes = 64 * 1024;
16171632

16181633
const includeGlobal = opts.includeGlobal === true;
16191634
const includeUnknown = opts.includeUnknown === true;
@@ -1720,6 +1735,7 @@ export function listSessionsFromStore(params: {
17201735
now,
17211736
includeDerivedTitles,
17221737
includeLastMessage,
1738+
transcriptUsageMaxBytes: sessionListTranscriptUsageMaxBytes,
17231739
}),
17241740
);
17251741

0 commit comments

Comments
 (0)