Claude Code 源码分析Claude Code 源码分析
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
  • 概览

    • Claude Code 源码分析
    • 源码统计
    • CodeGraph 图谱
  • 架构

    • 系统架构
    • UML 图表索引
    • 查询引擎
    • 核心流程
    • 消息系统
    • 状态管理
  • 功能模块

    • 工具系统
    • 斜杠命令
    • 服务层
    • MCP 协议
    • Skills 技能
    • 子代理系统
  • 分层深度

    • 入口层
    • UI / Ink 层
    • utils 基础设施
    • 桥接 / 远程
    • 上下文压缩
  • 原理与安全

    • 底层原理
    • 技术难点
    • 权限与安全
    • 内部机制
    • 遥测与分析
  • 深度专题

    • Hooks 系统
    • 插件系统
    • 记忆系统
    • API 通信层
    • Ink 终端 UI
    • 认证系统
    • 构建与发布
    • 术语表
  • 调用分析

    • 调用链分析
    • 核心文件索引
  • 模块详解

    • utils

      • 模块: utils
      • messages · 消息工厂与规范化
      • session-storage · JSONL 会话持久化
      • permissions · 工具权限决策
      • shell-hooks · 用户 Shell Hook 系统
    • components

      • 模块: components
      • REPL · 主屏编排
      • messages · 消息行渲染
      • PermissionRequest · 权限弹窗
      • PromptInput · 底部输入
    • services

      • 模块: services
      • api-claude · Anthropic API 流式与重试
      • mcp-client · MCP 连接与工具调用
      • compact · 上下文压缩与自动触发
      • analytics · GrowthBook、Datadog 与 1P 事件
    • tools

      • 模块: tools
      • tool-interface · Tool 契约与注册表
      • bash-tool · Shell 执行与权限
      • streaming-executor · 流式工具并发调度
      • agent-tool · 子 Agent 委派
    • commands

      • 模块: commands
      • command-registry · commands.ts 注册与分派
      • model-command · /model 模型选择
      • mcp-commands · /mcp 服务器管理
      • compact-memory-commands · /compact 与 /memory
    • ink

      • 模块: ink
      • Ink 渲染管线 · Screen 与终端输出
      • 终端事件 · resize、paste、stdin
      • Ink Hooks · 输入、搜索、终端状态
      • Ink 组件 · Box、Text、ScrollBox 原语
    • hooks

      • 模块: hooks
      • useCanUseTool · 权限 UI 接缝
      • 输入与快捷键 Hook
      • 合并态 Hook(MCP + 本地)
      • notifs 通知 Hook
    • bridge

      • 模块: bridge
      • repl-bridge · REPL 桥初始化与传输
      • bridge-messaging · 桥消息路由与入站处理
      • remote-bridge-core · env-less 核心与守护主循环
      • bridge-permissions-ui · 权限、API 与 TUI
    • cli

      • 模块: cli
      • Structured IO · NDJSON SDK 协议
      • CLI Transports · Session Ingress 传输层
      • CLI Handlers · 子命令懒加载实现
      • Update & Upload · 自更新与串行上传原语
    • screens

      • 模块: screens
      • REPL 屏 · Screen 类型与顶层路由
      • ResumeConversation · 会话恢复选择器
      • Doctor · 安装诊断全屏
    • entrypoints

      • 模块: entrypoints
      • cli-entrypoint · Bootstrap 与快路径
      • sdk-types · core / control / runtime 类型体系
      • mcp-entrypoint · MCP stdio 服务器
      • sandbox-types · 沙箱配置单一真相源
    • skills

      • 模块: skills
      • skills-loading · 磁盘加载与 bundled 注册表
      • bundled-skills · 内置 skill 与 initBundledSkills
      • mcp-skills · MCP prompt 转 skill
      • skill-tool-integration · SkillTool 与命令注册
    • types

      • 模块: types
      • message-types · Message 联合与 content blocks
      • tool-permission-types · Tool、Permission、Command 类型
      • api-sdk-types · API 与 Hooks 协议类型
      • misc-types · ids、plugin、generated 与其余类型
    • tasks

      • 模块: tasks
      • local-agent-task · 本地 Agent 与主会话后台化
      • remote-agent-task · 远程 CCR 与 In-Process Teammate
      • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
      • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybindings

      • 模块: keybindings
      • keybinding-registry · 注册、Provider 与 useKeybinding
      • default-bindings · 默认键位表与平台差异
      • command-bindings · command:* 动态斜杠命令绑定
      • vim-bindings · Vim 模式与 keybindings 边界
    • memdir

      • 模块: memdir
      • memdir-core · 路径、加载与 MEMORY.md
      • memory-extraction · extractMemories 与 SessionMemory
      • memdir-commands · /memory、/remember 与命令集成
    • state

      • 模块: state
      • app-state-core · store、AppState 类型与 Provider
      • app-state-selectors · selectors 与 onChangeAppState
      • teammate-state · 队友视图与 swarm 状态
      • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query

      • 模块: query
      • query config 与 deps · 配置快照与依赖注入
      • query tokenBudget · +500k 自动续跑
      • query transitions · Continue / Terminal 状态机
      • query stopHooks · Stop 事件与 turn 结束编排
  • 模块详解(扩展)

    • messages · 消息工厂与规范化
    • session-storage · JSONL 会话持久化
    • permissions · 工具权限决策
    • shell-hooks · 用户 Shell Hook 系统
    • REPL · 主屏编排
    • messages · 消息行渲染
    • PermissionRequest · 权限弹窗
    • PromptInput · 底部输入
    • api-claude · Anthropic API 流式与重试
    • mcp-client · MCP 连接与工具调用
    • compact · 上下文压缩与自动触发
    • analytics · GrowthBook、Datadog 与 1P 事件
    • tool-interface · Tool 契约与注册表
    • bash-tool · Shell 执行与权限
    • streaming-executor · 流式工具并发调度
    • agent-tool · 子 Agent 委派
    • command-registry · commands.ts 注册与分派
    • model-command · /model 模型选择
    • mcp-commands · /mcp 服务器管理
    • compact-memory-commands · /compact 与 /memory
    • Ink 渲染管线 · Screen 与终端输出
    • 终端事件 · resize、paste、stdin
    • Ink Hooks · 输入、搜索、终端状态
    • Ink 组件 · Box、Text、ScrollBox 原语
    • useCanUseTool · 权限 UI 接缝
    • 输入与快捷键 Hook
    • 合并态 Hook(MCP + 本地)
    • notifs 通知 Hook
    • repl-bridge · REPL 桥初始化与传输
    • bridge-messaging · 桥消息路由与入站处理
    • remote-bridge-core · env-less 核心与守护主循环
    • bridge-permissions-ui · 权限、API 与 TUI
    • Structured IO · NDJSON SDK 协议
    • CLI Transports · Session Ingress 传输层
    • CLI Handlers · 子命令懒加载实现
    • Update & Upload · 自更新与串行上传原语
    • REPL 屏 · Screen 类型与顶层路由
    • ResumeConversation · 会话恢复选择器
    • Doctor · 安装诊断全屏
    • cli-entrypoint · Bootstrap 与快路径
    • sdk-types · core / control / runtime 类型体系
    • mcp-entrypoint · MCP stdio 服务器
    • sandbox-types · 沙箱配置单一真相源
    • skills-loading · 磁盘加载与 bundled 注册表
    • bundled-skills · 内置 skill 与 initBundledSkills
    • mcp-skills · MCP prompt 转 skill
    • skill-tool-integration · SkillTool 与命令注册
    • message-types · Message 联合与 content blocks
    • tool-permission-types · Tool、Permission、Command 类型
    • api-sdk-types · API 与 Hooks 协议类型
    • misc-types · ids、plugin、generated 与其余类型
    • local-agent-task · 本地 Agent 与主会话后台化
    • remote-agent-task · 远程 CCR 与 In-Process Teammate
    • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
    • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybinding-registry · 注册、Provider 与 useKeybinding
    • default-bindings · 默认键位表与平台差异
    • command-bindings · command:* 动态斜杠命令绑定
    • vim-bindings · Vim 模式与 keybindings 边界
    • memdir-core · 路径、加载与 MEMORY.md
    • memory-extraction · extractMemories 与 SessionMemory
    • memdir-commands · /memory、/remember 与命令集成
    • app-state-core · store、AppState 类型与 Provider
    • app-state-selectors · selectors 与 onChangeAppState
    • teammate-state · 队友视图与 swarm 状态
    • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query config 与 deps · 配置快照与依赖注入
    • query tokenBudget · +500k 自动续跑
    • query transitions · Continue / Terminal 状态机
    • query stopHooks · Stop 事件与 turn 结束编排
  • 工具详解

    • tool-interface · Tool 契约与注册表
    • tool-permission-types · Tool、Permission、Command 类型
    • 工具: Bash
    • 工具: PowerShell
    • 工具: Agent
    • 工具: LSP
    • 工具: FileEdit
    • 工具: FileRead
    • 工具: Skill
    • 工具: WebFetch
    • 工具: MCP
    • 工具: SendMessage
    • 工具: FileWrite
    • 工具: Config
    • 工具: Grep
    • 工具: Brief
    • 工具: ExitPlanMode
    • 工具: ToolSearch
    • 工具: NotebookEdit
    • 工具: TaskOutput
    • 工具: WebSearch
    • 工具: ScheduleCron

