Skip to main content

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 a type 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 with createdAt 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): For TypeScript definitions in your project, inspect the installed @bastani/atomic, @bastani/pi-ai, and @earendil-works/pi-agent-core packages under their dist/ directories.

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 @bastani/pi-ai)

The 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 (except SessionHeader) extend SessionEntryBase:

Entry Types

SessionHeader

First line of the file. Metadata only, not a tree entry. Its id identifies the session; it has no 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 or incomplete workflow markers remain visible in normal history. Atomic does not infer workflow ownership from 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. 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":
The rebuilt context contains one visible 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 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.

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.
Targets may be user, assistant, tool-result, or custom-message entries. 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.
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. 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.
A 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 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 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 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. Await create, 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 supplied SessionStore. Workflow-owned sessions require the pair internal: true and workflow: { runId, stageId, stageName }.
  • SessionManager.open(store, sessionId, cwdOverride?) - Open an existing session by exact ID in that store. A missing ID raises SessionNotFoundError; 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?) or continueRecent(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 await manager.forkTo(targetCwd, targetStore?, options?).
  • SessionManager.importFromJsonl(inputPath, cwd, targetStoreOrDir?) - Import a JSONL export without changing its ID, upgrading older supported formats. An existing ID raises SessionExistsError.

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.
Use the returned 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 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 JSONL file directly. For ID-based loading, use SessionManager.open(store, sessionId) to obtain a new manager.
  • createBranchedSession(leafId) - Extract a branch into a new session in the same store
  • forkTo(cwd, targetStoreOrDir?, options?) - Copy the conversation into a new session at the destination
  • deleteSession(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 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; an empty string clears it
  • appendSessionSummary(summary, summarizedThroughId, usage?) - Store a generated resume-picker summary, anchored to the message it describes
  • 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

  • buildSessionProjection() - Get the context-edited projection: source entries with their model-visible messages, plus thinkingLevel and model
  • buildSessionContext() - Get messages, thinkingLevel, and model for LLM
  • buildContextEntries() - Active branch entries after the latest compaction boundary
  • appendContextEdit(targetId, replacement) - Append a branch-local context edit (null omits the target; { content } replaces only its content)
  • getEntries() - All entries (excluding header)
  • getHeader() - Session header metadata
  • getSessionName() - Get display name from latest session_info entry (undefined when 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 named
  • getCwd() - Working directory
  • getSessionDir() - Local JSONL directory, or an empty string for non-JSONL storage
  • getSessionId() - Exact session ID, not necessarily a UUID
  • getSessionStore() - Active SessionStore
  • getSessionFile() - Optional JSONL file location for display
  • isPersisted() - Whether the store is not a MemorySessionStore