Skip to main content

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

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

Deleting Sessions

Sessions can be removed by deleting their .jsonl files under ~/.atomic/agent/sessions/ (legacy ~/.pi/agent/sessions/ may exist from older Pi installs). Atomic also supports deleting sessions interactively from /resume (select a session and press CTRL+D, then confirm). When available, Atomic uses the trash CLI to avoid permanent deletion.

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 hookMessage role to custom (extensions unification)
Existing sessions are automatically migrated to the current version (v3) when loaded.

Source Files

Source on GitHub (atomic): Base message and agent event types are provided by Atomic’s installed runtime dependencies (@earendil-works/pi-ai and @earendil-works/pi-agent-core), not by separate packages/ai or packages/agent directories in this monorepo. For TypeScript definitions in your project, inspect node_modules/@bastani/atomic/dist/, node_modules/@earendil-works/pi-ai/dist/, and node_modules/@earendil-works/pi-agent-core/dist/.

Message Types

Session entries contain AgentMessage objects. Understanding these types is essential for parsing sessions and writing extensions.

Content Blocks

Messages contain arrays of typed content blocks:

Base Message Types (from @earendil-works/pi-ai)

Extended Message Types (from Atomic coding-agent)

compactionSummary is a historical message role that appears only in older session files; Atomic never produces it and treats historical occurrences as inert. Active verbatim boundaries are synthesized at rebuild time as visible custom messages with customType: "compaction"; convertToLlm() maps them to provider-facing user messages.

AgentMessage Union

Entry Base

All entries (except SessionHeader) extend SessionEntryBase:

Entry Types

SessionHeader

First line of the file. Metadata only, not part of the tree (no id/parentId).
For sessions with a parent (created via /fork, /clone, or newSession({ parentSession })):
Workflow-owned sessions carry both internal: true and complete workflow linkage. Atomic writes this classification before the transcript becomes visible wherever possible, including workflow stage forks and fresh/forked subagents. A session is excluded from normal resume history only when internal is the exact boolean true and workflow.runId, workflow.stageId, and workflow.stageName are all non-empty strings:
Malformed, incomplete, workflow-only, or truthy non-boolean legacy markers remain visible in normal history. Atomic does not infer workflow ownership from parentSession, so ordinary user-created forks are unaffected. Valid workflow classification is inherited when an internal workflow transcript is branched or forked.

SessionMessageEntry

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

ModelChangeEntry

Emitted when the user switches models mid-session.

ThinkingLevelChangeEntry

Emitted when the user changes the thinking/reasoning level.

ContextWindowChangeEntry

Emitted when the user selects a supported context-window size for the active model. The value is a token count, independent of thinking/reasoning level. Explicit startup selections are journaled even when they equal the model’s scalar default so the user’s budget choice survives later settings changes and resume.
buildSessionContext() replays the latest context_window_change on the active branch. In-place tree navigation also applies the branch’s replayed context window to the active model without appending another context_window_change entry or writing context-window defaults to settings. If a historical value is no longer supported by the current model, session creation/navigation falls back to the model default the same way other context-window restore paths do.

CompactionEntry

Created by /compact, RPC compact, and automatic compaction. The summary field contains the mechanically reconstructed verbatim transcript string, not generated summary prose. firstKeptEntryId is the first context-visible entry retained outside compaction, or null when no pre-boundary context-visible message is retained (including preserve_recent: 0). An entry is active only when details.strategy is exactly "verbatim-lines":
On rebuild, Atomic emits the durable summary as a synthesized visible custom message, then emits original entries beginning at a string firstKeptEntryId. When the field is null, it emits no pre-boundary ordinary entries. In both cases, messages appended after the boundary are emitted. This exact state survives resume without rerunning a planner. Existing records with string IDs retain their behavior; details.rung is "planned" or "extension", and details.backupPath is optional. Historical compaction records without details.strategy: "verbatim-lines" are retired summary-compaction records. They remain parseable and visible to audit/export tools but are inert in active LLM context.

ContextCompactionEntry (Retired)

Older Atomic versions stored logical entry/content-block deletions in type:"context_compaction" records:
These records are archival and never produced or applied by current Atomic. When an old session resumes, previously hidden content may re-enter context until verbatim-line compaction creates an active boundary.

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.
Optional fields:
  • details: File tracking data ({ readFiles: string[], modifiedFiles: string[] }) for default, or custom data for extensions
  • fromHook: true if generated by an extension, false/undefined if Atomic-generated (legacy field name)

