InkdownInkdown
Start writing

Claude-Code

62 files·4 subfolders

Shared Workspace

Claude-Code
codex

19-session-persistence

Shared from "Claude-Code" on Inkdown

Session Persistence & Storage Architecture

Overview

Sessions are the unit of durability in Claude Code. Every conversation is recorded, compressed, and stored for later retrieval, resume, or analysis. This system enables long-term memory and conversation recovery.

Plain text
┌─────────────────────────────────────────────────────────────────────────────┐
│                    SESSION LIFECYCLE                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  CREATE                                                                      │
│     │                                                                        │
│     ▼                                                                        │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐                     │
│  │  Start      │───►│  Messages   │───►│  Write to   │                     │
│  │  Session    │    │  Buffer     │    │  Disk       │                     │
│  │             │    │  (memory)   │    │  (.jsonl)   │                     │
│  └─────────────┘    └──────┬──────┘    └──────┬──────┘                     │
│                           │                   │                              │
│  RUNTIME ──► Continuous append to disk        │                              │
│                           │                   │                              │
│                           ▼                   ▼                              │
│                    ┌─────────────┐    ┌─────────────┐                       │
│                    │  Auto-Save  │    │  Sessions   │                       │
│                    │  (every 5s) │    │  Registry   │                       │
│                    └─────────────┘    │  (index)    │                       │
│                                        └──────┬──────┘                       │
│                                               │                              │
│  END ──► Final flush + metadata update        │                              │
│                                               │                              │
│  ┌─────────────┐    ┌─────────────┐           │                              │
│  │  Compress   │───►│  Update     │◄──────────┘                              │
│  │  History    │    │  Index      │                                          │
│  │  (gzip)     │    │             │                                          │
│  └─────────────┘    └─────────────┘                                          │
│                                                                             │
│  RESUME ──► Load from disk ──► Replay messages ──► Continue conversation   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
0000_start_here_index_and_recommended_reading_order.md
0100_project_overview_tech_stack_runtime_modes_and_folder_map.md
0200_startup_flow_entry_points_and_cold_start_sequence.md
0300_codebase_modules_layers_state_models_and_schemas.md
0400_system_architecture_and_design_rationale.md
0500_interactive_repl_request_flow_end_to_end.md
0600_headless_sdk_and_print_mode_request_flow_end_to_end.md
0700_mcp_integration_connection_and_tool_call_flow.md
0800_external_services_sdks_storage_and_local_dependencies.md
0900_environment_variables_settings_feature_flags_and_failure_modes.md
1000_non_obvious_patterns_gotchas_and_debugging_traps.md
1100_full_codebase_file_inventory_grouped_by_directory.md
kimi
00-overview.md
01-entrypoints.md
02-state-management.md
03-query-system.md
04-tools-system.md
05-tasks-system.md
06-ui-components.md
07-bridge-remote.md
08-services.md
09-skills-plugins.md
10-commands.md
11-testing-architecture.md
12-permission-system.md
13-build-system.md
14-ink-internals.md
15-git-internals.md
16-context-compaction.md
17-vim-mode.md
18-mailbox-notifications.md
19-session-persistence.md
20-hooks-system.md
21-error-recovery.md
README.md
qwen
00-overview.md
01-entry-points.md
02-query-engine.md
03-tools-and-tasks.md
04-commands-and-skills.md
05-state-management.md
06-ink-rendering.md
07-bridge-remote.md
08-mcp-services.md
09-services-overview.md
10-multi-agent.md
11-system-prompt-constants.md
12-tool-interface.md
13-memory-system.md
14-buddy-companion.md
15-keybindings.md
16-stop-hooks.md
17-vim-mode.md
18-upstreamproxy.md
19-cost-tracking-history.md
20-contexts-styles-onboarding.md
21-hooks.md
22-screens.md
tweets-explain
claude-code-memory-analysis.md
compact
memory-system
agentic-architecture

Core Files

FilePurpose
utils/sessionStorage.tsSession persistence API
utils/conversationRecovery.tsCrash recovery
utils/sessionStorage/index.tsStorage backends
commands/resume/index.ts/resume command
commands/session/index.ts/session command

