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( 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; }