# 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/--<cwd>--/<timestamp>_<sessionId>.jsonl
```

Default base directory comes from `getAgentDir()` (overridable via `PI_CODING_AGENT_DIR`).
`<cwd>` is the working directory with the leading slash removed and `/`, `\`, `:` replaced by `-`.
`<timestamp>` 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
