/** * Session transcript — append-only JSONL file in the workspace. * * Each line is one AI SDK {@link ModelMessage}, JSON-encoded. The transcript lives at * `workspaceDir/.cph/sessions/.jsonl`, following the same "workspace * is a directory tree" principle as the engineering file itself (ADR-0007). * * This mirrors how Claude Code stores its transcript (a `.jsonl` file per * session). The agent can read its own history via `read_file` — no special * "load history" code path. ADR-0002's lock guarantees one run per project at a * time, so the transcript is never concurrently written. * * ADR-0003: this stores **agent session messages** (prompt + response + tool * calls produced by the Hub), NOT Feishu group chat history. The two are * distinct; storing session messages does not conflict with "the Hub avoids * becoming a full chat history store." */ import { readFile, mkdir, appendFile } from "node:fs/promises"; import { join, dirname } from "node:path"; import type { ModelMessage } from "ai"; /// Subdirectory within a workspace where session transcripts live. export const TRANSCRIPT_DIR = ".cph/sessions"; /** Path for a session's transcript file within a workspace. */ export function transcriptPath(workspaceDir: string, sessionId: string): string { return join(workspaceDir, TRANSCRIPT_DIR, `${sessionId}.jsonl`); } /** * Read prior messages from a transcript file. Returns an empty array if the * file doesn't exist yet (first run in this session). Corrupt lines are * skipped — a partially-written last line (crash mid-append) won't break the * next run. */ export async function readTranscript(path: string): Promise { let content: string; try { content = await readFile(path, "utf8"); } catch { return []; } const messages: ModelMessage[] = []; for (const line of content.split("\n")) { const trimmed = line.trim(); if (trimmed === "") continue; try { messages.push(JSON.parse(trimmed) as ModelMessage); } catch { // Skip corrupt line — likely a partial write from a crash. } } return messages; } /** * Append messages to a transcript file. Creates the parent directory if needed. * Each message is written as one JSON line. */ export async function appendTranscript(path: string, messages: readonly ModelMessage[]): Promise { await mkdir(dirname(path), { recursive: true }); const lines = messages.map((m) => JSON.stringify(m)).join("\n") + "\n"; await appendFile(path, lines, "utf8"); }