Skip to content

Latest commit

 

History

History
437 lines (326 loc) · 13 KB

File metadata and controls

437 lines (326 loc) · 13 KB

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.

Scope

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

On-Disk Layout

Default session file location:

~/.omp/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:

~/.omp/agent/blobs/<sha256>

Terminal breadcrumb files are written under:

~/.omp/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

Session files are JSONL: one JSON object per line.

  • Line 1 is always the session header (type: "session").
  • Remaining lines are SessionEntry values.
  • Entries are append-only at runtime; branch navigation moves a pointer (leafId) rather than mutating existing entries.

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:

  • version is optional in v1 files; absence means v1.
  • parentSession is an opaque lineage string. Current code writes either a session id or a session path depending on flow (fork, forkFrom, createBranchedSession, or explicit newSession({ parentSession })). Treat as metadata, not a typed foreign key.

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

SessionEntry is the union of:

  • message
  • thinking_level_change
  • model_change
  • compaction
  • branch_summary
  • custom
  • custom_message
  • label
  • ttsr_injection
  • session_init
  • mode_change

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

{
  "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

{
  "type": "thinking_level_change",
  "id": "c1d2e3f4",
  "parentId": "b1c2d3e4",
  "timestamp": "2026-02-16T10:22:00.000Z",
  "thinkingLevel": "high"
}

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

{
  "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

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

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 }
}

label

{
  "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

{
  "type": "ttsr_injection",
  "id": "c2d3e4f5",
  "parentId": "b2c3d4e5",
  "timestamp": "2026-02-16T10:28:00.000Z",
  "injectedRules": ["ruleA", "ruleB"]
}

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

{
  "type": "mode_change",
  "id": "e2f3a4b5",
  "parentId": "d2e3f4a5",
  "timestamp": "2026-02-16T10:30:00.000Z",
  "mode": "plan",
  "data": { "planFile": "/tmp/plan.md" }
}

Versioning and Migration

Current session version: 3.

v1 -> v2

Applied when header version is missing or < 2:

  • Adds id and parentId to each non-header entry.
  • Reconstructs a linear parent chain using file order.
  • Migrates compaction field firstKeptEntryIndex -> firstKeptEntryId when present.
  • Sets header version = 2.

v2 -> v3

Applied when header version < 3:

  • For message entries: rewrites legacy message.role === "hookMessage" to "custom".
  • Sets header version = 3.

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

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 string id) -> 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

The underlying model is append-only tree + mutable leaf pointer:

  • Every append method creates exactly one new entry whose parentId is current leafId.
  • The new entry becomes the new leafId.
  • branch(entryId) moves only leafId; existing entries remain unchanged.
  • resetLeaf() sets leafId = null; next append creates a new root entry (parentId: null).
  • branchWithSummary() sets leaf to branch target and appends a branch_summary entry.

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)

buildSessionContext(entries, leafId, byId?) resolves what is sent to the model.

Algorithm:

  1. Determine leaf:
    • leafId === null -> return empty context.
    • explicit leafId -> use that entry if found.
    • otherwise fallback to last entry.
  2. Walk parentId chain from leaf to root and reverse to root->leaf path.
  3. Derive runtime state across path:
    • thinkingLevel from latest thinking_level_change (default "off")
    • model map from model_change entries (role ?? "default")
    • fallback models.default from assistant message provider/model if no explicit model change
    • deduplicated injectedTtsrRules from all ttsr_injection entries
    • mode/modeData from latest mode_change (default mode "none")
  4. Build message list:
    • message entries pass through
    • custom_message entries become custom AgentMessages via createCustomMessage
    • branch_summary entries become branchSummary AgentMessages via createBranchSummaryMessage
    • if a compaction exists on path:
      • emit compaction summary first (createCompactionSummaryMessage)
      • emit path entries starting at firstKeptEntryId up to the compaction boundary
      • emit entries after the compaction boundary

custom and session_init entries do not inject model context directly.

Persistence Guarantees and Failure Model

Persist vs in-memory

  • SessionManager.create/open/continueRecent/forkFrom -> persistent mode (persist = true).
  • SessionManager.inMemory -> non-persistent mode (persist = false) with MemorySessionStorage.

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

  • flush() flushes writer and calls fsync().
  • 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

  • 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

Before persisting entries:

  • Large strings are truncated to MAX_PERSIST_CHARS (500,000 chars) with notice:
    • "[Session persistence truncated large content]"
  • Transient fields partialJson and jsonlEvents are removed.
  • If object has both content and lineCount, line count is recomputed after truncation.
  • Image blocks in content arrays with base64 length >= 1024 are externalized to blob refs:
    • stored as blob:sha256:<hash>
    • raw bytes written to blob store (BlobStore.put)

On load, blob refs are resolved back to base64 for message/custom_message image blocks.

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

Defined in session-manager.ts:

  • getRecentSessions(sessionDir, limit) -> lightweight metadata for UI/session picker
  • findMostRecentSession(sessionDir) -> newest by mtime
  • list(cwd, sessionDir?) -> sessions in one project scope
  • listAll() -> sessions across all project scopes under ~/.omp/agent/sessions

Metadata extraction reads only a prefix (readTextPrefix(..., 4096)) where possible.

Related but Distinct: Prompt History Storage

HistoryStorage (history-storage.ts) is a separate SQLite subsystem for prompt recall/search, not session replay.

  • DB: ~/.omp/agent/history.db
  • Table: history(id, prompt, created_at, cwd)
  • FTS5 index: history_fts with 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.