Session File Format
Sessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with atype field. Session entries form a tree structure via id/parentId fields, enabling in-place branching without creating new files.
File Location
<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/parentIdlinking - Version 3: Renamed
hookMessagerole tocustom(extensions unification)
Source Files
Source on GitHub (atomic):packages/coding-agent/src/core/session-manager.ts- Session entry types and SessionManagerpackages/coding-agent/src/core/messages.ts- Extended message types (BashExecutionMessage, CustomMessage, etc.)
@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 containAgentMessage 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 (exceptSessionHeader) extend SessionEntryBase:
Entry Types
SessionHeader
First line of the file. Metadata only, not part of the tree (noid/parentId).
/fork, /clone, or newSession({ parentSession })):
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:
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. Themessage 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":
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 intype:"context_compaction" records:
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.
details: File tracking data ({ readFiles: string[], modifiedFiles: string[] }) for default, or custom data for extensionsfromHook:trueif generated by an extension,false/undefinedif Atomic-generated (legacy field name)
CustomEntry
Extension state persistence. Does NOT participate in LLM context.customType to identify your extension’s entries on reload.
CustomMessageEntry
Extension-injected messages that DO participate in LLM context.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.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.
/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
firstKeptEntryIdis a string, Atomic emits its durable string as a custom-rolecustomType:"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
firstKeptEntryIdis absent, Atomic emits the boundary followed by post-boundary messages rather than resurrecting all older content. - Legacy
context_compactionentries and non-verbatimcompactionentries 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 pairinternal: trueandworkflow: { 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 persistenceSessionManager.forkFrom(sourcePath, targetCwd, sessionDir?, options?)- Fork a session from another project. RelevantNewSessionOptions, 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 theirSessionInfo.workflowlinkage.SessionManager.listAll(sessionDir?, onProgress?, options?)- List sessions across projects, or from a custom session directory. The sameincludeInternaldefault and opt-in apply.
/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 includeparentSession,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 filecreateBranchedSession(leafId)- Extract branch to new session file
Instance Methods - Appending (all return entry ID)
appendMessage(message)- Add messageappendThinkingLevelChange(level)- Record thinking changeappendContextWindowChange(contextWindow)- Record context-window selection in tokensappendModelChange(provider, modelId)- Record model changeappendCompaction(compactedText, firstKeptEntryId, tokensBefore, details)- Add a durable verbatim-line compaction boundary; passnullwhen no pre-boundary message is retainedappendCustomEntry(customType, data?)- Extension state (not in context)appendSessionInfo(name)- Set session display nameappendCustomMessageEntry(customType, content, display, details?)- Extension message (in context)appendLabelChange(targetId, label)- Set/clear label
Instance Methods - Tree Navigation
getLeafId()- Current positiongetLeafEntry()- Get 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 entryresetLeaf()- 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 LLMgetEntries()- All entries (excluding header)getHeader()- Session header metadatagetSessionName()- Get display name from latest session_info entrygetCwd()- Working directorygetSessionDir()- Session storage directorygetSessionId()- Session UUIDgetSessionFile()- Session file path (undefined for in-memory)isPersisted()- Whether session is saved to disk