Storage Format: JSONL

Sessions use JSON Lines format - one JSON object per line for append-only efficiency.

Plain text
~/.claude/sessions/
├── abc123def.jsonl      # Session transcript
├── abc123def.meta.json  # Session metadata
├── def456ghi.jsonl
├── index.json           # Session registry
└── compressed/
    └── abc123def.jsonl.gz   # Archived sessions
Transcript Format (.jsonl)
JSON
// Line 1: System initialization
{"type":"system_init","timestamp":1703001600000,"cwd":"/Users/dev/project","model":"claude-sonnet-4-6"}

// Line 2: User message
{"type":"user","uuid":"msg-001","content":[{"type":"text","text":"Fix the bug"}],"timestamp":1703001605000}

// Line 3: Assistant response (streaming chunks aggregated)
{"type":"assistant","uuid":"msg-002","content":[{"type":"text","text":"I'll help..."}],"thinking":{"thinking":"Let me analyze...","signature":"..."},"timestamp":1703001610000,"usage":{"input_tokens":100,"output_tokens":500}}

// Line 4: Tool use
{"type":"tool_use","uuid":"tool-001","name":"BashTool","input":{"command":"grep -r bug src/"},"timestamp":1703001615000}

// Line 5: Tool result
{"type":"tool_result","tool_use_id":"tool-001","content":"Found in src/index.ts:42","is_error":false,"timestamp":1703001620000}
Metadata Format (.meta.json)
JSON
{
  "sessionId": "abc123def",
  "createdAt": "2024-01-15T10:00:00Z",
  "lastActiveAt": "2024-01-15T11:30:00Z",
  "messageCount": 42,
  "tokenUsage": {
    "input": 15000,
    "output": 25000
  },
  "cwd": "/Users/dev/project",
  "gitCommit": "a1b2c3d",
  "title": "Fixing authentication bug",
  "isArchived": false,
  "compressionRatio": 1.0
}

Session Storage API

TypeScript
// utils/sessionStorage.ts
export type SessionStorage = {
  // Session identification
  sessionId: string

  // Append message to transcript (atomic)
  appendMessage(message: Message): Promise<void>

  // Read all messages
  readMessages(): Promise<Message[]>

  // Read partial range (for large sessions)
  readMessagesRange(start: number, end: number): Promise<Message[]>

  // Update metadata
  updateMetadata(updates: Partial<SessionMetadata>): Promise<void>

  // Get metadata
  getMetadata(): Promise<SessionMetadata>

  // Compact the session (reclaim space)
  compact(): Promise<void>

  // Archive (compress to .gz)
  archive(): Promise<void>
}
Implementation
TypeScript
// utils/sessionStorage.ts
export async function createSessionStorage(
  sessionId: string
): Promise<SessionStorage> {
  const sessionDir = getSessionDir()
  const transcriptPath = join(sessionDir, `${sessionId}.jsonl`)
  const metaPath = join(sessionDir, `${sessionId}.meta.json`)

  // Ensure directory exists
  await mkdir(sessionDir, { recursive: true })

  return {
    sessionId,

    async appendMessage(message: Message) {
      const line = JSON.stringify(message) + '\n'
      await appendFile(transcriptPath, line)

      // Update metadata
      await this.updateMetadata({
        lastActiveAt: Date.now(),
        messageCount: (await this.getMetadata()).messageCount + 1,
      })
    },

    async readMessages(): Promise<Message[]> {
      const content = await readFile(transcriptPath, 'utf-8')
      const lines = content.trim().split('\n')

      return lines
        .filter(line => line.trim())
        .map(line => JSON.parse(line))
    },

    async readMessagesRange(start: number, end: number): Promise<Message[]> {
      // Efficient range read using streams
      const stream = createReadStream(transcriptPath, {
        encoding: 'utf-8',
      })

      const messages: Message[] = []
      let lineNumber = 0

      for await (const line of createInterface(stream)) {
        if (lineNumber >= start && lineNumber < end) {
          messages.push(JSON.parse(line))
        }
        lineNumber++
        if (lineNumber >= end) break
      }

      return messages
    },

    async updateMetadata(updates: Partial<SessionMetadata>) {
      const current = await this.getMetadata()
      const merged = { ...current, ...updates }
      await writeFile(metaPath, JSON.stringify(merged, null, 2))
    },

    async getMetadata(): Promise<SessionMetadata> {
      try {
        const content = await readFile(metaPath, 'utf-8')
        return JSON.parse(content)
      } catch {
        // Return default if not exists
        return {
          sessionId,
          createdAt: Date.now(),
          messageCount: 0,
        }
      }
    },

    async compact() {
      // Remove duplicate/redundant entries
      // Re-serialize efficiently
      const messages = await this.readMessages()
      const optimized = optimizeMessageHistory(messages)

      // Atomic rewrite
      const tempPath = transcriptPath + '.tmp'
      await writeFile(
        tempPath,
        optimized.map(m => JSON.stringify(m)).join('\n') + '\n'
      )
      await rename(tempPath, transcriptPath)
    },

    async archive() {
      const compressedPath = transcriptPath + '.gz'

      await pipeline(
        createReadStream(transcriptPath),
        createGzip(),
        createWriteStream(compressedPath)
      )

      await unlink(transcriptPath)

      await this.updateMetadata({
        isArchived: true,
      })
    },
  }
}

