- Home
- Documentation
- Sessions
- Session Storage and Entry Model
Session Storage and Entry Model
This document is the source of truth for how coding-agent sessions are represented, persisted, migrated, and reconstructed at runtime.
Covers:
- Session JSONL format and versioning
- Entry taxonomy and tree semantics (
id/parentId+ leaf pointer) - Migration/compatibility behavior when loading old or malformed files
- Context reconstruction (
buildSessionContext) - Persistence guarantees, failure behavior, truncation/blob externalization
- Storage abstractions (
FileSessionStorage,MemorySessionStorage) and related utilities
Does not cover /tree UI rendering behavior beyond semantics that affect session data.
Implementation Files
Section titled “Implementation Files”src/session/session-manager.tssrc/session/messages.tssrc/session/session-storage.tssrc/session/history-storage.tssrc/session/blob-store.ts
On-Disk Layout
Section titled “On-Disk Layout”Default session file location:
~/.xcsh/agent/sessions/--<cwd-encoded>--/<timestamp>_<sessionId>.jsonl<cwd-encoded> is derived from the working directory by stripping leading slash and replacing /, \\, and : with -.
Blob store location:
~/.xcsh/agent/blobs/<sha256>Terminal breadcrumb files are written under:
~/.xcsh/agent/terminal-sessions/<terminal-id>Breadcrumb content is two lines: original cwd, then session file path. continueRecent() prefers this terminal-scoped pointer before scanning most-recent mtime.
File Format
Section titled “File Format”Session files are JSONL: one JSON object per line.
- Line 1 is always the session header (
type: "session"). - Remaining lines are
SessionEntryvalues. - Entries are append-only at runtime; branch navigation moves a pointer (
leafId) rather than mutating existing entries.
Header (SessionHeader)
Section titled “Header (SessionHeader)”{ "type": "session", "version": 3, "id": "1f9d2a6b9c0d1234", "timestamp": "2026-02-16T10:20:30.000Z", "cwd": "/work/pi", "title": "optional session title", "parentSession": "optional lineage marker"}Notes:
versionis optional in v1 files; absence means v1.parentSessionis an opaque lineage string. Current code writes either a session id or a session path depending on flow (fork,forkFrom,createBranchedSession, or explicitnewSession({ parentSession })). Treat as metadata, not a typed foreign key.
Entry Base (SessionEntryBase)
Section titled “Entry Base (SessionEntryBase)”All non-header entries include:
{ "type": "...", "id": "8-char-id", "parentId": "previous-or-branch-parent", "timestamp": "2026-02-16T10:20:30.000Z"}parentId can be null for a root entry (first append, or after resetLeaf()).
Entry Taxonomy
Section titled “Entry Taxonomy”SessionEntry is the union of:
messagethinking_level_changemodel_changecompactionbranch_summarycustomcustom_messagelabelttsr_injectionsession_initmode_change
message
Section titled “message”Stores an AgentMessage directly.
{ "type": "message", "id": "a1b2c3d4", "parentId": null, "timestamp": "2026-02-16T10:21:00.000Z", "message": { "role": "assistant", "provider": "anthropic", "model": "claude-sonnet-4-5", "content": [{ "type": "text", "text": "Done." }], "usage": { "input": 100, "output": 20, "cacheRead": 0, "cacheWrite": 0, "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "total": 0 } }, "timestamp": 1760000000000 }}model_change
Section titled “model_change”{ "type": "model_change", "id": "b1c2d3e4", "parentId": "a1b2c3d4", "timestamp": "2026-02-16T10:21:30.000Z", "model": "openai/gpt-4o", "role": "default"}role is optional; missing is treated as default in context reconstruction.
thinking_level_change
Section titled “thinking_level_change”{ "type": "thinking_level_change", "id": "c1d2e3f4", "parentId": "b1c2d3e4", "timestamp": "2026-02-16T10:22:00.000Z", "thinkingLevel": "high"}compaction
Section titled “compaction”{ "type": "compaction", "id": "d1e2f3a4", "parentId": "c1d2e3f4", "timestamp": "2026-02-16T10:23:00.000Z", "summary": "Conversation summary", "shortSummary": "Short recap", "firstKeptEntryId": "a1b2c3d4", "tokensBefore": 42000, "details": { "readFiles": ["src/a.ts"] }, "preserveData": { "hookState": true }, "fromExtension": false}branch_summary
Section titled “branch_summary”{ "type": "branch_summary", "id": "e1f2a3b4", "parentId": "a1b2c3d4", "timestamp": "2026-02-16T10:24:00.000Z", "fromId": "a1b2c3d4", "summary": "Summary of abandoned path", "details": { "note": "optional" }, "fromExtension": true}If branching from root (branchFromId === null), fromId is the literal string "root".
custom
Section titled “custom”Extension state persistence; ignored by buildSessionContext.
{ "type": "custom", "id": "f1a2b3c4", "parentId": "e1f2a3b4", "timestamp": "2026-02-16T10:25:00.000Z", "customType": "my-extension", "data": { "state": 1 }}custom_message
Section titled “custom_message”Extension-provided message that does participate in LLM context.
{ "type": "custom_message", "id": "a2b3c4d5", "parentId": "f1a2b3c4", "timestamp": "2026-02-16T10:26:00.000Z", "customType": "my-extension", "content": "Injected context", "display": true, "details": { "debug": false }}{ "type": "label", "id": "b2c3d4e5", "parentId": "a2b3c4d5", "timestamp": "2026-02-16T10:27:00.000Z", "targetId": "a1b2c3d4", "label": "checkpoint"}label: undefined clears a label for targetId.
ttsr_injection
Section titled “ttsr_injection”{ "type": "ttsr_injection", "id": "c2d3e4f5", "parentId": "b2c3d4e5", "timestamp": "2026-02-16T10:28:00.000Z", "injectedRules": ["ruleA", "ruleB"]}session_init
Section titled “session_init”{ "type": "session_init", "id": "d2e3f4a5", "parentId": "c2d3e4f5", "timestamp": "2026-02-16T10:29:00.000Z", "systemPrompt": "...", "task": "...", "tools": ["read", "edit"], "outputSchema": { "type": "object" }}mode_change
Section titled “mode_change”{ "type": "mode_change", "id": "e2f3a4b5", "parentId": "d2e3f4a5", "timestamp": "2026-02-16T10:30:00.000Z", "mode": "plan", "data": { "planFile": "/tmp/plan.md" }}Versioning and Migration
Section titled “Versioning and Migration”Current session version: 3.
v1 -> v2
Section titled “v1 -> v2”Applied when header version is missing or < 2:
- Adds
idandparentIdto each non-header entry. - Reconstructs a linear parent chain using file order.
- Migrates compaction field
firstKeptEntryIndex->firstKeptEntryIdwhen present. - Sets header
version = 2.
v2 -> v3
Section titled “v2 -> v3”Applied when header version < 3:
- For
messageentries: rewrites legacymessage.role === "hookMessage"to"custom". - Sets header
version = 3.
Migration Trigger and Persistence
Section titled “Migration Trigger and Persistence”- Migrations run during session load (
setSessionFile). - If any migration ran, the entire file is rewritten to disk immediately.
- Migration mutates in-memory entries first, then persists rewritten JSONL.
Load and Compatibility Behavior
Section titled “Load and Compatibility Behavior”loadEntriesFromFile(path) behavior:
- Missing file (
ENOENT) -> returns[]. - Non-parseable lines are handled by lenient JSONL parser (
parseJsonlLenient). - If first parsed entry is not a valid session header (
type !== "session"or missing stringid) -> returns[].
SessionManager.setSessionFile() behavior:
[]from loader is treated as empty/nonexistent session and replaced with a new initialized session file at that path.- Valid files are loaded, migrated if needed, blob refs resolved, then indexed.
Tree and Leaf Semantics
Section titled “Tree and Leaf Semantics”The underlying model is append-only tree + mutable leaf pointer:
- Every append method creates exactly one new entry whose
parentIdis currentleafId. - The new entry becomes the new
leafId. branch(entryId)moves onlyleafId; existing entries remain unchanged.resetLeaf()setsleafId = null; next append creates a new root entry (parentId: null).branchWithSummary()sets leaf to branch target and appends abranch_summaryentry.
getEntries() returns all non-header entries in insertion order. Existing entries are not deleted in normal operation; rewrites preserve logical history while updating representation (migrations, move, targeted rewrite helpers).
Context Reconstruction (buildSessionContext)
Section titled “Context Reconstruction (buildSessionContext)”buildSessionContext(entries, leafId, byId?) resolves what is sent to the model.
Algorithm:
- Determine leaf:
leafId === null-> return empty context.- explicit
leafId-> use that entry if found. - otherwise fallback to last entry.
- Walk
parentIdchain from leaf to root and reverse to root->leaf path. - Derive runtime state across path:
thinkingLevelfrom latestthinking_level_change(default"off")- model map from
model_changeentries (role ?? "default") - fallback
models.defaultfrom assistant message provider/model if no explicit model change - deduplicated
injectedTtsrRulesfrom allttsr_injectionentries - mode/modeData from latest
mode_change(default mode"none")
- Build message list:
messageentries pass throughcustom_messageentries becomecustomAgentMessages viacreateCustomMessagebranch_summaryentries becomebranchSummaryAgentMessages viacreateBranchSummaryMessage- if a
compactionexists on path:- emit compaction summary first (
createCompactionSummaryMessage) - emit path entries starting at
firstKeptEntryIdup to the compaction boundary - emit entries after the compaction boundary
- emit compaction summary first (
custom and session_init entries do not inject model context directly.
Persistence Guarantees and Failure Model
Section titled “Persistence Guarantees and Failure Model”Persist vs in-memory
Section titled “Persist vs in-memory”SessionManager.create/open/continueRecent/forkFrom-> persistent mode (persist = true).SessionManager.inMemory-> non-persistent mode (persist = false) withMemorySessionStorage.
Write pipeline
Section titled “Write pipeline”Writes are serialized through an internal promise chain (#persistChain) and NdjsonFileWriter.
append*updates in-memory state immediately.- Persistence is deferred until at least one assistant message exists.
- Before first assistant: entries are retained in memory; no file append occurs.
- When first assistant exists: full in-memory session is flushed to file.
- Afterwards: new entries append incrementally.
Rationale in code: avoid persisting sessions that never produced an assistant response.
Durability operations
Section titled “Durability operations”flush()flushes writer and callsfsync().- Atomic full rewrites (
#rewriteFile) write to temp file, flush+fsync, close, then rename over target. - Used for migrations,
setSessionName,rewriteEntries, move operations, and tool-call arg rewrites.
Error behavior
Section titled “Error behavior”- Persistence errors are latched (
#persistError) and rethrown on subsequent operations. - First error is logged once with session file context.
- Writer close is best-effort but propagates the first meaningful error.
Data Size Controls and Blob Externalization
Section titled “Data Size Controls and Blob Externalization”Before persisting entries:
- Large strings are truncated to
MAX_PERSIST_CHARS(500,000 chars) with notice:"[Session persistence truncated large content]"
- Transient fields
partialJsonandjsonlEventsare removed. - If object has both
contentandlineCount, line count is recomputed after truncation. - Image blocks in
contentarrays with base64 length >= 1024 are externalized to blob refs:- stored as
blob:sha256:<hash> - raw bytes written to blob store (
BlobStore.put)
- stored as
On load, blob refs are resolved back to base64 for message/custom_message image blocks.
Storage Abstractions
Section titled “Storage Abstractions”SessionStorage interface provides all filesystem operations used by SessionManager:
- sync:
ensureDirSync,existsSync,writeTextSync,statSync,listFilesSync - async:
exists,readText,readTextPrefix,writeText,rename,unlink,openWriter
Implementations:
FileSessionStorage: real filesystem (Bun + node fs)MemorySessionStorage: map-backed in-memory implementation for tests/non-persistent sessions
SessionStorageWriter exposes writeLine, flush, fsync, close, getError.
Session Discovery Utilities
Section titled “Session Discovery Utilities”Defined in session-manager.ts:
getRecentSessions(sessionDir, limit)-> lightweight metadata for UI/session pickerfindMostRecentSession(sessionDir)-> newest by mtimelist(cwd, sessionDir?)-> sessions in one project scopelistAll()-> sessions across all project scopes under~/.xcsh/agent/sessions
Metadata extraction reads only a prefix (readTextPrefix(..., 4096)) where possible.
Related but Distinct: Prompt History Storage
Section titled “Related but Distinct: Prompt History Storage”HistoryStorage (history-storage.ts) is a separate SQLite subsystem for prompt recall/search, not session replay.
- DB:
~/.xcsh/agent/history.db - Table:
history(id, prompt, created_at, cwd) - FTS5 index:
history_ftswith trigger-maintained sync - Deduplicates consecutive identical prompts using in-memory last-prompt cache
- Async insertion (
setImmediate) so prompt capture does not block turn execution
Use session files for conversation graph/state replay; use HistoryStorage for prompt history UX.