# 横切关注点：贯穿整个系统的 12 个隐藏模式

Feature Flag、错误恢复、国际化、配置层叠——这些不属于任何单一模块的"隐藏模式"却贯穿整个系统。本章梳理 12 个横切关注点，揭示它们如何在不同模块间保持一致性，以及这种设计的局限与代价。

> **🌍 行业背景**：横切关注点（cross-cutting concerns）是软件工程中的经典概念——日志、认证、错误处理、配置管理这些功能不属于任何单一业务模块，却在所有模块中都需要。Java 生态用 AOP（面向切面编程）解决，Go 用 middleware 链，React 用 Context/Provider 模式。Claude Code 的 12 个横切关注点中，大部分是行业标准实践（OAuth PKCE、指数退避重试、Feature Flag），但有几个是 AI Agent 特有的创新：**Dream Mode（睡眠记忆整合）** 和 **Auto-Memory（对话实时提取）** 在其他 AI 编程工具中没有对标物——Cursor 和 Windsurf 都没有跨会话的自动记忆系统，Aider 只有手动的 `.aider` 配置文件。

---

## 引子：看不见的基础设施

每座城市都有看得见的建筑——办公楼、商场、住宅。但真正让城市运转的，是那些埋在地下、藏在墙里、你每天依赖却从未注意的东西：供水管网、电力线路、排水系统、通讯光缆。

Claude Code 也是如此。前面 12 章讲的是"建筑"——启动、查询循环、工具运行时、安全架构。这一章讲的是"管网"——那些不属于任何单一模块，却**贯穿整个系统**的横切关注点（cross-cutting concerns）。它们没有自己的章节，因为它们无处不在。

---

## 1. Feature Flag 系统：编译期门控

### 这是什么

Claude Code 使用了**两层** Feature Flag 体系。第一层是编译期的 `feature()` 函数，从 `bun:bundle` 导入；第二层是运行时的 GrowthBook 远程配置（见第 12 节）。这里先讲编译期层。

### 怎么工作的

`feature()` 从 Bun 的打包器模块 `bun:bundle` 导入，接收一个大写字符串常量作为参数：

```typescript
// src/services/api/withRetry.ts
import { feature } from 'bun:bundle'

...(feature('BASH_CLASSIFIER') ? (['bash_classifier'] as const) : []),
```

它的关键特性是**编译期求值**——Bun 打包时把 `feature('BASH_CLASSIFIER')` 替换为字面量 `true` 或 `false`。当结果为 `false` 时，整个分支被 Dead Code Elimination（DCE）移除，不会出现在最终产物中。

通过源码搜索，可以确认至少存在以下编译期 Flag：

| Flag 名称 | 用途 | 出现位置 |
|-----------|------|---------|
| `TEAMMEM` | 团队记忆功能 | `watcher.ts`, `extractMemories.ts`, `teamMemSecretGuard.ts` |
| `VOICE_MODE` | 语音模式 | `AppState.tsx`, `defaultBindings.ts` |
| `KAIROS` | 助手模式 | `metadata.ts`, `defaultBindings.ts` |
| `BASH_CLASSIFIER` | Bash 命令分类器 | `withRetry.ts` |
| `CACHED_MICROCOMPACT` | 缓存微压缩 | `claude.ts` |
| `CONNECTOR_TEXT` | 连接器文本块 | `claude.ts` |
| `TRANSCRIPT_CLASSIFIER` | 会话分类器 | `claude.ts` |
| `UNATTENDED_RETRY` | 无人值守重试 | `withRetry.ts` |
| `COMMIT_ATTRIBUTION` | 提交归因 | `setup.ts` |
| `UDS_INBOX` | Unix 域套接字消息 | `setup.ts` |
| `QUICK_SEARCH` | 快速搜索 | `defaultBindings.ts` |
| `TERMINAL_PANEL` | 终端面板 | `defaultBindings.ts` |
| `MESSAGE_ACTIONS` | 消息操作 | `defaultBindings.ts` |
| `BREAK_CACHE_COMMAND` | 缓存断裂命令 | `context.ts` |

这些 Flag 的核心价值在于**零运行时开销**。内部 Ant 构建（`USER_TYPE=ant`）打开全部 Flag，外部发布版只打开稳定功能。从 `bun:bundle` 导入意味着这些判断发生在打包时，不是运行时条件分支——关闭的功能根本不存在于交付给用户的二进制文件中。

一个典型的使用模式是条件 `require`：

```typescript
// src/services/extractMemories/extractMemories.ts
const teamMemPaths = feature('TEAMMEM')
  ? (require('../../memdir/teamMemPaths.js') as typeof import('../../memdir/teamMemPaths.js'))
  : null
```

