Session File Format
This page describes format-4 JSONL files used by explicit JSONL stores and exports. Sessions default to SQLite; see Session storage to choose a store. In JSONL, each line is a JSON object with atype field. Session entries form a tree through id and parentId.
File Location
sessions.storage.dir and --session-dir select the directory containing the JSONL files. Session IDs, not these paths, are used for resume and fork selection. Local artifacts and tool-result files remain on the filesystem even with a database-backed session.
Deleting Sessions
For an explicitly selected JSONL store, sessions can be removed by deleting their.jsonl files. Do not delete the shared SQLite database to remove a single session.
Delete individual sessions from /resume: select a session, press CTRL+D, then confirm. Database stores delete the selected session record. JSONL stores use the trash CLI when available.
Session Version
SessionManager writes version 4 headers withcreatedAt and parentSessionId. Sessions in formats 1, 2, and 3, and early version-4 files whose header has timestamp and parentSession instead, are upgraded automatically when opened, resumed, imported, or forked. Reading converts older records in memory; listing sessions and looking up headers leave files unchanged. Opening an older JSONL session persists its upgrade atomically under the file lock before new entries are saved. Future versions, unrecognized version values, and structurally invalid entries remain rejected; unloadable sessions are omitted from resume listings. A complete final record without a newline is accepted; a torn final record is rejected.
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.)
@bastani/atomic, @bastani/pi-ai, and @earendil-works/pi-agent-core packages under their dist/ directories.
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 @bastani/pi-ai)
StopReason type also includes "pending", the reason a message carries while it is still streaming. The terminal event replaces it before the assistant message is written, so "pending" does not appear in session JSONL.
System messages record prompt and tool changes in chronological order. Later content adds instructions, sections patches names (null deletes), and tool declarations add or remove availability. Resume and branch selection replay that branch’s state. Compaction entries can contain a systemMessage checkpoint, replayed before the compacted transcript; system instructions are not offered to the deletion planner. Older sessions acquire a declaration on their next request, not merely when opened. Forced per-run prompts affect provider requests but are not written to the transcript.
A process can stop after a toolUse assistant message reaches JSONL but before every corresponding toolResult is appended. Atomic does not rewrite that append-only history. When the inactive session is reopened, its derived context supplies an error result for each unanswered call before rendering, compaction, or another provider request. The error states that execution was interrupted and its result is unavailable; it does not claim the tool had no side effects. Orphaned and duplicate results are also removed from provider-bound derived context. The original JSONL remains the authoritative record of what was actually persisted.
Nested tool calls are recorded on their parent toolResult, not as independent messages. nestedCalls contains { calls, complete }: each call has its ID, name, status, optional arguments, duration and error. At most 256 calls are retained; arguments exceeding 8 KiB per call or 32 KiB across the parent are omitted. complete: false means the record is incomplete. Do not treat this metadata as a complete audit log or infer that omitted calls had no side effects. Nested output is not retained there; the parent reports any result needed by the conversation.
Extended Message Types (from Atomic coding-agent)
compactionSummary appears only in older session files and is inert. Active verbatim boundaries appear in rebuilt context as visible custom messages with customType: "compaction".
AgentMessage Union
Entry Base
All entries (exceptSessionHeader) extend SessionEntryBase:
Entry Types
SessionHeader
First line of the file. Metadata only, not a tree entry. Itsid identifies the session; it has no 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:
parentSessionId, 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":
custom message with customType: "compaction", combining the durable summary and retained tail. Full tool-result text and retained images remain available. firstKeptEntryId: null means there is no retained pre-boundary tail. Post-boundary messages remain ordinary messages, and resume needs no new planning request.
details.rung is "planned" or "extension"; 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:
ContextEditEntry
Append-only edit of one earlier context-producing entry. It changes only future model context; the target entry and its metadata remain unchanged in raw history, UI, exports, and session accounting.replacement: null omits the target from model context. A non-null replacement replaces only the target message content. String replacements for assistant and tool-result entries are normalized to one text block because those roles require content arrays. If several edits target the same entry, the latest edit on the active branch wins. Edits are branch-relative: navigating to a point before the edit reveals the target’s original contribution again.
Atomic writes these entries itself when it durably omits a failed attempt from model context (an automatic retry after a provider error, a model fallback, an overflow or length recovery, or an interrupted empty reply that resumes an admitted queued message). Extensions append them through the turn_end and agent_before_settle boundaries.
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.
Appending a session_info entry with an empty name clears the title. The distinction is durable: a cleared session keeps its trailing empty-name entry, while a never-named session has no session_info entry at all. Readers can tell them apart through getSessionNameState() ({ hasName: false } for never-named, { hasName: true } for cleared) and through the hasName flag on listed SessionInfo rows; the bare name string is undefined in both cases.
SessionSummaryEntry
A generated one-line description of the session, written automatically once the agent goes idle and shown in the session selector. Never sent to the LLM.summary: The stored line. Whitespace is collapsed and length is clamped before writing.summarizedThroughId: Entry ID of the last user/assistant message the summary covers. The selector shows the summary only while this is still the newest conversation message; anything newer makes it stale and the selector falls back to the session name or first message. Tool results do not count.usage: Optional token usage from the model call, counted toward session usage totals.
branch_summary written after a session_summary also retires it, because the branch it described was abandoned.
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
buildSessionProjection() 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", then applies the latest context_edit for each selected target. It returns the model-visible messages together with their source entries (entries[].sourceEntry and entries[].messages); omitted targets produce no message, replacements retain the source entry’s role and metadata while changing only content, and the raw selected entries are not modified. buildSessionContext() returns just the messages, thinking level, and model from that projection. Provider requests are built from this projection.
- 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 context edits applied and retained images kept as image blocks on that message), then the messages appended after the boundary. Kept-tail entries project no standalone messages of their own. - 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. Awaitcreate, open, continueRecent, forkFrom, and all session mutations. A resolved mutation has committed to its store; a failed write rejects with its typed storage error without publishing the failed entry. There is no flush method. inMemory() explicitly selects memory and performs no filesystem I/O. Factories without an explicit store or directory resolve the configured storage, defaulting to the shared local SQLite database. A directory argument explicitly selects JSONL. Store-open failures never fall back to memory.
Static Creation Methods
SessionManager.create(cwd, sessionDir?, options?)- New session in configured storage, or explicit JSONL when a directory is supplied.SessionManager.create(store, cwd, options?)- New session in a suppliedSessionStore. Workflow-owned sessions require the pairinternal: trueandworkflow: { runId, stageId, stageName }.SessionManager.open(store, sessionId, cwdOverride?)- Open an existing session by exact ID in that store. A missing ID raisesSessionNotFoundError; it does not create a session.SessionManager.open(path, sessionDir?, cwdOverride?)- Open a JSONL file directly, creating it if missing. This is a file API, not the CLI session selector.SessionManager.continueRecent(cwd, sessionDir?, options?)orcontinueRecent(store, cwd, options?)- Continue the most recent regular session or create one. Pass{ includeInternal: true }only to include workflow sessions.SessionManager.inMemory(cwd?, options?, entries?)- Explicit memory storage.SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?, options?)- Fork a JSONL file into the configured or explicit destination. To fork a database session, open it by ID and awaitmanager.forkTo(targetCwd, targetStore?, options?).SessionManager.importFromJsonl(inputPath, cwd, targetStoreOrDir?)- Import a JSONL export without changing its ID, upgrading older supported formats. An existing ID raisesSessionExistsError.
Static Listing Methods
SessionManager.list(cwd, storeOrDir?, onProgress?, options?)- List project sessions. Internal workflow sessions are excluded by default.SessionManager.listAll(storeOrDir?, onProgress?, options?)- List sessions across projects in the selected store. Both methods accept{ includeInternal: true, signal }to include workflow sessions or cancel listing.
SessionInfo.id to select a session. Its path contains a JSONL location when available and otherwise the ID; do not treat it as a filesystem path. To list workflow sessions in the active store:
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 JSONL file directly. For ID-based loading, useSessionManager.open(store, sessionId)to obtain a new manager.createBranchedSession(leafId)- Extract a branch into a new session in the same storeforkTo(cwd, targetStoreOrDir?, options?)- Copy the conversation into a new session at the destinationdeleteSession(sessionId)- Delete another session and its entries from the active store. Rejects for the session this manager has open; deleting an ID that is not in the store does nothing.SessionStore.delete(sessionId)is the unguarded store-level operation behind it.
Instance Methods - Appending (all return a promise of the 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 name; an empty string clears itappendSessionSummary(summary, summarizedThroughId, usage?)- Store a generated resume-picker summary, anchored to the message it describesappendCustomMessageEntry(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
buildSessionProjection()- Get the context-edited projection: source entries with their model-visible messages, plus thinkingLevel and modelbuildSessionContext()- Get messages, thinkingLevel, and model for LLMbuildContextEntries()- Active branch entries after the latest compaction boundaryappendContextEdit(targetId, replacement)- Append a branch-local context edit (nullomits the target;{ content }replaces only its content)getEntries()- All entries (excluding header)getHeader()- Session header metadatagetSessionName()- Get display name from latest session_info entry (undefinedwhen cleared or never named)getSessionNameState()- Get the name state:{ hasName: false }when never named,{ hasName: true }when the latest entry cleared it,{ hasName: true, name }when currently namedgetCwd()- Working directorygetSessionDir()- Local JSONL directory, or an empty string for non-JSONL storagegetSessionId()- Exact session ID, not necessarily a UUIDgetSessionStore()- ActiveSessionStoregetSessionFile()- Optional JSONL file location for displayisPersisted()- Whether the store is not aMemorySessionStore