fix: 内存优化 — 预测性 compact 阈值、增量 lookups orphaned 修复、deferred slice 引用优化

- P0: REPL.tsx 用 useMemo 包裹 deferred messages slice,避免每次渲染创建新数组引用导致不必要的后台重渲染
- P1: 预测性 compact 阈值改用 effectiveContextWindow - growth,消除与 autocompact buffer 的双重预留;TOOL_RESULT_GROWTH_ESTIMATE 从 20K 降至 15K
- P2: 增量 lookups 增加 lastAssistantMsgId 一致性检查和 orphaned server_tool_use/mcp_tool_use 扫描,防止 UI 永久 loading
- P3: reactiveCompact 类型断言改为直接使用 'compact' 字面量
- docs: CLAUDE.md 统一使用 precheck 替代分散的 typecheck/lint/test 命令

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
claude-code-best 2026-05-02 20:32:00 +08:00 committed by James Feng
parent 23ea5a67d3
commit aff7b0e853
3 changed files with 665 additions and 177 deletions

331
CLAUDE.md
View File

@ -1,10 +1,25 @@
# CLAUDE.md # CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. This file provides guidance to Claude Code (claude.ai/code) and other AI coding agents when working with code in this repository.
## Project Overview ## Project Overview
This is a **reverse-engineered / decompiled** version of Anthropic's official Claude Code CLI tool. The goal is to restore core functionality while trimming secondary capabilities. Many modules are stubbed or feature-flagged off. The codebase has ~1341 tsc errors from decompilation (mostly `unknown`/`never`/`{}` types) — these do **not** block Bun runtime execution. This is a **reverse-engineered / decompiled** version of Anthropic's official Claude Code CLI tool. The goal is to restore core functionality while trimming secondary capabilities. Many modules are stubbed or feature-flagged off. TypeScript strict mode is enforced — **`bun run precheck` 必须零错误通过**(包含 typecheck + lint fix + test
## Git Commit Message Convention
使用 **Conventional Commits** 规范:
```
<type>: <描述>
```
常见 type`feat`、`fix`、`docs`、`chore`、`refactor`
示例:
- `feat: 添加模型 1M 上下文切换`
- `fix: 修复初次登陆的校验问题`
- `chore: remove prefetchOfficialMcpUrls call on startup`
## Commands ## Commands
@ -21,18 +36,23 @@ bun run dev:inspect
# Pipe mode # Pipe mode
echo "say hello" | bun run src/entrypoints/cli.tsx -p echo "say hello" | bun run src/entrypoints/cli.tsx -p
# Build (code splitting, outputs dist/cli.js + ~450 chunk files) # Build (code splitting, outputs dist/cli.js + chunk files)
bun run build bun run build
# Test # Build with Vite (alternative build pipeline)
bun test # run all tests bun run build:vite
bun test src/utils/__tests__/hash.test.ts # run single file
bun test --coverage # with coverage report
# Lint & Format (Biome) # Test
bun run lint # check only bun test # run all tests
bun run lint:fix # auto-fix bun test src/utils/__tests__/hash.test.ts # run single file
bun run format # format all src/ bun test --coverage # with coverage report
# Lint & Format (Biome) — 日常开发用 precheck 代替单独调用
bun run lint # lint check (全项目)
bun run lint:fix # auto-fix lint issues
bun run format # format all (全项目)
bun run check # lint + format check (全项目)
bun run check:fix # lint + format auto-fix
# Health check # Health check
bun run health bun run health
@ -40,6 +60,12 @@ bun run health
# Check unused exports # Check unused exports
bun run check:unused bun run check:unused
# Full check (typecheck + lint fix + test) — 任务完成后必须运行
bun run precheck
# Remote Control Server
bun run rcs
# Docs dev server (Mintlify) # Docs dev server (Mintlify)
bun run docs:dev bun run docs:dev
``` ```
@ -52,14 +78,16 @@ bun run docs:dev
- **Runtime**: Bun (not Node.js). All imports, builds, and execution use Bun APIs. - **Runtime**: Bun (not Node.js). All imports, builds, and execution use Bun APIs.
- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。Build 默认启用 19 个 feature见下方 Feature Flag 段)。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。构建时会将 `vendor/audio-capture/``src/utils/vendor/ripgrep/` 复制到 `dist/vendor/` 下。 - **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。Build 默认启用 19 个 feature见下方 Feature Flag 段)。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。构建时会将 `vendor/audio-capture/``src/utils/vendor/ripgrep/` 复制到 `dist/vendor/` 下。
- **Build (Vite)**: `vite.config.ts` + `scripts/post-build.ts`代码分割模式chunk 输出到 `dist/chunks/`。post-build 遍历 `dist/``dist/chunks/` 下所有 `.js` 文件做 `globalThis.Bun` 解构 patch复制 vendor 文件到 `dist/vendor/` - **Build (Vite)**: `vite.config.ts` + `scripts/post-build.ts`chunk 输出到 `dist/chunks/`。post-build 同样复制 vendor 文件到 `dist/vendor/`
- **Vendor 路径解析**: 构建后 chunk 文件位于 `dist/``dist/chunks/`vendor 二进制在 `dist/vendor/`。`src/utils/distRoot.ts` 提供共享的 `distRoot` 函数,通过 `import.meta.url` 路径中 `lastIndexOf('dist')``lastIndexOf('src')` 定位根目录。`ripgrep.ts`、`computerUse/setup.ts`、`claudeInChrome/setup.ts` 均使用 `distRoot` 而非内联 `import.meta.url` 路径推算。 - **Vendor 路径解析**: 构建后 chunk 文件位于 `dist/``dist/chunks/`vendor 二进制在 `dist/vendor/`。`src/utils/ripgrep.ts` 和 `packages/audio-capture-napi/src/index.ts` 均通过 `import.meta.url` 路径中 `lastIndexOf('dist')` 定位 dist 根目录,再拼接 `vendor/` 子路径,确保不同构建产物层级下路径一致。
- **为什么 Vite 必须代码分割**: Bun/JSC 会全量解析单个大 JS 文件的 bytecode 和 JIT单文件 17MB 产物导致 RSS 暴涨至 ~1GBNode/V8 懒解析仅需 ~220MB。代码分割为 600+ 小 chunk 后 Bun 按需加载,`--version` RSS 从 966MB 降至 35MB完整加载从 1GB+ 降至 ~500MB。
- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines运行 `src/entrypoints/cli.tsx`。默认启用全部 feature。 - **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines运行 `src/entrypoints/cli.tsx`。默认启用全部 feature。
- **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform. - **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform.
- **Monorepo**: Bun workspaces — internal packages live in `packages/` resolved via `workspace:*`. - **Monorepo**: Bun workspaces — 15 个 workspace packages + 若干辅助目录 in `packages/` resolved via `workspace:*`
- **Lint/Format**: Biome (`biome.json`)。`bun run lint` / `bun run lint:fix` / `bun run format` - **Lint/Format**: Biome (`biome.json`)。覆盖 `src/`、`scripts/`、`packages/` 全项目(含 `packages/@ant/`)。`bun run lint` / `bun run lint:fix` / `bun run format` / `bun run check` / `bun run check:fix`。42 条规则因 decompiled 代码被关闭,仅保留 `recommended` 基线。
- **Pre-commit**: husky + lint-staged。提交时自动对暂存文件执行 `biome check --fix`TS/JS`biome format --write`JSON
- **CI Lint**: `ci.yml` 在依赖安装后、类型检查前执行 `bunx biome ci .`lint 或格式化不达标则 CI 失败。
- **Defines**: 集中管理在 `scripts/defines.ts`。当前版本 `2.1.888` - **Defines**: 集中管理在 `scripts/defines.ts`。当前版本 `2.1.888`
- **CI**: GitHub Actions — `ci.yml`lint + 构建 + 测试)、`release-rcs.yml`RCS 发布)、`update-contributors.yml`(自动更新贡献者)。
### Entry & Bootstrap ### Entry & Bootstrap
@ -67,13 +95,16 @@ bun run docs:dev
- `--version` / `-v` — 零模块加载 - `--version` / `-v` — 零模块加载
- `--dump-system-prompt` — feature-gated (DUMP_SYSTEM_PROMPT) - `--dump-system-prompt` — feature-gated (DUMP_SYSTEM_PROMPT)
- `--claude-in-chrome-mcp` / `--chrome-native-host` - `--claude-in-chrome-mcp` / `--chrome-native-host`
- `--computer-use-mcp` — 独立 MCP server 模式
- `--daemon-worker=<kind>` — feature-gated (DAEMON) - `--daemon-worker=<kind>` — feature-gated (DAEMON)
- `remote-control` / `rc` / `bridge` — feature-gated (BRIDGE_MODE) - `remote-control` / `rc` / `remote` / `sync` / `bridge` — feature-gated (BRIDGE_MODE)
- `daemon` — feature-gated (DAEMON) - `daemon` [subcommand] — feature-gated (DAEMON)
- `ps` / `logs` / `attach` / `kill` / `--bg` — feature-gated (BG_SESSIONS) - `ps` / `logs` / `attach` / `kill` / `--bg` — feature-gated (BG_SESSIONS)
- `new` / `list` / `reply` — Template job commands
- `environment-runner` / `self-hosted-runner` — BYOC runner
- `--tmux` + `--worktree` 组合 - `--tmux` + `--worktree` 组合
- 默认路径:加载 `main.tsx` 启动完整 CLI - 默认路径:加载 `main.tsx` 启动完整 CLI
2. **`src/main.tsx`** (~4680 行) — Commander.js CLI definition。注册大量 subcommands`mcp` (serve/add/remove/list...)、`server`、`ssh`、`open`、`auth`、`plugin`、`agents`、`auto-mode`、`doctor`、`update` 等。主 `.action()` 处理器负责权限、MCP、会话恢复、REPL/Headless 模式分发。 2. **`src/main.tsx`** (~6981 行) — Commander.js CLI definition。注册大量 subcommands`mcp` (serve/add/remove/list...)、`server`、`ssh`、`open`、`auth`、`plugin`、`agents`、`auto-mode`、`doctor`、`update` 等。主 `.action()` 处理器负责权限、MCP、会话恢复、REPL/Headless 模式分发。
3. **`src/entrypoints/init.ts`** — One-time initialization (telemetry, config, trust dialog)。 3. **`src/entrypoints/init.ts`** — One-time initialization (telemetry, config, trust dialog)。
### Core Loop ### Core Loop
@ -85,21 +116,28 @@ bun run docs:dev
### API Layer ### API Layer
- **`src/services/api/claude.ts`** — Core API client. Builds request params (system prompt, messages, tools, betas), calls the Anthropic SDK streaming endpoint, and processes `BetaRawMessageStreamEvent` events. - **`src/services/api/claude.ts`** — Core API client. Builds request params (system prompt, messages, tools, betas), calls the Anthropic SDK streaming endpoint, and processes `BetaRawMessageStreamEvent` events.
- Supports multiple providers: Anthropic direct, AWS Bedrock, Google Vertex, Azure. - **7 providers**: `firstParty` (Anthropic direct), `bedrock` (AWS), `vertex` (Google Cloud), `foundry`, `openai`, `gemini`, `grok` (xAI)。
- Provider selection in `src/utils/model/providers.ts`. - Provider selection in `src/utils/model/providers.ts`。优先级modelType 参数 > 环境变量 > 默认 firstParty。
### Tool System ### Tool System
- **`src/Tool.ts`** — Tool interface definition (`Tool` type) and utilities (`findToolByName`, `toolMatchesName`). - **`src/Tool.ts`** — Tool interface definition (`Tool` type) and utilities (`findToolByName`, `toolMatchesName`).
- **`src/tools.ts`** — Tool registry. Assembles the tool list; some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`. - **`src/tools.ts`** — Tool registry. Assembles the tool list; tools are imported from `@claude-code-best/builtin-tools` package. Some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`.
- **`src/tools/<ToolName>/`** — 61 个 tool 目录(如 BashTool, FileEditTool, GrepTool, AgentTool, WebFetchTool, LSPTool, MCPTool 等)。每个 tool 包含 `name`、`description`、`inputSchema`、`call()` 及可选的 React 渲染组件。 - **`packages/builtin-tools/src/tools/`** — 59 个子目录(含 shared/testing 等工具目录),通过 `@claude-code-best/builtin-tools` 包导出。主要分类:
- **`src/tools/shared/`** — Tool 共享工具函数。 - **文件操作**: FileEditTool, FileReadTool, FileWriteTool, GlobTool, GrepTool
- **Shell/执行**: BashTool, PowerShellTool, REPLTool
- **Agent 系统**: AgentTool, TaskCreateTool, TaskUpdateTool, TaskListTool, TaskGetTool
- **规划**: EnterPlanModeTool, ExitPlanModeV2Tool, VerifyPlanExecutionTool
- **Web/MCP**: WebFetchTool, WebSearchTool, MCPTool, McpAuthTool
- **调度**: CronCreateTool, CronDeleteTool, CronListTool
- **其他**: LSPTool, ConfigTool, SkillTool, EnterWorktreeTool, ExitWorktreeTool 等
- **`src/tools/shared/`** / **`packages/builtin-tools/src/tools/shared/`** — Tool 共享工具函数。
### UI Layer (Ink) ### UI Layer (Ink)
- **`src/ink.ts`** — Ink render wrapper with ThemeProvider injection. - **`src/ink.ts`** — Ink render wrapper with ThemeProvider injection.
- **`src/ink/`** — Custom Ink framework (forked/internal): custom reconciler, hooks (`useInput`, `useTerminalSize`, `useSearchHighlight`), virtual list rendering. - **`packages/@ant/ink/`** — Custom Ink frameworkforked/internal包含 components、core、hooks、keybindings、theme、utils。注意不是 `src/ink/`
- **`src/components/`** — 大量 React 组件170+ 项),渲染于终端 Ink 环境中。关键组件: - **`src/components/`** — 149 个组件目录/文件,渲染于终端 Ink 环境中。关键组件:
- `App.tsx` — Root provider (AppState, Stats, FpsMetrics) - `App.tsx` — Root provider (AppState, Stats, FpsMetrics)
- `Messages.tsx` / `MessageRow.tsx` — Conversation message rendering - `Messages.tsx` / `MessageRow.tsx` — Conversation message rendering
- `PromptInput/` — User input handling - `PromptInput/` — User input handling
@ -115,10 +153,45 @@ bun run docs:dev
- **`src/state/selectors.ts`** — State selectors. - **`src/state/selectors.ts`** — State selectors.
- **`src/bootstrap/state.ts`** — Module-level singletons for session-global state (session ID, CWD, project root, token counts, model overrides, client type, permission mode). - **`src/bootstrap/state.ts`** — Module-level singletons for session-global state (session ID, CWD, project root, token counts, model overrides, client type, permission mode).
### Workspace Packages
| Package | 说明 |
|---------|------|
| `packages/@ant/ink/` | Forked Ink 框架components、hooks、keybindings、theme |
| `packages/@ant/computer-use-mcp/` | Computer Use MCP server截图/键鼠/剪贴板/应用管理) |
| `packages/@ant/computer-use-input/` | 键鼠模拟dispatcher + darwin/win32/linux backend |
| `packages/@ant/computer-use-swift/` | 截图 + 应用管理dispatcher + per-platform backend |
| `packages/@ant/claude-for-chrome-mcp/` | Chrome 浏览器控制(通过 `--chrome` 启用) |
| `packages/@ant/model-provider/` | Model provider 抽象层 |
| `packages/builtin-tools/` | 内置工具集60 个 tool 实现,通过 `@claude-code-best/builtin-tools` 导出) |
| `packages/agent-tools/` | Agent 工具集 |
| `packages/acp-link/` | ACP 代理服务器WebSocket → ACP agent 桥接) |
| `packages/cc-knowledge/` | Claude Code 知识库(非 workspace 包) |
| `packages/langfuse-dashboard/` | Langfuse 可观测性面板(非 workspace 包) |
| `packages/mcp-client/` | MCP 客户端库 |
| `packages/mcp-server/` | MCP 服务端库(非 workspace 包) |
| `packages/remote-control-server/` | 自托管 Remote Control ServerDocker 部署,含 Web UI— Web UI 已重构为 React + Vite + Radix UI支持 ACP agent 接入 |
| `packages/swarm/` | Swarm 解耦模块(非 workspace 包) |
| `packages/shell/` | Shell 抽象(非 workspace 包) |
| `packages/audio-capture-napi/` | 原生音频捕获(已恢复) |
| `packages/color-diff-napi/` | 颜色差异计算完整实现11 tests |
| `packages/image-processor-napi/` | 图像处理(已恢复) |
| `packages/modifiers-napi/` | 键盘修饰键检测macOS FFI 实现) |
| `packages/url-handler-napi/` | URL scheme 处理(环境变量 + CLI 参数读取) |
### Bridge / Remote Control ### Bridge / Remote Control
- **`src/bridge/`** (~35 files) — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts` - **`src/bridge/`** — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`
- **`packages/remote-control-server/`** — 自托管 RCS支持 Docker 部署,含 Web UI 控制面板React 19 + Vite + Radix UI。支持 ACP agent 通过 acp-link 接入ACP WebSocket handler、relay handler、SSE event stream。通过 `bun run rcs` 启动。
- CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge` - CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge`
- 详见 `docs/features/remote-control-self-hosting.md`
### ACP Protocol (Agent Client Protocol)
- **`src/services/acp/`** — ACP agent 实现,包含 `agent.ts`AcpAgent 类)、`bridge.ts`Claude Code ↔ ACP 桥接)、`permissions.ts`(权限处理)、`entry.ts`(入口)。
- **`packages/acp-link/`** — ACP 代理服务器,将 WebSocket 客户端桥接到 ACP agent。提供 `acp-link` CLI 命令,支持自定义端口/HTTPS/认证/会话管理、RCS 集成REST 注册 + WS identify 两步流程、权限模式透传fallback: 客户端传值 > config > `ACP_PERMISSION_MODE` 环境变量)。
- ACP 权限管道改进:`createAcpCanUseTool` 统一权限流水线,`applySessionMode` 模式同步,`bypassPermissions` 可用性检测(非 root/sandbox 环境)。
- ACP Plan 可视化已支持 `session/update plan` 类型的消息展示PlanView 组件,含进度条/状态图标/优先级标签)。
### Daemon Mode ### Daemon Mode
@ -131,91 +204,70 @@ bun run docs:dev
### Feature Flag System ### Feature Flag System
Feature flags control which functionality is enabled at runtime: Feature flags control which functionality is enabled at runtime. 代码中统一通过 `import { feature } from 'bun:bundle'` 导入,调用 `feature('FLAG_NAME')` 返回 `boolean`
- **在代码中使用**: 统一通过 `import { feature } from 'bun:bundle'` 导入,调用 `feature('FLAG_NAME')` 返回 `boolean`。**不要**在 `cli.tsx` 或其他文件里自己定义 `feature` 函数或覆盖这个 import。 **启用方式**: 环境变量 `FEATURE_<FLAG_NAME>=1`。例如 `FEATURE_BUDDY=1 bun run dev`
- **启用方式**: 通过环境变量 `FEATURE_<FLAG_NAME>=1`。例如 `FEATURE_BUDDY=1 bun run dev` 启用 BUDDY 功能。
- **Dev 默认 features**: `BUDDY`、`TRANSCRIPT_CLASSIFIER`、`BRIDGE_MODE`、`AGENT_TRIGGERS_REMOTE`、`CHICAGO_MCP`、`VOICE_MODE`(见 `scripts/dev.ts`)。 **Build 默认 features**19 个,见 `build.ts`:
- **Build 默认 features**: `AGENT_TRIGGERS_REMOTE`、`CHICAGO_MCP`、`VOICE_MODE`(见 `build.ts`)。 - 基础: `BUDDY`, `TRANSCRIPT_CLASSIFIER`, `BRIDGE_MODE`, `AGENT_TRIGGERS_REMOTE`, `CHICAGO_MCP`, `VOICE_MODE`
- **常见 flag**: `BUDDY`, `DAEMON`, `BRIDGE_MODE`, `BG_SESSIONS`, `PROACTIVE`, `KAIROS`, `VOICE_MODE`, `FORK_SUBAGENT`, `SSH_REMOTE`, `DIRECT_CONNECT`, `TEMPLATES`, `CHICAGO_MCP`, `BYOC_ENVIRONMENT_RUNNER`, `SELF_HOSTED_RUNNER`, `COORDINATOR_MODE`, `UDS_INBOX`, `LODESTONE`, `ABLATION_BASELINE` 等。 - 统计/缓存: `SHOT_STATS`, `PROMPT_CACHE_BREAK_DETECTION`, `TOKEN_BUDGET`
- **类型声明**: `src/types/internal-modules.d.ts` 中声明了 `bun:bundle` 模块的 `feature` 函数签名。 - P0 本地: `AGENT_TRIGGERS`, `ULTRATHINK`, `BUILTIN_EXPLORE_PLAN_AGENTS`, `LODESTONE`
- P1 API 依赖: `EXTRACT_MEMORIES`, `VERIFICATION_AGENT`, `KAIROS_BRIEF`, `AWAY_SUMMARY`, `ULTRAPLAN`
- P2: `DAEMON`
**Dev mode 默认**: 全部启用(见 `scripts/dev.ts`)。
**类型声明**: `src/types/internal-modules.d.ts` 中声明了 `bun:bundle` 模块的 `feature` 函数签名。
**新增功能的正确做法**: 保留 `import { feature } from 'bun:bundle'` + `feature('FLAG_NAME')` 的标准模式,在运行时通过环境变量或配置控制,不要绕过 feature flag 直接 import。 **新增功能的正确做法**: 保留 `import { feature } from 'bun:bundle'` + `feature('FLAG_NAME')` 的标准模式,在运行时通过环境变量或配置控制,不要绕过 feature flag 直接 import。
### Multi-API 兼容层
所有兼容层均采用流适配器模式:将第三方 API 格式转为 Anthropic 内部格式,下游代码完全不改。通过 `/login` 命令配置。
#### OpenAI 兼容层
通过 `CLAUDE_CODE_USE_OPENAI=1` 启用,支持 Ollama/DeepSeek/vLLM 等任意 OpenAI Chat Completions 协议端点。含 DeepSeek thinking mode 支持。
- **`src/services/api/openai/`** — client、消息/工具转换、流适配、模型映射
- 关键环境变量:`CLAUDE_CODE_USE_OPENAI`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL`
#### Gemini 兼容层
通过 `CLAUDE_CODE_USE_GEMINI=1` 启用。独立环境变量体系。
- **`src/services/api/gemini/`** — client、模型映射、类型定义
- 关键环境变量:`GEMINI_API_KEY`(必填)、`GEMINI_MODEL`(直接指定)、`GEMINI_DEFAULT_SONNET_MODEL`/`GEMINI_DEFAULT_OPUS_MODEL`(按能力映射)
- 模型映射优先级:`GEMINI_MODEL` > `GEMINI_DEFAULT_*_MODEL` > `ANTHROPIC_DEFAULT_*_MODEL`(已废弃) > 原样返回
#### Grok 兼容层
通过 `CLAUDE_CODE_USE_GROK=1` 启用。自定义模型映射支持 xAI Grok API。
- **`src/services/api/grok/`** — client、模型映射
详见各兼容层的 docs 文档。
### 穷鬼模式Budget Mode
- 通过 `/poor` 命令切换,持久化到 `settings.json`
- 启用后跳过 `extract_memories`、`prompt_suggestion` 和 `verification_agent`,显著减少 token 消耗。
- 实现在 `src/commands/poor/poorMode.ts`
### Stubbed/Deleted Modules ### Stubbed/Deleted Modules
| Module | Status | | Module | Status |
|--------|--------| |--------|--------|
| Computer Use (`@ant/*`) | Restored — `computer-use-swift`, `computer-use-input`, `computer-use-mcp`, `claude-for-chrome-mcp` 均有完整实现macOS + Windows 可用Linux 后端待完成 | | Computer Use (`@ant/*`) | Restored — macOS + Windows + Linux后端完整度不一 |
| `*-napi` packages | `audio-capture-napi`、`image-processor-napi` 已恢复实现;`color-diff-napi` 完整实现;`url-handler-napi`、`modifiers-napi` 仍为 stub | | `*-napi` packages | 全部已恢复/实现:`audio-capture-napi`、`image-processor-napi` 已恢复;`color-diff-napi` 完整;`modifiers-napi`macOS FFI`url-handler-napi`(环境变量+CLI |
| Voice Mode | Restored — `src/voice/`、`src/hooks/useVoiceIntegration.tsx`、`src/services/voiceStreamSTT.ts` 等Push-to-Talk 语音输入(需 Anthropic OAuth | | Voice Mode | Restored — Push-to-Talk 语音输入(需 Anthropic OAuth |
| OpenAI 兼容层 | Restored — `src/services/api/openai/`,支持 Ollama/DeepSeek/vLLM 等任意 OpenAI 协议端点,通过 `CLAUDE_CODE_USE_OPENAI=1` 启用 | | OpenAI/Gemini/Grok 兼容层 | Restored |
| Remote Control Server | Restored — 自托管 RCS + Web UI |
| Analytics / GrowthBook / Sentry | Empty implementations | | Analytics / GrowthBook / Sentry | Empty implementations |
| Magic Docs / LSP Server | Removed | | Magic Docs / LSP Server | Restored — Magic Docs 自动更新 + LSP 服务器管理器 |
| Plugins / Marketplace | Removed | | Plugins / Marketplace | Restored — 插件安装/卸载/启用/禁用 + Marketplace 浏览 |
| MCP OAuth | Simplified | | MCP OAuth | Simplified |
### Computer Use
Feature flag `CHICAGO_MCP`dev/build 默认启用。实现跨平台屏幕操控macOS + Windows 可用Linux 待完成)。
- **`packages/@ant/computer-use-mcp/`** — MCP server注册截图/键鼠/剪贴板/应用管理工具
- **`packages/@ant/computer-use-input/`** — 键鼠模拟dispatcher + per-platform backend`backends/darwin.ts`、`win32.ts`、`linux.ts`
- **`packages/@ant/computer-use-swift/`** — 截图 + 应用管理,同样 dispatcher + per-platform backend
- **`packages/@ant/claude-for-chrome-mcp/`** — Chrome 浏览器控制(独立于 Computer Use通过 `--chrome` CLI 参数启用)
详见 `docs/features/computer-use.md`
### Voice Mode
Feature flag `VOICE_MODE`dev/build 默认启用。Push-to-Talk 语音输入,音频通过 WebSocket 流式传输到 Anthropic STTNova 3。需要 Anthropic OAuth非 API key
- **`src/voice/voiceModeEnabled.ts`** — 三层门控feature flag + GrowthBook + OAuth auth
- **`src/hooks/useVoice.ts`** — React hook 管理录音状态和 WebSocket 连接
- **`src/services/voiceStreamSTT.ts`** — STT WebSocket 流式传输
详见 `docs/features/voice-mode.md`
### OpenAI 兼容层
通过 `CLAUDE_CODE_USE_OPENAI=1` 环境变量启用,支持任意 OpenAI Chat Completions 协议端点Ollama、DeepSeek、vLLM 等)。流适配器模式:在 `queryModel()` 中将 Anthropic 格式请求转为 OpenAI 格式,再将 SSE 流转换回 `BetaRawMessageStreamEvent`,下游代码完全不改。
- **`src/services/api/openai/`** — client、消息/工具转换、流适配、模型映射
- **`src/utils/model/providers.ts`** — 添加 `'openai'` provider 类型(最高优先级)
关键环境变量:`CLAUDE_CODE_USE_OPENAI`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL`、`OPENAI_DEFAULT_OPUS_MODEL`、`OPENAI_DEFAULT_SONNET_MODEL`、`OPENAI_DEFAULT_HAIKU_MODEL`。详见 `docs/plans/openai-compatibility.md`
### Gemini 兼容层
通过 `CLAUDE_CODE_USE_GEMINI=1` 环境变量或 `modelType: "gemini"` 设置启用,支持 Google Gemini API。独立的环境变量体系不与 OpenAI 或 Anthropic 配置混杂。
- **`src/services/api/gemini/`** — client、模型映射、类型定义
- **`src/utils/model/providers.ts`** — 添加 `'gemini'` provider 类型
- **`src/utils/managedEnvConstants.ts`** — Gemini 专用的 managed env vars
关键环境变量:
- `CLAUDE_CODE_USE_GEMINI` - 启用 Gemini provider
- `GEMINI_API_KEY` - API 密钥(必填)
- `GEMINI_BASE_URL` - API 端点(可选,默认 `https://generativelanguage.googleapis.com/v1beta`
- `GEMINI_MODEL` - 直接指定模型(最高优先级)
- `GEMINI_DEFAULT_HAIKU_MODEL` / `GEMINI_DEFAULT_SONNET_MODEL` / `GEMINI_DEFAULT_OPUS_MODEL` - 按能力级别映射
- `GEMINI_DEFAULT_HAIKU_MODEL_NAME` / `DESCRIPTION` / `SUPPORTED_CAPABILITIES` - 显示名称和描述
- `GEMINI_SMALL_FAST_MODEL` - 快速任务使用的模型(可选)
模型映射优先级(`src/services/api/gemini/modelMapping.ts`
1. `GEMINI_MODEL` - 直接覆盖
2. `GEMINI_DEFAULT_*_MODEL` - 独立配置(推荐)
3. `ANTHROPIC_DEFAULT_*_MODEL` - 向后兼容 fallback已废弃
4. 原样返回 Anthropic 模型名
使用示例:
```bash
export CLAUDE_CODE_USE_GEMINI=1
export GEMINI_API_KEY="your-api-key"
export GEMINI_DEFAULT_SONNET_MODEL="gemini-2.5-flash"
export GEMINI_DEFAULT_OPUS_MODEL="gemini-2.5-pro"
```
### Key Type Files ### Key Type Files
- **`src/types/global.d.ts`** — Declares `MACRO`, `BUILD_TARGET`, `BUILD_ENV` and internal Anthropic-only identifiers. - **`src/types/global.d.ts`** — Declares `MACRO`, `BUILD_TARGET`, `BUILD_ENV` and internal Anthropic-only identifiers.
@ -230,16 +282,83 @@ export GEMINI_DEFAULT_OPUS_MODEL="gemini-2.5-pro"
- **集成测试**: `tests/integration/` — 4 个文件cli-arguments, context-build, message-pipeline, tool-chain - **集成测试**: `tests/integration/` — 4 个文件cli-arguments, context-build, message-pipeline, tool-chain
- **共享 mock/fixture**: `tests/mocks/`api-responses, file-system, fixtures/ - **共享 mock/fixture**: `tests/mocks/`api-responses, file-system, fixtures/
- **命名**: `describe("functionName")` + `test("behavior description")`,英文 - **命名**: `describe("functionName")` + `test("behavior description")`,英文
- **Mock 模式**: 对重依赖模块使用 `mock.module()` + `await import()` 解锁(必须内联在测试文件中,不能从共享 helper 导入) - **包测试**: `packages/` 下各包也有独立测试(如 `color-diff-napi` 11 tests
- **当前状态**: ~1623 tests / 114 files (110 unit + 4 integration) / 0 fail详见 `docs/testing-spec.md`
### Mock 使用规范
**只 mock 有副作用的依赖链,不 mock 纯函数/纯数据模块。**
被迫 mock 的根源:`log.ts` / `debug.ts``bootstrap/state.ts`(模块级 `realpathSync` / `randomUUID` 副作用)。必须 mock 的模块:`log.ts`、`debug.ts`、`bun:bundle`、`settings/settings.js`、`config.ts`、`auth.ts`、第三方网络库。
**`log.ts` 和 `debug.ts` 使用共享 mock**`tests/mocks/log.ts` / `tests/mocks/debug.ts`),不要在测试文件中内联 mock 定义。使用方式:
```ts
import { logMock } from "../../../tests/mocks/log";
mock.module("src/utils/log.ts", logMock);
import { debugMock } from "../../../../tests/mocks/debug";
mock.module("src/utils/debug.ts", debugMock);
```
源文件导出变更时只需更新 `tests/mocks/` 下的对应文件,不需要逐个修改测试。
不要 mock纯函数模块`errors.ts`、`stringUtils.js`、mock 值与真实实现相同的模块、mock 路径与实际 import 不匹配的模块。
路径规则:统一用 `.ts` 扩展名 + `src/*` 别名路径,禁止双重 mock 同一模块。
### 类型检查
项目使用 TypeScript strict 模式,**tsc 必须零错误**。每次修改后运行:
```bash
bun run precheck
```
**类型规范**
- 生产代码禁止 `as any`;测试文件中 mock 数据可用 `as any`
- 类型不匹配优先用 `as unknown as SpecificType` 双重断言,或补充 interface
- 未知结构对象用 `Record<string, unknown>` 替代 `any`
- 联合类型用类型守卫type guard收窄不要强转
- `msg.request` 属性访问:`const req = msg.request as Record<string, unknown>`
- Ink `color` prop`as keyof Theme` 而非 `as any`
## Working with This Codebase ## Working with This Codebase
- **Don't try to fix all tsc errors** — they're from decompilation and don't affect runtime. - **precheck must pass** — `bun run precheck`typecheck + lint fix + test必须零错误任何修改都不能引入新的类型/lint/测试错误。
- **Feature flags** — 默认全部关闭(`feature()` 返回 `false`。Dev/build 各有自己的默认启用列表。不要在 `cli.tsx` 中重定义 `feature` 函数。 - **Feature flags** — 默认全部关闭(`feature()` 返回 `false`。Dev/build 各有自己的默认启用列表。不要在 `cli.tsx` 中重定义 `feature` 函数。
- **React Compiler output** — Components have decompiled memoization boilerplate (`const $ = _c(N)`). This is normal. - **React Compiler output** — Components have decompiled memoization boilerplate (`const $ = _c(N)`). This is normal.
- **`bun:bundle` import** — `import { feature } from 'bun:bundle'` 是 Bun 内置模块,由运行时/构建器解析。不要用自定义函数替代它。 - **`bun:bundle` import** — `import { feature } from 'bun:bundle'` 是 Bun 内置模块,由运行时/构建器解析。不要用自定义函数替代它。**`feature()` 只能直接用在 `if` 语句或三元表达式的条件位置**Bun 编译器限制),不能赋值给变量、不能放在箭头函数体里、不能作为 `&&` 链的一部分。正确:`if (feature('X')) {}` 或 `feature('X') ? a : b`
- **`src/` path alias** — tsconfig maps `src/*` to `./src/*`. Imports like `import { ... } from 'src/utils/...'` are valid. - **`src/` path alias** — tsconfig maps `src/*` to `./src/*`. Imports like `import { ... } from 'src/utils/...'` are valid.
- **MACRO defines** — 集中管理在 `scripts/defines.ts`。Dev mode 通过 `bun -d` 注入build 通过 `Bun.build({ define })` 注入。修改版本号等常量只改这个文件。 - **MACRO defines** — 集中管理在 `scripts/defines.ts`。Dev mode 通过 `bun -d` 注入build 通过 `Bun.build({ define })` 注入。修改版本号等常量只改这个文件。
- **构建产物兼容 Node.js**`build.ts` 会自动后处理 `import.meta.require`,产物可直接用 `node dist/cli.js` 运行。 - **构建产物兼容 Node.js**`build.ts` 会自动后处理 `import.meta.require`,产物可直接用 `node dist/cli.js` 运行。
- **Biome 配置** — 大量 lint 规则被关闭decompiled 代码不适合严格 lint。`.tsx` 文件用 120 行宽 + 强制分号;其他文件 80 行宽 + 按需分号。 - **Biome 配置** — 42 条 lint 规则因 decompiled 代码被关闭,仅保留 `recommended` 基线。格式化覆盖全项目(`src/`、`scripts/`、`packages/`,含 `packages/@ant/`)。`.tsx` 文件用 120 行宽 + 强制分号;其他文件 80 行宽 + 按需分号。JSON 格式化已启用。`.editorconfig` 与 Biome 配置对齐2-space 缩进)。修改任何代码后应运行 `bun run precheck` 确认无类型/lint/格式/测试问题pre-commit hook 会自动拦截不合格提交。
- **tsc 与 Biome 冲突处理** — 当 tsc 要求声明属性(赋值使用)但 biome 报 `noUnusedPrivateClassMembers`(只写不读)时,用 `// biome-ignore lint/correctness/noUnusedPrivateClassMembers: <原因>` 抑制 lint 警告,保留类型声明。`biome ci` 必须零 warnings。
- **`@ts-expect-error` 维护** — 只在下方代码确实有类型错误时保留 `@ts-expect-error`。如果类型系统已更新导致 directive 变为 unusedTS2578直接移除注释。MACRO 替换产生的永假比较(如 `'production' === 'development'`)仍需保留 `@ts-expect-error`
- **Ink 框架在 `packages/@ant/ink/`** — 不是 `src/ink/`该目录不存在。Ink 相关的组件、hooks、keybindings 都在 packages 中。
- **Provider 优先级**`modelType` 参数 > 环境变量 > 默认 `firstParty`。新增 provider 需在 `src/utils/model/providers.ts` 注册。
## Design Context
Impeccable 设计上下文保存在 `.impeccable.md` 中。设计 Web UIRCS 控制面板、文档站、着陆页)时必须参考该文件。
### 核心设计原则
1. **Considered over clever** — 每个设计选择都应感觉有意为之,而非追逐潮流
2. **Warmth through subtlety** — 通过橙色色调的中性色、留白布局、有温度的文案来传达温暖
3. **Density with clarity** — 技术用户需要信息密度,但不能混乱
4. **Community voice** — 设计应感觉是由使用者创造的,而非遥远的设计团队
5. **Anthropic's shadow** — 遵循 Anthropic 的设计直觉:干净的布局、充足的间距、温暖的色温
### 品牌色
- 主色Claude Orange `#D77757`terra cotta
- 辅色Claude Blue `#5769F7`
- 暗色模式使用温暖的深色表面(非冷蓝黑色)
### 目标用户
技术团队/企业,在专业工作流中使用 AI 辅助编程。友好的开源社区氛围,非企业 SaaS 风格。
### 视觉参考
Anthropic 公司的设计风格 — 干净、考究、温暖的底色。大量留白,以排版为核心。避免 AI 产品常见的设计套路(渐变文字、玻璃态、霓虹色)。

View File

@ -18,6 +18,7 @@ import type { Tools } from '../Tool.js';
import { findToolByName } from '../Tool.js'; import { findToolByName } from '../Tool.js';
import type { AgentDefinitionsResult } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'; import type { AgentDefinitionsResult } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js';
import type { import type {
AssistantMessage,
Message as MessageType, Message as MessageType,
NormalizedMessage, NormalizedMessage,
ProgressMessage as ProgressMessageType, ProgressMessage as ProgressMessageType,
@ -36,6 +37,7 @@ import {
buildMessageLookups, buildMessageLookups,
computeMessageStructureKey, computeMessageStructureKey,
type MessageLookups, type MessageLookups,
updateMessageLookupsIncremental,
createAssistantMessage, createAssistantMessage,
deriveUUID, deriveUUID,
getMessagesAfterCompactBoundary, getMessagesAfterCompactBoundary,
@ -516,7 +518,13 @@ const MessagesImpl = ({
// message content changed during streaming (text/thinking deltas). The key // message content changed during streaming (text/thinking deltas). The key
// captures only structural info (types, IDs), so content-only deltas skip // captures only structural info (types, IDs), so content-only deltas skip
// the rebuild entirely. // the rebuild entirely.
const lookupsCacheRef = useRef<{ key: string; lookups: MessageLookups } | null>(null); const lookupsCacheRef = useRef<{
key: string;
lookups: MessageLookups;
normalizedCount: number;
messageCount: number;
lastAssistantMsgId: string | undefined;
} | null>(null);
// Expensive message transforms — filter, reorder, group, collapse, lookups. // Expensive message transforms — filter, reorder, group, collapse, lookups.
// All O(n) over 27k messages. Split from the renderRange slice so scrolling // All O(n) over 27k messages. Split from the renderRange slice so scrolling
@ -587,12 +595,57 @@ const MessagesImpl = ({
); );
const lookupsKey = computeMessageStructureKey(normalizedMessages, messagesToShow as MessageType[]); const lookupsKey = computeMessageStructureKey(normalizedMessages, messagesToShow as MessageType[]);
const currentLastAssistantMsgId = (() => {
const lastMsg = (messagesToShow as MessageType[]).at(-1);
return lastMsg?.type === 'assistant' ? (lastMsg as AssistantMessage).message?.id : undefined;
})();
let lookups: MessageLookups; let lookups: MessageLookups;
if (lookupsCacheRef.current && lookupsCacheRef.current.key === lookupsKey) { if (lookupsCacheRef.current && lookupsCacheRef.current.key === lookupsKey) {
lookups = lookupsCacheRef.current.lookups; lookups = lookupsCacheRef.current.lookups;
} else if (
lookupsCacheRef.current &&
normalizedMessages.length >= lookupsCacheRef.current.normalizedCount &&
(messagesToShow as MessageType[]).length >= lookupsCacheRef.current.messageCount &&
// If lastAssistantMsgId changed, previous "in-progress" assistant may
// now be orphaned — force a full rebuild to pick up the new status.
lookupsCacheRef.current.lastAssistantMsgId === currentLastAssistantMsgId
) {
// Try incremental update when only new messages were appended
const updated = updateMessageLookupsIncremental(
lookupsCacheRef.current.lookups,
lookupsCacheRef.current.normalizedCount,
lookupsCacheRef.current.messageCount,
normalizedMessages,
messagesToShow as MessageType[],
);
if (updated) {
lookups = updated;
lookupsCacheRef.current = {
key: lookupsKey,
lookups,
normalizedCount: normalizedMessages.length,
messageCount: (messagesToShow as MessageType[]).length,
lastAssistantMsgId: currentLastAssistantMsgId,
};
} else {
lookups = buildMessageLookups(normalizedMessages, messagesToShow as MessageType[]);
lookupsCacheRef.current = {
key: lookupsKey,
lookups,
normalizedCount: normalizedMessages.length,
messageCount: (messagesToShow as MessageType[]).length,
lastAssistantMsgId: currentLastAssistantMsgId,
};
}
} else { } else {
lookups = buildMessageLookups(normalizedMessages, messagesToShow as MessageType[]); lookups = buildMessageLookups(normalizedMessages, messagesToShow as MessageType[]);
lookupsCacheRef.current = { key: lookupsKey, lookups }; lookupsCacheRef.current = {
key: lookupsKey,
lookups,
normalizedCount: normalizedMessages.length,
messageCount: (messagesToShow as MessageType[]).length,
lastAssistantMsgId: currentLastAssistantMsgId,
};
} }
const hiddenMessageCount = messagesToShowNotTruncated.length - MAX_MESSAGES_TO_SHOW_IN_TRANSCRIPT_MODE; const hiddenMessageCount = messagesToShowNotTruncated.length - MAX_MESSAGES_TO_SHOW_IN_TRANSCRIPT_MODE;

View File

@ -315,7 +315,9 @@ export function isSyntheticMessage(message: Message): boolean {
message.type !== 'system' && message.type !== 'system' &&
Array.isArray(message.message?.content) && Array.isArray(message.message?.content) &&
message.message?.content[0]?.type === 'text' && message.message?.content[0]?.type === 'text' &&
SYNTHETIC_MESSAGES.has((message.message?.content[0] as { text: string }).text) SYNTHETIC_MESSAGES.has(
(message.message?.content[0] as { text: string }).text,
)
) )
} }
@ -754,7 +756,9 @@ export function normalizeMessages(messages: Message[]): NormalizedMessage[] {
switch (message.type) { switch (message.type) {
case 'assistant': { case 'assistant': {
const aMsg = message as AssistantMessage const aMsg = message as AssistantMessage
const assistantContent = Array.isArray(aMsg.message.content) ? aMsg.message.content : [] const assistantContent = Array.isArray(aMsg.message.content)
? aMsg.message.content
: []
isNewChain = isNewChain || assistantContent.length > 1 isNewChain = isNewChain || assistantContent.length > 1
return assistantContent.map((_, index) => { return assistantContent.map((_, index) => {
const uuid = isNewChain const uuid = isNewChain
@ -813,10 +817,17 @@ export function normalizeMessages(messages: Message[]): NormalizedMessage[] {
...createUserMessage({ ...createUserMessage({
content: [_], content: [_],
toolUseResult: uMsg.toolUseResult, toolUseResult: uMsg.toolUseResult,
mcpMeta: uMsg.mcpMeta as { _meta?: Record<string, unknown>; structuredContent?: Record<string, unknown> }, mcpMeta: uMsg.mcpMeta as {
_meta?: Record<string, unknown>
structuredContent?: Record<string, unknown>
},
isMeta: uMsg.isMeta === true ? true : undefined, isMeta: uMsg.isMeta === true ? true : undefined,
isVisibleInTranscriptOnly: uMsg.isVisibleInTranscriptOnly === true ? true : undefined, isVisibleInTranscriptOnly:
isVirtual: (uMsg.isVirtual as boolean | undefined) === true ? true : undefined, uMsg.isVisibleInTranscriptOnly === true ? true : undefined,
isVirtual:
(uMsg.isVirtual as boolean | undefined) === true
? true
: undefined,
timestamp: uMsg.timestamp as string | undefined, timestamp: uMsg.timestamp as string | undefined,
imagePasteIds: imageId !== undefined ? [imageId] : undefined, imagePasteIds: imageId !== undefined ? [imageId] : undefined,
origin: uMsg.origin as MessageOrigin | undefined, origin: uMsg.origin as MessageOrigin | undefined,
@ -842,7 +853,9 @@ export function isToolUseRequestMessage(
message.type === 'assistant' && message.type === 'assistant' &&
// Note: stop_reason === 'tool_use' is unreliable -- it's not always set correctly // Note: stop_reason === 'tool_use' is unreliable -- it's not always set correctly
Array.isArray(message.message?.content) && Array.isArray(message.message?.content) &&
(message.message?.content as Array<{type: string}>).some(_ => _.type === 'tool_use') (message.message?.content as Array<{ type: string }>).some(
_ => _.type === 'tool_use',
)
) )
} }
@ -856,7 +869,8 @@ export function isToolUseResultMessage(
return ( return (
message.type === 'user' && message.type === 'user' &&
((Array.isArray(message.message?.content) && ((Array.isArray(message.message?.content) &&
(message.message?.content as Array<{type: string}>)[0]?.type === 'tool_result') || (message.message?.content as Array<{ type: string }>)[0]?.type ===
'tool_result') ||
Boolean(message.toolUseResult)) Boolean(message.toolUseResult))
) )
} }
@ -930,7 +944,8 @@ export function reorderMessagesInUI(
Array.isArray(message.message.content) && Array.isArray(message.message.content) &&
message.message.content[0]?.type === 'tool_result' message.message.content[0]?.type === 'tool_result'
) { ) {
const toolUseID = (message.message.content[0] as ToolResultBlockParam).tool_use_id const toolUseID = (message.message.content[0] as ToolResultBlockParam)
.tool_use_id
if (!toolUseGroups.has(toolUseID)) { if (!toolUseGroups.has(toolUseID)) {
toolUseGroups.set(toolUseID, { toolUseGroups.set(toolUseID, {
toolUse: null, toolUse: null,
@ -958,7 +973,6 @@ export function reorderMessagesInUI(
}) })
} }
toolUseGroups.get(toolUseID)!.postHooks.push(message) toolUseGroups.get(toolUseID)!.postHooks.push(message)
continue
} }
} }
@ -1062,8 +1076,10 @@ function getInProgressHookCount(
messages, messages,
_ => _ =>
_.type === 'progress' && _.type === 'progress' &&
(_.data as { type: string; hookEvent: HookEvent }).type === 'hook_progress' && (_.data as { type: string; hookEvent: HookEvent }).type ===
(_.data as { type: string; hookEvent: HookEvent }).hookEvent === hookEvent && 'hook_progress' &&
(_.data as { type: string; hookEvent: HookEvent }).hookEvent ===
hookEvent &&
_.parentToolUseID === toolUseID, _.parentToolUseID === toolUseID,
) )
} }
@ -1112,11 +1128,21 @@ export function getToolResultIDs(normalizedMessages: NormalizedMessage[]): {
} { } {
return Object.fromEntries( return Object.fromEntries(
normalizedMessages.flatMap(_ => normalizedMessages.flatMap(_ =>
_.type === 'user' && Array.isArray(_.message?.content) && (_.message?.content as Array<{type:string}>)[0]?.type === 'tool_result' _.type === 'user' &&
Array.isArray(_.message?.content) &&
(_.message?.content as Array<{ type: string }>)[0]?.type === 'tool_result'
? [ ? [
[ [
((_.message?.content as Array<{type:string}>)[0] as ToolResultBlockParam).tool_use_id, (
((_.message?.content as Array<{type:string}>)[0] as ToolResultBlockParam).is_error ?? false, (
_.message?.content as Array<{ type: string }>
)[0] as ToolResultBlockParam
).tool_use_id,
(
(
_.message?.content as Array<{ type: string }>
)[0] as ToolResultBlockParam
).is_error ?? false,
], ],
] ]
: ([] as [string, boolean][]), : ([] as [string, boolean][]),
@ -1137,7 +1163,9 @@ export function getSiblingToolUseIDs(
(_): _ is AssistantMessage => (_): _ is AssistantMessage =>
_.type === 'assistant' && _.type === 'assistant' &&
Array.isArray(_.message?.content) && Array.isArray(_.message?.content) &&
(_.message?.content as Array<{type:string; id?:string}>).some(block => block.type === 'tool_use' && block.id === toolUseID), (_.message?.content as Array<{ type: string; id?: string }>).some(
block => block.type === 'tool_use' && block.id === toolUseID,
),
) )
if (!unnormalizedMessage) { if (!unnormalizedMessage) {
return new Set() return new Set()
@ -1152,7 +1180,9 @@ export function getSiblingToolUseIDs(
return new Set( return new Set(
siblingMessages.flatMap(_ => siblingMessages.flatMap(_ =>
Array.isArray(_.message?.content) Array.isArray(_.message?.content)
? (_.message?.content as Array<{type:string; id?:string}>).filter(_ => _.type === 'tool_use').map(_ => _.id!) ? (_.message?.content as Array<{ type: string; id?: string }>)
.filter(_ => _.type === 'tool_use')
.map(_ => _.id!)
: [], : [],
), ),
) )
@ -1205,7 +1235,10 @@ export function buildMessageLookups(
const toolUseContent = content as ToolUseBlock const toolUseContent = content as ToolUseBlock
toolUseIDs.add(toolUseContent.id) toolUseIDs.add(toolUseContent.id)
toolUseIDToMessageID.set(toolUseContent.id, id) toolUseIDToMessageID.set(toolUseContent.id, id)
toolUseByToolUseID.set(toolUseContent.id, content as ToolUseBlockParam) toolUseByToolUseID.set(
toolUseContent.id,
content as ToolUseBlockParam,
)
} }
} }
} }
@ -1256,7 +1289,7 @@ export function buildMessageLookups(
// Build tool result lookup and resolved/errored sets // Build tool result lookup and resolved/errored sets
if (msg.type === 'user' && Array.isArray(msg.message?.content)) { if (msg.type === 'user' && Array.isArray(msg.message?.content)) {
for (const content of (msg.message?.content ?? [])) { for (const content of msg.message?.content ?? []) {
if (typeof content !== 'string' && content.type === 'tool_result') { if (typeof content !== 'string' && content.type === 'tool_result') {
const tr = content as ToolResultBlockParam const tr = content as ToolResultBlockParam
toolResultByToolUseID.set(tr.tool_use_id, msg) toolResultByToolUseID.set(tr.tool_use_id, msg)
@ -1269,7 +1302,7 @@ export function buildMessageLookups(
} }
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) { if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
for (const content of (msg.message?.content ?? [])) { for (const content of msg.message?.content ?? []) {
if (typeof content === 'string') continue if (typeof content === 'string') continue
// Track all server-side *_tool_result blocks (advisor, web_search, // Track all server-side *_tool_result blocks (advisor, web_search,
// code_execution, mcp, etc.) — any block with tool_use_id is a result. // code_execution, mcp, etc.) — any block with tool_use_id is a result.
@ -1364,6 +1397,172 @@ export function buildMessageLookups(
} }
} }
/**
* Incrementally update lookups by processing only newly appended messages.
* Returns the same lookups object (mutated in place) if update succeeds,
* or null if a full rebuild is needed (e.g., messages were removed).
*/
export function updateMessageLookupsIncremental(
existing: MessageLookups,
previousNormalizedCount: number,
previousMessageCount: number,
normalizedMessages: NormalizedMessage[],
messages: Message[],
): MessageLookups | null {
// Safety check: only handle append-only case
if (
normalizedMessages.length < previousNormalizedCount ||
messages.length < previousMessageCount
) {
return null
}
// No new messages — nothing to do
if (
normalizedMessages.length === previousNormalizedCount &&
messages.length === previousMessageCount
) {
return existing
}
// Process new messages entries (pass 1: assistant tool_use blocks)
const newMessageStart = previousMessageCount
for (let i = newMessageStart; i < messages.length; i++) {
const msg = messages[i]!
if (msg.type === 'assistant') {
const aMsg = msg as AssistantMessage
const id = aMsg.message.id!
if (Array.isArray(aMsg.message.content)) {
const newToolUseIDs: string[] = []
for (const content of aMsg.message.content) {
if (typeof content !== 'string' && content.type === 'tool_use') {
const toolUseContent = content as ToolUseBlock
newToolUseIDs.push(toolUseContent.id)
existing.toolUseByToolUseID.set(
toolUseContent.id,
content as ToolUseBlockParam,
)
}
}
// Update sibling lookup: all tool_use IDs in this message share siblings
const allSiblings = new Set(newToolUseIDs)
for (const toolUseID of newToolUseIDs) {
existing.siblingToolUseIDs.set(toolUseID, allSiblings)
}
}
}
}
// Process new normalizedMessages entries (pass 2: progress, hooks, tool results)
const newNormalizedStart = previousNormalizedCount
for (let i = newNormalizedStart; i < normalizedMessages.length; i++) {
const msg = normalizedMessages[i]!
if (msg.type === 'progress') {
const toolUseID = msg.parentToolUseID as string
const existing2 = existing.progressMessagesByToolUseID.get(toolUseID)
if (existing2) {
existing2.push(msg as ProgressMessage)
} else {
existing.progressMessagesByToolUseID.set(toolUseID, [
msg as ProgressMessage,
])
}
const progressData = msg.data as { type: string; hookEvent: HookEvent }
if (progressData.type === 'hook_progress') {
const hookEvent = progressData.hookEvent
let byHookEvent = existing.inProgressHookCounts.get(toolUseID)
if (!byHookEvent) {
byHookEvent = new Map()
existing.inProgressHookCounts.set(toolUseID, byHookEvent)
}
byHookEvent.set(hookEvent, (byHookEvent.get(hookEvent) ?? 0) + 1)
}
}
if (msg.type === 'user' && Array.isArray(msg.message?.content)) {
for (const content of msg.message?.content ?? []) {
if (typeof content !== 'string' && content.type === 'tool_result') {
const tr = content as ToolResultBlockParam
existing.toolResultByToolUseID.set(tr.tool_use_id, msg)
existing.resolvedToolUseIDs.add(tr.tool_use_id)
if (tr.is_error) {
existing.erroredToolUseIDs.add(tr.tool_use_id)
}
}
}
}
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
for (const content of msg.message?.content ?? []) {
if (typeof content === 'string') continue
if (
'tool_use_id' in content &&
typeof (content as { tool_use_id: string }).tool_use_id === 'string'
) {
existing.resolvedToolUseIDs.add(
(content as { tool_use_id: string }).tool_use_id,
)
}
if ((content.type as string) === 'advisor_tool_result') {
const result = content as {
tool_use_id: string
content: { type: string }
}
if (result.content.type === 'advisor_tool_result_error') {
existing.erroredToolUseIDs.add(result.tool_use_id)
}
}
}
}
if (isHookAttachmentMessage(msg)) {
const toolUseID = msg.attachment.toolUseID
const hookEvent = msg.attachment.hookEvent
const hookName = (msg.attachment as HookAttachmentWithName).hookName
if (hookName !== undefined) {
let byHookEvent = existing.resolvedHookCounts.get(toolUseID)
if (!byHookEvent) {
byHookEvent = new Map()
existing.resolvedHookCounts.set(toolUseID, byHookEvent)
}
byHookEvent.set(hookEvent, (byHookEvent.get(hookEvent) ?? 0) + 1)
}
}
}
existing.normalizedMessageCount = normalizedMessages.length
// Mark orphaned server_tool_use / mcp_tool_use blocks as errored.
// Only scan the new normalizedMessages since the previous count —
// existing entries were already checked by a prior full build.
const lastMsg = messages.at(-1)
const lastAssistantMsgId =
lastMsg?.type === 'assistant' ? lastMsg.message?.id : undefined
for (let i = newNormalizedStart; i < normalizedMessages.length; i++) {
const msg = normalizedMessages[i]!
if (msg.type !== 'assistant') continue
const aMsg = msg as AssistantMessage
if (aMsg.message.id === lastAssistantMsgId) continue
if (!Array.isArray(aMsg.message.content)) continue
for (const content of aMsg.message.content) {
if (
typeof content !== 'string' &&
((content.type as string) === 'server_tool_use' ||
(content.type as string) === 'mcp_tool_use') &&
!existing.resolvedToolUseIDs.has((content as { id: string }).id)
) {
const id = (content as { id: string }).id
existing.resolvedToolUseIDs.add(id)
existing.erroredToolUseIDs.add(id)
}
}
}
return existing
}
/** /**
* Compute a lightweight structural fingerprint for buildMessageLookups caching. * Compute a lightweight structural fingerprint for buildMessageLookups caching.
* Only captures information that affects lookup results (types, IDs, counts), * Only captures information that affects lookup results (types, IDs, counts),
@ -1457,7 +1656,10 @@ export function buildSubagentLookups(
if (msg.type === 'assistant' && Array.isArray(msg.message.content)) { if (msg.type === 'assistant' && Array.isArray(msg.message.content)) {
for (const content of msg.message.content) { for (const content of msg.message.content) {
if (typeof content !== 'string' && content.type === 'tool_use') { if (typeof content !== 'string' && content.type === 'tool_use') {
toolUseByToolUseID.set((content as ToolUseBlock).id, content as ToolUseBlockParam) toolUseByToolUseID.set(
(content as ToolUseBlock).id,
content as ToolUseBlockParam,
)
} }
} }
} else if (msg.type === 'user' && Array.isArray(msg.message.content)) { } else if (msg.type === 'user' && Array.isArray(msg.message.content)) {
@ -1541,9 +1743,10 @@ export function getToolUseIDs(
(_): _ is NormalizedAssistantMessage<BetaToolUseBlock> => (_): _ is NormalizedAssistantMessage<BetaToolUseBlock> =>
_.type === 'assistant' && _.type === 'assistant' &&
Array.isArray(_.message?.content) && Array.isArray(_.message?.content) &&
(_.message?.content as Array<{type:string}>)[0]?.type === 'tool_use', (_.message?.content as Array<{ type: string }>)[0]?.type ===
'tool_use',
) )
.map(_ => ((_.message?.content as Array<BetaToolUseBlock>)[0]).id), .map(_ => (_.message?.content as Array<BetaToolUseBlock>)[0].id),
) )
} }
@ -1573,7 +1776,8 @@ export function reorderAttachmentsForAPI(messages: Message[]): Message[] {
message.type === 'assistant' || message.type === 'assistant' ||
(message.type === 'user' && (message.type === 'user' &&
Array.isArray(message.message?.content) && Array.isArray(message.message?.content) &&
(message.message?.content as Array<{type:string}>)[0]?.type === 'tool_result') (message.message?.content as Array<{ type: string }>)[0]?.type ===
'tool_result')
if (isStoppingPoint && pendingAttachments.length > 0) { if (isStoppingPoint && pendingAttachments.length > 0) {
// Hit a stopping point — attachments stop here (go after the stopping point). // Hit a stopping point — attachments stop here (go after the stopping point).
@ -1816,10 +2020,15 @@ export function stripToolReferenceBlocksFromUserMessage(
export function stripCallerFieldFromAssistantMessage( export function stripCallerFieldFromAssistantMessage(
message: AssistantMessage, message: AssistantMessage,
): AssistantMessage { ): AssistantMessage {
const contentArr = Array.isArray(message.message.content) ? message.message.content : [] const contentArr = Array.isArray(message.message.content)
? message.message.content
: []
const hasCallerField = contentArr.some( const hasCallerField = contentArr.some(
block => block =>
typeof block !== 'string' && block.type === 'tool_use' && 'caller' in block && block.caller !== null, typeof block !== 'string' &&
block.type === 'tool_use' &&
'caller' in block &&
block.caller !== null,
) )
if (!hasCallerField) { if (!hasCallerField) {
@ -2285,11 +2494,16 @@ export function normalizeMessagesForAPI(
...message, ...message,
message: { message: {
...message.message, ...message.message,
content: (Array.isArray(message.message.content) ? message.message.content : []).map(block => { content: (Array.isArray(message.message.content)
? message.message.content
: []
).map(block => {
if (typeof block === 'string') return block if (typeof block === 'string') return block
if (block.type === 'tool_use') { if (block.type === 'tool_use') {
const toolUseBlk = block as ToolUseBlock const toolUseBlk = block as ToolUseBlock
const tool = tools.find(t => toolMatchesName(t, toolUseBlk.name)) const tool = tools.find(t =>
toolMatchesName(t, toolUseBlk.name),
)
const normalizedInput = tool const normalizedInput = tool
? normalizeToolInputForAPI( ? normalizeToolInputForAPI(
tool, tool,
@ -2310,8 +2524,9 @@ export function normalizeMessagesForAPI(
// When tool search is NOT enabled, strip tool-search-only fields // When tool search is NOT enabled, strip tool-search-only fields
// like 'caller', but preserve other provider metadata attached to // like 'caller', but preserve other provider metadata attached to
// the block (for example Gemini thought signatures on tool_use). // the block (for example Gemini thought signatures on tool_use).
const { caller: _caller, ...toolUseRest } = block as ToolUseBlock & const { caller: _caller, ...toolUseRest } =
Record<string, unknown> & { caller?: unknown } block as ToolUseBlock &
Record<string, unknown> & { caller?: unknown }
return { return {
...toolUseRest, ...toolUseRest,
type: 'tool_use' as const, type: 'tool_use' as const,
@ -2341,7 +2556,6 @@ export function normalizeMessagesForAPI(
result[i] = mergeAssistantMessages(msg, normalizedMessage) result[i] = mergeAssistantMessages(msg, normalizedMessage)
return return
} }
continue
} }
} }
@ -2455,8 +2669,12 @@ export function mergeUserMessagesAndToolResults(
a: UserMessage, a: UserMessage,
b: UserMessage, b: UserMessage,
): UserMessage { ): UserMessage {
const lastContent = normalizeUserTextContent(a.message.content as string | ContentBlockParam[]) const lastContent = normalizeUserTextContent(
const currentContent = normalizeUserTextContent(b.message.content as string | ContentBlockParam[]) a.message.content as string | ContentBlockParam[],
)
const currentContent = normalizeUserTextContent(
b.message.content as string | ContentBlockParam[],
)
return { return {
...a, ...a,
message: { message: {
@ -2490,12 +2708,18 @@ function isToolResultMessage(msg: Message): boolean {
} }
const content = msg.message?.content const content = msg.message?.content
if (!content || typeof content === 'string') return false if (!content || typeof content === 'string') return false
return (content as Array<{type:string}>).some(block => block.type === 'tool_result') return (content as Array<{ type: string }>).some(
block => block.type === 'tool_result',
)
} }
export function mergeUserMessages(a: UserMessage, b: UserMessage): UserMessage { export function mergeUserMessages(a: UserMessage, b: UserMessage): UserMessage {
const lastContent = normalizeUserTextContent(a.message.content as string | ContentBlockParam[]) const lastContent = normalizeUserTextContent(
const currentContent = normalizeUserTextContent(b.message.content as string | ContentBlockParam[]) a.message.content as string | ContentBlockParam[],
)
const currentContent = normalizeUserTextContent(
b.message.content as string | ContentBlockParam[],
)
if (feature('HISTORY_SNIP')) { if (feature('HISTORY_SNIP')) {
// A merged message is only meta if ALL merged messages are meta. If any // A merged message is only meta if ALL merged messages are meta. If any
// operand is real user content, the result must not be flagged isMeta // operand is real user content, the result must not be flagged isMeta
@ -2855,9 +3079,15 @@ export function getToolUseID(message: NormalizedMessage): string | null {
} }
return null return null
case 'assistant': { case 'assistant': {
const aContent = Array.isArray(message.message?.content) ? message.message?.content : [] const aContent = Array.isArray(message.message?.content)
? message.message?.content
: []
const firstBlock = aContent![0] const firstBlock = aContent![0]
if (!firstBlock || typeof firstBlock === 'string' || firstBlock.type !== 'tool_use') { if (
!firstBlock ||
typeof firstBlock === 'string' ||
firstBlock.type !== 'tool_use'
) {
return null return null
} }
return (firstBlock as ToolUseBlock).id return (firstBlock as ToolUseBlock).id
@ -2866,9 +3096,15 @@ export function getToolUseID(message: NormalizedMessage): string | null {
if (message.sourceToolUseID) { if (message.sourceToolUseID) {
return message.sourceToolUseID as string return message.sourceToolUseID as string
} }
const uContent = Array.isArray(message.message?.content) ? message.message?.content : [] const uContent = Array.isArray(message.message?.content)
? message.message?.content
: []
const firstUBlock = uContent![0] const firstUBlock = uContent![0]
if (!firstUBlock || typeof firstUBlock === 'string' || firstUBlock.type !== 'tool_result') { if (
!firstUBlock ||
typeof firstUBlock === 'string' ||
firstUBlock.type !== 'tool_result'
) {
return null return null
} }
return (firstUBlock as ToolResultBlockParam).tool_use_id return (firstUBlock as ToolResultBlockParam).tool_use_id
@ -2897,7 +3133,11 @@ export function filterUnresolvedToolUses(messages: Message[]): Message[] {
if (msg.type !== 'user' && msg.type !== 'assistant') continue if (msg.type !== 'user' && msg.type !== 'assistant') continue
const content = msg.message?.content const content = msg.message?.content
if (!Array.isArray(content)) continue if (!Array.isArray(content)) continue
for (const block of content as Array<{type:string; id?:string; tool_use_id?:string}>) { for (const block of content as Array<{
type: string
id?: string
tool_use_id?: string
}>) {
if (block.type === 'tool_use') { if (block.type === 'tool_use') {
toolUseIds.add(block.id!) toolUseIds.add(block.id!)
} }
@ -2921,7 +3161,7 @@ export function filterUnresolvedToolUses(messages: Message[]): Message[] {
const content = msg.message?.content const content = msg.message?.content
if (!Array.isArray(content)) return true if (!Array.isArray(content)) return true
const toolUseBlockIds: string[] = [] const toolUseBlockIds: string[] = []
for (const b of content as Array<{type:string; id?:string}>) { for (const b of content as Array<{ type: string; id?: string }>) {
if (b.type === 'tool_use') { if (b.type === 'tool_use') {
toolUseBlockIds.push(b.id!) toolUseBlockIds.push(b.id!)
} }
@ -2940,7 +3180,7 @@ export function getAssistantMessageText(message: Message): string | null {
// For content blocks array, extract and concatenate text blocks // For content blocks array, extract and concatenate text blocks
if (Array.isArray(message.message?.content)) { if (Array.isArray(message.message?.content)) {
return ( return (
(message.message?.content as Array<{type:string; text?:string}>) (message.message?.content as Array<{ type: string; text?: string }>)
.filter(block => block.type === 'text') .filter(block => block.type === 'text')
.map(block => block.text ?? '') .map(block => block.text ?? '')
.join('\n') .join('\n')
@ -3055,11 +3295,17 @@ export function handleMessageFromStream(
// Capture complete thinking blocks for real-time display in transcript mode // Capture complete thinking blocks for real-time display in transcript mode
if (message.type === 'assistant') { if (message.type === 'assistant') {
const assistMsg = message as Message const assistMsg = message as Message
const contentArr = Array.isArray(assistMsg.message?.content) ? assistMsg.message.content : [] const contentArr = Array.isArray(assistMsg.message?.content)
? assistMsg.message.content
: []
const thinkingBlock = contentArr.find( const thinkingBlock = contentArr.find(
block => typeof block !== 'string' && block.type === 'thinking', block => typeof block !== 'string' && block.type === 'thinking',
) )
if (thinkingBlock && typeof thinkingBlock !== 'string' && thinkingBlock.type === 'thinking') { if (
thinkingBlock &&
typeof thinkingBlock !== 'string' &&
thinkingBlock.type === 'thinking'
) {
const tb = thinkingBlock as ThinkingBlock const tb = thinkingBlock as ThinkingBlock
onStreamingThinking?.(() => ({ onStreamingThinking?.(() => ({
thinking: tb.thinking, thinking: tb.thinking,
@ -3082,7 +3328,28 @@ export function handleMessageFromStream(
} }
// At this point, message is a stream event with an `event` property // At this point, message is a stream event with an `event` property
const streamMsg = message as { type: string; event: { type: string; content_block: { type: string; id?: string; name?: string; input?: Record<string, unknown> }; index: number; delta: { type: string; text: string; partial_json: string; thinking: string }; [key: string]: unknown }; ttftMs?: number; [key: string]: unknown } const streamMsg = message as {
type: string
event: {
type: string
content_block: {
type: string
id?: string
name?: string
input?: Record<string, unknown>
}
index: number
delta: {
type: string
text: string
partial_json: string
thinking: string
}
[key: string]: unknown
}
ttftMs?: number
[key: string]: unknown
}
if (streamMsg.event.type === 'message_start') { if (streamMsg.event.type === 'message_start') {
if (streamMsg.ttftMs != null) { if (streamMsg.ttftMs != null) {
@ -3597,7 +3864,6 @@ Read the team config to discover your teammates' names. Check the task list peri
} }
} }
// skill_discovery handled here (not in the switch) so the 'skill_discovery' // skill_discovery handled here (not in the switch) so the 'skill_discovery'
// string literal lives inside a feature()-guarded block. A case label can't // string literal lives inside a feature()-guarded block. A case label can't
// be gated, but this pattern can — same approach as teammate_mailbox above. // be gated, but this pattern can — same approach as teammate_mailbox above.
@ -3837,8 +4103,7 @@ Read the team config to discover your teammates' names. Check the task list peri
case 'queued_command': { case 'queued_command': {
// Prefer explicit origin carried from the queue; fall back to commandMode // Prefer explicit origin carried from the queue; fall back to commandMode
// for task notifications (which predate origin). // for task notifications (which predate origin).
const origin = const origin = (attachment.origin ??
(attachment.origin ??
(attachment.commandMode === 'task-notification' (attachment.commandMode === 'task-notification'
? { kind: 'task-notification' } ? { kind: 'task-notification' }
: undefined)) as MessageOrigin | undefined : undefined)) as MessageOrigin | undefined
@ -4124,7 +4389,10 @@ You have exited auto mode. The user may now want to interact more directly. You
case 'async_hook_response': { case 'async_hook_response': {
const response = attachment.response as { const response = attachment.response as {
systemMessage?: string | ContentBlockParam[] systemMessage?: string | ContentBlockParam[]
hookSpecificOutput?: { additionalContext?: string | ContentBlockParam[]; [key: string]: unknown } hookSpecificOutput?: {
additionalContext?: string | ContentBlockParam[]
[key: string]: unknown
}
[key: string]: unknown [key: string]: unknown
} }
const messages: UserMessage[] = [] const messages: UserMessage[] = []
@ -4147,7 +4415,9 @@ You have exited auto mode. The user may now want to interact more directly. You
) { ) {
messages.push( messages.push(
createUserMessage({ createUserMessage({
content: response.hookSpecificOutput.additionalContext as string | ContentBlockParam[], content: response.hookSpecificOutput.additionalContext as
| string
| ContentBlockParam[],
isMeta: true, isMeta: true,
}), }),
) )
@ -4781,7 +5051,7 @@ export function shouldShowUserMessage(
export function isThinkingMessage(message: Message): boolean { export function isThinkingMessage(message: Message): boolean {
if (message.type !== 'assistant') return false if (message.type !== 'assistant') return false
if (!Array.isArray(message.message?.content)) return false if (!Array.isArray(message.message?.content)) return false
return (message.message?.content as Array<{type:string}>).every( return (message.message?.content as Array<{ type: string }>).every(
block => block.type === 'thinking' || block.type === 'redacted_thinking', block => block.type === 'thinking' || block.type === 'redacted_thinking',
) )
} }
@ -4799,7 +5069,9 @@ export function countToolCalls(
for (const msg of messages) { for (const msg of messages) {
if (!msg) continue if (!msg) continue
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) { if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
const hasToolUse = (msg.message?.content as Array<{type:string; name?:string}>).some( const hasToolUse = (
msg.message?.content as Array<{ type: string; name?: string }>
).some(
(block): block is ToolUseBlock => (block): block is ToolUseBlock =>
block.type === 'tool_use' && block.name === toolName, block.type === 'tool_use' && block.name === toolName,
) )
@ -4828,7 +5100,13 @@ export function hasSuccessfulToolCall(
const msg = messages[i] const msg = messages[i]
if (!msg) continue if (!msg) continue
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) { if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
const toolUse = (msg.message?.content as Array<{type:string; name?:string; id?:string}>).find( const toolUse = (
msg.message?.content as Array<{
type: string
name?: string
id?: string
}>
).find(
(block): block is ToolUseBlock => (block): block is ToolUseBlock =>
block.type === 'tool_use' && block.name === toolName, block.type === 'tool_use' && block.name === toolName,
) )
@ -4846,7 +5124,13 @@ export function hasSuccessfulToolCall(
const msg = messages[i] const msg = messages[i]
if (!msg) continue if (!msg) continue
if (msg.type === 'user' && Array.isArray(msg.message?.content)) { if (msg.type === 'user' && Array.isArray(msg.message?.content)) {
const toolResult = (msg.message?.content as Array<{type:string; tool_use_id?:string; is_error?:boolean}>).find( const toolResult = (
msg.message?.content as Array<{
type: string
tool_use_id?: string
is_error?: boolean
}>
).find(
(block): block is ToolResultBlockParam => (block): block is ToolResultBlockParam =>
block.type === 'tool_result' && block.type === 'tool_result' &&
block.tool_use_id === mostRecentToolUseId, block.tool_use_id === mostRecentToolUseId,
@ -4892,7 +5176,11 @@ function filterTrailingThinkingFromLastAssistant(
const content = lastMessage.message.content const content = lastMessage.message.content
if (!Array.isArray(content)) return messages if (!Array.isArray(content)) return messages
const lastBlock = content.at(-1) const lastBlock = content.at(-1)
if (!lastBlock || typeof lastBlock === 'string' || !isThinkingBlock(lastBlock)) { if (
!lastBlock ||
typeof lastBlock === 'string' ||
!isThinkingBlock(lastBlock)
) {
return messages return messages
} }
@ -5012,7 +5300,10 @@ export function filterWhitespaceOnlyAssistantMessages(
for (const message of filtered) { for (const message of filtered) {
const prev = merged.at(-1) const prev = merged.at(-1)
if (message.type === 'user' && prev?.type === 'user') { if (message.type === 'user' && prev?.type === 'user') {
merged[merged.length - 1] = mergeUserMessages(prev as UserMessage, message as UserMessage) // lvalue merged[merged.length - 1] = mergeUserMessages(
prev as UserMessage,
message as UserMessage,
) // lvalue
} else { } else {
merged.push(message) merged.push(message)
} }
@ -5108,7 +5399,7 @@ export function filterOrphanedThinkingOnlyMessages(
const content = msg.message?.content const content = msg.message?.content
if (!Array.isArray(content)) continue if (!Array.isArray(content)) continue
const hasNonThinking = (content as Array<{type:string}>).some( const hasNonThinking = (content as Array<{ type: string }>).some(
block => block.type !== 'thinking' && block.type !== 'redacted_thinking', block => block.type !== 'thinking' && block.type !== 'redacted_thinking',
) )
if (hasNonThinking && msg.message?.id) { if (hasNonThinking && msg.message?.id) {
@ -5128,7 +5419,7 @@ export function filterOrphanedThinkingOnlyMessages(
} }
// Check if ALL content blocks are thinking blocks // Check if ALL content blocks are thinking blocks
const allThinking = (content as Array<{type:string}>).every( const allThinking = (content as Array<{ type: string }>).every(
block => block.type === 'thinking' || block.type === 'redacted_thinking', block => block.type === 'thinking' || block.type === 'redacted_thinking',
) )
@ -5307,8 +5598,15 @@ export function ensureToolResultPairing(
// Collect server-side tool result IDs (*_tool_result blocks have tool_use_id). // Collect server-side tool result IDs (*_tool_result blocks have tool_use_id).
const serverResultIds = new Set<string>() const serverResultIds = new Set<string>()
const aMsg5 = msg as AssistantMessage const aMsg5 = msg as AssistantMessage
for (const c of aMsg5.message.content as (ContentBlockParam | ContentBlock)[]) { for (const c of aMsg5.message.content as (
if (typeof c !== 'string' && 'tool_use_id' in c && typeof (c as { tool_use_id: string }).tool_use_id === 'string') { | ContentBlockParam
| ContentBlock
)[]) {
if (
typeof c !== 'string' &&
'tool_use_id' in c &&
typeof (c as { tool_use_id: string }).tool_use_id === 'string'
) {
serverResultIds.add((c as { tool_use_id: string }).tool_use_id) serverResultIds.add((c as { tool_use_id: string }).tool_use_id)
} }
} }
@ -5326,7 +5624,9 @@ export function ensureToolResultPairing(
// has no matching *_tool_result and the API rejects with e.g. "advisor // has no matching *_tool_result and the API rejects with e.g. "advisor
// tool use without corresponding advisor_tool_result". // tool use without corresponding advisor_tool_result".
const seenToolUseIds = new Set<string>() const seenToolUseIds = new Set<string>()
const assistantContent = Array.isArray(aMsg5.message.content) ? aMsg5.message.content : [] const assistantContent = Array.isArray(aMsg5.message.content)
? aMsg5.message.content
: []
const finalContent = assistantContent.filter(block => { const finalContent = assistantContent.filter(block => {
if (typeof block === 'string') return true if (typeof block === 'string') return true
if (block.type === 'tool_use') { if (block.type === 'tool_use') {
@ -5338,7 +5638,8 @@ export function ensureToolResultPairing(
seenToolUseIds.add((block as ToolUseBlock).id) seenToolUseIds.add((block as ToolUseBlock).id)
} }
if ( if (
((block.type as string) === 'server_tool_use' || (block.type as string) === 'mcp_tool_use') && ((block.type as string) === 'server_tool_use' ||
(block.type as string) === 'mcp_tool_use') &&
!serverResultIds.has((block as { id: string }).id) !serverResultIds.has((block as { id: string }).id)
) { ) {
repaired = true repaired = true
@ -5348,7 +5649,8 @@ export function ensureToolResultPairing(
}) })
const assistantContentChanged = const assistantContentChanged =
finalContent.length !== (aMsg5.message.content as (ContentBlockParam | ContentBlock)[]).length finalContent.length !==
(aMsg5.message.content as (ContentBlockParam | ContentBlock)[]).length
// If stripping orphaned server tool uses empties the content array, // If stripping orphaned server tool uses empties the content array,
// insert a placeholder so the API doesn't reject empty assistant content. // insert a placeholder so the API doesn't reject empty assistant content.
@ -5436,8 +5738,13 @@ export function ensureToolResultPairing(
let content: (ContentBlockParam | ContentBlock)[] = Array.isArray( let content: (ContentBlockParam | ContentBlock)[] = Array.isArray(
nextUserMsg.message.content, nextUserMsg.message.content,
) )
? nextUserMsg.message.content as (ContentBlockParam | ContentBlock)[] ? (nextUserMsg.message.content as (ContentBlockParam | ContentBlock)[])
: [{ type: 'text' as const, text: (nextUserMsg.message.content as string | undefined) ?? '' }] : [
{
type: 'text' as const,
text: (nextUserMsg.message.content as string | undefined) ?? '',
},
]
// Strip orphaned tool_results and dedupe duplicate tool_result IDs // Strip orphaned tool_results and dedupe duplicate tool_result IDs
if (orphanedIds.length > 0 || hasDuplicateToolResults) { if (orphanedIds.length > 0 || hasDuplicateToolResults) {
@ -5513,13 +5820,18 @@ export function ensureToolResultPairing(
// Capture diagnostic info to help identify root cause // Capture diagnostic info to help identify root cause
const messageTypes = messages.map((m, idx) => { const messageTypes = messages.map((m, idx) => {
if (m.type === 'assistant') { if (m.type === 'assistant') {
const contentArr = Array.isArray(m.message.content) ? m.message.content : [] const contentArr = Array.isArray(m.message.content)
? m.message.content
: []
const toolUses = contentArr const toolUses = contentArr
.filter(b => typeof b !== 'string' && b.type === 'tool_use') .filter(b => typeof b !== 'string' && b.type === 'tool_use')
.map(b => (b as ToolUseBlock | ToolUseBlockParam).id) .map(b => (b as ToolUseBlock | ToolUseBlockParam).id)
const serverToolUses = contentArr const serverToolUses = contentArr
.filter( .filter(
b => typeof b !== 'string' && ((b.type as string) === 'server_tool_use' || (b.type as string) === 'mcp_tool_use'), b =>
typeof b !== 'string' &&
((b.type as string) === 'server_tool_use' ||
(b.type as string) === 'mcp_tool_use'),
) )
.map(b => (b as { id: string }).id) .map(b => (b as { id: string }).id)
const parts = [ const parts = [
@ -5580,8 +5892,12 @@ export function stripAdvisorBlocks(
let changed = false let changed = false
const result = messages.map(msg => { const result = messages.map(msg => {
if (msg.type !== 'assistant') return msg if (msg.type !== 'assistant') return msg
const content = Array.isArray(msg.message.content) ? msg.message.content : [] const content = Array.isArray(msg.message.content)
const filtered = content.filter(b => typeof b !== 'string' && !isAdvisorBlock(b)) ? msg.message.content
: []
const filtered = content.filter(
b => typeof b !== 'string' && !isAdvisorBlock(b),
)
if (filtered.length === content.length) return msg if (filtered.length === content.length) return msg
changed = true changed = true
if ( if (