当 `TEAMMEM` 为 `false` 时，`teamMemPaths.js` 整个模块都不会被打包进来。

> **通俗理解**：Feature Flag 就像餐厅的隐藏菜单——菜品已经研发好了、厨师也会做，但只对内部员工和 VIP 客人开放。等到菜品经过充分测试，才写到公开菜单上让所有顾客点。编译期 Flag 更激进：对于不在菜单上的菜，连原料都不放进厨房。

> 📚 **课程关联**：编译期 Feature Flag 本质上是**条件编译**（conditional compilation）——C/C++ 用 `#ifdef`，Rust 用 `#[cfg(feature)]`，Bun 的 `feature()` 是 JavaScript 生态的等价物。在编译原理课程中，这属于**常量折叠**（constant folding）+ **死代码消除**（dead code elimination）的经典优化组合。

---

## 2. 错误处理模式：分层重试与降级

### 错误类型体系

Claude Code 的错误处理围绕 Anthropic SDK 的 `APIError` 体系构建，同时定义了自己的错误类型。核心文件 `src/services/api/withRetry.ts` 定义了两个关键错误类：

```typescript
// src/services/api/withRetry.ts
export class CannotRetryError extends Error {
  constructor(
    public readonly originalError: unknown,
    public readonly retryContext: RetryContext,
  ) { ... }
}

export class FallbackTriggeredError extends Error {
  constructor(
    public readonly originalModel: string,
    public readonly fallbackModel: string,
  ) { ... }
}
```

`CannotRetryError` 表示重试耗尽、或明确不该重试的场景。`FallbackTriggeredError` 表示主模型连续失败后触发了降级到备选模型。

### 重试机制

`withRetry()` 本身的函数签名接近 `async function`（接收一个产生 API 请求结果的 callback、返回 `Promise<T>`），**在持久重试模式下通过 yield 心跳事件的方式暴露为 AsyncGenerator 接口**（源码 `src/services/api/withRetry.ts` 中的 `yield` 用于"心跳 + 外部可取消"，不是逐 chunk 返回流式结果）。本节用"AsyncGenerator"是强调其"边跑边出事件"的 generator 风格，不是在说它把 API 响应切成小块流式返回。核心逻辑如下：

1. **最大重试次数**：默认 10 次（`DEFAULT_MAX_RETRIES`），可通过环境变量 `CLAUDE_CODE_MAX_RETRIES` 覆盖
2. **指数退避**：`BASE_DELAY_MS = 500`，每次翻倍，上限 32 秒。退避公式：`min(500 * 2^(attempt-1), 32000) + jitter`
3. **Retry-After 头**：如果 API 返回 `retry-after` 头，直接使用服务端指定的等待时间
4. **529 过载保护**：连续 3 次 529 后触发降级（`MAX_529_RETRIES = 3`）

```typescript
export function getRetryDelay(attempt: number, retryAfterHeader?: string | null, maxDelayMs = 32000): number {
  if (retryAfterHeader) {
    const seconds = parseInt(retryAfterHeader, 10)
    if (!isNaN(seconds)) return seconds * 1000
  }
  const baseDelay = Math.min(BASE_DELAY_MS * Math.pow(2, attempt - 1), maxDelayMs)
  const jitter = Math.random() * 0.25 * baseDelay
  return baseDelay + jitter
}
```

> 📚 **课程关联**：指数退避 + 抖动（exponential backoff with jitter）是分布式系统课程中的基础算法——AWS 的官方文档将其列为"必须实现"的客户端行为。公式 `min(base * 2^attempt, maxDelay) + random_jitter` 在 Claude Code、AWS SDK、gRPC 客户端中几乎一字不差。抖动（jitter）防止**惊群效应**（thundering herd）——如果所有客户端在完全相同的时间重试，服务端会再次过载。

### 根据查询来源决定重试策略

一个关键设计是**非前台查询不重试 529**。`FOREGROUND_529_RETRY_SOURCES` 集合定义了哪些查询来源值得重试——只有用户直接等待结果的操作（如 `repl_main_thread`、`compact`、`sdk`）才重试；后台操作（摘要、标题生成、分类器）直接丢弃，避免在容量级联故障时放大请求量。

### 持久重试模式

当环境变量 `CLAUDE_CODE_UNATTENDED_RETRY` 启用时（Ant 内部使用），429/529 错误会无限重试，退避上限提升到 5 分钟，总等待时间上限 6 小时。期间每 30 秒发送一个心跳 yield，防止宿主环境判定会话空闲。

