/** * Session state codec. * * Implements the Paperclip `AdapterSessionCodec` interface * (serialize/deserialize on `Record`). The session * contains the MiniMax conversation thread so a heartbeat can resume * across runs. * * The on-disk form is identical to the wire form (a plain JSON * object); base64 wrapping is unnecessary because Paperclip already * stores the value in JSONB. We also write a JSONL sidecar to * `os.tmpdir()/paperclip-adapter-minimax/.jsonl` for live * debugging. */ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { randomUUID } from "node:crypto"; import type { ChatMessage, JsonObject } from "./types.js"; const SCHEMA_VERSION = 1; export interface SessionHeader { schema: typeof SCHEMA_VERSION; sessionId: string; createdAt: string; lastUsedAt: string; model: string; messageCount: number; } export interface SessionState { schema: number; header: SessionHeader; messages: ChatMessage[]; } export function newSession(model: string): SessionState { const now = new Date().toISOString(); return { schema: SCHEMA_VERSION, header: { schema: SCHEMA_VERSION, sessionId: randomUUID(), createdAt: now, lastUsedAt: now, model, messageCount: 0, }, messages: [], }; } /** * Paperclip `AdapterSessionCodec` implementation. * - `serialize` turns the in-memory session into a plain JSON object * that Paperclip persists in JSONB. * - `deserialize` is the inverse: a JSON object (or null) back into * the session. * - `getDisplayId` returns the short session id for the UI. */ export const sessionCodec = { serialize(params: Record | null): Record | null { if (!params) return null; return params; }, deserialize(raw: unknown): Record | null { if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null; const r = raw as Record; if (r.schema !== SCHEMA_VERSION) return null; if (!Array.isArray(r.messages)) return null; return r; }, getDisplayId(params: Record | null): string | null { if (!params) return null; const header = params.header as SessionHeader | undefined; if (header?.sessionId) return header.sessionId.slice(0, 8); return null; }, }; export function defaultSessionDir(workspacePath?: string): string { const base = workspacePath?.trim() || path.join(os.homedir(), ".minimax", "paperclips"); return path.join(base, "sessions"); } export function appendTranscript(state: SessionState, event: JsonObject): void { const dir = state.header.sessionId ? path.join(os.tmpdir(), "paperclip-adapter-minimax") : os.tmpdir(); fs.mkdirSync(dir, { recursive: true }); const file = path.join(dir, `${state.header.sessionId}.jsonl`); fs.appendFileSync(file, JSON.stringify({ ts: new Date().toISOString(), event }) + "\n", "utf8"); }