InkdownInkdown
Start writing

Claude-Code

62 files·4 subfolders

Shared Workspace

Claude-Code
codex

02-state-management

Shared from "Claude-Code" on Inkdown

State Management Architecture

Overview

Claude Code uses a centralized store pattern for state management, similar to Zustand or Redux but custom-built. State is immutable and updates flow through a single pipeline.

Plain text
┌─────────────────────────────────────────────────────────────────┐
│                     STATE ARCHITECTURE                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────┐      ┌─────────────┐      ┌─────────────┐     │
│  │   Actions   │─────►│   Store     │─────►│  Subscribers│     │
│  │ (setState)  │      │ (AppState)  │      │  (useAppState)    │
│  └─────────────┘      └──────┬──────┘      └─────────────┘     │
│                              │                                  │
│                              ▼                                  │
│                     ┌─────────────────┐                         │
│                     │   Immutable     │                         │
│                     │   State Tree    │                         │
│                     └─────────────────┘                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
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 State Files

FilePurpose
state/AppStateStore.tsState type definitions + default state
state/AppState.tsxReact provider + useAppState hook
state/store.tsStore implementation (subscribe/getState/setState)
bootstrap/state.tsPre-React bootstrap state

AppState Structure

TypeScript
// Simplified view of the state tree
type AppState = {
  // Settings & Config
  settings: SettingsJson           // User preferences
  verbose: boolean               // Debug output
  mainLoopModel: ModelSetting    // Which LLM (Claude 4.5, 4.6, etc.)

  // UI State
  statusLineText: string | undefined
  expandedView: 'none' | 'tasks' | 'teammates'
  spinnerTip?: string

  // Permission System
  toolPermissionContext: ToolPermissionContext

  // Conversation
  messages: Message[]            // Full conversation history

  // Tasks (Background Work)
  tasks: { [taskId: string]: TaskState }
  foregroundedTaskId?: string

  // MCP (Model Context Protocol)
  mcp: {
    clients: MCPServerConnection[]
    tools: Tool[]
    commands: Command[]
    resources: Record<string, ServerResource[]>
  }

  // Plugins
  plugins: {
    enabled: LoadedPlugin[]
    disabled: LoadedPlugin[]
    errors: PluginError[]
  }

  // Bridge (Remote Control)
  replBridgeEnabled: boolean
  replBridgeConnected: boolean
  replBridgeSessionActive: boolean

  // Speculation (Smart Suggestions)
  speculation: SpeculationState
}

Store Implementation

The Store Pattern
TypeScript
// state/store.ts
export function createStore<T>(
  initialState: T,
  onChange?: (args: { newState: T; oldState: T }) => void
): Store<T> {
  let state = initialState
  const listeners = new Set<(state: T) => void>()

  return {
    getState: () => state,
    setState: (updater) => {
      const oldState = state
      state = updater(state)  // Immutable update
      listeners.forEach(l => l(state))
      onChange?.({ newState: state, oldState })
    },
    subscribe: (listener) => {
      listeners.add(listener)
      return () => listeners.delete(listener)  // Unsubscribe
    },
  }
}
Why Not Redux/Zustand?

Custom store provides:

  • Full control over update semantics
  • Deep immutable types for safety
  • Synchronous updates (no async middleware complexity)
  • React-agnostic core (works in non-React contexts like headless)

Using State in Components

Reading State: useAppState
TypeScript
// Select a single field - only re-renders when that field changes
const verbose = useAppState(s => s.verbose)
const messages = useAppState(s => s.messages)

// Select nested field
const { text, promptId } = useAppState(s => s.promptSuggestion)

// ❌ DON'T: Create new objects - always triggers re-render
const bad = useAppState(s => ({ count: s.messages.length }))

// ✅ DO: Select existing references
const good = useAppState(s => s.messages)
const count = good.length  // Compute in render
Updating State: useSetAppState
TypeScript
const setAppState = useSetAppState()