> **通俗理解**：就像快递配送失败的重试策略——第一次你不在家，快递员留张纸条 30 分钟后再来；第二次还没人，等 1 小时再来；第三次还不行，直接退回寄件人（CannotRetryError），或者转交给另一个快递站（FallbackTriggeredError）。但如果是重要公文（前台查询），会多试几次；普通广告传单（后台查询）就直接不送了。

---

## 3. Analytics/Telemetry 管道：事件从产生到投递

### 架构概览

分析管道由 `src/services/analytics/` 下的 8 个文件协同完成。设计遵循一个核心原则：**零依赖入口 + 延迟绑定**。

`index.ts` 是整个系统的公共入口，它**没有任何 import 依赖**（注释明确说"This module has NO dependencies to avoid import cycles"）。事件在 sink 连接前被排入内存队列：

```typescript
// src/services/analytics/index.ts
const eventQueue: QueuedEvent[] = []
let sink: AnalyticsSink | null = null

export function logEvent(eventName: string, metadata: LogEventMetadata): void {
  if (sink === null) {
    eventQueue.push({ eventName, metadata, async: false })
    return
  }
  sink.logEvent(eventName, metadata)
}
```

### 事件流路径

```
logEvent() → [Queue] → attachAnalyticsSink() → sink.logEvent()
                                                    ↓
                                              ┌─────┴─────┐
                                              ↓           ↓
                                          Datadog     1P Logger
                                     (stripProtoFields)  (Full payload)
```

1. **产生**：代码中随处调用 `logEvent('tengu_xxx', {...})`
2. **排队**：如果 sink 还没连接（启动早期），事件进入 `eventQueue`
3. **绑定**：`initializeAnalyticsSink()` 在启动时调用，队列通过 `queueMicrotask` 异步排空
4. **路由**：`sink.ts` 中 `logEventImpl` 执行双通道分发
5. **采样**：`firstPartyEventLogger.ts` 的 `shouldSampleEvent()` 根据 GrowthBook 动态配置决定是否丢弃
6. **PII 保护**：`stripProtoFields()` 在发往 Datadog 前移除 `_PROTO_*` 前缀的字段（这些字段携带 PII，只给有权限的 1P 后端看）

### 安全类型标记

一个独特设计是 `AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS` 类型，它是 `never` 类型别名，用于强制开发者显式 `as` 转换字符串后才能放入 metadata——提醒开发者确认该字符串不含代码片段或文件路径。

### Killswitch

`sinkKillswitch.ts` 提供了远程关闭个别 sink 的能力。GrowthBook 配置 `tengu_frond_boric` 是一个 JSON 对象，设置 `{ datadog: true }` 即可立即停止 Datadog 数据发送，无需发版。

> **通俗理解**：就像行车记录仪——全程默默录制（logEvent），录好的视频存到 SD 卡缓冲（eventQueue），等车载 WiFi 连上后自动上传到两个地方：一份给保险公司看（Datadog，脱敏版），一份存在自己私有云盘（1P，完整版）。遇到重大事故时回放查看，平时谁也不看。

> 🌍 **竞品对比**：几乎所有商业开发工具都有遥测系统——VS Code 使用 Application Insights，Cursor 和 Windsurf 都有匿名使用数据收集。Claude Code 的独特之处在于**双通道分发**（Datadog 脱敏版 + 1P 完整版）和 `_PROTO_*` 字段的 PII 隔离机制——这比大多数工具的遥测实现更严格。`AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS` 这个超长类型名本身就是一种"代码即文档"的安全实践。

---

## 4. OAuth 认证流程：PKCE + Token 刷新

### 认证架构

`src/services/oauth/` 实现了完整的 OAuth 2.0 Authorization Code Flow with PKCE（Proof Key for Code Exchange）。核心类是 `OAuthService`（`index.ts`）。

### 登录流程

```
用户执行 /login
    ↓
OAuthService.startOAuthFlow()
    ↓
生成 codeVerifier + codeChallenge（SHA-256）
    ↓
构建 authUrl（包含 client_id, scope, code_challenge）
    ↓
├─ 自动流程：openBrowser(automaticFlowUrl) → localhost:PORT/callback
└─ 手动流程：显示 URL 让用户复制粘贴授权码
    ↓
exchangeCodeForTokens() → POST /oauth/token
    ↓
fetchProfileInfo() → 获取订阅类型、Rate Limit Tier
    ↓
返回 OAuthTokens { accessToken, refreshToken, expiresAt, scopes, subscriptionType }
```

### Token 刷新

`refreshOAuthToken()` 在 `client.ts` 中实现了智能刷新。关键优化：当本地已缓存了完整的 profile 信息（`billingType`、`accountCreatedAt`、`subscriptionCreatedAt`、`subscriptionType`、`rateLimitTier`），刷新时就**跳过 `/api/oauth/profile` 的额外网络请求**。源码注释中给出了 Anthropic 内部测算值（约"每天减少 700 万请求"量级），这是基于生产流量的内部估算而非独立核实数据——具体数字会随用户规模和刷新频率波动。

