/** * Local session store — pi JSONL v3 tree writer under `.openkai/sessions/` * (D-P2-3, scope §3). * * Each session is one JSONL file at `//session.jsonl`. Every * line is a tree entry with `{ type, id, seq, parentId, timestamp, … }` — the * pi-agent-core v3 entry shape ({@link MessageEntry}, {@link CustomEntry}, * {@link CompactionEntry}). `id` is a uuidv7; `parentId` chains entries into a * branch so a reader can walk the conversation tree leaf→root. The store * appends entries as the transport settles them; OpenKai owns the directory * layout, the entry shapes come from the pi session format. * * P2 writes message entries + custom entries (the loop does not compact * yet). The compaction entry shape is supported by the format so a later * phase can append `retainedTail` summaries without a migration. * * Rehydration: opening a store over an existing `session.jsonl` resumes it — * the header is validated (`version === 3`, else {@link SessionFormatError}) * and `seq`/`leafId` continue from the tail, so a restarted TUI appends to * the same tree instead of writing a second header (ren review). Readers * skip unparseable lines so a truncated tail (crash mid-append) cannot brick * the session. * * Concurrency: `ensure()` takes a best-effort advisory lockfile * (`/.lock`, O_EXCL, pid+timestamp). A live pid holding the lock * refuses the open ({@link SessionLockError}); a dead pid's stale lock is * reclaimed. Released by {@link SessionStore.close}; a process killed * without closing leaves a stale lock the next open reclaims. Advisory only * — it coordinates OpenKai processes, it is not a security boundary. */ import type { Usage } from "@earendil-works/pi-ai"; import type { AgentMessage } from "@earendil-works/pi-agent-core"; /** Common fields of every JSONL tree entry (the v3 shape). */ export interface EntryBase { type: string; id: string; seq: number; parentId: string | null; timestamp: number; } /** A conversation message entry. */ export interface MessageEntry extends EntryBase { type: "message"; message: AgentMessage; terminate?: true; } /** A compaction entry carrying a summary and the retained tail. */ export interface CompactionEntry extends EntryBase { type: "compaction"; summary: string; retainedTail: AgentMessage[]; tokensBefore: number; details?: unknown; usage?: Usage; } /** A custom entry for extension/agent-owned state. */ export interface CustomEntry extends EntryBase { type: "custom"; customType: string; data?: unknown; } export type Entry = MessageEntry | CompactionEntry | CustomEntry; /** Session metadata persisted as the first line of the file (a v3 header). */ export interface SessionHeader { type: "header"; version: 3; id: string; createdAt: number; parentSessionId?: string | null; } /** Options for opening or creating a session store. */ export interface SessionStoreOptions { /** Root directory containing per-session subdirectories (default: `.openkai/sessions`). */ root?: string; /** Session id (uuidv7 by default). */ sessionId?: string; /** Parent session id for branch-forking (optional). */ parentSessionId?: string | null; } /** The session file exists but is not a v3 tree (bad header or version). */ export declare class SessionFormatError extends Error { constructor(message: string); } /** Another live OpenKai process holds the session's advisory lock. */ export declare class SessionLockError extends Error { constructor(message: string); } /** The local JSONL v3 session tree writer. */ export declare class SessionStore { readonly sessionId: string; readonly parentSessionId: string | null; readonly filePath: string; private readonly dirPath; private readonly lockPath; private seq; private leafId; private headerWritten; private lockHeld; constructor(options?: SessionStoreOptions); /** * Ensure the session directory exists, take the advisory lock, and either * rehydrate from an existing tree or write the header for a new one. */ ensure(): Promise; /** Release the advisory lock (best-effort; safe to call more than once). */ close(): Promise; /** * Take the advisory lockfile. A live pid holding it refuses the open; a * dead pid's stale lock is reclaimed (O_EXCL create + kill(pid, 0) probe). */ private acquireLock; /** Parse the pid out of an existing lockfile (undefined if unreadable). */ private lockHolderPid; /** * Resume an existing tree: validate the header and continue seq/leafId from * the tail. Unparseable lines are skipped (truncated tail tolerance), so a * crash mid-append loses at most the partial line. */ private rehydrate; /** All non-empty lines of the session file, or undefined when absent. */ private readLinesOrUndefined; /** * Append one JSONL line: redacted, owner-readable only. * * Redaction is at the single write seam so every entry shape is covered * (messages, custom data, compaction summaries) — an approved `bash cat .env` * is the realistic path that puts a live key into a turn (E001 finding F7). */ private appendLine; /** Append a message entry to the tree. Returns the entry id. */ appendMessage(message: AgentMessage, terminate?: true): Promise; /** Append a custom entry for extension/agent-owned state. Returns the entry id. */ appendCustom(customType: string, data?: unknown): Promise; /** Append a compaction entry (reserved for later phases). */ appendCompaction(summary: string, retainedTail: AgentMessage[], tokensBefore: number, details?: unknown, usage?: Usage): Promise; /** Low-level entry append — assigns id/seq/parentId/timestamp and persists. */ private appendEntry; /** Read all entries (skipping the header) in append order. Bad lines are skipped. */ readEntries(): Promise; /** Read the session header (first line); undefined if missing/unparseable. */ readHeader(): Promise; /** * Every user message in the tree, oldest first, with its entry id (E017 * contract #2 — the fork picker's rows). Text is the message's joined text * parts, single-line-normalised for picker display. */ listUserMessages(): Promise>; /** * Fork at a past entry (E017 contract #2 — rewind-to-point): copy the * root→`entryId` path into a NEW session with a fresh id whose header's * `parentSessionId` names this session. Entry ids are re-minted and * parentIds re-anchored along the copy so the fork is a self-contained * linear tree. Returns the new store (locked open; caller closes). * Throws when `entryId` names no entry in this tree. */ forkAtEntry(entryId: string): Promise; } /** List session ids (directory names) under a root, sorted. */ export declare function listSessions(root?: string): Promise; /** Read the message entries (AgentMessage) from a session file in append order. */ export declare function readSessionMessages(filePath: string): Promise; /** Default sessions root: `.openkai/sessions` relative to process.cwd(). */ export declare function defaultRoot(): string; /** * Fork a session (droid's background `/fork`): a new v3 branch whose header * points at the source session, seeded with the source's messages so the * forked context continues intact. Returns the new session's identity for * the paste-able resume receipt. */ export declare function forkSession(source: SessionStore): Promise<{ sessionId: string; filePath: string; }>; /** One row of the session tree view. */ export interface SessionTreeRow { sessionId: string; parentSessionId: string | null; createdAt: number; messages: number; } /** The session forest: every session under the root with its parent link. */ export declare function sessionTree(root?: string): Promise; //# sourceMappingURL=session-store.d.ts.map