Session Registry

Index Management
TypeScript
// utils/sessionStorage/registry.ts
type SessionRegistry = {
  sessions: Array<{
    id: string
    createdAt: number
    lastActiveAt: number
    title?: string
    messageCount: number
    isArchived: boolean
  }>
}

export async function updateRegistry(
  sessionId: string,
  updates: Partial<SessionEntry>
): Promise<void> {
  const registryPath = join(getSessionDir(), 'index.json')

  let registry: SessionRegistry
  try {
    const content = await readFile(registryPath, 'utf-8')
    registry = JSON.parse(content)
  } catch {
    registry = { sessions: [] }
  }

  const existingIndex = registry.sessions.findIndex(s => s.id === sessionId)

  if (existingIndex >= 0) {
    registry.sessions[existingIndex] = {
      ...registry.sessions[existingIndex],
      ...updates,
    }
  } else {
    registry.sessions.push({
      id: sessionId,
      createdAt: Date.now(),
      lastActiveAt: Date.now(),
      messageCount: 0,
      isArchived: false,
      ...updates,
    })
  }

  // Sort by last active (newest first)
  registry.sessions.sort((a, b) => b.lastActiveAt - a.lastActiveAt)

  await writeFile(registryPath, JSON.stringify(registry, null, 2))
}

export async function listSessions(): Promise<SessionEntry[]> {
  const registryPath = join(getSessionDir(), 'index.json')

  try {
    const content = await readFile(registryPath, 'utf-8')
    const registry: SessionRegistry = JSON.parse(content)
    return registry.sessions.filter(s => !s.isArchived)
  } catch {
    return []
  }
}

Conversation Recovery

Crash Detection & Recovery
TypeScript
// utils/conversationRecovery.ts
export async function checkForCrashedSessions(): Promise<string[]> {
  const sessions = await listSessions()
  const crashed: string[] = []

  for (const session of sessions) {
    const storage = await createSessionStorage(session.id)
    const metadata = await storage.getMetadata()

    // Check if session has recent activity but wasn't properly closed
    const timeSinceLastActivity = Date.now() - metadata.lastActiveAt
    const wasRecentlyActive = timeSinceLastActivity < 60000  // 1 minute
    const hasNoEndMarker = !metadata.endedAt

    if (wasRecentlyActive && hasNoEndMarker) {
      crashed.push(session.id)
    }
  }

  return crashed
}

export async function recoverSession(sessionId: string): Promise<Message[]> {
  const storage = await createSessionStorage(sessionId)

  // Read messages
  const messages = await storage.readMessages()

  // Validate integrity
  const corruptedIndices = validateMessageChain(messages)

  if (corruptedIndices.length > 0) {
    // Try to repair by removing corrupted entries
    const repaired = messages.filter((_, i) => !corruptedIndices.includes(i))

    // Log repair
    await storage.updateMetadata({
      repairedAt: Date.now(),
      removedMessages: corruptedIndices.length,
    })

    return repaired
  }

  return messages
}