本章总览

AppState 不是 Claude Code 状态的全部。bootstrap/state.ts 持有进程级 sessionId、cost、model override;utils/sessionStorage.ts 把 transcript 写入 JSONL;utils/fileStateCache.ts 在 Tool 层缓存读文件内容供 Edit/Write 与 compact 使用。本章画出三条边界如何与 onChangeAppState、getDefaultAppState 接缝,以及为何「不要在 bootstrap 再加 STATE 字段」与「FileStateCache 不进 AppState」是 deliberate 架构选择。

学完本章你应该能

  • 区分 bootstrap STATE 与 AppState 的职责
  • 解释 switchSession 与 getSessionProjectDir 的原子性
  • 描述 sessionStorage 与 bootstrap sessionId 的对齐要求
  • 说明 FileStateCache 为何放在 ToolUseContext 而非 AppState
  • 理解 compact 时 cloneFileStateCache / mergeFileStateCaches 流程
  • 列举 mainLoopModel 从 AppState 到 bootstrap override 的同步链

核心概念(先读懂这些)

bootstrap 是 DAG 叶节点

bootstrap/state.ts 注释 DO NOT ADD MORE STATE HERE——文件已 1700+ 行。新 session 级状态应优先 AppState 或专用模块;bootstrap 仅保留 query/telemetry 必须在 React 外可读的数据。import 规则 bootstrap-isolation 阻止 bootstrap 从 src/utils 拉重型依赖。

