Files
oh-my-pi/packages/coding-agent/docs/session.md
T
can1357 b59b104bad refactor(idgen): replaced nanoid with Snowflake ID generator
- Replaced nanoid and randomUUID with Snowflake ID generator across codebase for consistent distributed ID generation.
- Created new Snowflake utility module in packages/utils with support for 64-bit distributed ID generation with configurable epoch, machine ID, and sequence numbers.
- Updated session ID format documentation from nanoid to snowflake hex string format (16 hex characters).
- Removed nanoid dependency from package.json as it is now replaced by internal Snowflake implementation.
2026-02-05 03:48:08 +01:00

369 lines
11 KiB
Markdown

# 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