192 lines
5.6 KiB
TypeScript
192 lines
5.6 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
import { resolveSessionTranscriptPath } from "../config/sessions.js";
|
|
|
|
export function readSessionMessages(
|
|
sessionId: string,
|
|
storePath: string | undefined,
|
|
sessionFile?: string,
|
|
): unknown[] {
|
|
const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile);
|
|
|
|
const filePath = candidates.find((p) => fs.existsSync(p));
|
|
if (!filePath) return [];
|
|
|
|
const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
|
|
const messages: unknown[] = [];
|
|
for (const line of lines) {
|
|
if (!line.trim()) continue;
|
|
try {
|
|
const parsed = JSON.parse(line);
|
|
if (parsed?.message) {
|
|
messages.push(parsed.message);
|
|
}
|
|
} catch {
|
|
// ignore bad lines
|
|
}
|
|
}
|
|
return messages;
|
|
}
|
|
|
|
export function resolveSessionTranscriptCandidates(
|
|
sessionId: string,
|
|
storePath: string | undefined,
|
|
sessionFile?: string,
|
|
agentId?: string,
|
|
): string[] {
|
|
const candidates: string[] = [];
|
|
if (sessionFile) candidates.push(sessionFile);
|
|
if (storePath) {
|
|
const dir = path.dirname(storePath);
|
|
candidates.push(path.join(dir, `${sessionId}.jsonl`));
|
|
}
|
|
if (agentId) {
|
|
candidates.push(resolveSessionTranscriptPath(sessionId, agentId));
|
|
}
|
|
candidates.push(path.join(os.homedir(), ".clawdbot", "sessions", `${sessionId}.jsonl`));
|
|
return candidates;
|
|
}
|
|
|
|
export function archiveFileOnDisk(filePath: string, reason: string): string {
|
|
const ts = new Date().toISOString().replaceAll(":", "-");
|
|
const archived = `${filePath}.${reason}.${ts}`;
|
|
fs.renameSync(filePath, archived);
|
|
return archived;
|
|
}
|
|
|
|
function jsonUtf8Bytes(value: unknown): number {
|
|
try {
|
|
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
} catch {
|
|
return Buffer.byteLength(String(value), "utf8");
|
|
}
|
|
}
|
|
|
|
export function capArrayByJsonBytes<T>(
|
|
items: T[],
|
|
maxBytes: number,
|
|
): { items: T[]; bytes: number } {
|
|
if (items.length === 0) return { items, bytes: 2 };
|
|
const parts = items.map((item) => jsonUtf8Bytes(item));
|
|
let bytes = 2 + parts.reduce((a, b) => a + b, 0) + (items.length - 1);
|
|
let start = 0;
|
|
while (bytes > maxBytes && start < items.length - 1) {
|
|
bytes -= parts[start] + 1;
|
|
start += 1;
|
|
}
|
|
const next = start > 0 ? items.slice(start) : items;
|
|
return { items: next, bytes };
|
|
}
|
|
|
|
const MAX_LINES_TO_SCAN = 10;
|
|
|
|
type TranscriptMessage = {
|
|
role?: string;
|
|
content?: string | Array<{ type: string; text?: string }>;
|
|
};
|
|
|
|
function extractTextFromContent(content: TranscriptMessage["content"]): string | null {
|
|
if (typeof content === "string") return content.trim() || null;
|
|
if (!Array.isArray(content)) return null;
|
|
for (const part of content) {
|
|
if (!part || typeof part.text !== "string") continue;
|
|
if (part.type === "text" || part.type === "output_text" || part.type === "input_text") {
|
|
const trimmed = part.text.trim();
|
|
if (trimmed) return trimmed;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function readFirstUserMessageFromTranscript(
|
|
sessionId: string,
|
|
storePath: string | undefined,
|
|
sessionFile?: string,
|
|
agentId?: string,
|
|
): string | null {
|
|
const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId);
|
|
const filePath = candidates.find((p) => fs.existsSync(p));
|
|
if (!filePath) return null;
|
|
|
|
let fd: number | null = null;
|
|
try {
|
|
fd = fs.openSync(filePath, "r");
|
|
const buf = Buffer.alloc(8192);
|
|
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
if (bytesRead === 0) return null;
|
|
const chunk = buf.toString("utf-8", 0, bytesRead);
|
|
const lines = chunk.split(/\r?\n/).slice(0, MAX_LINES_TO_SCAN);
|
|
|
|
for (const line of lines) {
|
|
if (!line.trim()) continue;
|
|
try {
|
|
const parsed = JSON.parse(line);
|
|
const msg = parsed?.message as TranscriptMessage | undefined;
|
|
if (msg?.role === "user") {
|
|
const text = extractTextFromContent(msg.content);
|
|
if (text) return text;
|
|
}
|
|
} catch {
|
|
// skip malformed lines
|
|
}
|
|
}
|
|
} catch {
|
|
// file read error
|
|
} finally {
|
|
if (fd !== null) fs.closeSync(fd);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const LAST_MSG_MAX_BYTES = 16384;
|
|
const LAST_MSG_MAX_LINES = 20;
|
|
|
|
export function readLastMessagePreviewFromTranscript(
|
|
sessionId: string,
|
|
storePath: string | undefined,
|
|
sessionFile?: string,
|
|
agentId?: string,
|
|
): string | null {
|
|
const candidates = resolveSessionTranscriptCandidates(sessionId, storePath, sessionFile, agentId);
|
|
const filePath = candidates.find((p) => fs.existsSync(p));
|
|
if (!filePath) return null;
|
|
|
|
let fd: number | null = null;
|
|
try {
|
|
fd = fs.openSync(filePath, "r");
|
|
const stat = fs.fstatSync(fd);
|
|
const size = stat.size;
|
|
if (size === 0) return null;
|
|
|
|
const readStart = Math.max(0, size - LAST_MSG_MAX_BYTES);
|
|
const readLen = Math.min(size, LAST_MSG_MAX_BYTES);
|
|
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(-LAST_MSG_MAX_LINES);
|
|
|
|
for (let i = tailLines.length - 1; i >= 0; i--) {
|
|
const line = tailLines[i];
|
|
try {
|
|
const parsed = JSON.parse(line);
|
|
const msg = parsed?.message as TranscriptMessage | undefined;
|
|
if (msg?.role === "user" || msg?.role === "assistant") {
|
|
const text = extractTextFromContent(msg.content);
|
|
if (text) return text;
|
|
}
|
|
} catch {
|
|
// skip malformed
|
|
}
|
|
}
|
|
} catch {
|
|
// file error
|
|
} finally {
|
|
if (fd !== null) fs.closeSync(fd);
|
|
}
|
|
return null;
|
|
}
|