# 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/--<path>--/<timestamp>_<uuid>.jsonl
```

Where `<path>` is the working directory with `/` replaced by `-`.

## 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

Existing v1 sessions are automatically migrated to v2 when loaded.

## Type Definitions

- [`src/core/session-manager.ts`](../src/core/session-manager.ts) - Session entry types
- [`packages/agent/src/types.ts`](../../agent/src/types.ts) - `AgentMessage`, `Attachment`, `ThinkingLevel`
- [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `UserMessage`, `AssistantMessage`, `ToolResultMessage`, `Usage`, `ToolCall`

## Entry Base

All entries (except `SessionHeader`) extend `SessionEntryBase`:

```typescript
interface SessionEntryBase {
	type: string;
	id: string; // 8-char hex ID
	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`).

```json
{ "type": "session", "version": 2, "id": "uuid", "timestamp": "2024-12-03T14:00:00.000Z", "cwd": "/path/to/project" }
```

For sessions with a parent (created via `/branch` or `newSession({ parentSession })`):

```json
{
	"type": "session",
	"version": 2,
	"id": "uuid",
	"timestamp": "2024-12-03T14:00:00.000Z",
	"cwd": "/path/to/project",
	"parentSession": "/path/to/original/session.jsonl"
}
```

### SessionMessageEntry

A message in the conversation. The `message` field contains an `AgentMessage`.

```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.

```json
{
	"type": "model_change",
	"id": "d4e5f6g7",
	"parentId": "c3d4e5f6",
	"timestamp": "2024-12-03T14:05:00.000Z",
	"provider": "openai",
	"modelId": "gpt-4o"
}
```

### 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"
}
```

### 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...",
	"firstKeptEntryId": "c3d4e5f6",
	"tokensBefore": 50000
}
```

Optional fields:

- `details`: Compaction-implementation specific data (e.g., file operations for default implementation, or custom data for custom hook implementations)
- `fromHook`: `true` if generated by a hook, `false`/`undefined` if omp-generated

### BranchSummaryEntry

Created when switching branches via `/tree` with an LLM generated summary of the left branch up to the common ancestor. Captures context from the abandoned path.

```json
{
	"type": "branch_summary",
	"id": "g7h8i9j0",
	"parentId": "a1b2c3d4",
	"timestamp": "2024-12-03T14:15:00.000Z",
	"fromId": "f6g7h8i9",
	"summary": "Branch explored approach A..."
}
```

Optional fields:

- `details`: File tracking data (`{ readFiles: string[], modifiedFiles: string[] }`) for default implementation, arbitrary for custom implementation
- `fromHook`: `true` if generated by a hook

### CustomEntry

Hook state persistence. Does NOT participate in LLM context.

```json
{
	"type": "custom",
	"id": "h8i9j0k1",
	"parentId": "g7h8i9j0",
	"timestamp": "2024-12-03T14:20:00.000Z",
	"customType": "my-hook",
	"data": { "count": 42 }
}
```

Use `customType` to identify your hook's entries on reload.

### CustomMessageEntry

Hook-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-hook",
	"content": "Injected context...",
	"display": true
}
```

Fields:

- `content`: String or `(TextContent | ImageContent)[]` (same as UserMessage)
- `display`: `true` = show in TUI with purple styling, `false` = hidden
- `details`: Optional hook-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.

## 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 and thinking level settings
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

## 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}] Hook message (${entry.customType}): ${entry.content}`);
			break;
		case "label":
			console.log(`[${entry.id}] Label "${entry.label}" on ${entry.targetId}`);
			break;
		case "model_change":
			console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);
			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(provider, modelId)` - Record model change
- `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - Add compaction
- `appendCustomEntry(customType, data?)` - Hook state (not in context)
- `appendCustomMessageEntry(customType, content, display, details?)` - Hook message (in context)
- `appendLabelChange(targetId, label)` - Set/clear label

### Tree Navigation

- `getLeafId()` - Current position
- `getEntry(id)` - Get entry by ID
- `getPath(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, summary, details?, fromHook?)` - Branch with context summary

### Context

- `buildSessionContext()` - Get messages for LLM
- `getEntries()` - All entries (excluding header)
- `getHeader()` - Session metadata