CustomEntry

Extension state persistence. Does NOT participate in LLM context.
Use customType to identify your extension’s entries on reload.

CustomMessageEntry

Extension-injected messages that DO participate in LLM context.
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.
Set label to undefined to clear a label.

SessionInfoEntry

Session metadata (e.g., user-defined display name). Set via /name, --name / -n, or pi.setSessionName() in extensions.
The session name is displayed in the session selector (/resume) instead of the first message when set.

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

Context Building

buildSessionContext() walks the active branch from root to leaf and replays model, thinking-level, and context-window changes. It selects the latest compaction entry whose details.strategy is "verbatim-lines".
  • With no active boundary, normal message, custom-message, and branch-summary entries are emitted verbatim.
  • With a boundary whose firstKeptEntryId is a string, Atomic emits its durable string as a custom-role customType:"compaction" message, then original messages from that ID onward, including messages appended after the boundary.
  • With firstKeptEntryId: null, Atomic emits the boundary and post-boundary messages but no pre-boundary ordinary message.
  • If a corrupt/foreign boundary’s non-null firstKeptEntryId is absent, Atomic emits the boundary followed by post-boundary messages rather than resurrecting all older content.
  • Legacy context_compaction entries and non-verbatim compaction entries are skipped as inert archival records.

Parsing Example

SessionManager API

Key methods for working with sessions programmatically.

Static Creation Methods

  • SessionManager.create(cwd, sessionDir?, options?) - New session. Workflow-owned sessions require the pair internal: true and workflow: { runId, stageId, stageName }.
  • SessionManager.open(path, sessionDir?) - Open a specific session file directly, including an internal session.
  • SessionManager.continueRecent(cwd, sessionDir?, options?) - Continue the most recent regular session or create a new one. Pass { includeInternal: true } only for workflow-specific recovery or diagnostics.
  • SessionManager.inMemory(cwd?, options?) - No file persistence
  • SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?, options?) - Fork a session from another project. Relevant NewSessionOptions, including valid workflow classification, are written in the initial header.

Static Listing Methods

  • SessionManager.list(cwd, sessionDir?, onProgress?, options?) - List project sessions. Internal workflow sessions are excluded by default; pass { includeInternal: true } to include them and expose their SessionInfo.workflow linkage.
  • SessionManager.listAll(sessionDir?, onProgress?, options?) - List sessions across projects, or from a custom session directory. The same includeInternal default and opt-in apply.
Normal /resume, atomic -r, and --continue callers use the default filtering. Workflow-specific code can opt in without changing user-facing history:

Instance Methods - Session Management

  • newSession(options?) - Start a new session. Options include parentSession, internal, and workflow run/stage linkage; classification requires a complete marker pair.
  • markSessionInternal(workflow?) - Apply valid workflow ownership to the current session, repairing malformed markers while preserving an existing valid marker.
  • setSessionFile(path) - Switch to a different session file
  • createBranchedSession(leafId) - Extract branch to new session file

Instance Methods - Appending (all return entry ID)

  • appendMessage(message) - Add message
  • appendThinkingLevelChange(level) - Record thinking change
  • appendContextWindowChange(contextWindow) - Record context-window selection in tokens
  • appendModelChange(provider, modelId) - Record model change
  • appendCompaction(compactedText, firstKeptEntryId, tokensBefore, details) - Add a durable verbatim-line compaction boundary; pass null when no pre-boundary message is retained
  • appendCustomEntry(customType, data?) - Extension state (not in context)
  • appendSessionInfo(name) - Set session display name
  • appendCustomMessageEntry(customType, content, display, details?) - Extension message (in context)
  • appendLabelChange(targetId, label) - Set/clear label

Instance Methods - Tree Navigation

  • getLeafId() - Current position
  • getLeafEntry() - Get 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
  • resetLeaf() - Reset leaf to null (before any entries)
  • branchWithSummary(entryId, summary, details?, fromHook?) - Branch with context summary

Instance Methods - Context & Info

  • buildSessionContext() - Get messages, thinkingLevel, and model for LLM
  • getEntries() - All entries (excluding header)
  • getHeader() - Session header metadata
  • getSessionName() - Get display name from latest session_info entry
  • getCwd() - Working directory
  • getSessionDir() - Session storage directory
  • getSessionId() - Session UUID
  • getSessionFile() - Session file path (undefined for in-memory)
  • isPersisted() - Whether session is saved to disk