From 1609dec52ada885822898220fd3e8a4d507dfe9e Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Sun, 31 May 2026 18:18:40 -0700 Subject: [PATCH] refactor: add transcript reader seam facade --- .../session-transcript-readers.test.ts | 160 ++++++++ src/gateway/session-transcript-readers.ts | 350 ++++++++++++++++++ src/gateway/session-utils.fs.ts | 63 +++- src/gateway/session-utils.ts | 41 +- 4 files changed, 576 insertions(+), 38 deletions(-) create mode 100644 src/gateway/session-transcript-readers.test.ts create mode 100644 src/gateway/session-transcript-readers.ts diff --git a/src/gateway/session-transcript-readers.test.ts b/src/gateway/session-transcript-readers.test.ts new file mode 100644 index 000000000000..1f3857024eb2 --- /dev/null +++ b/src/gateway/session-transcript-readers.test.ts @@ -0,0 +1,160 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { + readLatestRecentSessionUsageFromTranscriptAsync, + readRecentSessionMessagesWithStats, + readRecentSessionTranscriptLines, + readSessionMessageByIdAsync, + readSessionMessageCountAsync, + readSessionMessagesAsync, + readSessionTitleFieldsFromTranscript, + type SessionTranscriptReadScope, +} from "./session-transcript-readers.js"; + +describe("session transcript reader facade", () => { + let tempDir: string; + let storePath: string; + let originalStateDir: string | undefined; + + beforeEach(() => { + originalStateDir = process.env.OPENCLAW_STATE_DIR; + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-transcript-readers-")); + storePath = path.join(tempDir, "sessions.json"); + process.env.OPENCLAW_STATE_DIR = tempDir; + }); + + afterEach(() => { + if (originalStateDir === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = originalStateDir; + } + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + function writeTranscript(sessionId: string, events: unknown[]): SessionTranscriptReadScope { + const transcriptPath = path.join(tempDir, `${sessionId}.jsonl`); + fs.writeFileSync( + transcriptPath, + `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, + "utf-8", + ); + return { sessionId, storePath }; + } + + test("reads active-branch messages and message ids through a scope", async () => { + const scope = writeTranscript("reader-active-branch", [ + { type: "session", version: 3, id: "reader-active-branch" }, + { + type: "message", + id: "root", + parentId: null, + message: { role: "user", content: "root prompt" }, + }, + { + type: "message", + id: "inactive", + parentId: "root", + message: { role: "assistant", content: "stale answer" }, + }, + { + type: "message", + id: "active", + parentId: "root", + message: { role: "assistant", content: "active answer" }, + }, + ]); + + await expect( + readSessionMessagesAsync(scope, { mode: "full", reason: "facade active branch test" }), + ).resolves.toMatchObject([{ content: "root prompt" }, { content: "active answer" }]); + await expect(readSessionMessageCountAsync(scope)).resolves.toBe(2); + await expect(readSessionMessageByIdAsync(scope, "active")).resolves.toMatchObject({ + found: true, + oversized: false, + seq: 2, + }); + }); + + test("reads recent tails with total counts through a scope", () => { + const scope = writeTranscript("reader-recent-tail", [ + { type: "session", version: 1, id: "reader-recent-tail" }, + { message: { role: "user", content: "old" } }, + { message: { role: "assistant", content: "middle" } }, + { message: { role: "user", content: "recent" } }, + { message: { role: "assistant", content: "latest" } }, + ]); + + const messages = readRecentSessionMessagesWithStats(scope, { + maxMessages: 2, + maxBytes: 2048, + }); + const tail = readRecentSessionTranscriptLines({ ...scope, maxLines: 3 }); + + expect(messages.totalMessages).toBe(4); + expect(messages.messages).toMatchObject([{ content: "recent" }, { content: "latest" }]); + expect(tail?.totalLines).toBe(5); + expect(tail?.lines.map((line) => JSON.parse(line).message?.content)).toEqual([ + "middle", + "recent", + "latest", + ]); + }); + + test("reads title fields and recent usage through a scope", async () => { + const scope = writeTranscript("reader-title-usage", [ + { type: "session", version: 1, id: "reader-title-usage" }, + { message: { role: "user", content: "derive this title" } }, + { + message: { + role: "assistant", + content: "metered answer", + provider: "openai", + model: "gpt-5.5", + usage: { input: 11, output: 7 }, + }, + }, + ]); + + expect(readSessionTitleFieldsFromTranscript(scope)).toEqual({ + firstUserMessage: "derive this title", + lastMessagePreview: "metered answer", + }); + await expect( + readLatestRecentSessionUsageFromTranscriptAsync(scope, 4096), + ).resolves.toMatchObject({ + inputTokens: 11, + model: "gpt-5.5", + modelProvider: "openai", + outputTokens: 7, + }); + }); + + test("honors agent ids when no store path or session file is provided", async () => { + const sessionId = "reader-agent-scope"; + const transcriptDir = path.join(tempDir, "agents", "agent-one", "sessions"); + fs.mkdirSync(transcriptDir, { recursive: true }); + fs.writeFileSync( + path.join(transcriptDir, `${sessionId}.jsonl`), + `${JSON.stringify({ + type: "message", + id: "agent-message", + parentId: null, + message: { role: "user", content: "agent scoped prompt" }, + })}\n`, + "utf-8", + ); + const scope = { agentId: "agent-one", sessionId }; + + await expect(readSessionMessageCountAsync(scope)).resolves.toBe(1); + await expect(readSessionMessageByIdAsync(scope, "agent-message")).resolves.toMatchObject({ + found: true, + seq: 1, + }); + await expect( + readSessionMessagesAsync(scope, { mode: "full", reason: "facade agent scope test" }), + ).resolves.toMatchObject([{ content: "agent scoped prompt" }]); + }); +}); diff --git a/src/gateway/session-transcript-readers.ts b/src/gateway/session-transcript-readers.ts new file mode 100644 index 000000000000..75cf420b16ec --- /dev/null +++ b/src/gateway/session-transcript-readers.ts @@ -0,0 +1,350 @@ +import type { + ReadRecentSessionMessagesOptions, + ReadSessionMessagesAsyncOptions, +} from "./session-utils.fs.js"; +import { + readFirstUserMessageFromTranscript as readFirstUserMessageFromTranscriptFile, + readLatestRecentSessionUsageFromTranscriptAsync as readLatestRecentSessionUsageFromTranscriptAsyncFile, + readLatestSessionUsageFromTranscript as readLatestSessionUsageFromTranscriptFile, + readLatestSessionUsageFromTranscriptAsync as readLatestSessionUsageFromTranscriptAsyncFile, + readRecentSessionMessages as readRecentSessionMessagesFile, + readRecentSessionMessagesAsync as readRecentSessionMessagesAsyncFile, + readRecentSessionMessagesWithStats as readRecentSessionMessagesWithStatsFile, + readRecentSessionMessagesWithStatsAsync as readRecentSessionMessagesWithStatsAsyncFile, + readRecentSessionTranscriptLines as readRecentSessionTranscriptLinesFile, + readRecentSessionUsageFromTranscript as readRecentSessionUsageFromTranscriptFile, + readRecentSessionUsageFromTranscriptAsync as readRecentSessionUsageFromTranscriptAsyncFile, + readSessionMessageByIdAsync as readSessionMessageByIdAsyncFile, + readSessionMessageCount as readSessionMessageCountFile, + readSessionMessageCountAsync as readSessionMessageCountAsyncFile, + readSessionMessages as readSessionMessagesFile, + readSessionMessagesAsync as readSessionMessagesAsyncFile, + readSessionPreviewItemsFromTranscript as readSessionPreviewItemsFromTranscriptFile, + readSessionTitleFieldsFromTranscript as readSessionTitleFieldsFromTranscriptFile, + readSessionTitleFieldsFromTranscriptAsync as readSessionTitleFieldsFromTranscriptAsyncFile, + visitSessionMessages as visitSessionMessagesFile, + visitSessionMessagesAsync as visitSessionMessagesAsyncFile, +} from "./session-utils.fs.js"; + +export type { ReadRecentSessionMessagesOptions, ReadSessionMessagesAsyncOptions }; + +export type SessionTranscriptReadScope = { + agentId?: string; + sessionFile?: string; + sessionId: string; + storePath?: string; +}; + +type SessionTitleFields = { + firstUserMessage: string | null; + lastMessagePreview: string | null; +}; + +type ReadRecentSessionMessagesResult = { + messages: unknown[]; + totalMessages: number; +}; + +type ReadSessionMessageByIdResult = { + message?: unknown; + seq?: number; + oversized: boolean; + found: boolean; +}; + +type SessionTranscriptUsageSnapshot = { + modelProvider?: string; + model?: string; + inputTokens?: number; + outputTokens?: number; + cacheRead?: number; + cacheWrite?: number; + totalTokens?: number; + totalTokensFresh?: boolean; + costUsd?: number; +}; + +/** Reads display messages from a session transcript through the reader seam. */ +export function readSessionMessages(scope: SessionTranscriptReadScope): unknown[] { + return readSessionMessagesFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + scope.agentId, + ); +} + +/** Reads recent display messages from a session transcript through the reader seam. */ +export function readRecentSessionMessages( + scope: SessionTranscriptReadScope, + opts?: ReadRecentSessionMessagesOptions, +): unknown[] { + return readRecentSessionMessagesFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + opts, + scope.agentId, + ); +} + +/** Visits display messages from a session transcript through the reader seam. */ +export function visitSessionMessages( + scope: SessionTranscriptReadScope, + visit: (message: unknown, seq: number) => void, +): number { + return visitSessionMessagesFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + visit, + scope.agentId, + ); +} + +/** Counts display messages in a session transcript through the reader seam. */ +export function readSessionMessageCount(scope: SessionTranscriptReadScope): number { + return readSessionMessageCountFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + scope.agentId, + ); +} + +/** Reads display messages asynchronously through the reader seam. */ +export async function readSessionMessagesAsync( + scope: SessionTranscriptReadScope, + opts: ReadSessionMessagesAsyncOptions, +): Promise { + return await readSessionMessagesAsyncFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + opts, + scope.agentId, + ); +} + +/** Reads recent display messages asynchronously through the reader seam. */ +export async function readRecentSessionMessagesAsync( + scope: SessionTranscriptReadScope, + opts?: ReadRecentSessionMessagesOptions, +): Promise { + return await readRecentSessionMessagesAsyncFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + opts, + scope.agentId, + ); +} + +/** Finds one display message by transcript id through the reader seam. */ +export async function readSessionMessageByIdAsync( + scope: SessionTranscriptReadScope, + messageId: string, +): Promise { + return await readSessionMessageByIdAsyncFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + messageId, + { agentId: scope.agentId }, + ); +} + +/** Visits display messages asynchronously through the reader seam. */ +export async function visitSessionMessagesAsync( + scope: SessionTranscriptReadScope, + visit: (message: unknown, seq: number) => void, + opts: { mode: "full"; reason: string; cache?: "reuse" | "skip" }, +): Promise { + return await visitSessionMessagesAsyncFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + visit, + opts, + scope.agentId, + ); +} + +/** Counts display messages asynchronously through the reader seam. */ +export async function readSessionMessageCountAsync( + scope: SessionTranscriptReadScope, +): Promise { + return await readSessionMessageCountAsyncFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + scope.agentId, + ); +} + +/** Reads recent messages with total-count metadata through the reader seam. */ +export function readRecentSessionMessagesWithStats( + scope: SessionTranscriptReadScope, + opts: ReadRecentSessionMessagesOptions, +): ReadRecentSessionMessagesResult { + return readRecentSessionMessagesWithStatsFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + opts, + scope.agentId, + ); +} + +/** Reads recent messages with total-count metadata asynchronously through the reader seam. */ +export async function readRecentSessionMessagesWithStatsAsync( + scope: SessionTranscriptReadScope, + opts: ReadRecentSessionMessagesOptions, +): Promise { + return await readRecentSessionMessagesWithStatsAsyncFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + opts, + scope.agentId, + ); +} + +/** Reads a bounded transcript tail for compaction and diagnostics through the reader seam. */ +export function readRecentSessionTranscriptLines( + params: SessionTranscriptReadScope & { + maxLines: number; + }, +): { lines: string[]; totalLines: number } | null { + return readRecentSessionTranscriptLinesFile({ + sessionId: params.sessionId, + storePath: params.storePath, + sessionFile: params.sessionFile, + agentId: params.agentId, + maxLines: params.maxLines, + }); +} + +/** Reads title and preview text from a transcript through the reader seam. */ +export function readSessionTitleFieldsFromTranscript( + scope: SessionTranscriptReadScope, + opts?: { includeInterSession?: boolean }, +): SessionTitleFields { + return readSessionTitleFieldsFromTranscriptFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + scope.agentId, + opts, + ); +} + +/** Reads title and preview text asynchronously through the reader seam. */ +export async function readSessionTitleFieldsFromTranscriptAsync( + scope: SessionTranscriptReadScope, + opts?: { includeInterSession?: boolean }, +): Promise { + return await readSessionTitleFieldsFromTranscriptAsyncFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + scope.agentId, + opts, + ); +} + +/** Reads the first user message from a transcript through the reader seam. */ +export function readFirstUserMessageFromTranscript( + scope: SessionTranscriptReadScope, + opts?: { includeInterSession?: boolean }, +): string | null { + return readFirstUserMessageFromTranscriptFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + scope.agentId, + opts, + ); +} + +/** Reads aggregate usage from a full transcript through the reader seam. */ +export function readLatestSessionUsageFromTranscript( + scope: SessionTranscriptReadScope, +): SessionTranscriptUsageSnapshot | null { + return readLatestSessionUsageFromTranscriptFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + scope.agentId, + ); +} + +/** Reads aggregate usage from a full transcript asynchronously through the reader seam. */ +export async function readLatestSessionUsageFromTranscriptAsync( + scope: SessionTranscriptReadScope, +): Promise { + return await readLatestSessionUsageFromTranscriptAsyncFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + scope.agentId, + ); +} + +/** Reads aggregate usage from a bounded transcript tail through the reader seam. */ +export async function readRecentSessionUsageFromTranscriptAsync( + scope: SessionTranscriptReadScope, + maxBytes: number, +): Promise { + return await readRecentSessionUsageFromTranscriptAsyncFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + scope.agentId, + maxBytes, + ); +} + +/** Reads latest usage from a bounded transcript tail through the reader seam. */ +export async function readLatestRecentSessionUsageFromTranscriptAsync( + scope: SessionTranscriptReadScope, + maxBytes: number, +): Promise { + return await readLatestRecentSessionUsageFromTranscriptAsyncFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + scope.agentId, + maxBytes, + ); +} + +/** Reads aggregate usage from a bounded transcript tail synchronously through the reader seam. */ +export function readRecentSessionUsageFromTranscript( + scope: SessionTranscriptReadScope, + maxBytes: number, +): SessionTranscriptUsageSnapshot | null { + return readRecentSessionUsageFromTranscriptFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + scope.agentId, + maxBytes, + ); +} + +/** Reads compact session preview items through the reader seam. */ +export function readSessionPreviewItemsFromTranscript( + scope: SessionTranscriptReadScope, + maxItems: number, + maxChars: number, +): ReturnType { + return readSessionPreviewItemsFromTranscriptFile( + scope.sessionId, + scope.storePath, + scope.sessionFile, + scope.agentId, + maxItems, + maxChars, + ); +} diff --git a/src/gateway/session-utils.fs.ts b/src/gateway/session-utils.fs.ts index 04cd6afd6642..3f19ad443a2c 100644 --- a/src/gateway/session-utils.fs.ts +++ b/src/gateway/session-utils.fs.ts @@ -155,8 +155,9 @@ export function readSessionMessages( sessionId: string, storePath: string | undefined, sessionFile?: string, + agentId?: string, ): unknown[] { - const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile); + const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile, agentId); if (!filePath) { return []; } @@ -216,13 +217,14 @@ export function readRecentSessionMessages( storePath: string | undefined, sessionFile?: string, opts?: ReadRecentSessionMessagesOptions, + agentId?: string, ): unknown[] { const { maxMessages, maxBytes, maxLines } = normalizeRecentSessionReadOptions(opts); if (maxMessages === 0) { return []; } - const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile); + const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile, agentId); if (!filePath) { return []; } @@ -546,8 +548,9 @@ export function visitSessionMessages( storePath: string | undefined, sessionFile: string | undefined, visit: (message: unknown, seq: number) => void, + agentId?: string, ): number { - const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile); + const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile, agentId); if (!filePath) { return 0; } @@ -563,8 +566,9 @@ export function readSessionMessageCount( sessionId: string, storePath: string | undefined, sessionFile?: string, + agentId?: string, ): number { - const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile); + const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile, agentId); if (!filePath) { return 0; } @@ -578,7 +582,7 @@ export function readSessionMessageCount( } catch { // Count from the transcript reader below when stat metadata is unavailable. } - const count = visitSessionMessages(sessionId, storePath, sessionFile, () => undefined); + const count = visitSessionMessages(sessionId, storePath, sessionFile, () => undefined, agentId); if (stat) { setCachedTranscriptMessageCount(filePath, stat, count); } @@ -590,8 +594,15 @@ export async function readSessionMessagesAsync( storePath: string | undefined, sessionFile: string | undefined, opts: ReadSessionMessagesAsyncOptions, + agentId?: string, ): Promise { - const result = await readSessionMessagesWithSourceAsync(sessionId, storePath, sessionFile, opts); + const result = await readSessionMessagesWithSourceAsync( + sessionId, + storePath, + sessionFile, + opts, + agentId, + ); return result.messages; } @@ -600,6 +611,7 @@ export async function readSessionMessagesWithSourceAsync( storePath: string | undefined, sessionFile: string | undefined, opts: ReadSessionMessagesAsyncOptions, + agentId?: string, ): Promise { if (opts.mode === "recent") { const { mode: _modeValue, ...recentOpts } = opts; @@ -608,13 +620,14 @@ export async function readSessionMessagesWithSourceAsync( storePath, sessionFile, recentOpts, + agentId, ); return result; } const filePath = opts.allowResetArchiveFallback === true - ? await findExistingTranscriptHistoryPathAsync(sessionId, storePath, sessionFile) - : findExistingTranscriptPath(sessionId, storePath, sessionFile); + ? await findExistingTranscriptHistoryPathAsync(sessionId, storePath, sessionFile, agentId) + : findExistingTranscriptPath(sessionId, storePath, sessionFile, agentId); if (!filePath) { return { messages: [] }; } @@ -630,12 +643,17 @@ export async function readSessionMessageByIdAsync( storePath: string | undefined, sessionFile: string | undefined, messageId: string, - opts?: { allowResetArchiveFallback?: boolean }, + opts?: { allowResetArchiveFallback?: boolean; agentId?: string }, ): Promise<{ message?: unknown; seq?: number; oversized: boolean; found: boolean }> { const filePath = opts?.allowResetArchiveFallback === true - ? await findExistingTranscriptHistoryPathAsync(sessionId, storePath, sessionFile) - : findExistingTranscriptPath(sessionId, storePath, sessionFile); + ? await findExistingTranscriptHistoryPathAsync( + sessionId, + storePath, + sessionFile, + opts.agentId, + ) + : findExistingTranscriptPath(sessionId, storePath, sessionFile, opts?.agentId); if (!filePath) { return { oversized: false, found: false }; } @@ -660,8 +678,9 @@ export async function visitSessionMessagesAsync( sessionFile: string | undefined, visit: (message: unknown, seq: number) => void, opts: { mode: "full"; reason: string; cache?: "reuse" | "skip" }, + agentId?: string, ): Promise { - const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile); + const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile, agentId); if (!filePath) { return 0; } @@ -682,8 +701,9 @@ export async function readSessionMessageCountAsync( sessionId: string, storePath: string | undefined, sessionFile?: string, + agentId?: string, ): Promise { - const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile); + const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile, agentId); if (!filePath) { return 0; } @@ -710,9 +730,10 @@ export function readRecentSessionMessagesWithStats( storePath: string | undefined, sessionFile: string | undefined, opts: ReadRecentSessionMessagesOptions, + agentId?: string, ): ReadRecentSessionMessagesResult { - const totalMessages = readSessionMessageCount(sessionId, storePath, sessionFile); - const messages = readRecentSessionMessages(sessionId, storePath, sessionFile, opts); + const totalMessages = readSessionMessageCount(sessionId, storePath, sessionFile, agentId); + const messages = readRecentSessionMessages(sessionId, storePath, sessionFile, opts, agentId); const firstSeq = Math.max(1, totalMessages - messages.length + 1); const messagesWithSeq = messages.map((message, index) => attachOpenClawTranscriptMeta(message, { seq: firstSeq + index }), @@ -725,12 +746,14 @@ export async function readRecentSessionMessagesAsync( storePath: string | undefined, sessionFile?: string, opts?: ReadRecentSessionMessagesOptions, + agentId?: string, ): Promise { const result = await readRecentSessionMessagesWithSourceAsync( sessionId, storePath, sessionFile, opts, + agentId, ); return result.messages; } @@ -740,6 +763,7 @@ async function readRecentSessionMessagesWithSourceAsync( storePath: string | undefined, sessionFile?: string, opts?: ReadRecentSessionMessagesOptions, + agentId?: string, ): Promise { const normalized = normalizeRecentSessionReadOptions(opts); const { maxMessages } = normalized; @@ -749,8 +773,8 @@ async function readRecentSessionMessagesWithSourceAsync( const filePath = opts?.allowResetArchiveFallback === true - ? await findExistingTranscriptHistoryPathAsync(sessionId, storePath, sessionFile) - : findExistingTranscriptPath(sessionId, storePath, sessionFile); + ? await findExistingTranscriptHistoryPathAsync(sessionId, storePath, sessionFile, agentId) + : findExistingTranscriptPath(sessionId, storePath, sessionFile, agentId); if (!filePath) { return { messages: [] }; } @@ -786,11 +810,12 @@ export async function readRecentSessionMessagesWithStatsAsync( storePath: string | undefined, sessionFile: string | undefined, opts: ReadRecentSessionMessagesOptions, + agentId?: string, ): Promise { const filePath = opts.allowResetArchiveFallback === true - ? await findExistingTranscriptHistoryPathAsync(sessionId, storePath, sessionFile) - : findExistingTranscriptPath(sessionId, storePath, sessionFile); + ? await findExistingTranscriptHistoryPathAsync(sessionId, storePath, sessionFile, agentId) + : findExistingTranscriptPath(sessionId, storePath, sessionFile, agentId); if (!filePath) { return { messages: [], totalMessages: 0 }; } diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 0517744e6f84..fcc7de8b023f 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -98,10 +98,10 @@ import { resolveStoredSessionKeyForAgentStore, } from "./session-store-key.js"; import { - readRecentSessionUsageFromTranscript, - readSessionTitleFieldsFromTranscriptAsync, - readSessionTitleFieldsFromTranscript, -} from "./session-utils.fs.js"; + readRecentSessionUsageFromTranscript as readScopedRecentSessionUsageFromTranscript, + readSessionTitleFieldsFromTranscriptAsync as readScopedSessionTitleFieldsFromTranscriptAsync, + readSessionTitleFieldsFromTranscript as readScopedSessionTitleFieldsFromTranscript, +} from "./session-transcript-readers.js"; import type { GatewayAgentRow, GatewaySessionRow, @@ -135,6 +135,7 @@ export { resolveSessionTranscriptCandidates, } from "./session-utils.fs.js"; export type { ReadSessionMessagesAsyncOptions } from "./session-utils.fs.js"; +export type { SessionTranscriptReadScope } from "./session-transcript-readers.js"; export { canonicalizeSpawnedByForAgent, resolveSessionStoreKey } from "./session-store-key.js"; export type { GatewayAgentRow, @@ -877,11 +878,13 @@ function resolveTranscriptUsageFallback(params: { const agentId = parsed?.agentId ? normalizeAgentId(parsed.agentId) : normalizeAgentId(params.agentId ?? resolveDefaultAgentId(params.cfg)); - const snapshot = readRecentSessionUsageFromTranscript( - entry.sessionId, - params.storePath, - entry.sessionFile, - agentId, + const snapshot = readScopedRecentSessionUsageFromTranscript( + { + agentId, + sessionFile: entry.sessionFile, + sessionId: entry.sessionId, + storePath: params.storePath, + }, typeof params.maxTranscriptBytes === "number" ? params.maxTranscriptBytes : 256 * 1024, ); if (!snapshot) { @@ -2188,12 +2191,12 @@ export function buildGatewaySessionRow(params: { let derivedTitle: string | undefined; let lastMessagePreview: string | undefined; if (entry?.sessionId && (params.includeDerivedTitles || params.includeLastMessage)) { - const fields = readSessionTitleFieldsFromTranscript( - entry.sessionId, + const fields = readScopedSessionTitleFieldsFromTranscript({ + agentId: sessionAgentId, + sessionFile: entry.sessionFile, + sessionId: entry.sessionId, storePath, - entry.sessionFile, - sessionAgentId, - ); + }); if (params.includeDerivedTitles) { derivedTitle = deriveSessionTitle(entry, fields.firstUserMessage); } @@ -2887,12 +2890,12 @@ export async function listSessionsFromStoreAsync(params: { const sessionAgentId = rowAgentId ?? (parsed?.agentId ? normalizeAgentId(parsed.agentId) : resolveDefaultAgentId(cfg)); - const fields = await readSessionTitleFieldsFromTranscriptAsync( - entry.sessionId, + const fields = await readScopedSessionTitleFieldsFromTranscriptAsync({ + agentId: sessionAgentId, + sessionFile: entry.sessionFile, + sessionId: entry.sessionId, storePath, - entry.sessionFile, - sessionAgentId, - ); + }); if (includeDerivedTitles) { row.derivedTitle = deriveSessionTitle(entry, fields.firstUserMessage); }