```typescript
// src/services/oauth/client.ts
export function isOAuthTokenExpired(expiresAt: number | null): boolean {
  if (expiresAt === null) return false
  const bufferTime = 5 * 60 * 1000  // 5 分钟提前量
  return Date.now() + bufferTime >= expiresAt
}
```

Token 过期检查带有 5 分钟的 buffer——不等到真正过期才刷新，而是提前 5 分钟主动刷新，避免请求中途 token 失效。

> 📚 **课程关联**：PKCE（Proof Key for Code Exchange）是 OAuth 2.1 标准的强制要求——它解决了公共客户端（CLI 工具没有 client_secret）的授权码拦截攻击。在网络安全课程中，这是**防中间人攻击**的经典方案：客户端生成随机 code_verifier → SHA-256 哈希为 code_challenge 发送给授权服务器 → 交换 token 时出示原始 code_verifier → 服务器验证哈希匹配。

### 双轨认证

`shouldUseClaudeAIAuth()` 检查 scope 中是否包含 `CLAUDE_AI_INFERENCE_SCOPE`，来区分 Console API Key 认证和 Claude.ai OAuth 认证这两条路径。

> **通俗理解**：就像银行卡刷卡流程——插卡（打开浏览器登录）→ 输密码（PKCE 验证）→ 银行授权（Token Exchange）→ 拿到临时授权码（accessToken）。授权码有有效期，快过期时银行自动续期（refreshToken），不用你重新插卡。你的会员等级（subscriptionType）决定了你能用多少额度。

---

## 5. Rate Limiting：检测、退避与用户沟通

### 检测层

Rate Limiting 的检测分散在多个位置。`withRetry.ts` 中的核心判断：

- **429**：标准速率限制。对 Claude AI 订阅用户（非 Enterprise）不重试（因为限制窗口通常长达数小时）
- **529**：过载错误。连续 3 次后触发模型降级

```typescript
function shouldRetry(error: APIError): boolean {
  // 429 对订阅用户不重试（Enterprise 除外，因为他们用 PAYG）
  if (error.status === 429) {
    return !isClaudeAISubscriber() || isEnterpriseSubscriber()
  }
  // ...
}
```

### Fast Mode 降级

当用户启用了 Fast Mode（高速模式）遇到限流时：
- **短延迟（< 20 秒）**：保持 Fast Mode，等待后重试（保留 prompt cache）
- **长延迟（>= 20 秒）**：触发 cooldown，切换到标准速度模型，最低冷却 10 分钟

### 用户界面沟通

`RateLimitMessage.tsx` 根据用户的订阅类型显示不同的操作建议：

- **Max 20x 用户**：提示 `/extra-usage` 或切换到 API 计费账户
- **Team/Enterprise 用户**：提示 `/extra-usage` 请求管理员增加额度
- **普通用户**：提示 `/upgrade` 升级或 `/extra-usage`

速率限制重置头 `anthropic-ratelimit-unified-reset` 提供了服务端的精确重置时间（Unix 时间戳），在持久重试模式下直接使用这个时间而非估算。

> **通俗理解**：就像高速公路收费站的限流——车太多时（429），入口闸道亮红灯控制进入速度。如果整条路都堵了（529），交警会引导你走备选路线（模型降级）。VIP 车道（Enterprise）有独立入口不受普通限流影响。收费站电子屏（RateLimitMessage）会实时告诉你预计等待时间和备选方案。

---

## 6. Dream Mode：睡眠中的记忆整合

### 什么是 Dream

Dream Mode 是 Claude Code 的**后台记忆整理机制**。当用户持续使用一段时间后，系统会在会话间隙自动运行一个"做梦"子代理，回顾近期的会话记录，把零散的信息整理成结构化的长期记忆。

### 触发条件

`src/services/autoDream/autoDream.ts` 中定义了严格的门控链（cheapest check first）：

1. **功能开关**：`isAutoDreamEnabled()` 返回 true（通过 GrowthBook 配置 `tengu_onyx_plover`）
2. **非特殊模式**：不在 KAIROS 模式、不在远程模式
3. **时间门控**：距离上次整合 >= 24 小时（默认 `minHours: 24`）
4. **会话门控**：自上次整合以来有 >= 5 个新会话（默认 `minSessions: 5`）
5. **锁门控**：没有其他进程正在执行整合（防止并发冲突）

```typescript
const DEFAULTS: AutoDreamConfig = {
  minHours: 24,
  minSessions: 5,
}
```

