import * as fs from "node:fs"; import * as path from "node:path"; export interface SessionSummary { name: string; firstPrompt: string; lastUserPrompt: string; recentEdits: string[]; recentBash: string[]; lastAssistantNote: string; entryCount: number; truncated: boolean; } /** * Candidate session-dir paths for a worktree path. Learns the exact name encoding from a * known (dirName ↔ cwd) example when provided, with documented-format fallbacks and a * root scan as last resort. */ export function sessionDirsForPath( target: string, root: string, known?: { sessionDir: string; cwd: string }, ): string[] { const enc = target.replace(/\//g, "-"); const candidates = new Set([`--${enc}--`, `-${enc}-`, enc]); if (known) { const ownBase = path.basename(known.sessionDir); const ownEnc = known.cwd.replace(/\//g, "-"); const idx = ownBase.indexOf(ownEnc); if (idx >= 0) candidates.add(ownBase.slice(0, idx) + enc + ownBase.slice(idx + ownEnc.length)); } const hits: string[] = []; for (const c of candidates) { const p = path.join(root, c); if (fs.existsSync(p)) hits.push(p); } if (hits.length === 0) { try { for (const d of fs.readdirSync(root)) if (d.includes(enc)) hits.push(path.join(root, d)); } catch {} } return hits; } /** * Duck-typed summary of a session JSONL without loading it into LLM context. * Large files are read as head (~128KB) + tail (~1MB): first prompt and early session * names come from the head; recent activity from the tail. */ export function summarizeSessionJsonl(file: string): SessionSummary | null { let raw = ""; let truncated = false; try { const stat = fs.statSync(file); if (stat.size > 2_000_000) { truncated = true; const headLen = 131_072; const tailLen = 1_000_000; const fd = fs.openSync(file, "r"); const headBuf = Buffer.alloc(headLen); fs.readSync(fd, headBuf, 0, headLen, 0); const tailBuf = Buffer.alloc(tailLen); fs.readSync(fd, tailBuf, 0, tailLen, stat.size - tailLen); fs.closeSync(fd); let head = headBuf.toString("utf-8"); head = head.slice(0, head.lastIndexOf("\n") + 1); let tail = tailBuf.toString("utf-8"); tail = tail.slice(tail.indexOf("\n") + 1); raw = head + tail; } else { raw = fs.readFileSync(file, "utf-8"); } } catch { return null; } const out: SessionSummary = { name: "", firstPrompt: "", lastUserPrompt: "", recentEdits: [], recentBash: [], lastAssistantNote: "", entryCount: 0, truncated, }; for (const line of raw.split("\n")) { if (!line.trim()) continue; let e: any; try { e = JSON.parse(line); } catch { continue; } out.entryCount++; if (e?.type === "session_info" && typeof e.name === "string" && e.name) out.name = e.name; if (e?.type !== "message") continue; const m = e.message ?? {}; const blocks: any[] = Array.isArray(m.content) ? m.content : typeof m.content === "string" ? [{ type: "text", text: m.content }] : []; if (m.role === "user") { const t = blocks .filter((b) => b?.type === "text") .map((b) => b.text) .join("\n") .trim(); if (t && !t.includes("[[AGENT-COMM]]")) { out.lastUserPrompt = t; if (!out.firstPrompt) out.firstPrompt = t; } } else if (m.role === "assistant") { for (const b of blocks) { if (b?.type === "toolCall") { const a = b.arguments ?? {}; if ((b.name === "edit" || b.name === "write") && (a.file_path ?? a.path)) { out.recentEdits.push(a.file_path ?? a.path); } else if (b.name === "bash" && typeof a.command === "string") { out.recentBash.push(a.command); } } else if (b?.type === "text" && b.text?.trim()) { out.lastAssistantNote = b.text.trim(); } } } } out.recentEdits = [...new Set(out.recentEdits)].slice(-10); out.recentBash = out.recentBash.slice(-5); return out; } /** Extract recent activity from in-memory session branch entries (same duck-typing). */ export function activityFromEntries(entries: any[]): { lastUserPrompt: string; recentEdits: { tool: string; file?: string }[]; recentBash: string[]; lastAssistantNote: string; } { const out = { lastUserPrompt: "", recentEdits: [] as { tool: string; file?: string }[], recentBash: [] as string[], lastAssistantNote: "", }; for (const entry of entries ?? []) { if (entry?.type !== "message") continue; const m = entry.message ?? {}; const blocks: any[] = Array.isArray(m.content) ? m.content : []; if (m.role === "user") { const text = blocks .filter((b) => b?.type === "text") .map((b) => b.text) .join("\n") .trim(); if (text && !text.includes("[[AGENT-COMM]]")) out.lastUserPrompt = text; } else if (m.role === "assistant") { for (const b of blocks) { if (b?.type === "toolCall") { const args = b.arguments ?? {}; if (b.name === "edit" || b.name === "write") { out.recentEdits.push({ tool: b.name, file: args.file_path ?? args.path ?? args.filePath }); } else if (b.name === "bash" && typeof args.command === "string") { out.recentBash.push(args.command); } } else if (b?.type === "text" && b.text?.trim()) { out.lastAssistantNote = b.text.trim(); } } } } out.recentEdits = out.recentEdits.slice(-15); out.recentBash = out.recentBash.slice(-10); return out; }