feat: add sessions preview rpc and menu prewarm
This commit is contained in:
@@ -140,6 +140,8 @@ import {
|
||||
SessionsListParamsSchema,
|
||||
type SessionsPatchParams,
|
||||
SessionsPatchParamsSchema,
|
||||
type SessionsPreviewParams,
|
||||
SessionsPreviewParamsSchema,
|
||||
type SessionsResetParams,
|
||||
SessionsResetParamsSchema,
|
||||
type SessionsResolveParams,
|
||||
@@ -229,6 +231,9 @@ export const validateNodeInvokeResultParams = ajv.compile<NodeInvokeResultParams
|
||||
);
|
||||
export const validateNodeEventParams = ajv.compile<NodeEventParams>(NodeEventParamsSchema);
|
||||
export const validateSessionsListParams = ajv.compile<SessionsListParams>(SessionsListParamsSchema);
|
||||
export const validateSessionsPreviewParams = ajv.compile<SessionsPreviewParams>(
|
||||
SessionsPreviewParamsSchema,
|
||||
);
|
||||
export const validateSessionsResolveParams = ajv.compile<SessionsResolveParams>(
|
||||
SessionsResolveParamsSchema,
|
||||
);
|
||||
@@ -376,6 +381,7 @@ export {
|
||||
NodeListParamsSchema,
|
||||
NodeInvokeParamsSchema,
|
||||
SessionsListParamsSchema,
|
||||
SessionsPreviewParamsSchema,
|
||||
SessionsPatchParamsSchema,
|
||||
SessionsResetParamsSchema,
|
||||
SessionsDeleteParamsSchema,
|
||||
@@ -488,6 +494,7 @@ export type {
|
||||
NodeInvokeResultParams,
|
||||
NodeEventParams,
|
||||
SessionsListParams,
|
||||
SessionsPreviewParams,
|
||||
SessionsResolveParams,
|
||||
SessionsPatchParams,
|
||||
SessionsResetParams,
|
||||
|
||||
@@ -108,6 +108,7 @@ import {
|
||||
SessionsDeleteParamsSchema,
|
||||
SessionsListParamsSchema,
|
||||
SessionsPatchParamsSchema,
|
||||
SessionsPreviewParamsSchema,
|
||||
SessionsResetParamsSchema,
|
||||
SessionsResolveParamsSchema,
|
||||
} from "./sessions.js";
|
||||
@@ -155,6 +156,7 @@ export const ProtocolSchemas: Record<string, TSchema> = {
|
||||
NodeEventParams: NodeEventParamsSchema,
|
||||
NodeInvokeRequestEvent: NodeInvokeRequestEventSchema,
|
||||
SessionsListParams: SessionsListParamsSchema,
|
||||
SessionsPreviewParams: SessionsPreviewParamsSchema,
|
||||
SessionsResolveParams: SessionsResolveParamsSchema,
|
||||
SessionsPatchParams: SessionsPatchParamsSchema,
|
||||
SessionsResetParams: SessionsResetParamsSchema,
|
||||
|
||||
@@ -26,6 +26,15 @@ export const SessionsListParamsSchema = Type.Object(
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const SessionsPreviewParamsSchema = Type.Object(
|
||||
{
|
||||
keys: Type.Array(NonEmptyString, { minItems: 1 }),
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
maxChars: Type.Optional(Type.Integer({ minimum: 20 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const SessionsResolveParamsSchema = Type.Object(
|
||||
{
|
||||
key: Type.Optional(NonEmptyString),
|
||||
|
||||
@@ -101,6 +101,7 @@ import type {
|
||||
SessionsDeleteParamsSchema,
|
||||
SessionsListParamsSchema,
|
||||
SessionsPatchParamsSchema,
|
||||
SessionsPreviewParamsSchema,
|
||||
SessionsResetParamsSchema,
|
||||
SessionsResolveParamsSchema,
|
||||
} from "./sessions.js";
|
||||
@@ -144,6 +145,7 @@ export type NodeInvokeParams = Static<typeof NodeInvokeParamsSchema>;
|
||||
export type NodeInvokeResultParams = Static<typeof NodeInvokeResultParamsSchema>;
|
||||
export type NodeEventParams = Static<typeof NodeEventParamsSchema>;
|
||||
export type SessionsListParams = Static<typeof SessionsListParamsSchema>;
|
||||
export type SessionsPreviewParams = Static<typeof SessionsPreviewParamsSchema>;
|
||||
export type SessionsResolveParams = Static<typeof SessionsResolveParamsSchema>;
|
||||
export type SessionsPatchParams = Static<typeof SessionsPatchParamsSchema>;
|
||||
export type SessionsResetParams = Static<typeof SessionsResetParamsSchema>;
|
||||
|
||||
@@ -34,6 +34,7 @@ const BASE_METHODS = [
|
||||
"voicewake.get",
|
||||
"voicewake.set",
|
||||
"sessions.list",
|
||||
"sessions.preview",
|
||||
"sessions.patch",
|
||||
"sessions.reset",
|
||||
"sessions.delete",
|
||||
|
||||
@@ -59,6 +59,7 @@ const READ_METHODS = new Set([
|
||||
"skills.status",
|
||||
"voicewake.get",
|
||||
"sessions.list",
|
||||
"sessions.preview",
|
||||
"cron.list",
|
||||
"cron.status",
|
||||
"cron.runs",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { stopSubagentsForRequester } from "../../auto-reply/reply/abort.js";
|
||||
import { clearSessionQueues } from "../../auto-reply/reply/queue.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
snapshotSessionOrigin,
|
||||
resolveMainSessionKey,
|
||||
type SessionEntry,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
validateSessionsDeleteParams,
|
||||
validateSessionsListParams,
|
||||
validateSessionsPatchParams,
|
||||
validateSessionsPreviewParams,
|
||||
validateSessionsResetParams,
|
||||
validateSessionsResolveParams,
|
||||
} from "../protocol/index.js";
|
||||
@@ -27,9 +29,12 @@ import {
|
||||
listSessionsFromStore,
|
||||
loadCombinedSessionStoreForGateway,
|
||||
loadSessionEntry,
|
||||
readSessionPreviewItemsFromTranscript,
|
||||
resolveGatewaySessionStoreTarget,
|
||||
resolveSessionTranscriptCandidates,
|
||||
type SessionsPatchResult,
|
||||
type SessionsPreviewEntry,
|
||||
type SessionsPreviewResult,
|
||||
} from "../session-utils.js";
|
||||
import { applySessionsPatchToStore } from "../sessions-patch.js";
|
||||
import { resolveSessionKeyFromResolveParams } from "../sessions-resolve.js";
|
||||
@@ -59,6 +64,74 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
});
|
||||
respond(true, result, undefined);
|
||||
},
|
||||
"sessions.preview": ({ params, respond }) => {
|
||||
if (!validateSessionsPreviewParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid sessions.preview params: ${formatValidationErrors(
|
||||
validateSessionsPreviewParams.errors,
|
||||
)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const p = params as import("../protocol/index.js").SessionsPreviewParams;
|
||||
const keysRaw = Array.isArray(p.keys) ? p.keys : [];
|
||||
const keys = keysRaw
|
||||
.map((key) => String(key ?? "").trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 64);
|
||||
const limit =
|
||||
typeof p.limit === "number" && Number.isFinite(p.limit) ? Math.max(1, p.limit) : 12;
|
||||
const maxChars =
|
||||
typeof p.maxChars === "number" && Number.isFinite(p.maxChars)
|
||||
? Math.max(20, p.maxChars)
|
||||
: 240;
|
||||
|
||||
if (keys.length === 0) {
|
||||
respond(true, { ts: Date.now(), previews: [] } satisfies SessionsPreviewResult, undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const cfg = loadConfig();
|
||||
const storeCache = new Map<string, Record<string, SessionEntry>>();
|
||||
const previews: SessionsPreviewEntry[] = [];
|
||||
|
||||
for (const key of keys) {
|
||||
try {
|
||||
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
||||
const store = storeCache.get(target.storePath) ?? loadSessionStore(target.storePath);
|
||||
storeCache.set(target.storePath, store);
|
||||
const entry =
|
||||
target.storeKeys.map((candidate) => store[candidate]).find(Boolean) ??
|
||||
store[target.canonicalKey];
|
||||
if (!entry?.sessionId) {
|
||||
previews.push({ key, status: "missing", items: [] });
|
||||
continue;
|
||||
}
|
||||
const items = readSessionPreviewItemsFromTranscript(
|
||||
entry.sessionId,
|
||||
target.storePath,
|
||||
entry.sessionFile,
|
||||
target.agentId,
|
||||
limit,
|
||||
maxChars,
|
||||
);
|
||||
previews.push({
|
||||
key,
|
||||
status: items.length > 0 ? "ok" : "empty",
|
||||
items,
|
||||
});
|
||||
} catch {
|
||||
previews.push({ key, status: "error", items: [] });
|
||||
}
|
||||
}
|
||||
|
||||
respond(true, { ts: Date.now(), previews } satisfies SessionsPreviewResult, undefined);
|
||||
},
|
||||
"sessions.resolve": ({ params, respond }) => {
|
||||
if (!validateSessionsResolveParams(params)) {
|
||||
respond(
|
||||
|
||||
@@ -103,6 +103,7 @@ describe("gateway server sessions", () => {
|
||||
expect((hello as unknown as { features?: { methods?: string[] } }).features?.methods).toEqual(
|
||||
expect.arrayContaining([
|
||||
"sessions.list",
|
||||
"sessions.preview",
|
||||
"sessions.patch",
|
||||
"sessions.reset",
|
||||
"sessions.delete",
|
||||
@@ -338,6 +339,53 @@ describe("gateway server sessions", () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("sessions.preview returns transcript previews", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-preview-"));
|
||||
const storePath = path.join(dir, "sessions.json");
|
||||
testState.sessionStorePath = storePath;
|
||||
const sessionId = "sess-preview";
|
||||
const transcriptPath = path.join(dir, `${sessionId}.jsonl`);
|
||||
const lines = [
|
||||
JSON.stringify({ type: "session", version: 1, id: sessionId }),
|
||||
JSON.stringify({ message: { role: "user", content: "Hello" } }),
|
||||
JSON.stringify({ message: { role: "assistant", content: "Hi" } }),
|
||||
JSON.stringify({
|
||||
message: { role: "assistant", content: [{ type: "toolcall", name: "weather" }] },
|
||||
}),
|
||||
JSON.stringify({ message: { role: "assistant", content: "Forecast ready" } }),
|
||||
];
|
||||
await fs.writeFile(transcriptPath, lines.join("\n"), "utf-8");
|
||||
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
const preview = await rpcReq<{
|
||||
previews: Array<{
|
||||
key: string;
|
||||
status: string;
|
||||
items: Array<{ role: string; text: string }>;
|
||||
}>;
|
||||
}>(ws, "sessions.preview", { keys: ["main"], limit: 3, maxChars: 120 });
|
||||
|
||||
expect(preview.ok).toBe(true);
|
||||
const entry = preview.payload?.previews[0];
|
||||
expect(entry?.key).toBe("main");
|
||||
expect(entry?.status).toBe("ok");
|
||||
expect(entry?.items.map((item) => item.role)).toEqual(["assistant", "tool", "assistant"]);
|
||||
expect(entry?.items[1]?.text).toContain("call weather");
|
||||
|
||||
ws.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("sessions.delete rejects main and aborts active runs", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-"));
|
||||
const storePath = path.join(dir, "sessions.json");
|
||||
|
||||
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import {
|
||||
readFirstUserMessageFromTranscript,
|
||||
readLastMessagePreviewFromTranscript,
|
||||
readSessionPreviewItemsFromTranscript,
|
||||
} from "./session-utils.fs.js";
|
||||
|
||||
describe("readFirstUserMessageFromTranscript", () => {
|
||||
@@ -341,3 +342,65 @@ describe("readLastMessagePreviewFromTranscript", () => {
|
||||
expect(result).toBe("Valid UTF-8: 你好世界 🌍");
|
||||
});
|
||||
});
|
||||
|
||||
describe("readSessionPreviewItemsFromTranscript", () => {
|
||||
let tmpDir: string;
|
||||
let storePath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-session-preview-test-"));
|
||||
storePath = path.join(tmpDir, "sessions.json");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("returns recent preview items with tool summary", () => {
|
||||
const sessionId = "preview-session";
|
||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||
const lines = [
|
||||
JSON.stringify({ type: "session", version: 1, id: sessionId }),
|
||||
JSON.stringify({ message: { role: "user", content: "Hello" } }),
|
||||
JSON.stringify({ message: { role: "assistant", content: "Hi" } }),
|
||||
JSON.stringify({
|
||||
message: { role: "assistant", content: [{ type: "toolcall", name: "weather" }] },
|
||||
}),
|
||||
JSON.stringify({ message: { role: "assistant", content: "Forecast ready" } }),
|
||||
];
|
||||
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
|
||||
|
||||
const result = readSessionPreviewItemsFromTranscript(
|
||||
sessionId,
|
||||
storePath,
|
||||
undefined,
|
||||
undefined,
|
||||
3,
|
||||
120,
|
||||
);
|
||||
|
||||
expect(result.map((item) => item.role)).toEqual(["assistant", "tool", "assistant"]);
|
||||
expect(result[1]?.text).toContain("call weather");
|
||||
});
|
||||
|
||||
test("truncates preview text to max chars", () => {
|
||||
const sessionId = "preview-truncate";
|
||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||
const longText = "a".repeat(60);
|
||||
const lines = [JSON.stringify({ message: { role: "assistant", content: longText } })];
|
||||
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
|
||||
|
||||
const result = readSessionPreviewItemsFromTranscript(
|
||||
sessionId,
|
||||
storePath,
|
||||
undefined,
|
||||
undefined,
|
||||
1,
|
||||
24,
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]?.text.length).toBe(24);
|
||||
expect(result[0]?.text.endsWith("...")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { resolveSessionTranscriptPath } from "../config/sessions.js";
|
||||
import { stripEnvelope } from "./chat-sanitize.js";
|
||||
import type { SessionPreviewItem } from "./session-utils.types.js";
|
||||
|
||||
export function readSessionMessages(
|
||||
sessionId: string,
|
||||
@@ -189,3 +191,202 @@ export function readLastMessagePreviewFromTranscript(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const PREVIEW_READ_SIZES = [64 * 1024, 256 * 1024, 1024 * 1024];
|
||||
const PREVIEW_MAX_LINES = 200;
|
||||
|
||||
type TranscriptContentEntry = {
|
||||
type?: string;
|
||||
text?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
type TranscriptPreviewMessage = {
|
||||
role?: string;
|
||||
content?: string | TranscriptContentEntry[];
|
||||
text?: string;
|
||||
toolName?: string;
|
||||
tool_name?: string;
|
||||
};
|
||||
|
||||
function normalizeRole(role: string | undefined, isTool: boolean): SessionPreviewItem["role"] {
|
||||
if (isTool) return "tool";
|
||||
switch ((role ?? "").toLowerCase()) {
|
||||
case "user":
|
||||
return "user";
|
||||
case "assistant":
|
||||
return "assistant";
|
||||
case "system":
|
||||
return "system";
|
||||
case "tool":
|
||||
return "tool";
|
||||
default:
|
||||
return "other";
|
||||
}
|
||||
}
|
||||
|
||||
function truncatePreviewText(text: string, maxChars: number): string {
|
||||
if (maxChars <= 0 || text.length <= maxChars) return text;
|
||||
if (maxChars <= 3) return text.slice(0, maxChars);
|
||||
return `${text.slice(0, maxChars - 3)}...`;
|
||||
}
|
||||
|
||||
function extractPreviewText(message: TranscriptPreviewMessage): string | null {
|
||||
if (typeof message.content === "string") {
|
||||
const trimmed = message.content.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
if (Array.isArray(message.content)) {
|
||||
const parts = message.content
|
||||
.map((entry) => (typeof entry?.text === "string" ? entry.text : ""))
|
||||
.filter((text) => text.trim().length > 0);
|
||||
if (parts.length > 0) {
|
||||
return parts.join("\n").trim();
|
||||
}
|
||||
}
|
||||
if (typeof message.text === "string") {
|
||||
const trimmed = message.text.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isToolCall(message: TranscriptPreviewMessage): boolean {
|
||||
if (message.toolName || message.tool_name) return true;
|
||||
if (!Array.isArray(message.content)) return false;
|
||||
return message.content.some((entry) => {
|
||||
if (entry?.name) return true;
|
||||
const raw = typeof entry?.type === "string" ? entry.type.toLowerCase() : "";
|
||||
return raw === "toolcall" || raw === "tool_call";
|
||||
});
|
||||
}
|
||||
|
||||
function extractToolNames(message: TranscriptPreviewMessage): string[] {
|
||||
const names: string[] = [];
|
||||
if (Array.isArray(message.content)) {
|
||||
for (const entry of message.content) {
|
||||
if (typeof entry?.name === "string" && entry.name.trim()) {
|
||||
names.push(entry.name.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
const toolName = typeof message.toolName === "string" ? message.toolName : message.tool_name;
|
||||
if (typeof toolName === "string" && toolName.trim()) {
|
||||
names.push(toolName.trim());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function extractMediaSummary(message: TranscriptPreviewMessage): string | null {
|
||||
if (!Array.isArray(message.content)) return null;
|
||||
for (const entry of message.content) {
|
||||
const raw = typeof entry?.type === "string" ? entry.type.trim().toLowerCase() : "";
|
||||
if (!raw || raw === "text" || raw === "toolcall" || raw === "tool_call") continue;
|
||||
return `[${raw}]`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildPreviewItems(
|
||||
messages: TranscriptPreviewMessage[],
|
||||
maxItems: number,
|
||||
maxChars: number,
|
||||
): SessionPreviewItem[] {
|
||||
const items: SessionPreviewItem[] = [];
|
||||
for (const message of messages) {
|
||||
const toolCall = isToolCall(message);
|
||||
const role = normalizeRole(message.role, toolCall);
|
||||
let text = extractPreviewText(message);
|
||||
if (!text) {
|
||||
const toolNames = extractToolNames(message);
|
||||
if (toolNames.length > 0) {
|
||||
const shown = toolNames.slice(0, 2);
|
||||
const overflow = toolNames.length - shown.length;
|
||||
text = `call ${shown.join(", ")}`;
|
||||
if (overflow > 0) text += ` +${overflow}`;
|
||||
}
|
||||
}
|
||||
if (!text) {
|
||||
text = extractMediaSummary(message);
|
||||
}
|
||||
if (!text) continue;
|
||||
let trimmed = text.trim();
|
||||
if (!trimmed) continue;
|
||||
if (role === "user") {
|
||||
trimmed = stripEnvelope(trimmed);
|
||||
}
|
||||
trimmed = truncatePreviewText(trimmed, maxChars);
|
||||
items.push({ role, text: trimmed });
|
||||
}
|
||||
|
||||
if (items.length <= maxItems) return items;
|
||||
return items.slice(-maxItems);
|
||||
}
|
||||
|
||||
function readRecentMessagesFromTranscript(
|
||||
filePath: string,
|
||||
maxMessages: number,
|
||||
readBytes: number,
|
||||
): TranscriptPreviewMessage[] {
|
||||
let fd: number | null = null;
|
||||
try {
|
||||
fd = fs.openSync(filePath, "r");
|
||||
const stat = fs.fstatSync(fd);
|
||||
const size = stat.size;
|
||||
if (size === 0) return [];
|
||||
|
||||
const readStart = Math.max(0, size - readBytes);
|
||||
const readLen = Math.min(size, readBytes);
|
||||
const buf = Buffer.alloc(readLen);
|
||||
fs.readSync(fd, buf, 0, readLen, readStart);
|
||||
|
||||
const chunk = buf.toString("utf-8");
|
||||
const lines = chunk.split(/\r?\n/).filter((l) => l.trim());
|
||||
const tailLines = lines.slice(-PREVIEW_MAX_LINES);
|
||||
|
||||
const collected: TranscriptPreviewMessage[] = [];
|
||||
for (let i = tailLines.length - 1; i >= 0; i--) {
|
||||
const line = tailLines[i];
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
const msg = parsed?.message as TranscriptPreviewMessage | undefined;
|
||||
if (msg && typeof msg === "object") {
|
||||
collected.push(msg);
|
||||
if (collected.length >= maxMessages) break;
|
||||
}
|
||||
} catch {
|
||||
// skip malformed lines
|
||||
}
|
||||
}
|
||||
return collected.reverse();
|
||||
} catch {
|
||||
return [];
|
||||
} finally {
|
||||
if (fd !== null) fs.closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
export function readSessionPreviewItemsFromTranscript(
|
||||
sessionId: string,
|
||||
storePath: string | undefined,
|
||||
sessionFile: string | undefined,
|
||||
agentId: string | undefined,
|
||||
maxItems: number,
|
||||
maxChars: number,
|
||||
): SessionPreviewItem[] {
|
||||
const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId);
|
||||
const filePath = candidates.find((p) => fs.existsSync(p));
|
||||
if (!filePath) return [];
|
||||
|
||||
const boundedItems = Math.max(1, Math.min(maxItems, 50));
|
||||
const boundedChars = Math.max(20, Math.min(maxChars, 2000));
|
||||
|
||||
for (const readSize of PREVIEW_READ_SIZES) {
|
||||
const messages = readRecentMessagesFromTranscript(filePath, boundedItems, readSize);
|
||||
if (messages.length > 0 || readSize === PREVIEW_READ_SIZES[PREVIEW_READ_SIZES.length - 1]) {
|
||||
return buildPreviewItems(messages, boundedItems, boundedChars);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ export {
|
||||
capArrayByJsonBytes,
|
||||
readFirstUserMessageFromTranscript,
|
||||
readLastMessagePreviewFromTranscript,
|
||||
readSessionPreviewItemsFromTranscript,
|
||||
readSessionMessages,
|
||||
resolveSessionTranscriptCandidates,
|
||||
} from "./session-utils.fs.js";
|
||||
@@ -47,6 +48,8 @@ export type {
|
||||
GatewaySessionsDefaults,
|
||||
SessionsListResult,
|
||||
SessionsPatchResult,
|
||||
SessionsPreviewEntry,
|
||||
SessionsPreviewResult,
|
||||
} from "./session-utils.types.js";
|
||||
|
||||
const DERIVED_TITLE_MAX_LEN = 60;
|
||||
|
||||
@@ -55,6 +55,22 @@ export type GatewayAgentRow = {
|
||||
};
|
||||
};
|
||||
|
||||
export type SessionPreviewItem = {
|
||||
role: "user" | "assistant" | "tool" | "system" | "other";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type SessionsPreviewEntry = {
|
||||
key: string;
|
||||
status: "ok" | "empty" | "missing" | "error";
|
||||
items: SessionPreviewItem[];
|
||||
};
|
||||
|
||||
export type SessionsPreviewResult = {
|
||||
ts: number;
|
||||
previews: SessionsPreviewEntry[];
|
||||
};
|
||||
|
||||
export type SessionsListResult = {
|
||||
ts: number;
|
||||
path: string;
|
||||
|
||||
Reference in New Issue
Block a user