import path from "node:path"; export const MAX_MESSAGE_TEXT_BYTES = 50_000; export type RecallRole = "user" | "assistant"; export type RecallScope = "all" | "cwd" | "session"; export interface RecallSourceContext { sessionId: string; sessionName?: string; cwd: string; } export interface MessageCandidate { entryId: string; role: RecallRole; text: string; messageTimestamp: number; source: RecallSourceContext; } export interface RecallMessageRecord { type: "recall_message"; version: 1; id: string; savedAt: string; source: { sessionId: string; entryId: string; sessionName?: string; cwd: string; messageTimestamp: number; [key: string]: unknown; }; role: RecallRole; text: string; [key: string]: unknown; } export interface RecallScopeContext { sessionId: string; cwd: string; } export function extractMessageCandidates(entries: readonly unknown[], source: RecallSourceContext): MessageCandidate[] { const candidates: MessageCandidate[] = []; for (const value of entries) { if (!isRecord(value) || value.type !== "message" || typeof value.id !== "string") continue; const message = value.message; if (!isRecord(message) || (message.role !== "user" && message.role !== "assistant")) continue; if (typeof message.timestamp !== "number" || !isValidTimestamp(message.timestamp)) { continue; } const text = textFromMessageContent(message.content); if (!text.trim() || Buffer.byteLength(text, "utf8") > MAX_MESSAGE_TEXT_BYTES) continue; candidates.push({ entryId: value.id, role: message.role, text, messageTimestamp: message.timestamp, source, }); } return candidates.reverse(); } export function filterRecallMessages( records: readonly RecallMessageRecord[], scope: RecallScope, current: RecallScopeContext, platform: NodeJS.Platform = process.platform, ): RecallMessageRecord[] { if (scope === "all") return [...records]; if (scope === "session") { return records.filter((record) => record.source.sessionId === current.sessionId); } const currentCwd = normalizeCwd(current.cwd, platform); return records.filter((record) => normalizeCwd(record.source.cwd, platform) === currentCwd); } export function normalizeCwd(value: string, platform: NodeJS.Platform | "win32"): string { if (platform === "win32") return path.win32.resolve(value).toLowerCase(); return path.resolve(value); } export function messagePreview(text: string, maximumCharacters = 80): string { const normalized = text.trim().replace(/\s+/gu, " "); const characters = [...normalized]; if (characters.length <= maximumCharacters) return normalized; if (maximumCharacters <= 1) return "…"; return `${characters.slice(0, maximumCharacters - 1).join("")}…`; } export function formatRecallQuote(record: RecallMessageRecord): string { const timestamp = new Date(record.source.messageTimestamp).toISOString(); return `\n${escapeXml(record.text)}\n\n\nThe user intentionally recalled and quoted the saved message above.\n\n`; } export function candidateIdentity(sessionId: string, entryId: string): string { return `${sessionId}\u0000${entryId}`; } function textFromMessageContent(content: unknown): string { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content .flatMap((block) => isRecord(block) && block.type === "text" && typeof block.text === "string" ? [block.text] : [], ) .join("\n"); } export function isValidTimestamp(value: number): boolean { return value >= 0 && Number.isFinite(value) && !Number.isNaN(new Date(value).getTime()); } function escapeXml(value: string): string { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); }