### 执行过程

Dream 使用 `runForkedAgent()` 创建一个**完美分支**的子代理，共享父会话的 prompt cache。整合提示（`consolidationPrompt.ts`）分为 4 个阶段：

1. **Orient（定位）**：`ls` 记忆目录，读取索引文件
2. **Gather（收集）**：扫描近期会话日志，grep 关键信息
3. **Consolidate（整合）**：更新或创建记忆文件，合并重复，修正过时信息
4. **Prune（修剪）**：保持索引文件在 200 行以内

### UI 表现

`src/tasks/DreamTask/DreamTask.ts` 将 Dream 注册为一个可见的后台任务，在底部状态栏显示为 pill。用户可以通过 Shift+Down 查看详情，也可以手动中止（kill 时会回滚 consolidation lock 的 mtime）。

> **通俗理解**：就像你手机的夜间自动优化——你睡觉时（会话空闲），手机自动清理缓存、整理照片、备份数据。Claude Code 的"做梦"也是一样：在你不用的时候，它悄悄回顾最近几天的对话，把学到的东西整理成笔记。你早上醒来（下次对话），它就变得更了解你了。

> 🌍 **竞品对比**：Dream Mode（睡眠记忆整合）在 AI 编程工具中是独创设计。**Cursor** 的 `.cursorrules` 只是静态配置文件，不会自动更新；**Windsurf** 有 "Cascade Memory" 但仅在单次会话内有效；**Aider** 的 `.aider.conf.yml` 完全手动维护。Claude Code 的 Dream + Auto-Memory 双系统实现了"用越多越了解你"的自适应行为——这更接近 Notion AI 或 Rewind 的个人知识库概念，而非传统 IDE 插件的配置文件。

---

## 7. Auto-Memory 提取：对话中的自动笔记

### 与 Dream 的区别

Dream 是**会话间**的批量整理（间隔 24 小时），Auto-Memory 是**对话中**的实时提取——每次模型生成完回复，就在后台检查是否有值得记住的信息。

### 工作机制

`src/services/extractMemories/extractMemories.ts` 采用闭包封装所有可变状态：

```typescript
export function initExtractMemories(): void {
  let lastMemoryMessageUuid: string | undefined   // 游标：上次处理到哪
  let inProgress = false                            // 防止重叠执行
  let turnsSinceLastExtraction = 0                  // 节流计数器
  let pendingContext: { ... } | undefined           // 延迟执行的上下文
  // ...
}
```

关键流程：
1. 每次模型回复后，`handleStopHooks` 触发 `executeExtractMemories()`
2. 检查 GrowthBook 门控 `tengu_passport_quail` 是否开启
3. 检查是否跳过（远程模式、子代理、auto-memory 未开启等）
4. 如果主代理已经直接写过记忆文件（`hasMemoryWritesSince`），跳过（互斥设计）
5. 每 N 轮才执行一次（`tengu_bramble_lintel` 控制频率，默认每轮）
6. 运行一个 `runForkedAgent`，最多 5 轮对话，只允许使用有限的工具

### 工具权限控制

提取代理的权限被严格限制——`createAutoMemCanUseTool()` 函数只允许：
- 读取类：`FileRead`、`Grep`、`Glob`（不受限）
- Bash：仅只读命令（ls、find、cat 等）
- 写入类：`FileEdit`、`FileWrite` 仅限记忆目录路径

### 防重叠机制

如果一次提取正在执行时又触发了新的提取请求，系统不会启动第二个——而是把新请求的上下文暂存（`pendingContext`），等当前提取完成后执行一次"尾随提取"。这保证不会丢失中间的对话内容。

> **通俗理解**：就像会议纪要的自动生成——你开会时不用特意做笔记，AI 秘书在旁边默默听着，每过一段时间就自动提炼出要点写到备忘录里。你明确说"记住这一点"时它立刻记录，你没说的时候它也会判断哪些信息值得保留。

---

## 8. Notification 系统：优先级队列通知

### 架构设计

`src/context/notifications.tsx` 实现了一个**优先级队列**通知系统。通知分为四个优先级：

```typescript
type Priority = 'low' | 'medium' | 'high' | 'immediate'
```

### 核心数据结构

每条通知包含：
- `key`：唯一标识符，用于去重和合并
- `priority`：优先级
- `invalidates`：这条通知会使哪些旧通知失效
- `fold`：当同 key 通知重复出现时的合并函数（类似 Array.reduce）
- `timeoutMs`：显示持续时间，默认 8000ms

通知可以是纯文本（`TextNotification`）或 JSX 组件（`JSXNotification`）。

### 处理流程

