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

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

Entry Base

All entries (except SessionHeader) extend SessionEntryBase:

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.

{
	"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):

{
	"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.).

{"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 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.

{
	"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.

{
	"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 = hidden
  • details: 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:

  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

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