sessionStorage 读 bootstrap,不写 AppState

sessionStorage import getSessionId、getOriginalCwd、switchSession。resume 加载 transcript 后由 REPL setMessages,AppState.tasks 由 task 系统填充——JSONL 不是 AppState 镜像,而是 audit / resume 真相源。

FileStateCache 是 Tool 会话缓存

REPL 创建 createFileStateCacheWithSizeLimit,传入 QueryEngine / ToolUseContext.readFileState。compact 快照 cacheToObject 进 JSONL,恢复时 load。与 AppState.fileHistory(undo 快照)互补而非重复。

建议学习步骤

  1. 阅读 bootstrap getInitialState 与 sessionId API
  2. 阅读 switchSession / onSessionSwitch
  3. 阅读 sessionStorage getTranscriptPath 与 bootstrap 注释
  4. 阅读 FileStateCache LRU 构造
  5. 阅读 REPL mergeFileStateCaches 用法
  6. 追踪 onChangeAppState → setMainLoopModelOverride

常见误区

注意

import 时缓存 cwd 会导致 session 路径 split-brain——sessionStorage 注释反复强调

注意

isSessionPersistenceDisabled 跳磁盘写入但不阻止内存 messages

注意

FileStateCache normalize 路径——Windows / vs \ 混用仍命中

