- Updated documentation to reflect product name change from 'pi' to 'omp' throughout guides and API references. - Restructured extension and hook documentation to clarify discovery mechanisms, loading behavior, and configuration across multiple config systems (.omp, .pi, .claude, .codex). - Updated SDK API documentation with new method signatures: discoverHooks() -> discoverExtensions(), SessionManager methods now async, settings format changed to YAML. - Expanded session architecture documentation with new entry types (TtsrInjectionEntry, SessionInitEntry), updated field names (fromHook -> fromExtension), and clarified session file format versioning. - Simplified session-tree-plan.md from detailed implementation checklist to architecture summary, removing completed tasks and rollout details.
11 KiB
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 OMP_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 nanoid.
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/parentIdlinking - Version 3: Renamed legacy
hookMessagerole tocustom
Existing sessions are automatically migrated to the latest version when loaded.
Type Definitions
src/session/session-manager.ts- Session entry types andSessionManagersrc/session/messages.ts- Custom message roles and LLM conversionpackages/agent/src/types.ts-AgentMessage,ThinkingLevelpackages/ai/src/types.ts-Message, content blocks,Usage,ToolCall
Entry Base
All entries (except SessionHeader) extend SessionEntryBase:
interface SessionEntryBase {
type: string;
id: string; // Short nanoid (8 chars, URL-safe)
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.
{ "type": "session", "version": 3, "id": "nanoid", "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):
{
"type": "session",
"version": 3,
"id": "nanoid",
"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.).
{"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.
{
"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.
{
"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.
{
"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 contextspreserveData: Hook/extension data to persist across compactionfromExtension:trueif generated by an extension,false/undefinedif pi-generated
BranchSummaryEntry
Created when switching branches with an LLM-generated summary of the abandoned path. Captures context from the previous branch.
{
"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:trueif generated by an extension
CustomEntry
Extension state persistence. Does NOT participate in LLM context.
{
"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.
{
"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= hiddendetails: Optional extension-specific metadata (not sent to LLM)
LabelEntry
User-defined bookmark/marker on an entry.
{
"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.
{
"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.
{
"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:
- Collects all entries on the path
- Extracts current model map, thinking level, and injected TTSR rules
- If a
CompactionEntryis on the path:- Emits the summary first
- Then messages from
firstKeptEntryIdto compaction - Then messages after compaction
- Converts
BranchSummaryEntryandCustomMessageEntryto appropriate message formats
Return value is a SessionContext containing messages, models, thinkingLevel, and injectedTtsrRules.
Parsing Example
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 sessionSessionManager.open(path, sessionDir?)- Open existingSessionManager.continueRecent(cwd, sessionDir?)- Continue most recent or create newSessionManager.inMemory(cwd?)- No file persistence
Appending (all return entry ID)
appendMessage(message)- Add messageappendThinkingLevelChange(level)- Record thinking changeappendModelChange(model, role?)- Record model change (model=provider/modelId)appendSessionInit(init)- Record initial subagent contextappendCompaction(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 rulesappendLabelChange(targetId, label)- Set/clear label
Tree Navigation
getLeafId()- Current positiongetLeafEntry()- Current leaf entrygetEntry(id)- Get entry by IDgetBranch(fromId?)- Walk from entry to rootgetTree()- Get full tree structuregetChildren(parentId)- Get direct childrengetLabel(id)- Get label for entrybranch(entryId)- Move leaf to earlier entrybranchWithSummary(entryId | null, summary, details?, fromExtension?)- Branch with context summaryresetLeaf()- Move leaf to before first entry
Context
buildSessionContext()- Get messages for LLMgetEntries()- All entries (excluding header)getHeader()- Session metadata