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.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 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 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 a single custom-rolecustomType:"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
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