注意

bootstrap regenerateSessionId 清 planSlugCache 条目防泄漏

三层状态全景

┌─────────────────────────────────────────────────────────┐
│ AppState (React store)                                   │
│  UI、tasks、mcp、permissions、viewingAgentTaskId         │
└───────────────┬─────────────────────┬───────────────────┘
                │ onChangeAppState    │ getState 快照
                ▼                     ▼
┌───────────────────────┐   ┌─────────────────────────────┐
│ bootstrap/state.ts    │   │ ToolUseContext               │
│ sessionId, cost, cwd  │   │ readFileState: FileStateCache│
│ modelOverride         │   └─────────────────────────────┘
└───────────┬───────────┘
            │ getSessionId / getOriginalCwd
            ▼
┌───────────────────────────────────────────────────────────┐
│ utils/sessionStorage.ts → ~/.claude/projects/.../id.jsonl │
└───────────────────────────────────────────────────────────┘

读代码时先问:这个字段重启后要不要保留?要 → JSONL 或 settings;仅 session → bootstrap 或 AppState;仅 turn → Tool context / local variable。

bootstrap/state.ts 核心 API

State 类型含 80+ 字段(cost、telemetry、session flags、beta header latches…)。

关键 session API:

函数作用
getSessionId当前 UUID
regenerateSessionId新 session;可选 setCurrentAsParent
switchSession(id, projectDir)原子切换 session + 项目目录
onSessionSwitch订阅 switch(concurrentSessions PID 同步)
getSessionProjectDir跨项目 resume 的 transcript 目录
getOriginalCwd / getProjectRoot项目身份 vs 当前文件 cwd

getMainLoopModelOverride / setMainLoopModelOverride 与 AppState.mainLoopModel 通过 onChange 同步。

注释:DO NOT ADD MORE STATE HERE — 新功能默认不进 bootstrap。

