mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
refactor: add transcript reader seam facade
This commit is contained in:
160
src/gateway/session-transcript-readers.test.ts
Normal file
160
src/gateway/session-transcript-readers.test.ts
Normal 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" }]);
|
||||
});
|
||||
});
|
||||
350
src/gateway/session-transcript-readers.ts
Normal file
350
src/gateway/session-transcript-readers.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user