refactor: add transcript reader seam facade

This commit is contained in:
Josh Lehman
2026-05-31 18:18:40 -07:00
parent b39023fc3c
commit 7c74ee7ae2
4 changed files with 575 additions and 34 deletions

View File

@@ -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" }]);
});
});

View File

@@ -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<unknown[]> {
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<unknown[]> {
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<ReadSessionMessageByIdResult> {
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<number> {
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<number> {
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<ReadRecentSessionMessagesResult> {
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<SessionTitleFields> {
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<SessionTranscriptUsageSnapshot | null> {
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<SessionTranscriptUsageSnapshot | null> {
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<SessionTranscriptUsageSnapshot | null> {
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<typeof readSessionPreviewItemsFromTranscriptFile> {
return readSessionPreviewItemsFromTranscriptFile(
scope.sessionId,
scope.storePath,
scope.sessionFile,
scope.agentId,
maxItems,
maxChars,
);
}

View File

@@ -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<unknown[]> {
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<number> {
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<number> {
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<unknown[]> {
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<ReadRecentSessionMessagesResult> {
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 }),

View File

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