1. `addNotification()` 被调用
2. 如果是 `immediate` 优先级：立刻清除当前通知，直接显示
3. 否则：加入队列，`processQueue()` 按优先级取出下一条
4. 显示完毕（timeout 到期）后，自动弹出下一条

### 实际使用场景

以 `useFastModeNotification.tsx` 为例，Fast Mode 的各种状态变化都通过通知系统告知用户：
- 组织启用了 Fast Mode：`"Fast mode is now available · /fast to turn on"`
- 组织禁用了 Fast Mode：`"Fast mode has been disabled by your organization"`
- 限流后冷却结束：自动恢复通知

> **通俗理解**：就像手机的通知中心——不同 App 的消息统一管理，重要消息（immediate）直接弹出推送，普通消息（low）安静地等在通知栏里。同类消息会合并（fold），新消息会自动撤销过时的旧消息（invalidates）。

---

## 9. State Management 架构：极简响应式 Store

### Store 核心

`src/state/store.ts` 是整个状态管理的核心——仅 34 行代码，实现了一个极简的响应式 Store：

```typescript
export function createStore<T>(initialState: T, onChange?: OnChange<T>): Store<T> {
  let state = initialState
  const listeners = new Set<Listener>()

  return {
    getState: () => state,
    setState: (updater: (prev: T) => T) => {
      const prev = state
      const next = updater(prev)
      if (Object.is(next, prev)) return   // 引用相等则跳过
      state = next
      onChange?.({ newState: next, oldState: prev })
      for (const listener of listeners) listener()
    },
    subscribe: (listener: Listener) => {
      listeners.add(listener)
      return () => listeners.delete(listener)
    },
  }
}
```

关键设计：`Object.is(next, prev)` 引用相等检查——如果 updater 返回同一个对象引用，所有 listener 都不会被触发。这要求所有状态更新必须返回新对象（不可变更新）。

> 📚 **课程关联**：这 34 行代码是**发布-订阅模式**（Pub-Sub pattern）的教科书级实现。对比 Redux（2,500+ 行）、MobX（10,000+ 行）、Zustand（~600 行），Claude Code 选择了最简方案——没有 middleware、没有 devtools、没有 time-travel debugging。这个选择反映了一个工程判断：CLI 工具的状态管理不需要 Web 应用级别的复杂度。

### AppState 结构

`src/state/AppStateStore.ts` 定义了巨大的 `AppState` 类型，用 `DeepImmutable<>` 包装确保深层不可变。关键字段包括：

| 字段 | 类型 | 用途 |
|------|------|------|
| `settings` | `SettingsJson` | 用户设置 |
| `mainLoopModel` | `ModelSetting` | 当前模型 |
| `toolPermissionContext` | `ToolPermissionContext` | 工具权限上下文 |
| `notifications` | `{queue, current}` | 通知队列 |
| `kairosEnabled` | `boolean` | 助手模式开关 |
| `speculation` | `SpeculationState` | 推测执行状态 |
| `tasks` | `TaskState[]` | 后台任务列表 |

### 与 React 集成

`AppState.tsx` 通过 React Context 将 Store 注入组件树。它使用了 React 编译器（`react/compiler-runtime`）的自动 memo 化——通过 `_c()` 函数创建编译期缓存数组，避免手动写 `useMemo`/`useCallback`。

`useAppState(selector)` 和 `useSyncExternalStore` 结合使用，实现选择性订阅：只有 selector 返回值变化的组件才会重新渲染。

> **通俗理解**：就像大型商场的中控室——所有楼层的状态（人流、温度、安防、电力）汇集到一个中央面板（AppState）。任何变化都会通知到相关区域（listener），但只有实际发生变化的楼层才需要响应（Object.is 检查）。面板本身不做决策，只负责让所有人看到同一个实时状态。

---

## 10. LSP 集成：语言服务器协议桥接

### 架构分层

`src/services/lsp/` 实现了一个完整的 LSP 客户端管理系统，分为三层：

1. **LSPClient**（`LSPClient.ts`）：单个 LSP 服务器的底层通信。使用 `vscode-jsonrpc` 通过 stdio 与服务器进程交互
2. **LSPServerInstance**（`LSPServerInstance.ts`）：在 LSPClient 上包装了启动/停止/文件同步逻辑
3. **LSPServerManager**（`LSPServerManager.ts`）：管理多个服务器实例，根据文件扩展名路由请求

### 服务器发现与配置

`config.ts` 显示 LSP 服务器**只通过插件加载**——没有用户/项目级别的手动配置：

```typescript
export async function getAllLspServers(): Promise<{
  servers: Record<string, ScopedLspServerConfig>
}> {
  const { enabled: plugins } = await loadAllPluginsCacheOnly()
  // 每个插件并行加载其 LSP 服务器配置
  // 后加载的插件在名称冲突时覆盖先加载的
}
```