// Update function receives previous state
setAppState(prev => ({
  ...prev,
  verbose: !prev.verbose,
}))
Store Access (Non-React)
TypeScript
const store = useAppStateStore()

// Direct access for imperative code
const currentState = store.getState()
store.setState(updater)

Deep Immutability

Claude Code uses DeepImmutable types for safety:

TypeScript
// Makes all nested properties readonly
type DeepImmutable<T> = {
  readonly [K in keyof T]: DeepImmutable<T[K]>
}

type AppState = DeepImmutable<{
  messages: Message[]
  tasks: { [id: string]: TaskState }
  // ... all fields become deeply readonly
}>

This prevents accidental mutations at compile time.


Message State (Conversation History)

Messages are the primary source of truth for conversation state.

Message Types
TypeScript
type Message =
  | UserMessage
  | AssistantMessage
  | ToolUseBlock  // Assistant requesting tool
  | ToolResultBlock // Tool execution result
  | SystemMessage  // Internal system events
  | ProgressMessage // Tool progress updates

// Example flow:
// 1. UserMessage: "Fix the bug"
// 2. AssistantMessage: "I'll search for bugs..."
// 3. ToolUseBlock: { name: 'GrepTool', input: { pattern: 'bug' }}
// 4. ToolResultBlock: { content: 'Found in src/index.ts' }
// 5. AssistantMessage: "I found it in src/index.ts..."
Message Pipeline
Plain text
User Input → UserMessage → Query Engine → API Call
                                              ↓
Assistant Response ← Message[] ← API Response
      ↓
May contain ToolUseBlocks → Tool Execution → ToolResultBlocks
      ↓
Final AssistantMessage

Task State

Background tasks have their own state machine:

TypeScript
type TaskState = {
  id: string
  type: TaskType
  status: 'pending' | 'running' | 'completed' | 'failed' | 'killed'
  description: string
  startTime: number
  endTime?: number
  outputFile: string      // Disk-backed output
  outputOffset: number    // Bytes read so far
}
Task State Flow
Plain text
Pending → Running → Completed/Failed/Killed
   │         │
   └─────────┘
   (can restart)
Why Disk-Backed?

Tasks write output to files on disk (outputFile), not memory:

  • Prevents memory bloat from long-running tasks
  • Survives process restarts
  • Can be streamed incrementally

Permission State

TypeScript
type ToolPermissionContext = DeepImmutable<{
  mode: PermissionMode  // 'default' | 'auto' | 'bypass'

  // Rules by source (settings, .claude/, session)
  alwaysAllowRules: ToolPermissionRulesBySource
  alwaysDenyRules: ToolPermissionRulesBySource
  alwaysAskRules: ToolPermissionRulesBySource

  // Capabilities
  isBypassPermissionsModeAvailable: boolean
  isAutoModeAvailable?: boolean
}>
Permission Modes
ModeDescription
defaultAsk for dangerous tools
autoAllow after automated safety checks
bypassAllow everything (use with caution!)

MCP State

Model Context Protocol servers have state:

TypeScript
type MCPServerConnection = {
  name: string
  transport: Transport
  tools: Tool[]
  resources: ServerResource[]
  status: 'connecting' | 'connected' | 'error'
}

MCP tools are merged into the main tool list dynamically.


Speculation State

Smart suggestions use a speculation system:

TypeScript
type SpeculationState =
  | { status: 'idle' }
  | {
      status: 'active'
      id: string
      messagesRef: { current: Message[] }
      writtenPathsRef: { current: Set<string> }
      boundary: CompletionBoundary | null
    }

This lets Claude "think ahead" and pre-compute responses.


State Persistence

Session Storage
TypeScript
// utils/sessionStorage.ts
export async function recordTranscript(messages: Message[]): Promise<void>
export async function loadTranscriptFromFile(sessionId: string): Promise<Message[]>

Conversations are saved to ~/.claude/sessions/{sessionId}.jsonl

