- Home
- Documentation
- sessions
- Session Storage and Entry Model
Session Storage and Entry Model
Session Storage and Entry Model
Section titled “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.