### 诊断推送

`LSPDiagnosticRegistry.ts` 接收服务器推送的诊断信息（类型错误、lint 警告等），通过限流机制（每文件最多 10 条、全局最多 30 条）和 LRU 去重（最多跟踪 500 个文件）后，作为 Attachment 注入到下一轮对话中。这让 Claude 能"看到" IDE 的错误提示。

### 文件同步

Manager 维护了一个 `openedFiles` 映射（URI → server name），当用户通过 Claude Code 编辑文件时，自动发送 `didOpen`、`didChange`、`didSave`、`didClose` 通知给对应的 LSP 服务器，保持语言服务器对文件状态的感知。

> **通俗理解**：就像请专家顾问——Claude 本身不是 TypeScript 编译器或 Python 类型检查器，但它可以把代码发给专家顾问（LSP 服务器），让专家告诉它"第 42 行有个类型错误"。多个专家按专业领域分工（TypeScript 服务器处理 .ts 文件、Python 服务器处理 .py 文件），有专门的秘书（Manager）负责分发。

---

## 11. Team Memory 同步：跨设备团队知识库

### 同步协议

`src/services/teamMemorySync/index.ts` 实现了一个基于 HTTP API 的双向同步系统：

- **Pull**：`GET /api/claude_code/team_memory?repo={owner/repo}` 拉取服务端内容，**服务端覆盖本地**（server wins）
- **Push**：`PUT` 仅上传 content hash 变化的条目（delta upload），服务端使用 upsert 语义
- **删除不传播**：删除本地文件不会删除服务端数据，下次 pull 会恢复

### 安全守卫

`secretScanner.ts` 在上传前进行客户端密钥扫描。它内嵌了一套精选的 gitleaks 规则（源自 MIT 协议的公开配置），只选了高置信度、有独特前缀的规则：

```typescript
const SECRET_RULES: SecretRule[] = [
  { id: 'aws-access-token', source: '\\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\\b' },
  { id: 'gcp-api-key', source: '\\b(AIza[\\w-]{35})...' },
  { id: 'anthropic-api-key', source: `\\b(${ANT_KEY_PFX}03-...)` },
  // GitHub PAT, Slack tokens, Stripe keys 等
]
```

注意 `ANT_KEY_PFX` 使用 `['sk', 'ant', 'api'].join('-')` 拼接，而非直接写字面量——避免 Anthropic 自己的 API key 前缀出现在发布的代码中。

### Watcher 机制

`watcher.ts` 使用 `fs.watch()` 监控团队记忆目录，2 秒去抖后触发 push。设计了永久失败检测——如果 push 因为不可恢复的原因失败（无 OAuth、404 等），watcher 会抑制后续 push，防止无限重试（源码注释中记录了一个真实事故数据："2.5 天内 167K 次 push 事件"，来自 Anthropic 内部 BQ 观测，本书直接引用注释原文而非独立核实）。

### 容量限制

- 单文件最大 250KB（`MAX_FILE_SIZE_BYTES`）
- PUT 请求体最大 200KB（`MAX_PUT_BODY_BYTES`），超出时分批发送
- 服务端条目数上限由 GrowthBook 按组织动态配置

> **通俗理解**：就像公司内部知识库——团队成员（同一 Git 仓库的协作者）共享一套文档。你写了一条经验笔记上传到服务器，同事打开 Claude Code 时自动同步下来。上传前有安全检查员（secretScanner）翻看每页确保没有密码和密钥混进去。

---

## 12. GrowthBook 集成：远程配置与 A/B 测试

### 定位

如果说 `feature()` 是编译期的开关（第 1 节），GrowthBook 就是**运行时的遥控器**。它允许 Anthropic 在不发版的情况下动态控制功能开关、调整参数配置、运行 A/B 实验。

### 用户属性

`src/services/analytics/growthbook.ts` 定义了发送给 GrowthBook 的用户属性：

```typescript
export type GrowthBookUserAttributes = {
  id: string                    // 用户 ID
  sessionId: string             // 会话 ID
  deviceID: string              // 设备 ID
  platform: 'win32' | 'darwin' | 'linux'
  organizationUUID?: string     // 组织 ID
  subscriptionType?: string     // 订阅类型
  rateLimitTier?: string        // 速率限制层级
  appVersion?: string           // 应用版本
  // ...
}
```

这些属性用于目标定位——比如只对 `subscriptionType: 'max'` 的用户开启某功能、或按 `platform` 分 A/B 组。

### 五层值获取优先级

获取一个 Feature 的值时，优先级从高到低：