Settings Storage
TypeScript
// utils/config.ts
export function getGlobalConfig(): GlobalConfig
export function saveGlobalConfig(config: GlobalConfig): void

Stored in ~/.claude/config.json


State Update Patterns

1. Simple Update
TypeScript
setAppState(prev => ({
  ...prev,
  verbose: true,
}))
2. Nested Update
TypeScript
setAppState(prev => ({
  ...prev,
  mcp: {
    ...prev.mcp,
    clients: [...prev.mcp.clients, newClient],
  },
}))
3. Task Update
TypeScript
setAppState(prev => ({
  ...prev,
  tasks: {
    ...prev.tasks,
    [taskId]: {
      ...prev.tasks[taskId]!,
      status: 'running',
    },
  },
}))
4. Message Append
TypeScript
setAppState(prev => ({
  ...prev,
  messages: [...prev.messages, newMessage],
}))

State Flow Diagram

Plain text
┌─────────────────────────────────────────────────────────────────┐
│                        STATE FLOW                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌──────────────┐                                            │
│   │  User Types  │                                            │
│   │   "hello"    │                                            │
│   └──────┬───────┘                                            │
│          │                                                      │
│          ▼                                                      │
│   ┌──────────────┐     ┌──────────────┐                      │
│   │ Create       │────►│ setAppState  │                      │
│   │ UserMessage  │     │ (add to      │                      │
│   └──────────────┘     │ messages)    │                      │
│                        └──────┬───────┘                      │
│                               │                                 │
│                               ▼                                 │
│                        ┌──────────────┐                      │
│                        │  New State   │                      │
│                        │  Created     │                      │
│                        └──────┬───────┘                      │
│                               │                                 │
│          ┌────────────────────┼────────────────────┐           │
│          │                    │                    │           │
│          ▼                    ▼                    ▼           │
│   ┌─────────────┐    ┌─────────────┐    ┌─────────────┐      │
│   │ React       │    │ QueryEngine │    │ Persistence │      │
│   │ Re-renders  │    │ Sees new    │    │ Saves to    │      │
│   │ UI          │    │ message     │    │ disk        │      │
│   └─────────────┘    └──────┬──────┘    └─────────────┘      │
│                             │                                  │
│                             ▼                                  │
│                      ┌──────────────┐                       │
│                      │ API Call     │                       │
│                      │ to Claude    │                       │
│                      └──────────────┘                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Common State-Related Bugs

1. Stale Closures
TypeScript
// ❌ BAD: Captures stale state
const handleClick = () => {
  console.log(appState.messages)  // May be stale
}

// ✅ GOOD: Use setAppState with function
setAppState(prev => {
  console.log(prev.messages)  // Always current
  return prev
})
2. Mutation in Update
TypeScript
// ❌ BAD: Mutating existing state
setAppState(prev => {
  prev.messages.push(newMessage)  // MUTATION!
  return prev
})

// ✅ GOOD: Create new array
setAppState(prev => ({
  ...prev,
  messages: [...prev.messages, newMessage],
}))
3. Selector Creating Objects
TypeScript
// ❌ BAD: New object every render
const stats = useAppState(s => ({
  count: s.messages.length,
  tokens: s.usage.total_tokens,
}))

// ✅ GOOD: Select existing values
const messages = useAppState(s => s.messages)
const usage = useAppState(s => s.usage)
const count = messages.length
const tokens = usage.total_tokens

Key Takeaways

  1. Single Store: All state in one tree, one source of truth
  2. Immutable Updates: Always create new objects/arrays
  3. Selective Subscriptions: Use specific selectors to avoid re-renders
  4. Disk-Backed Tasks: Long output goes to files, not memory
  5. Messages Are Primary: Conversation state == message array
  6. Deep Immutable Types: Compile-time protection against mutations

Related Documentation

  • Query System - How state flows to/from API
  • Tools System - How tools update state
  • Tasks System - Background task state