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..fa9dc3d6bf06 --- /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, + 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 60286a4446b4..6759d51d523b 100644 --- a/src/gateway/session-utils.fs.ts +++ b/src/gateway/session-utils.fs.ts @@ -148,8 +148,9 @@ export function readSessionMessages( sessionId: string, storePath: string | undefined, sessionFile?: string, + agentId?: string, ): unknown[] { - const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile); + const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId); const filePath = candidates.find((p) => fs.existsSync(p)); if (!filePath) { @@ -203,13 +204,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 []; } @@ -533,8 +535,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; } @@ -550,8 +553,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; } @@ -565,7 +569,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); } @@ -577,12 +581,19 @@ export async function readSessionMessagesAsync( storePath: string | undefined, sessionFile: string | undefined, opts: ReadSessionMessagesAsyncOptions, + agentId?: string, ): Promise { if (opts.mode === "recent") { const { mode: _modeValue, ...recentOpts } = opts; - return await readRecentSessionMessagesAsync(sessionId, storePath, sessionFile, recentOpts); + return await readRecentSessionMessagesAsync( + sessionId, + storePath, + sessionFile, + recentOpts, + agentId, + ); } - const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile); + const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile, agentId); if (!filePath) { return []; } @@ -595,8 +606,9 @@ export async function readSessionMessageByIdAsync( storePath: string | undefined, sessionFile: string | undefined, messageId: string, + agentId?: string, ): Promise<{ message?: unknown; seq?: number; oversized: boolean; found: boolean }> { - const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile); + const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile, agentId); if (!filePath) { return { oversized: false, found: false }; } @@ -621,8 +633,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; } @@ -643,8 +656,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; } @@ -671,9 +685,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 }), @@ -686,6 +701,7 @@ export async function readRecentSessionMessagesAsync( storePath: string | undefined, sessionFile?: string, opts?: ReadRecentSessionMessagesOptions, + agentId?: string, ): Promise { const normalized = normalizeRecentSessionReadOptions(opts); const { maxMessages } = normalized; @@ -693,7 +709,7 @@ export async function readRecentSessionMessagesAsync( return []; } - const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile); + const filePath = findExistingTranscriptPath(sessionId, storePath, sessionFile, agentId); if (!filePath) { return []; } @@ -718,9 +734,21 @@ export async function readRecentSessionMessagesWithStatsAsync( storePath: string | undefined, sessionFile: string | undefined, opts: ReadRecentSessionMessagesOptions, + agentId?: string, ): Promise { - const totalMessages = await readSessionMessageCountAsync(sessionId, storePath, sessionFile); - const messages = await readRecentSessionMessagesAsync(sessionId, storePath, sessionFile, opts); + const totalMessages = await readSessionMessageCountAsync( + sessionId, + storePath, + sessionFile, + agentId, + ); + const messages = await readRecentSessionMessagesAsync( + 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 }), diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 7095394a70c3..2249ae95baae 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -92,10 +92,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, @@ -127,6 +127,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, @@ -865,11 +866,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) { @@ -2105,12 +2108,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); } @@ -2804,12 +2807,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); }