源码引用: src/bootstrap/state.ts · 第 31–50 行(共 1759 行)

  31| // DO NOT ADD MORE STATE HERE - BE JUDICIOUS WITH GLOBAL STATE
  32| 
  33| // dev: true on entries that came via --dangerously-load-development-channels.
  34| // The allowlist gate checks this per-entry (not the session-wide
  35| // hasDevChannels bit) so passing both flags doesn't let the dev dialog's
  36| // acceptance leak allowlist-bypass to the --channels entries.
  37| export type ChannelEntry =
  38|   | { kind: 'plugin'; name: string; marketplace: string; dev?: boolean }
  39|   | { kind: 'server'; name: string; dev?: boolean }
  40| 
  41| export type AttributedCounter = {
  42|   add(value: number, additionalAttributes?: Attributes): void
  43| }
  44| 
  45| type State = {
  46|   originalCwd: string
  47|   // Stable project root - set once at startup (including by --worktree flag),
  48|   // never updated by mid-session EnterWorktreeTool.
  49|   // Use for project identity (history, skills, sessions) not file operations.
  50|   projectRoot: string

源码引用: src/bootstrap/state.ts · 第 431–478 行(共 1759 行)

 431| export function getSessionId(): SessionId {
 432|   return STATE.sessionId
 433| }
 434| 
 435| export function regenerateSessionId(
 436|   options: { setCurrentAsParent?: boolean } = {},
 437| ): SessionId {
 438|   if (options.setCurrentAsParent) {
 439|     STATE.parentSessionId = STATE.sessionId
 440|   }
 441|   // Drop the outgoing session's plan-slug entry so the Map doesn't
 442|   // accumulate stale keys. Callers that need to carry the slug across
 443|   // (REPL.tsx clearContext) read it before calling clearConversation.
 444|   STATE.planSlugCache.delete(STATE.sessionId)
 445|   // Regenerated sessions live in the current project: reset projectDir to
 446|   // null so getTranscriptPath() derives from originalCwd.
 447|   STATE.sessionId = randomUUID() as SessionId
 448|   STATE.sessionProjectDir = null
 449|   return STATE.sessionId
 450| }
 451| 
 452| export function getParentSessionId(): SessionId | undefined {
 453|   return STATE.parentSessionId
 454| }
 455| 
 456| /**
 457|  * Atomically switch the active session. `sessionId` and `sessionProjectDir`
 458|  * always change together — there is no separate setter for either, so they
 459|  * cannot drift out of sync (CC-34).
 460|  *
 461|  * @param projectDir — directory containing `<sessionId>.jsonl`. Omit (or
 462|  *   pass `null`) for sessions in the current project — the path will derive
 463|  *   from originalCwd at read time. Pass `dirname(transcriptPath)` when the
 464|  *   session lives in a different project directory (git worktrees,
 465|  *   cross-project resume). Every call resets the project dir; it never
 466|  *   carries over from the previous session.
 467|  */
 468| export function switchSession(
 469|   sessionId: SessionId,
 470|   projectDir: string | null = null,
 471| ): void {
 472|   // Drop the outgoing session's plan-slug entry so the Map stays bounded
 473|   // across repeated /resume. Only the current session's slug is ever read
 474|   // (plans.ts getPlanSlug defaults to getSessionId()).
 475|   STATE.planSlugCache.delete(STATE.sessionId)
 476|   STATE.sessionId = sessionId
 477|   STATE.sessionProjectDir = projectDir
 478|   sessionSwitched.emit(sessionId)

源码引用: src/bootstrap/state.ts · 第 838–854 行(共 1759 行)

 838| export function getMainLoopModelOverride(): ModelSetting | undefined {
 839|   return STATE.mainLoopModelOverride
 840| }
 841| 
 842| export function getInitialMainLoopModel(): ModelSetting {
 843|   return STATE.initialMainLoopModel
 844| }
 845| 
 846| export function setMainLoopModelOverride(
 847|   model: ModelSetting | undefined,
 848| ): void {
 849|   STATE.mainLoopModelOverride = model
 850| }
 851| 
 852| export function setInitialMainLoopModel(model: ModelSetting): void {
 853|   STATE.initialMainLoopModel = model
 854| }

bootstrap 与 cost / telemetry

Cost 与 API 用量在 bootstrap 累加:

  • addToTotalCostState、getTotalCostUSD
  • modelUsage 字典 per model
  • turnToolDurationMs / turnHookDurationMs 分 turn 统计
  • setCostStateForRestore — resume 时从 JSONL metadata 恢复

AppState 不 mirror cost——StatusLine、/cost 读 bootstrap。UI spinner 读 AppState.tasks 与 queryGuard。

markPostCompaction / consumePostCompaction 供 analytics 标记 compaction 后首次 API。

这与 AppState 分工:bootstrap = 计量与 session 身份;AppState = 交互态。

源码引用: src/bootstrap/state.ts · 第 557–575 行(共 1759 行)

 557| export function addToTotalCostState(
 558|   cost: number,
 559|   modelUsage: ModelUsage,
 560|   model: string,
 561| ): void {
 562|   STATE.modelUsage[model] = modelUsage
 563|   STATE.totalCostUSD += cost
 564| }
 565| 
 566| export function getTotalCostUSD(): number {
 567|   return STATE.totalCostUSD
 568| }
 569| 
 570| export function getTotalAPIDuration(): number {
 571|   return STATE.totalAPIDuration
 572| }
 573| 
 574| export function getTotalDuration(): number {
 575|   return Date.now() - STATE.startTime

源码引用: src/bootstrap/state.ts · 第 769–781 行(共 1759 行)

 769| /** Mark that a compaction just occurred. The next API success event will
 770|  *  include isPostCompaction=true, then the flag auto-resets. */
 771| export function markPostCompaction(): void {
 772|   STATE.pendingPostCompaction = true
 773| }
 774| 
 775| /** Consume the post-compaction flag. Returns true once after compaction,
 776|  *  then returns false until the next compaction. */
 777| export function consumePostCompaction(): boolean {
 778|   const was = STATE.pendingPostCompaction
 779|   STATE.pendingPostCompaction = false
 780|   return was
 781| }

sessionStorage 与 bootstrap 对齐

sessionStorage.ts(约 5000 行)import bootstrap:

  • getSessionId() — 当前写入的 jsonl 文件名
  • getOriginalCwd() — 项目 hash 路径(每 call site 读取,不在 module init 缓存)
  • getSessionProjectDir() — 跨 worktree / cross-project resume
  • switchSession — /resume 切换活跃 session
  • isSessionPersistenceDisabled — --no-session-persistence

模块注释解释 split-brain 陷阱:import 时 getCwd() 可能早于 bootstrap realpathSync,导致写入目录与读取目录不一致,/resume 找不到文件。

isTranscriptMessage / isChainParticipant 决定 JSONL 链——progress 不进 parentUuid(#14373)。

AppState messages 与 JSONL 通过 useLogMessages / recordTranscript 异步同步,非镜像复制。

源码引用: src/utils/sessionStorage.ts · 第 20–32 行(共 5106 行)

  20| import {
  21|   type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
  22|   logEvent,
  23| } from 'src/services/analytics/index.js'
  24| import {
  25|   getOriginalCwd,
  26|   getPlanSlugCache,
  27|   getPromptId,
  28|   getSessionId,
  29|   getSessionProjectDir,
  30|   isSessionPersistenceDisabled,
  31|   switchSession,
  32| } from '../bootstrap/state.js'

源码引用: src/utils/sessionStorage.ts · 第 108–112 行(共 5106 行)

 108| // Use getOriginalCwd() at each call site instead of capturing at module load
 109| // time. getCwd() at import time may run before bootstrap resolves symlinks via
 110| // realpathSync, causing a different sanitized project directory than what
 111| // getOriginalCwd() returns after bootstrap. This split-brain made sessions
 112| // saved under one path invisible when loaded via the other.

源码引用: src/utils/sessionStorage.ts · 第 128–146 行(共 5106 行)

 128| /**
 129|  * Type guard to check if an entry is a transcript message.
 130|  * Transcript messages include user, assistant, attachment, and system messages.
 131|  * IMPORTANT: This is the single source of truth for what constitutes a transcript message.
 132|  * loadTranscriptFile() uses this to determine which messages to load into the chain.
 133|  *
 134|  * Progress messages are NOT transcript messages. They are ephemeral UI state
 135|  * and must not be persisted to the JSONL or participate in the parentUuid
 136|  * chain. Including them caused chain forks that orphaned real conversation
 137|  * messages on resume (see #14373, #23537).
 138|  */
 139| export function isTranscriptMessage(entry: Entry): entry is TranscriptMessage {
 140|   return (
 141|     entry.type === 'user' ||
 142|     entry.type === 'assistant' ||
 143|     entry.type === 'attachment' ||
 144|     entry.type === 'system'
 145|   )
 146| }

FileStateCache 设计与 API

FileState 条目:content、timestamp、offset、limit、可选 isPartialView(CLAUDE.md 注入仅 partial 时 Edit 须先 Read)。

FileStateCache 包装 LRUCache:

  • 构造:max entries + maxSize bytes(默认 25MB)
  • normalize(key) 于 get/set — 路径 canonical
  • dump/load — compact 序列化
  • cloneFileStateCache / mergeFileStateCaches — 按 timestamp 合并

READ_FILE_STATE_CACHE_SIZE = 100 entries。

工厂 createFileStateCacheWithSizeLimit 用于 REPL、MCP entrypoint、QueryEngine。

不进 AppState 的原因:体积大、Turn 局部、fork subagent 需 clone——放 ToolUseContext 随 query 生命周期。

源码引用: src/utils/fileStateCache.ts · 第 4–39 行(共 143 行)

   4| export type FileState = {
   5|   content: string
   6|   timestamp: number
   7|   offset: number | undefined
   8|   limit: number | undefined
   9|   // True when this entry was populated by auto-injection (e.g. CLAUDE.md) and
  10|   // the injected content did not match disk (stripped HTML comments, stripped
  11|   // frontmatter, truncated MEMORY.md). The model has only seen a partial view;
  12|   // Edit/Write must require an explicit Read first. `content` here holds the
  13|   // RAW disk bytes (for getChangedFiles diffing), not what the model saw.
  14|   isPartialView?: boolean
  15| }
  16| 
  17| // Default max entries for read file state caches
  18| export const READ_FILE_STATE_CACHE_SIZE = 100
  19| 
  20| // Default size limit for file state caches (25MB)
  21| // This prevents unbounded memory growth from large file contents
  22| const DEFAULT_MAX_CACHE_SIZE_BYTES = 25 * 1024 * 1024
  23| 
  24| /**
  25|  * A file state cache that normalizes all path keys before access.
  26|  * This ensures consistent cache hits regardless of whether callers pass
  27|  * relative vs absolute paths with redundant segments (e.g. /foo/../bar)
  28|  * or mixed path separators on Windows (/ vs \).
  29|  */
  30| export class FileStateCache {
  31|   private cache: LRUCache<string, FileState>
  32| 
  33|   constructor(maxEntries: number, maxSizeBytes: number) {
  34|     this.cache = new LRUCache<string, FileState>({
  35|       max: maxEntries,
  36|       maxSize: maxSizeBytes,
  37|       sizeCalculation: value => Math.max(1, Buffer.byteLength(value.content)),
  38|     })
  39|   }

源码引用: src/utils/fileStateCache.ts · 第 101–142 行(共 143 行)

 101| export function createFileStateCacheWithSizeLimit(
 102|   maxEntries: number,
 103|   maxSizeBytes: number = DEFAULT_MAX_CACHE_SIZE_BYTES,
 104| ): FileStateCache {
 105|   return new FileStateCache(maxEntries, maxSizeBytes)
 106| }
 107| 
 108| // Helper function to convert cache to object (used by compact.ts)
 109| export function cacheToObject(
 110|   cache: FileStateCache,
 111| ): Record<string, FileState> {
 112|   return Object.fromEntries(cache.entries())
 113| }
 114| 
 115| // Helper function to get all keys from cache (used by several components)
 116| export function cacheKeys(cache: FileStateCache): string[] {
 117|   return Array.from(cache.keys())
 118| }
 119| 
 120| // Helper function to clone a FileStateCache
 121| // Preserves size limit configuration from the source cache
 122| export function cloneFileStateCache(cache: FileStateCache): FileStateCache {
 123|   const cloned = createFileStateCacheWithSizeLimit(cache.max, cache.maxSize)
 124|   cloned.load(cache.dump())
 125|   return cloned
 126| }
 127| 
 128| // Merge two file state caches, with more recent entries (by timestamp) overriding older ones
 129| export function mergeFileStateCaches(
 130|   first: FileStateCache,
 131|   second: FileStateCache,
 132| ): FileStateCache {
 133|   const merged = cloneFileStateCache(first)
 134|   for (const [filePath, fileState] of second.entries()) {
 135|     const existing = merged.get(filePath)
 136|     // Only override if the new entry is more recent
 137|     if (!existing || fileState.timestamp > existing.timestamp) {
 138|       merged.set(filePath, fileState)
 139|     }
 140|   }
 141|   return merged
 142| }

REPL / QueryEngine 中的缓存流

REPL.tsx 启动:

readFileState = createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE)
→ 传入 query options.readFileState
→ Tool 执行更新 cache
compact 边界:
  cacheToObject(readFileState) 写入 JSONL file_history / snapshot
resume / fork:
  mergeFileStateCaches(old, restored)

QueryEngine 构造参数 readFileCache,getReadFileState() 暴露给 compact microCompact。

Tool.ts ToolUseContext 类型含 readFileState: FileStateCache——所有 Bash/Read/Edit 共享同一 cache,保证 getChangedFiles 与 permission 一致。

源码引用: src/QueryEngine.ts · 第 56–58 行(共 1296 行)

  56|   cloneFileStateCache,
  57|   type FileStateCache,
  58| } from './utils/fileStateCache.js'

源码引用: src/QueryEngine.ts · 第 1255–1260 行(共 1296 行)

1255|     canUseTool,
1256|     getAppState,
1257|     setAppState,
1258|     initialMessages: mutableMessages,
1259|     readFileCache: cloneFileStateCache(getReadFileCache()),
1260|     customSystemPrompt,

源码引用: src/Tool.ts · 第 59–59 行(共 793 行)

  59| import type { FileStateCache } from './utils/fileStateCache.js'

onChangeAppState 跨边界同步

AppState → bootstrap / 磁盘 的集中出口:

AppState 字段边界目标
mainLoopModeluserSettings + setMainLoopModelOverride
toolPermissionContext.modeCCR metadata + SDK stream
expandedViewglobalConfig todos/spinner flags
verboseglobalConfig.verbose
settingsauth cache clear + env re-apply

无 AppState → sessionStorage 直接路径——messages 由 logging 层写 JSONL。

无 AppState → FileStateCache——compact 读 Tool context。

新增跨边界 sync 时优先扩展 onChangeAppState,而非在 UI 组件写磁盘。

源码引用: src/state/onChangeAppState.ts · 第 94–112 行(共 172 行)

  94|   // mainLoopModel: remove it from settings?
  95|   if (
  96|     newState.mainLoopModel !== oldState.mainLoopModel &&
  97|     newState.mainLoopModel === null
  98|   ) {
  99|     // Remove from settings
 100|     updateSettingsForSource('userSettings', { model: undefined })
 101|     setMainLoopModelOverride(null)
 102|   }
 103| 
 104|   // mainLoopModel: add it to settings?
 105|   if (
 106|     newState.mainLoopModel !== oldState.mainLoopModel &&
 107|     newState.mainLoopModel !== null
 108|   ) {
 109|     // Save to settings
 110|     updateSettingsForSource('userSettings', { model: newState.mainLoopModel })
 111|     setMainLoopModelOverride(newState.mainLoopModel)
 112|   }

调试边界问题 checklist

症状先查
/resume 空会话getSessionProjectDir 与 jsonl 路径;originalCwd symlink
模型切换不生效AppState.mainLoopModel vs bootstrap override vs settings.json
Edit 报未读文件FileStateCache isPartialView;cache 是否 compact 后丢失
CCR mode 不同步onChange permission diff;toExternalPermissionMode
队友 session 错项目switchSession projectDir 是否传入 dirname(transcript)
cost 显示 0bootstrap restoreCostStateForSession 是否 resume 调用

边界设计目标:AppState 可整树替换(/clear)而不泄漏 bootstrap sessionId;bootstrap 可在无 React 的 SDK 路径运行;JSONL 是可审计长线;FileStateCache 是可丢弃热缓存。

本章小结与延伸

state-boundaries = AppState 外的三条持久化/进程边界。回到 app-state-core 对照 Provider initialState 如何从 resume hydrate。 继续学习:

  • app-state-core
  • utils session-storage
Prev
teammate-state · 队友视图与 swarm 状态
Next
query config 与 deps · 配置快照与依赖注入