1. **环境变量覆盖**：`CLAUDE_INTERNAL_FC_OVERRIDES`（仅 Ant 用户，用于评测框架）
2. **Config 覆盖**：`/config` Gates 标签页设置的本地覆盖（仅 Ant 用户）
3. **远程值**：`remoteEvalFeatureValues` Map（GrowthBook 服务器返回）
4. **磁盘缓存**：`cachedGrowthBookFeatures`（上一次会话的值）
5. **默认值**：调用者指定的 fallback

### 命名混淆

源码中大量使用了混淆的 feature key 名称，以 `tengu_` 为前缀（tengu 是天狗，Claude Code 的内部代号）：

| Key | 实际功能 |
|-----|---------|
| `tengu_passport_quail` | Auto-Memory 提取开关 |
| `tengu_onyx_plover` | Dream 模式配置 |
| `tengu_bramble_lintel` | 记忆提取频率控制 |
| `tengu_frond_boric` | Analytics sink killswitch |
| `tengu_event_sampling_config` | 事件采样配置 |
| `tengu_log_datadog_events` | Datadog 事件记录开关 |
| `tengu_moth_copse` | 记忆索引跳过开关 |
| `tengu_disable_keepalive_on_econnreset` | 连接重置时禁用 keep-alive |

### 实验曝光日志

当用户被分配到某个实验时，`logExposureForFeature()` 通过 1P（First Party）日志记录实验曝光事件，包含 `experimentId`、`variationId` 等数据。每个 feature 每个会话只记录一次（`loggedExposures` Set 去重）。

### 刷新监听

`onGrowthBookRefresh()` 允许其他系统注册回调——当 GrowthBook 值更新时重建依赖这些值的长生命周期对象。例如 1P Event Logger 在初始化时读取批次配置，GrowthBook 刷新后需要用新配置重建 LoggerProvider。

> **通俗理解**：就像连锁餐厅总部通过中央系统远程控制分店菜单——总部可以随时决定"北京地区门店上架新品"、"50% 的上海用户看到新界面"。分店（Claude Code 客户端）定期同步最新指令，同时在本地缓存上一次的指令以防断网。每个分店还可以本地覆盖总部设置（仅限内部测试门店）。

---

## 总结：12 大横切关注点速查表

| # | 模式 | 核心文件 | 一句话描述 |
|---|------|---------|-----------|
| 1 | Feature Flag | `bun:bundle` → `feature()` | 编译期功能门控，关闭的功能从二进制中彻底消失 |
| 2 | 错误处理 | `src/services/api/withRetry.ts` | 指数退避 + 529 降级 + 持久重试模式 |
| 3 | Analytics | `src/services/analytics/` | 零依赖入口 + 延迟绑定 + 双通道分发（Datadog + 1P） |
| 4 | OAuth | `src/services/oauth/` | PKCE 授权码流 + 智能 Token 刷新（省 700 万请求/天） |
| 5 | Rate Limiting | `withRetry.ts` + `RateLimitMessage.tsx` | 按订阅类型差异化重试 + Fast Mode 冷却降级 |
| 6 | Dream Mode | `src/services/autoDream/` + `src/tasks/DreamTask/` | 24h 定时 + 5 会话阈值 → 分叉代理整理记忆 |
| 7 | Auto-Memory | `src/services/extractMemories/` | 每轮对话后分叉代理提取，防重叠 + 互斥主代理写入 |
| 8 | Notification | `src/context/notifications.tsx` | 优先级队列 + fold 合并 + invalidates 失效机制 |
| 9 | State Mgmt | `src/state/store.ts`（34 行） | 极简发布-订阅 + DeepImmutable + React 编译器集成 |
| 10 | LSP | `src/services/lsp/` | 三层架构（Client → Instance → Manager），插件驱动 |
| 11 | Team Memory | `src/services/teamMemorySync/` | Server-wins 同步 + 客户端密钥扫描 + 分批 PUT |
| 12 | GrowthBook | `src/services/analytics/growthbook.ts` | 运行时遥控器：环境变量覆盖 > Config 覆盖 > 远程值 > 磁盘缓存 > 默认值，五层优先级 |

这 12 个模式构成了 Claude Code 的"隐藏基础设施"。它们没有华丽的 UI，没有独立的命令入口，但每一次 API 调用、每一条通知显示、每一次记忆保存背后，都是这些横切关注点在默默工作。理解了它们，才能理解为什么 Claude Code 不是一个简单的"LLM 聊天客户端"，而是一个工程上精心设计的**客户端系统**——它不是分布式的（只有一个进程），但它的内部复杂度（多个子系统通过事件和共享状态协调、后台任务并行执行、与多个外部服务交互）已经达到了中型后端服务的水平。
