# Session File Format Sessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with a `type` field. Session entries form a tree structure via `id`/`parentId` fields, enabling in-place branching without creating new files. ## File Location ``` ~/.omp/agent/sessions/----/_.jsonl ``` Default base directory comes from `getAgentDir()` (overridable via `PI_CODING_AGENT_DIR`). `` is the working directory with the leading slash removed and `/`, `\`, `:` replaced by `-`. `` is ISO-8601 with `:`/`.` replaced by `-`. `sessionId` is a snowflake hex string. ## Session Version Sessions have a version field in the header: - **Version 1**: Linear entry sequence (legacy, auto-migrated on load) - **Version 2**: Tree structure with `id`/`parentId` linking - **Version 3**: Renamed legacy `hookMessage` role to `custom` Existing sessions are automatically migrated to the latest version when loaded. ## Type Definitions - [`src/session/session-manager.ts`](../src/session/session-manager.ts) - Session entry types and `SessionManager` - [`src/session/messages.ts`](../src/session/messages.ts) - Custom message roles and LLM conversion - [`packages/agent/src/types.ts`](../../agent/src/types.ts) - `AgentMessage`, `ThinkingLevel` - [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `Message`, content blocks, `Usage`, `ToolCall` ## Entry Base All entries (except `SessionHeader`) extend `SessionEntryBase`: ```typescript interface SessionEntryBase { type: string; id: string; // Short snowflake suffix (8 hex chars) parentId: string | null; // Parent entry ID (null for first entry) timestamp: string; // ISO timestamp } ``` ## Entry Types ### SessionHeader First line of the file. Metadata only, not part of the tree (no `id`/`parentId`). `version` is absent in v1 sessions. ```json { "type": "session", "version": 3, "id": "a1b2c3d4e5f60001", "timestamp": "2024-12-03T14:00:00.000Z", "cwd": "/path/to/project", "title": "Optional title" } ``` For sessions with a parent (created via `/branch`, `newSession({ parentSession })`, or fork operations): ```json { "type": "session", "version": 3, "id": "a1b2c3d4e5f60001", "timestamp": "2024-12-03T14:00:00.000Z", "cwd": "/path/to/project", "parentSession": "/path/to/original/session.jsonl" } ``` `parentSession` is an opaque string used for lineage tracking (typically a session file path). ### SessionMessageEntry A message in the conversation. The `message` field contains an `AgentMessage`, including base LLM messages plus coding-agent custom roles (bash/python execution, custom/legacy `hookMessage` messages from v2 sessions, file mentions, etc.). ```json {"type":"message","id":"a1b2c3d4","parentId":"prev1234","timestamp":"2024-12-03T14:00:01.000Z","message":{"role":"user","content":"Hello"}} {"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Hi!"}],"provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"stop"}} {"type":"message","id":"c3d4e5f6","parentId":"b2c3d4e5","timestamp":"2024-12-03T14:00:03.000Z","message":{"role":"toolResult","toolCallId":"call_123","toolName":"bash","content":[{"type":"text","text":"output"}],"isError":false}} ``` ### ModelChangeEntry Emitted when the user switches models mid-session. `model` is stored as `provider/modelId`. ```json { "type": "model_change", "id": "d4e5f6g7", "parentId": "c3d4e5f6", "timestamp": "2024-12-03T14:05:00.000Z", "model": "openai/gpt-4o", "role": "default" } ``` ### ThinkingLevelChangeEntry Emitted when the user changes the thinking/reasoning level. ```json { "type": "thinking_level_change", "id": "e5f6g7h8", "parentId": "d4e5f6g7", "timestamp": "2024-12-03T14:06:00.000Z", "thinkingLevel": "high" } ``` `thinkingLevel` matches `ThinkingLevel` from `packages/agent` (e.g., `off`, `minimal`, `low`, `medium`, `high`, `xhigh`). ### CompactionEntry Created when context is compacted. Stores a summary of earlier messages. ```json { "type": "compaction", "id": "f6g7h8i9", "parentId": "e5f6g7h8", "timestamp": "2024-12-03T14:10:00.000Z", "summary": "User discussed X, Y, Z...", "shortSummary": "Quick recap...", "firstKeptEntryId": "c3d4e5f6", "tokensBefore": 50000, "fromExtension": false } ``` Optional fields: - `details`: Compaction-implementation specific data (extension data, version markers, etc.) - `shortSummary`: Short-form summary for UI contexts - `preserveData`: Hook/extension data to persist across compaction - `fromExtension`: `true` if generated by an extension, `false`/`undefined` if pi-generated ### BranchSummaryEntry Created when switching branches with an LLM-generated summary of the abandoned path. Captures context from the previous branch. ```json { "type": "branch_summary", "id": "g7h8i9j0", "parentId": "a1b2c3d4", "timestamp": "2024-12-03T14:15:00.000Z", "fromId": "f6g7h8i9", "summary": "Branch explored approach A..." } ``` `fromId` is the branch point entry id; when branching from the root it is `"root"`. Optional fields: - `details`: Extension-specific data (not sent to LLM) - `fromExtension`: `true` if generated by an extension ### CustomEntry Extension state persistence. Does NOT participate in LLM context. ```json { "type": "custom", "id": "h8i9j0k1", "parentId": "g7h8i9j0", "timestamp": "2024-12-03T14:20:00.000Z", "customType": "my-extension", "data": { "count": 42 } } ``` Use `customType` to identify your extension's entries on reload. ### CustomMessageEntry Extension-injected messages that DO participate in LLM context. ```json { "type": "custom_message", "id": "i9j0k1l2", "parentId": "h8i9j0k1", "timestamp": "2024-12-03T14:25:00.000Z", "customType": "my-extension", "content": "Injected context...", "display": true } ``` Fields: - `content`: String or `(TextContent | ImageContent)[]` (same as UserMessage) - `display`: `true` = show in TUI with distinct styling, `false` = hidden - `details`: Optional extension-specific metadata (not sent to LLM) ### LabelEntry User-defined bookmark/marker on an entry. ```json { "type": "label", "id": "j0k1l2m3", "parentId": "i9j0k1l2", "timestamp": "2024-12-03T14:30:00.000Z", "targetId": "a1b2c3d4", "label": "checkpoint-1" } ``` Set `label` to `undefined` to clear a label. ### TtsrInjectionEntry Tracks which time-traveling stream rules were injected during the session. ```json { "type": "ttsr_injection", "id": "k1l2m3n4", "parentId": "j0k1l2m3", "timestamp": "2024-12-03T14:31:00.000Z", "injectedRules": ["rule-a", "rule-b"] } ``` ### SessionInitEntry Captures initial context for subagent sessions (debugging/replay). Not used in LLM context building. ```json { "type": "session_init", "id": "l2m3n4o5", "parentId": "k1l2m3n4", "timestamp": "2024-12-03T14:32:00.000Z", "systemPrompt": "...", "task": "Initial task...", "tools": ["bash", "read"], "outputSchema": { "type": "object" } } ``` ## Tree Structure Entries form a tree: - First entry has `parentId: null` - Each subsequent entry points to its parent via `parentId` - Branching creates new children from an earlier entry - The "leaf" is the current position in the tree ``` [user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf │ └─ [branch_summary] ─── [user msg] ← alternate branch ``` ## Context Building `buildSessionContext()` walks from the current leaf to the root, producing the message list for the LLM: 1. Collects all entries on the path 2. Extracts current model map, thinking level, and injected TTSR rules 3. If a `CompactionEntry` is on the path: - Emits the summary first - Then messages from `firstKeptEntryId` to compaction - Then messages after compaction 4. Converts `BranchSummaryEntry` and `CustomMessageEntry` to appropriate message formats Return value is a `SessionContext` containing `messages`, `models`, `thinkingLevel`, and `injectedTtsrRules`. ## Parsing Example ```typescript const text = await Bun.file("session.jsonl").text(); const entries = Bun.JSONL.parse(text); for (const entry of entries) { switch (entry.type) { case "session": console.log(`Session v${entry.version ?? 1}: ${entry.id}`); break; case "message": console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`); break; case "compaction": console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`); break; case "branch_summary": console.log(`[${entry.id}] Branch from ${entry.fromId}`); break; case "custom": console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`); break; case "custom_message": console.log(`[${entry.id}] Custom message (${entry.customType}): ${entry.content}`); break; case "ttsr_injection": console.log(`[${entry.id}] TTSR rules: ${entry.injectedRules.join(", ")}`); break; case "session_init": console.log(`[${entry.id}] Init: ${entry.tools.join(", ")}`); break; case "label": console.log(`[${entry.id}] Label "${entry.label}" on ${entry.targetId}`); break; case "model_change": console.log(`[${entry.id}] Model: ${entry.model} (${entry.role ?? "default"})`); break; case "thinking_level_change": console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`); break; } } ``` ## SessionManager API Key methods for working with sessions programmatically: ### Creation - `SessionManager.create(cwd, sessionDir?)` - New session - `SessionManager.open(path, sessionDir?)` - Open existing - `SessionManager.continueRecent(cwd, sessionDir?)` - Continue most recent or create new - `SessionManager.inMemory(cwd?)` - No file persistence ### Appending (all return entry ID) - `appendMessage(message)` - Add message - `appendThinkingLevelChange(level)` - Record thinking change - `appendModelChange(model, role?)` - Record model change (`model` = `provider/modelId`) - `appendSessionInit(init)` - Record initial subagent context - `appendCompaction(summary, shortSummary, firstKeptEntryId, tokensBefore, details?, fromExtension?, preserveData?)` - `appendCustomEntry(customType, data?)` - Extension state (not in context) - `appendCustomMessageEntry(customType, content, display, details?)` - Extension message (in context) - `appendTtsrInjection(ruleNames)` - Record injected TTSR rules - `appendLabelChange(targetId, label)` - Set/clear label ### Tree Navigation - `getLeafId()` - Current position - `getLeafEntry()` - Current leaf entry - `getEntry(id)` - Get entry by ID - `getBranch(fromId?)` - Walk from entry to root - `getTree()` - Get full tree structure - `getChildren(parentId)` - Get direct children - `getLabel(id)` - Get label for entry - `branch(entryId)` - Move leaf to earlier entry - `branchWithSummary(entryId | null, summary, details?, fromExtension?)` - Branch with context summary - `resetLeaf()` - Move leaf to before first entry ### Context - `buildSessionContext()` - Get messages for LLM - `getEntries()` - All entries (excluding header) - `getHeader()` - Session metadata