diff --git a/packages/coding-agent/docs/session-tree.md b/packages/coding-agent/docs/session-tree.md index 2564c422..839b23ca 100644 --- a/packages/coding-agent/docs/session-tree.md +++ b/packages/coding-agent/docs/session-tree.md @@ -16,16 +16,20 @@ Context is built by scanning linearly, applying compaction ranges. ## Proposed Format (Tree) -Each entry has a `uuid` and `parentUuid` field (null for root): +Each entry has a `uuid` and `parentUuid` field (null for root). Session header includes `version` for future migrations: ```jsonl -{"type":"session","uuid":"a1b2c3","parentUuid":null,"id":"...","cwd":"..."} +{"type":"session","version":2,"uuid":"a1b2c3","parentUuid":null,"id":"...","cwd":"..."} {"type":"message","uuid":"d4e5f6","parentUuid":"a1b2c3","message":{"role":"user",...}} {"type":"message","uuid":"g7h8i9","parentUuid":"d4e5f6","message":{"role":"assistant",...}} {"type":"message","uuid":"j0k1l2","parentUuid":"g7h8i9","message":{"role":"user",...}} {"type":"message","uuid":"m3n4o5","parentUuid":"j0k1l2","message":{"role":"assistant",...}} ``` +Version history: +- **v1** (implicit): Linear format, no uuid/parentUuid +- **v2**: Tree format with uuid/parentUuid + The **last entry** is always the current leaf. Context = walk from leaf to root via `parentUuid`. Using UUIDs (like Claude Code does) instead of indices because: @@ -312,33 +316,75 @@ With tree structure, could support: ## Migration -### File Format +### Strategy: Migrate on Load + Rewrite -Add `uuid` and `parentUuid` fields to all entries. Existing sessions get generated UUIDs with linear parentage: +When loading a session, check if migration is needed. If so, migrate in memory and rewrite the file. This is transparent to users and only happens once per session file. ```typescript -function migrateSession(content: string): string { - const lines = content.trim().split('\n'); - const uuids: string[] = []; +const CURRENT_VERSION = 2; + +function loadSession(path: string): SessionEntry[] { + const content = readFileSync(path, 'utf8'); + const entries = parseEntries(content); - return lines.map((line, i) => { - const entry = JSON.parse(line); - const uuid = generateUuid(); - uuids.push(uuid); - entry.uuid = uuid; - entry.parentUuid = i === 0 ? null : uuids[i - 1]; - return JSON.stringify(entry); - }).join('\n'); + const header = entries.find(e => e.type === 'session'); + const version = header?.version ?? 1; + + if (version < CURRENT_VERSION) { + migrateEntries(entries, version); + writeFileSync(path, entries.map(e => JSON.stringify(e)).join('\n') + '\n'); + } + + return entries; +} + +function migrateEntries(entries: SessionEntry[], fromVersion: number): void { + if (fromVersion < 2) { + // v1 → v2: Add uuid/parentUuid, convert firstKeptEntryIndex + const uuids: string[] = []; + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + const uuid = generateUuid(); + uuids.push(uuid); + + entry.uuid = uuid; + entry.parentUuid = i === 0 ? null : uuids[i - 1]; + + // Update session header version + if (entry.type === 'session') { + entry.version = CURRENT_VERSION; + } + + // Convert compaction index to UUID + if (entry.type === 'compaction' && 'firstKeptEntryIndex' in entry) { + entry.firstKeptEntryUuid = uuids[entry.firstKeptEntryIndex]; + delete entry.firstKeptEntryIndex; + } + } + } + + // Future migrations: if (fromVersion < 3) { ... } } ``` -Migrated sessions work exactly as before (linear path). +### What Gets Migrated + +| v1 Field | v2 Field | +|----------|----------| +| (none) | `uuid` (generated) | +| (none) | `parentUuid` (previous entry's uuid, null for root) | +| (none on session) | `version: 2` | +| `firstKeptEntryIndex` | `firstKeptEntryUuid` | + +Migrated sessions work exactly as before (linear path). Tree features become available. ### API Compatibility - `buildSessionContext()` returns same structure - `branch()` still works, just uses UUIDs - Existing hooks continue to work +- Old sessions auto-migrate on first load ## Complexity Analysis @@ -361,7 +407,7 @@ Abandoned branches remain in file but don't affect context building performance. ## Example: Full Session with Branching ```jsonl -{"type":"session","uuid":"ses1","parentUuid":null,"id":"abc","cwd":"/project"} +{"type":"session","version":2,"uuid":"ses1","parentUuid":null,"id":"abc","cwd":"/project"} {"type":"message","uuid":"m1","parentUuid":"ses1","message":{"role":"user","content":"Build a CLI"}} {"type":"message","uuid":"m2","parentUuid":"m1","message":{"role":"assistant","content":"I'll create..."}} {"type":"message","uuid":"m3","parentUuid":"m2","message":{"role":"user","content":"Add --verbose flag"}}