function validateMessageChain(messages: Message[]): number[] {
  const corrupted: number[] = []
  const seenToolUses = new Set<string>()

  for (let i = 0; i < messages.length; i++) {
    const msg = messages[i]

    // Check for duplicate UUIDs
    if (seenToolUses.has(msg.uuid)) {
      corrupted.push(i)
      continue
    }
    seenToolUses.add(msg.uuid)

    // Validate tool_use has corresponding tool_result
    if (msg.type === 'tool_use') {
      const hasResult = messages.slice(i + 1).some(
        m => m.type === 'tool_result' && m.tool_use_id === msg.id
      )
      if (!hasResult) {
        // Incomplete - might be corrupted or just interrupted
        corrupted.push(i)
      }
    }
  }

  return corrupted
}

Resume Flow

The /resume Command
TypeScript
// commands/resume/index.ts
const resumeCommand: PromptCommand = {
  type: 'prompt',
  name: 'resume',
  description: 'Resume a previous conversation',

  async getPromptForCommand(args, context) {
    const sessions = await listSessions()

    if (args[0]) {
      // Resume specific session
      const sessionId = args[0]
      const messages = await recoverSession(sessionId)

      return {
        messages: [
          createSystemMessage({
            content: `Resuming conversation ${sessionId}. Context loaded.`,
          }),
          ...messages,
        ],
      }
    }

    // Show session chooser
    const recentSessions = sessions.slice(0, 10)

    return {
      messages: [
        createUserMessage({
          content: `Choose a session to resume:\n${
            recentSessions.map((s, i) =>
              `${i + 1}. ${s.title || 'Untitled'} (${s.messageCount} messages)`
            ).join('\n')
          }`,
        }),
      ],
    }
  },
}
Lazy Loading Large Sessions
TypeScript
// For sessions with thousands of messages
export async function loadSessionLazy(
  sessionId: string,
  options: { recentOnly?: number } = {}
): Promise<Message[]> {
  const storage = await createSessionStorage(sessionId)
  const metadata = await storage.getMetadata()

  if (options.recentOnly && metadata.messageCount > options.recentOnly) {
    // Only load recent messages for performance
    const start = metadata.messageCount - options.recentOnly
    const messages = await storage.readMessagesRange(start, metadata.messageCount)

    // Add summary marker
    return [
      createSystemMessage({
        content: `[${start} earlier messages omitted for performance. Full history available.]`,
      }),
      ...messages,
    ]
  }

  return storage.readMessages()
}

Session Analytics

Usage Statistics
TypeScript
// commands/insights.ts
export async function generateInsights(): Promise<InsightReport> {
  const sessions = await listSessions()
  const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000

  const recentSessions = sessions.filter(s => s.lastActiveAt > thirtyDaysAgo)

  // Calculate metrics
  const totalMessages = recentSessions.reduce(
    (sum, s) => sum + s.messageCount,
    0
  )

  const totalTokens = recentSessions.reduce(
    (sum, s) => sum + (s.tokenUsage?.output || 0),
    0
  )

  const mostActiveDay = getMostActiveDay(recentSessions)

  const topCommands = getTopCommands(recentSessions)

  const avgSessionLength = totalMessages / recentSessions.length

  return {
    period: '30 days',
    sessionCount: recentSessions.length,
    totalMessages,
    totalTokens,
    mostActiveDay,
    topCommands,
    avgSessionLength,
  }
}

Key Design Decisions

  1. JSONL for Append-Only: Efficient for streaming writes, no rewrite on each message
  2. Separate Metadata: Fast index operations without parsing full transcripts
  3. Lazy Loading: Large sessions load recent messages first
  4. Crash Recovery: Detect interrupted sessions and offer recovery
  5. Compression: Archive old sessions to save disk space

Storage Location

Bash
~/.claude/
├── sessions/           # Session transcripts
├── config.json        # User settings
├── cache/            # Temporary data
├── plugins/          # Installed plugins
├── skills/           # Custom skills
└── snapshots/        # File snapshots for /rewind