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.

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 one synthesized visible custom message: the durable summary with the kept tail—the original entries from a string firstKeptEntryId up to the boundary—serialized into the same transcript grammar and concatenated onto its end. Those entries are not re-emitted as separate messages, so a tail that starts or ends mid-turn cannot yield out-of-order provider blocks. The tail keeps full tool-result text and carries retained images as image blocks on the boundary message. When the field is null, the boundary carries the summary alone. In both cases, messages appended after the boundary are emitted as real messages. This exact state survives resume without rerunning a planner. 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 a single custom-role customType:"compaction" message whose text is the durable string plus the losslessly serialized kept tail (the entries from that ID up to the boundary, with retained images kept as image blocks on that message), then the 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