---
url: /wave-agent/specs/core/agent-config.md
description: Agent 构造参数与 settings.json 分层配置：模型、token 上限、语言、env/headers 与热重载语义
---

# 功能规格说明：Agent 配置

**创建日期**：2025-01-27

## 用户场景与测试 *（必填）*

### 用户故事：显式 AI 服务配置（优先级：P1）

开发者需要通过 Agent 构造函数显式配置 AI 网关设置（API 密钥、基础 URL、模型 ID），而不是依赖环境变量，提供更好的可控性和可测试性。

**为什么是这个优先级**：这是启用显式配置管理并通过使必需配置可见和可控来提高 API 可用性的核心功能。

**独立测试**：可以通过使用自定义 AI 配置创建 Agent 实例并验证它使用这些设置而非环境变量来完整测试。

**验收场景**：

1. **假设**开发者使用自定义 AI 配置创建 Agent，**当**代理处理消息时，**则**它使用提供的配置值
2. **假设**未向 Agent 构造函数提供配置，**当**设置了环境变量时，**则**代理使用环境变量值

***

### 用户故事：Token 限制配置（优先级：P2）

开发者需要通过 Agent 构造函数配置自定义 token 限制，以控制消息压缩行为和最大输出 token，而无需设置环境变量。

**为什么是这个优先级**：Token 限制配置影响性能和成本管理，但次于基本 AI 功能。

**独立测试**：可以通过使用自定义 token 限制创建 Agent 并验证在指定限制处触发压缩来独立测试。

**验收场景**：

1. **假设**开发者通过 Agent 构造函数设置了自定义 token 限制，**当**token 使用超过该限制时，**则**触发消息压缩
2. **假设**未提供 token 限制，**当**创建代理时，**则**它使用合理的默认 token 限制

***

### 用户故事：模型选择配置（优先级：P3）

开发者需要通过 Agent 构造函数指定默认 AI 模型（代理模型和快速模型），以避免对环境变量的硬编码模型依赖。

**为什么是这个优先级**：模型配置提供灵活性，但作为核心功能的第三优先级，因为默认模型可以适用于大多数用例。

**独立测试**：可以通过使用自定义模型配置创建 Agent 并验证指定模型用于 AI 操作来测试。

**验收场景**：

1. **假设**开发者通过 Agent 构造函数指定了自定义模型，**当**执行 AI 操作时，**则**使用指定模型而非默认模型

***

### 用户故事：可配置最大输出 Token（优先级：P2）

作为开发者，我希望通过环境变量、代理创建选项或直接调用参数指定 AI 响应的最大输出 token。

**为什么是这个优先级**：对控制响应长度和成本至关重要。

**独立测试**：设置 `WAVE_MAX_OUTPUT_TOKENS=2048` 或向 `Agent.create` 传递 `maxTokens: 1024` 并验证 AI 服务调用使用正确的限制。

**验收场景**：

1. **假设**`WAVE_MAX_OUTPUT_TOKENS` 设置为 `2048`，**当**调用 `callAgent` 时，**则**请求使用 2048 作为最大 token 限制
2. **假设**代理创建时带有 `maxTokens: 1024`，**当**调用 `callAgent` 时，**则**请求使用 1024 作为最大 token 限制

***

### 用户故事：通过环境变量配置 SDK 自定义请求头（优先级：P2）

作为开发者，我希望使用 `WAVE_CUSTOM_HEADERS` 环境变量配置 SDK 的自定义 HTTP 请求头，以管理认证或环境特定的元数据。

**为什么是这个优先级**：支持替代认证方法和更好的安全实践。

**独立测试**：设置 `WAVE_CUSTOM_HEADERS="X-Test: 123\nY-Test: 456"`，初始化 SDK，并验证发出的请求包含这些请求头。

**验收场景**：

1. **假设**`WAVE_CUSTOM_HEADERS` 设置为 `X-Test: 123`，**当**SDK 发出请求时，**则**它包含请求头 `X-Test: 123`
2. **假设**未提供 `apiKey` 但在 `WAVE_CUSTOM_HEADERS` 中设置了自定义认证请求头，**当**SDK 初始化时，**则**它不抛出验证错误

***

### 用户故事：配置首选语言（优先级：P1）

作为用户，我希望在设置文件或代理选项中指定我的首选语言，以便代理用该语言与我交流。

**为什么是这个优先级**：使非英语使用者能够更有效地与代理交互。

**独立测试**：在 `Agent.create()` 或 `settings.json` 中设置 `language: "zh-CN"` 并验证系统提示包含语言指令；不设置任何语言时（全新安装）验证仍按默认 `zh-CN` 注入语言指令，且设置页「AI 回复语言」下拉处于「未设置（默认：中文）」态（**显示 ≡ 生效**）。

**验收场景**：

1. **假设**语言设置为 `zh-CN`，**当**我向代理提问时，**则**代理用中文回答
2. **假设**语言设置为 "Spanish"，**当**代理解释函数 `calculateTotal()` 时，**则**解释用西班牙语但 `calculateTotal()` 保持不变
3. **假设**`settings.json` 与 `AgentOptions` 都**未**设置 `language`（全新安装），**当**下一轮对话开始，**则**按默认值 `zh-CN` 生效（解析链 `覆盖项 > settings.json > 默认`），系统提示按该值注入语言指令——不得留空导致「模型按自身默认语言回答」；且设置页「AI 回复语言」下拉处于「未设置（默认：中文）」态、其默认值必须是同一个串（`zh-CN`），即**未设置时显示 ≡ 生效**（未设置态的表达式见「插件端配置入口」故事场景 7）

***

### 用户故事：自定义环境变量（优先级：P1）

开发者需要将自定义环境变量（API 密钥、数据库 URL、功能标志）传递给 Wave Agent SDK，而不需要在代码中硬编码。他们在 settings.json 文件中添加 "env" 字段，并期望这些变量在代理执行上下文中可用。这些变量被存入 Agent 的**会话级环境快照**（`ConfigurationService` 实例，每个会话独立），优先级高于 OS 环境变量，但**不写入 `process.env`**——这样在 stdio 多会话模式下，一个进程承载多个会话时不会相互污染。**例外**：`WAVE_SERVER_URL` 因需被进程级单例（`AuthService`、远端设置后台拉取）读取，会额外镜像写入 `process.env`（详见边界说明）。Wave Code CLI 将继承此功能，因为它使用 SDK。

**为什么是这个优先级**：这提供了必要的配置灵活性，并遵循安全最佳实践，将敏感数据保留在配置文件中而非代码中。

**独立测试**：可以通过将 env 变量添加到 settings.json 并验证它们在代理进程中可访问来完整测试，提供即时配置价值。

**验收场景**：

1. **假设**settings.json 文件包含带有键值对的 env 字段，**当**Wave Agent SDK 启动时，**则**这些环境变量存入该 Agent 的**会话级环境快照**（per-session env snapshot），优先级高于 OS 环境变量；它们**不写入 `process.env`**（`WAVE_SERVER_URL` 例外，见验收场景 6 与边界说明）
2. **假设**用户级和项目级 settings.json 文件都包含 env 字段，**当**代理运行时，**则**项目级环境变量覆盖同名的用户级变量
3. **假设**env 字段格式无效，**当**设置被加载时，**则**系统显示关于无效环境变量配置的清晰错误消息
4. **假设**一个 stdio 进程承载多个会话（不同项目/workdir），且各自的 settings.json `env` 设置了不同的 `WAVE_MODEL`/`WAVE_API_KEY`，**当**任一会话解析网关/模型配置时，**则**只读取本会话的快照，不发生"后启动会话覆盖前一会话"的污染（last-session-wins 污染消除）
5. **假设**settings.json `env` 中的变量被子进程（bash 工具、hooks、bash 模式命令、后台任务、MCP 模板替换）读取，**当**这些子进程启动时，**则**其环境为 `OS env + 本会话快照` 的合并（基础设施子进程如 git/worktree/LSP 仍只读 OS env）
6. **假设**settings.json `env` 中设置了 `WAVE_SERVER_URL`，**当**配置被加载时，**则**该值既存入会话快照、也镜像写入 `process.env`，使进程级单例 `AuthService`/`remoteSettingsService` 能读到它（详见边界说明）

***

### 用户故事：CLI 不在运行时强制 NODE_ENV，子进程环境与用户原始环境一致（优先级：P1）

Wave Code CLI 入口曾强制 `process.env.NODE_ENV ||= "production"`，以加载 React/ink 生产构建——react-reconciler 开发构建每次组件渲染都会向 Node 全局 perf buffer 写入 `performance.measure()` 条目（永不清空），长会话累积到百万条会触发 `MaxPerformanceEntryBufferExceededWarning` 并泄漏约 150MB。但 `process.env` 的修改会被所有 spawn 的子进程继承：wave 是常驻 daemon，它生成的每个 shell（Bash 工具、`!` bash 模式命令、后台任务、hooks、shell 快照捕获）都带 `NODE_ENV=production`，导致 `npm install`/`pnpm install` 跳过 devDependencies、构建与测试框架行为被改变。作为 wave 会话中的开发者，我希望子进程环境与启动 wave 时的原始环境一致（未设置 `NODE_ENV` 即不含该键），以便 `npm install` 等命令与普通登录 shell 行为一致。

**为什么是这个优先级**：这是静默行为破坏——依赖缺失报错时根因（`NODE_ENV`）与环境无关，排查成本高，影响所有把 wave 当作开发环境执行命令的用户。React 生产构建由构建期 esbuild define 保证（对齐 Claude Code 的做法：编译期替换 `process.env.NODE_ENV`，运行时不做修改），因此运行时强制并非必要。

**独立测试**：在无 `NODE_ENV` 的环境启动 wave，`echo $NODE_ENV` 应为空、`npm install` 正常安装 devDependencies；在 `NODE_ENV=development` 环境启动 wave，CLI 进程与子进程均保持 `development`。

**验收场景**：

1. **假设**用户环境未设置 `NODE_ENV` 且启动 wave CLI，**当**CLI 进程加载时，**则**CLI 自身 `process.env.NODE_ENV` 保持未设置（不注入 `production`），React 生产构建由构建期 define 保证，无 `MaxPerformanceEntryBufferExceededWarning`
2. **假设**用户显式设置 `NODE_ENV=development` 启动 wave，**当**CLI 加载时，**则**`process.env.NODE_ENV` 保持 `development`，不被覆盖
3. **假设**用户环境未设置 `NODE_ENV`，**当**wave 会话内通过 Bash 工具、`!` bash 模式命令、后台任务或 hooks 启动子进程时，**则**子进程环境不包含 `NODE_ENV`，`echo $NODE_ENV` 输出为空（与普通登录 shell 一致）
4. **假设**用户以 `NODE_ENV=development` 启动 wave，**当**wave 会话内启动子进程时，**则**子进程的 `NODE_ENV` 为 `development`
5. **假设**用户环境未设置 `NODE_ENV`，**当**wave 会话内运行 `npm install` 时，**则**devDependencies 被正常安装，不被 `production` 跳过

***

### 用户故事：设置实时重载（优先级：P1）

开发者与三端 GUI 用户正在积极工作，需要修改 settings.json 配置（hooks、环境变量、语言、模型、上下文长度、自动记忆、权限等）。他们希望这些更改立即生效而无需重启 SDK、也无需重建会话，实现配置的快速迭代。Wave Code CLI 与三端 GUI（VS Code 扩展 / JetBrains 插件 / 桌面端）都受益于此，因为它们都使用 SDK。

**为什么是这个优先级**：这是三端设置页「用户偏好保存」的承载机制——设置页保存的偏好类配置写入用户级 `~/.wave/settings.json` 后，必须先经过本故事定义的实时重载才能生效；没有它，保存只能靠「销毁并重建会话」强制生效（2026-09-10 前的现状），既慢（保存要等全部会话重建完才回执）又打断正在进行的工作。2026-09-10 用户拍板：用户偏好类配置一律走本通道，不得经 `AgentOptions` 覆盖层下发（分层职责见边界说明）。

**独立测试**：在 CLI/SDK 运行时修改用户级 `~/.wave/settings.json` 并验证新配置在下一轮对话开始时生效（无需重启、无需重建会话）；分别验证语言、模型、上下文长度、自动记忆开关/频率、权限、hooks、env、AGENTS.md 各自的热生效；关闭自动记忆开关后验证自动记忆目录在工作目录之外不再自动放行（安全区随开关热更新）。

**验收场景**：

1. **假设**Wave Agent SDK 正在运行，**当**用户修改 settings.json（用户级 `~/.wave/settings.json`、项目级 `{workdir}/.wave/settings.json`、`{workdir}/.wave/settings.local.json`）时，**则**更改被检测并应用于后续操作，无需重启 SDK、无需重建会话
2. **假设**Wave Agent SDK 正在处理请求，**当**settings.json 被更新时，**则**新设置用于后续代理执行（生效时点见场景 5）
3. **假设**保存了无效设置，**当**文件监视器检测到更改时，**则**系统记录错误但继续使用之前的有效配置
4. **假设**用户在 settings.json 中修改以下任一配置（**热生效键清单**），**当**下一轮对话开始，**则**新值生效（不重建会话、不重启 SDK）：`language`；`model` / `fastModel`；`models[<id>].*`（按模型配置，含 `contextWindow`、`disableThinkingOptions` 等）；`contextWindow`（或经由 `env.WAVE_CONTEXT_WINDOW`）；`autoMemoryEnabled` / `autoMemoryFrequency`；`permissions.*`（`allow` / `deny` / `defaultMode` / `additionalDirectories`）；`hooks`；`env.*`；以及记忆文件内容（用户级 `~/.wave/AGENTS.md`、项目级 `<workdir>/AGENTS.md`）
5. **假设**settings.json 在某一轮对话进行中被修改，**当**该轮仍在执行，**则**该轮内保持进入本轮时的配置快照（不得在同一轮内按请求抖动），变更在**下一轮开始时**整体生效——生效时点语义是「每轮（turn）开始时取一次快照」，既不是「每会话只读一次」，也不是「每个请求各读一次」
6. **假设**自动记忆目录（`~/.wave/projects/<项目>/memory`）与用户记忆文件（`~/.wave/AGENTS.md`）因自动记忆开启而属于**系统级安全区**（与用户可配的 `permissions.additionalDirectories` 不是同一集合），**当**用户在 settings.json 中关闭自动记忆，**则**这两项从系统级安全区移除（其内文件操作回到常规权限判定）；**当**重新开启，**则**重新加入——两次变更都无需重建会话。**构造期一次性注册的系统级白名单须改为动态**：复用实时重载路径按 `resolveAutoMemoryEnabled()` 幂等地增删（关闭时移除、重新开启时加入），不得只在 Agent 构造期注册一次。该系统级集合只影响权限判定——`Bash` 的 `cd` 不得把它当作可停留的当前目录（见 [tool-permission-system.md](tool-permission-system.md)「安全区域内的自动文件编辑」场景 3、[bash-tools.md](bash-tools.md)「执行前台命令」场景 5）
7. **假设**用户级 `~/.wave/settings.json` 在会话启动时不存在，**当**应用（设置页保存）首次创建并写入该文件，**则**运行中的会话必须感知并应用新配置（不得因「启动时文件不存在、未纳入监视」而漏掉首次保存）。**该保证不得只依赖文件监视**：设置页保存路径（stdio `updateUserSettings`）在写完文件后**显式重载**该进程内全部 live 会话的实时配置，并在回包**之前**完成——因此「保存回执已到 ⇒ 下一轮生效」是确定语义，不存在「回执已到、watcher 尚未落定」的竞态窗口；监视若也命中，重载是幂等的（并发重载按 `reloadInProgress` 去重）。空载荷（无差异保存、只回读）不落盘、也不重载

***

### 用户故事：配置变更不再需要重建会话（优先级：P1）

**（本故事原名「配置变更的构造期副作用与重建」；2026-09-18 拍板：构造期副作用清单已清空，重建路径整体废弃）**

作为用户，我希望任何配置变更与插件变更都不以「销毁并重建会话」作为生效手段，以便我的对话不会因为一次变更被掐断正在生成的回合、清空排队消息，也不会进入「稍后重启」这种把生效推迟到我后续动作的悬空状态。

**为什么是这个优先级**：2026-09-10 前的现状是「任何设置保存都重建池内全部 live 会话」——保存要等全部重建完才回执、会清空排队消息并打断正在流式输出的会话，而绝大多数用户偏好本可热生效（见「设置实时重载」故事）；该轮拍板把用户偏好改走实时重载，只给「真正产生构造期副作用的变更」留下重建这一条路，而当时的名单只有**插件装卸**一项。**2026-09-18 拍板：插件变更改为就地重载**——由用户主动敲 `/reload-plugins` 触发换装，不重建 Agent、不打断正在生成的回合、不需要用户选择时机（见 [plugin.md](../ecosystem/plugin.md)「插件变更的就地重载」）。至此构造期副作用清单为空，本规格范围内**不再有任何配置变更需要重建会话**，与之配套的「重建确认框（立即重启 / 稍后重启）」一并废止。

**构造期副作用清单：空。** 历史上两项的处置：

* **插件装卸**（`installPlugin` / `uninstallPlugin` / `enablePlugin` / `disablePlugin` / `updatePlugin`、内置插件开关如项目设置里的 SDD）：**2026-09-18 起改为就地重载**——变更落盘只置「待应用」信号并由宿主提示，用户敲 `/reload-plugins` 后在同一会话内就地换装七类能力（斜杠命令、技能、子代理、钩子、MCP、LSP、工作流），见 [plugin.md](../ecosystem/plugin.md)「插件变更的就地重载」。
* **自动记忆开关/频率**：**2026-09-10 起改为热生效**——安全区白名单随 settings.json 变更热更新（见「设置实时重载」故事场景 6）。

**独立测试**：保存一份仅有用户偏好变化（语言/模型/上下文长度/自动记忆）的配置，验证不触发任何会话重建、保存立即回执、排队消息与流式输出不受影响；安装或卸载一个项目级插件，验证**既不弹任何重建确认框、也不重建任何会话**，只出现一次「插件已变更」提示（桌面端为确认框，点「立即重新加载」或敲 `/reload-plugins` 后生效），在同一会话（sessionId 不变）内生效（七类能力的换装语义见 plugin.md 该故事的独立测试与验收场景）。

**验收场景**：

1. **假设**一次配置保存只涉及热生效键（见「设置实时重载」故事场景 4），**当**保存完成，**则**不得重建任何会话；保存回执不等待任何重建
2. **假设**配置内容与当前生效内容完全一致（无差异，含「一个字都没改就点保存」），**当**保存完成，**则**不得重建任何会话、不得清空排队消息、不得打断正在流式输出的会话
3. **假设**用户装卸/启停/更新插件（或切换内置插件开关），**当**变更落盘，**则**不得重建任何会话、不得弹重建确认框、不得打断任何正在生成的回合；变更只产生「待应用」信号与一次宿主提示，由用户经 `/reload-plugins` 触发就地换装（生效语义见 [plugin.md](../ecosystem/plugin.md)「插件变更的就地重载」）
4. **假设**任一配置变更或插件变更发生，**当**变更完成，**则**不得出现两按钮的「立即重启 / 稍后重启」确认框，不得登记任何「待重建」待办，也不存在「会话/pane 再次被激活时自动重建」的路径

***

### 用户故事：按子代理类型设置请求头（优先级：P2）

SDK 用户需要按子代理类型配置不同的 HTTP 请求头，以便 `customFetch` 可以区分主代理调用和子代理调用，实现请求级路由、速率限制和可观测性。

**为什么是这个优先级**：对多代理可观测性和路由重要，但次于核心配置。

**独立测试**：使用 `subagentHeaders: { "Explore": { "X-Subagent-Type": "Explore" } }` 创建 Agent，生成 Explore 子代理，并验证子代理的 `defaultHeaders` 包含类型特定请求头。

**验收场景**：

1. **假设**Agent 带有 `subagentHeaders: { "Explore": { "X-Subagent-Type": "Explore" } }`，**当**创建 Explore 子代理时，**则**子代理的 `defaultHeaders` 包含 `X-Subagent-Type: Explore`
2. **假设**Agent 配置了 `subagentHeaders`，**当**创建不在 `subagentHeaders` 中的子代理类型时，**则**子代理仅接收父级 `defaultHeaders`，无额外键
3. **假设**Agent 带有 `defaultHeaders: { "X-Shared": "base" }` 和 `subagentHeaders: { "Explore": { "X-Shared": "explore-override" } }`，**当**创建 Explore 子代理时，**则**子代理接收 `X-Shared: explore-override`
4. **假设**Agent 配置了 `subagentHeaders`，**当**为子代理请求调用 `customFetch` 时，**则**`init.headers` 包含合并后的类型特定请求头

***

### 用户故事：插件端配置入口（优先级：P1）

作为插件端用户，我希望通过 `/config` 斜杠命令或头部设置按钮打开设置页并选中「全局设置」选项卡，查看并修改当前生效的语言、上下文长度等配置，以便确认并调整插件的运行环境配置。

**为什么是这个优先级**：这是未登录用户使用插件端的主要配置入口，直接影响插件可用性。设置页（批次 2）已提供「全局设置」导航项，`/config` 复用设置页入口避免维护独立的配置对话框（2026-08-29 用户拍板：弹窗内容迁移到设置页选项卡，对齐 `/agents` → 子代理、`/skills` → 技能）。2026-08-31 曾拍板设置页为只读浏览视图（配置修改走 CLI 命令与配置文件）；2026-09-01 原型走查用户恢复可编辑：AI 回复语言（下拉）与上下文长度（数字输入）提供编辑控件，改动经 `updateConfiguration` RPC 写回配置并即时生效（**2026-11-04 起不再有「保存」按钮**，见「设置页控件的保存时机」故事），写盘**成功不弹任何提示**（控件的新状态即回执）、**失败**经宿主全局提示（文案带键名「设置未写入「<键名>」：<原因>」，**不带「打开设置文件」之类的入口**，语义见 desktop-settings「设置页反馈语义」），设置页不渲染页面内自建提示文字。2026-09-10 用户拍板改造保存路径（三端一致）：AI 回复语言、上下文长度、自动记忆开关/轮次等**用户偏好保存后写入用户级 `~/.wave/settings.json`**，经「设置实时重载」即时生效——**不再把用户偏好当 `AgentOptions` 覆盖项经 stdio 下发**（该层只保留会话级/单次覆盖语义，见边界说明），因此保存**不再触发会话重建**，回执与界面刷新不等待任何重建；VSCE / JetBrains / 桌面端必须同语义，不得只在桌面端生效造成多端漂移。

**独立测试**：在 IDE 中输入 `/config` 或点击头部设置按钮，验证打开设置页并选中「全局设置」选项卡，可编辑 AI 回复语言与上下文长度，改动即时写盘并生效（无保存按钮）；写盘**成功静默**（控件新状态即回执）；失败经宿主全局提示「设置未写入「<键名>」：<原因>」并可一键打开用户级 settings.json（不渲染页面内自建提示文字）。

**验收场景**：

1. **假设** 用户处于图形界面三端（VS Code 扩展 / JetBrains 插件 / 桌面端），**当** 用户输入 `/config` 并发送，**则** 打开设置页并选中「全局设置」选项卡（桌面端打开全页设置，IDE 打开编辑器区域设置标签页），不再弹出独立对话框。
2. **假设** 用户点击头部设置按钮，**当** 触发打开设置时，**则** 效果与 `/config` 一致，打开设置页「全局设置」选项卡。
3. **假设** 设置页「全局设置」视图已打开，**当** 页面渲染时，**则** 展示当前生效的 AI 回复语言（中文 / English，下拉选择，仅决定 AI 回复用语，不改变界面语言）与上下文长度（K 值，数字输入 16–1000，读写同一个全局键 `env.WAVE_CONTEXT_WINDOW`——**2026-09-10 拍板**，见边界说明「上下文长度的落点」），；上下文长度行须给出一句**可见说明**（如「全局默认；当前模型自带上下文上限时以模型配置为准」，从轻实现：不加区块、不加状态源），避免用户把「模型配置遮住全局值」误解为设置失效；**该键未设置**（settings.json 无此键）时控件必须表达「未设置」而不是假装一个真实值——语言下拉首项为「未设置（默认：中文）」并选中、上下文长度输入框留空并以灰字占位符显示系统默认（见场景 7）；**每次改动即时把该键写入用户级 `~/.wave/settings.json`**（经 SDK 实时重载生效，**不重建会话**），且**载荷只含本次改动的那一个键**（省略键 = 不改该键，见场景 7–8 与边界说明「省略键 = 不改该键」），成功回发新配置供对账（**成功不弹任何提示**，控件新状态即回执），失败回滚并保留用户输入、经宿主**具名**提示（「设置未写入「<键名>」：<原因>」，不带打开文件的入口，语义见 desktop-settings「设置页反馈语义」与「设置页控件的保存时机」场景 5、11），设置页自身不渲染页面内自建提示文字。
4. **假设** 设置页已打开，**当** 用户点击「返回」或切换其他选项卡，**则** 回到会话视图或切换到对应选项卡——改动在发生时已写盘，关闭/切换**不再触发**任何写入，也不需要「有未保存修改」的确认（设置页没有「未保存」状态）。
5. **假设** 用户处于桌面端且 pane 布局已生效（单 pane 或多 pane，`desktopPanes` 推送后），**当** 在任一对话输入框中输入 `/config`、`/agents`、`/skills` 或 `/mcp` 并发送，**则** 打开设置页并选中对应选项卡（「全局设置」/「子代理」/「技能」/「MCP 服务」）——pane 自身不渲染设置视图，命令委托给根实例的全页设置，与欢迎页/新对话状态行为一致。
6. **假设** 用户在任一宿主（VS Code 扩展 / JetBrains 插件 / 桌面端）的设置页改动用户偏好（AI 回复语言、快速模型、补全模型/补全开关、上下文长度、自动记忆开关/轮次——其中**补全模型与补全开关只由提供补全的插件端（VS Code / JetBrains）设置页渲染与写入，桌面端不展示这两行**，见 [code-completion.md](../editor/code-completion.md) 边界情况「补全设置行的宿主范围」），**当** 该键的写盘完成，**则** 三端必须一致地写入用户级 `~/.wave/settings.json` 并经实时重载生效（防多端漂移）——**不得**把用户偏好经 stdio `initialize` / `updateConfig` 当 `AgentOptions` 覆盖项下发（否则覆盖层永久遮蔽实时配置，端与端行为漂移）；设置页的**初始值读取**以该文件为**落点**（经宿主向**会话所在进程**查询），**不得**回读宿主私有存储（如 VS Code `globalState`）当作第二真源；但回包必须是**生效值**——该文件只是用户偏好的落点，企业下发的 Remote 组织配置 / 机器环境变量可以盖过它（取值链见场景 9 与边界说明「用户偏好的层与来源」），只读该文件会让设置页显示值与生效值分叉；该键**未设置**时（全新安装）初始值按**未设置态**展示（语言下拉显式项「未设置（默认：中文）」、数字输入留空 + 默认值占位符，见场景 7）、生效值落到同一个默认值（语言 `zh-CN`——默认值只在 SDK 解析链末尾有一处、与设置页未设置态显式项的默认值同串，见边界说明「语言默认值」），该键未设置时**不写入**（见场景 8），不得出现「设置页假装某个值、生效值却是另一个（或为空）」的分叉；上下文长度（K 值，16–1000）读写同一个键 `env.WAVE_CONTEXT_WINDOW`（按 K×1000 落盘、回读按同一换算展示；**不得改动 SDK 既有解析优先级**——该键是全局默认，见边界说明「上下文长度的落点」）；保存回执与界面刷新不得等待会话重建。**三端同批落地**（VS Code 扩展 / JetBrains 插件同样走用户级 `~/.wave/settings.json` 实时重载，不再经 `updateAllSessionsConfig` 重建会话生效）；用户在 IDE 内连远端（VS Code Remote/SSH、JetBrains 远程）时，保存经 stdio RPC 写入**会话所在进程**的用户级 `~/.wave/settings.json`（远端机器上的该文件），语义与本地一致。
7. **假设**用户级 `~/.wave/settings.json` 里**没有**某个用户偏好键（全新安装、或该键从未被写过），**当**设置页渲染，**则**该控件必须以「未设置」表达系统默认、而不是显示一个真实值：AI 回复语言下拉的列表首项显示「未设置（默认：中文）」并被选中（下拉没有 placeholder 语义，故用显式项表达）；上下文长度输入框留空、灰字占位符为「跟随模型配置（默认 200K）」；自动记忆轮次输入框留空、灰字占位符为「默认 1 轮」；补全模型下拉首项显示「未设置（跟随快速模型）」并被选中；补全开关与自动记忆开关**都不做占位态**（它们的真实默认就是「开」，三态开关更难用），继续显示为「开」。文案与默认值同源：语言项里的默认值 = SDK 解析链末尾的 `zh-CN`（见边界说明「语言默认值」）、上下文长度占位符里的 200K = SDK 默认 `DEFAULT_WAVE_CONTEXT_WINDOW`、轮次占位符里的 1 轮 = SDK 自动记忆频率默认值——三者都必须与「未设置时的实际生效值」一致，不得各写一份。
8. **假设**某个用户偏好键处于「未设置」态、或用户没有改动该控件（当前值与宿主回填的初始值相同），**则**该键**不会**出现在任何发给宿主的载荷里——「省略键 = 不改该键」是该载荷（webview → host → CLI `updateUserSettings`）的部分更新语义：只改语言时其余键（上下文长度、自动记忆开关/频率）都不得出现在报文里，未设置态一律不写该键；因此「用户系统环境里已有 `WAVE_CONTEXT_WINDOW`，进设置页随手保存一次就被钉成 200000」不再发生（没输入就不落盘）。把输入框清空（对一个已写过的值）等同于「不改该键」，**不提供「清除 / 恢复默认」按钮**（把已写入的值退回未设置态）——这是本规格的刻意取舍（载荷没有「删键」语义），用户若要去掉键需手改 settings.json；因此一旦文件里写过某个键，设置页就不再把该控件显示为「未设置」态。
9. **假设**某个用户偏好键的**生效值来自更高层**——企业下发的 Remote 组织配置（`remote`：用户级 `~/.wave/settings.json` 里也写了该键，或该键未设置）或机器环境变量（`env`：该键对应的 env 键有值而用户级文件里没有该键），**当**设置页渲染，**则**设置页必须**如实显示生效值**，**不得**回退显示用户文件里的值、也**不得**显示「未设置」态（前者会让用户以为本地值在生效，后者会让用户以为生效值还是系统默认）：①来源为 `remote` 时该控件**置为不可编辑**（用户在此无法本地覆盖组织策略），并给一句可见说明「由组织配置管理」；②来源为 `env` 时该控件**保持可编辑**（用户级文件优先级高于 OS 环境变量，用户在此保存即写入该文件并覆盖环境值），并给一句来源说明（当前值来自系统环境变量、保存后以本页设置为准）。宿主回包除生效值外还须带上**每个键的来源层**（`preferenceSources`：`remote` / `user` / `env` / `default`），设置页据此判定置灰与来源说明；来源为 `user` / `default` 的键与改造前完全一致（不显示任何来源说明）。本场景只覆盖**进程级可达**的层（Remote / 用户级文件 / 机器环境变量 / 默认），工程范围与视图落点见边界说明「用户偏好的层与来源」。

***

### 用户故事：企业知识库开关（优先级：P1）

作为企业成员，我希望在设置页「全局设置」里直接开关企业知识库，而不必手改 settings.json，以便确认 AI 现在是否会检索本企业上传的文档。

**为什么是这个优先级**：企业知识库把本企业上传的文档带进每次对话的上下文，属于「能力面扩大」，因此与远程控制、首方遥测同一 opt-in 口径、默认关闭；而此前开启的唯一入口是手改 `~/.wave/settings.json`（或项目 `.wave/settings.json`）的 `enableKnowledgeBase`，普通成员既没有 UI 入口也无从确认当前是开还是关。企业管理员仍可经托管配置统一开关（本故事不新增第二条管理通道）。

**独立测试**：在设置页「全局设置」的「基础设置」里拨动「企业知识库」开关（无需点任何保存按钮），验证顶层 `enableKnowledgeBase` 写入用户级 `~/.wave/settings.json`（其余顶层键与 `env` 下其它键原样保留）；开启后当前会话的下一轮对话模型即可见 `kb_search` / `kb_get_doc` / `kb_suggest`，关闭后从下一轮起消失；托管配置下发该键时该行显示生效值、置灰并提示「由组织配置管理」。

**验收场景**：

1. **假设** 设置页「全局设置」视图已打开，**当** 页面渲染，**则**「基础设置」卡片末尾（补全相关各行之后）有一行「企业知识库」开关，值为**生效值**；`enableKnowledgeBase` 未设置时显示为**关**——该能力的真实默认就是关，故不做「未设置」占位态（与「代码补全」「自动记忆开关」同一约定）。
2. **假设** 该行当前为关，**当** 用户打开开关，**则** **立即**把顶层 `enableKnowledgeBase: true` 写入用户级 `~/.wave/settings.json`（其余键原样保留）并即时生效——**不依赖底部「保存」按钮**（保存时机见「设置页控件的保存时机」故事）；写入结果经宿主全局 toast 提示。
3. **假设** 该行当前为开，**当** 用户关闭开关，**则** **立即**写入显式 `false`（不删键——载荷没有删键语义，与其余偏好键一致）。
4. **假设** 用户没有拨动这一行（本次打开设置页后从未点过它，或拨回原位），**当** 用户在页面上做别的改动（改语言、改上下文长度），**则** 该键**不得**出现在任何发给宿主的载荷里（省略键 = 不改该键）；反之拨动开关发出的载荷**只带这一个键**，不得捎上同卡片里别的控件的草稿（语言 / 上下文长度 / 补全模型）。
5. **假设** 当前会话正在运行，**当** 用户把该行从关拨到开（即时写盘完成），**则** 无需重启进程、也无需新建会话，**下一轮对话**模型即可调用三个 `kb_*` 工具；反向关闭同理（下一轮起工具从工具列表与 Exec 沙箱可调用集合里消失）。生效机制见边界说明「工具开关的重载时机」。
6. **假设** 托管配置下发 `enableKnowledgeBase`（回包 `preferenceSources.enableKnowledgeBase === "remote"`），**当** 该行渲染，**则** 显示生效值、置为不可编辑，并给一句「由组织配置管理」——与其余偏好键的 `remote` 来源语义一致（见「插件端配置入口」场景 9）。
7. **假设** 用户在 VS Code 扩展 / JetBrains 插件 / 桌面端任一宿主的设置页拨动该行，**则** 三端同语义：都写同一个用户级键、都即时生效，不得只在某一端生效。
8. **假设** 项目 `.wave/settings.json` 或 `.wave/settings.local.json` 也写了 `enableKnowledgeBase`，**当** 设置页渲染，**则** 仍按既有约定显示用户偏好层的归因（`project` / `local` 不在归因范围内，见边界说明「用户偏好的层与来源」）；实际是否注册工具仍按 SDK 合并优先级（remote > local > project > user > 默认）判定。

***

### 用户故事：设置页控件的保存时机（即时生效、无保存按钮，优先级：P1）

作为设置页用户，我希望**改完设置就生效、不用再找一个「保存」按钮**，以便不会把「改了却没生效」误当成功能坏了。

**为什么是这个优先级**：2026-11-04 客户反馈「企业知识库开关打开以后会自动关闭」——实测是拨了开关但**没点保存**就关掉设置页，重开时回包把控件回填成文件里的旧值（草稿被覆盖），用户看到的就是「自己关了」。这不是用户不小心，而是控件在撒谎：主流设计系统把「要点了保存才生效」和「开关」当成互斥的两件事——WinUI《Toggle switches》要求「use a toggle switch for binary operations that **take effect right after** the user flips the toggle switch」，并明确「**if the user must click a 'submit' or 'next' button to apply changes, use a check box**」；WinUI《Guidelines for app settings》要求「When a user changes a setting, the app should **immediately reflect the change**」；Carbon《Toggle》同样写「Toggles are only used for these binary actions that **occur immediately** after the user 'flips the switch'」「Ideal for settings or preferences that can be **immediately applied**」。VS Code 设置编辑器是这条路的完整样板（官方文档：「**There is no save action** — edits are written to `settings.json` as you make them」；写盘按类型分档防抖：`SETTING_UPDATE_FAST_DEBOUNCE = 200` 覆盖布尔/枚举/对象，`SETTING_UPDATE_SLOW_DEBOUNCE = 1000` 覆盖字符串/数字，**切到另一个设置项时立刻冲刷上一个待写值**）。2026-11-04 用户拍板：**设置页取消「保存」按钮，所有控件即时生效**（原「开关即时 + 文本草稿 + 未保存徽标 + 关闭确认」的两步方案作废——设置页唯一的文本输入已随私有化入口移除，剩下的全是开关、下拉与数字框）。

**独立测试**：拨动「企业知识库」开关后**不点任何按钮**直接关闭设置页、再打开，验证开关仍为拨后的值、下一轮对话的 `kb_*` 工具按新值存在/消失；在「AI 回复语言」下拉选中另一个值，验证立刻写盘（不关页面、不点按钮）；在「上下文长度」输入 `300` 时**不离开输入框**验证还没写盘，按回车或失焦后才写入；让宿主回发 `configurationError`，验证该控件回滚到写盘前的生效值。

**验收场景**：

1. **开关**：设置页的四行布尔开关——「企业知识库」「代码补全」「下一步编辑建议」（后两者的宿主范围与截面见 [code-completion.md](../editor/code-completion.md)、[next-edit-suggestion.md](../editor/next-edit-suggestion.md)）、个性化的「开启自动记忆」——**当** 用户拨动任一开关，**则** 立即经 `updateConfiguration` → 宿主 → CLI `updateUserSettings` 写盘并即时生效，**载荷只含这一个键**，没有「保存」按钮参与；关掉设置页再打开看到的是文件里的新值，不得被回包里的旧值回填成「自动关闭」。
2. **下拉**：「AI 回复语言」「补全模型」——**当** 用户选中另一项，**则** 选中即写盘（同上，载荷只含该键）；选中「未设置」项（该键本来就没写过时才出现）不产生写入。
3. **数字**：「上下文长度」「触发记忆提取会话轮次」——**当** 用户在输入框里打字，**则** 只更新本地草稿、**不写盘**（避免把输入途中的 `2` 当真实值）；**当** 输入框失焦或按下回车，**则** 提交一次（值未变、为空、或非数字时不写该键——省略键 = 不改该键）。
4. **没有「保存」按钮**：「基础设置」「自动记忆规则」两张卡片不再出现保存按钮；随之取消的还有「未保存」徽标、「放弃」按钮与关闭设置页的未保存确认——即时写盘后不存在「未保存」这个状态。（「个性化」里 AGENTS.md 的独立保存按钮**保留**：那是整文件写入，不是偏好键。）
5. **失败回滚并具名提示**：**假设** 写盘失败（宿主回发 `configurationError`：磁盘只读、RPC 报错等），**当** 失败回执到达，**则** 设置页按上一次生效值**强制回填**（撤销本次乐观改动）；宿主给出**指名道姓**的失败提示——「设置未写入「<键名>」：<原因>」，三端同形且**不带任何动作按钮**：失败原因是权限/磁盘/远端连接，打开用户级 `~/.wave/settings.json` 帮不上忙，真正的补救是界面已回滚到生效值（2026-11-04 拍板）。设置页自身仍不渲染页面内自建提示文字；不得停在「界面是开、文件里是关」的分叉态。
6. **未加载时不可交互**：**假设** 设置页刚打开、配置尚未回包（`configurationData` 为 null），**则** 所有偏好控件置灰，不得让用户改一个随后被回包覆盖的值（这是「自动关闭」的第二条来源）。
7. **回包不回拨**：**假设** 用户在本次打开设置页后改过某个键，**当** 该键的写盘回执（`configurationResponse`）到达，**则** 界面**不得**被回包改回旧值（回执只是对账，界面以本地乐观值为准）；反过来，用户**没碰过**的键仍随回包刷新（别的端或外部编辑改了文件时如实显示）。用户正在输入、尚未提交的草稿（场景 3）在任何回包到达时都**不得**被冲掉。
8. **托管行不变**：**假设** 某键来源为 `remote`（组织托管），**则** 沿用既有约定：显示生效值、不可编辑并提示「由组织配置管理」（见「插件端配置入口」场景 9）；来源为 `env` 的行保持可编辑并标注来源。
9. **写盘串行**：**假设** 用户连续快速改动多个控件（连拨两个开关、拨完再选下拉），**则** 这些写盘必须按发生顺序**串行**落到同一个 `~/.wave/settings.json`（CLI 侧 `updateUserSettings` 排队执行）——并发读-改-写会让后一次读到旧文件、覆盖掉前一次的键。
10. **三端一致**：**假设** 上述行为出现在 VS Code 扩展 / JetBrains 插件 / 桌面端任一宿主，**则** 三端同语义（同一份 webview 实现 + 各宿主既有的 `updateConfiguration` 路由器），不得只在某一端即时生效；宿主未注册 `updateConfiguration`（`onSave` 未传入，仅原型/夹具场景）时控件退化为只改本地状态，不出现点了没反应的死开关。
11. **假设** 用户拨了一只开关或选了一个下拉且写盘成功（2026-11-04 拍板：成功不打扰），**则** 宿主**不弹任何提示**——控件的新位置本身就是回执，不需要 toast/通知再说一遍「保存成功」。理由：改动即时生效后，一次「改语言 + 改上下文 + 拨开关」会产生三条成功提示，把反馈从「确认」变成噪音，并训练用户忽略提示，真失败时反而没人看；这也是主流做法（VS Code 设置写入静默、claude.ai 开关 mutation 显式 `noToast`、macOS 系统设置全程无提示）。「保存成功」只保留给**显式动作**（个性化里 AGENTS.md 的整文件保存、插件装卸/删除类操作）。

***

### 用户故事：快速模型思考禁用配置（优先级：P1）

用户将推理模型（如 deepseek-v4-flash）配置为快速模型（fastModel）后，WebFetch 内容处理、快速子代理（`model: fastModel`）等轻量任务会偶发 "Empty response from AI" 错误——推理模型的思考（reasoning）消耗了全部输出 token（max_tokens），导致 content 为空。用户希望为不同模型配置各自的"禁用思考"参数，使快速模型场景可按需禁用思考、稳定返回内容，同时不影响主代理对话（agent loop）中的思考能力。

**为什么是这个优先级**：这是快速模型场景空响应错误的直接修复，影响 WebFetch 等常用工具的可用性。

**独立测试**：分别配置与不配置 `models[X].disableThinkingOptions`，触发 WebFetch 内容处理与快速子代理，验证请求携带正确参数；同时验证主代理对话与自动记忆提取、上下文压缩等后台 fork 请求不受影响。

**验收场景**：

1. **假设**用户未配置 `disableThinkingOptions`，**当**快速模型场景（如 WebFetch 内容处理）发起请求时，**则**请求不携带任何禁用思考参数，交由模型默认处理（避免对不支持该参数的网关报错）
2. **假设**用户为模型 X 配置 `models[X].disableThinkingOptions: {"enable_thinking": false}`，**当**快速模型场景使用模型 X 时，**则**请求携带 `{"enable_thinking": false}`
3. **假设**用户为快速模型配置了 `models[fastModel].disableThinkingOptions`，**当** WebFetch 内容处理与快速子代理使用快速模型时，**则**两类请求都携带该模型的禁用思考参数
4. **假设**用户为快速模型配置了 `models[fastModel].disableThinkingOptions`，**当**自动记忆提取、上下文压缩等后台 fork 使用主模型（复用主对话 prompt cache，无 `modelOverride`）时，**则**请求不携带禁用思考参数，思考行为保持模型默认
5. **假设**用户配置了 `models[X].disableThinkingOptions`，**当**主代理对话（agent loop）使用模型 X 时，**则**请求不携带禁用思考参数，思考行为保持模型默认
6. **假设**用户为模型 X 配置 `disableThinkingOptions: {}`，**当**快速模型场景使用模型 X 时，**则**请求不携带任何禁用思考参数，交由模型默认处理

***

### 边界情况

* 当提供部分配置时会发生什么（如 apiKey 但无 baseURL）？
* 系统如何处理无效配置值（空字符串、格式错误的 URL、非数字 token 限制）？
* 当构造函数参数和环境变量同时存在时会发生什么？
* `WAVE_CUSTOM_HEADERS` 中的格式错误行如何处理？（应被忽略）
* 当 settings.json 在实时重载期间包含格式错误的 JSON 时会发生什么？
* 系统如何处理文件监视期间的文件系统权限错误？
* 系统如何处理快速连续的文件修改？
* 当文件监视器在系统启动时初始化失败会发生什么？
* 设置页加载已保存配置失败时如何展示？（设置页内显示错误与重试入口；编辑保存失败时经宿主全局 toast 提示错误并保留用户输入，不渲染页面内自建提示文字）
* 不同模型的禁用思考参数形态不同（`thinking: {type}` / `enable_thinking` / `reasoning_effort`），SDK 不做内置映射，由用户在 `models[X].disableThinkingOptions` 中按目标接口格式原样配置
* 未配置 `disableThinkingOptions` 时，快速模型场景不发送任何禁用思考参数；SDK 不内置默认值，避免直连不支持 `thinking` 参数的网关时报错

### 边界说明：环境变量作用域与优先级

* **分层职责：`AgentOptions` 覆盖层 vs settings.json 实时配置（2026-09-10 用户拍板）**：`AgentOptions`（构造参数，以及 stdio `initialize` / `updateConfig` 传入的会话参数）的语义是**会话级 / 单次覆盖**——用于 CLI `--model`、`--permission-mode` 这类一次性指定，以及程序化调用方的显式注入。**用户偏好类配置（语言、模型、上下文长度、自动记忆、权限、hooks、env）不得经该层下发**：各解析链优先级为 `覆盖项 > settings.json > env > 默认`，把用户偏好塞进覆盖层会**永久遮蔽** settings.json 的实时值（层倒置），使「保存即生效 / 实时重载」全部失效（这正是 2026-09-10 前保存必须重建会话的根因）。
* **用户偏好的落点 = 用户级 `~/.wave/settings.json`**：三端设置页保存用户偏好时写入该文件（SDK 已在监视的实时配置源），由「设置实时重载」故事生效；插件装卸等变更走就地重载（见 [plugin.md](../ecosystem/plugin.md)「插件变更的就地重载」），**没有任何配置变更需要重建会话**（见「配置变更不再需要重建会话」故事）。会话级覆盖仍可经 `AgentOptions` 下发，但不得用于承载用户偏好。
* **语言默认值（2026-09-10 追加）**：语言解析链是 `AgentOptions / stdio initialize 覆盖项 > settings.json > 默认`，末尾默认值为 `zh-CN`（SDK `DEFAULT_LANGUAGE`）。取值**必须与设置页「AI 回复语言」下拉未设置态显式项「未设置（默认：中文）」的默认值同串**（webview 的 `zh-CN` 项；不再是改造前各端各自的兜底串 `Chinese`），否则 settings.json 未设置该键时会出现「设置页显示中文、实际按别的语言回复」的分叉（见「配置首选语言」故事场景 3、「插件端配置入口」故事场景 7）。该值**原样**拼进系统提示 `# Language\nAlways respond in <值>`，因此用户写任意串（如 `Spanish`）都按原样生效，SDK 不做词汇映射。**三端宿主不得再各自注入语言默认值**（改造前 desktop `configStore.getConfiguration()`、VSCE `loadConfiguration()`、JB `ConfigurationData.language` 三处各自的默认值已随覆盖层链路一并删除）——默认值只在 SDK 解析链末尾有一处，保证 CLI 与三端 GUI 同语义。
* **省略键 = 不改该键（2026-09-10 追加）**：设置页保存用户偏好的载荷（webview → host → CLI `updateUserSettings` RPC）是**部分更新**语义——只写载荷里出现的用户偏好键，未出现的键保持文件中现值（CLI 侧 `updateUserPreferenceSettings` 读-改-写、宿主侧只把已提供的键放进 RPC patch）。设置页据此把「未设置」表达为「不提供该键」：用户没改过的字段、仍处于未设置态的字段一律不出现（见「插件端配置入口」故事场景 7–8），所以「进设置页不做任何改动」不会把任何键钉进文件。副作用是刻意保留的收益：`WAVE_CONTEXT_WINDOW` 只存在于用户**系统环境**（而非 settings.json）时，进设置页不做任何输入地保存一次不会把它钉成 `200000`。**不做「清除 / 恢复默认」按钮**（把已写入的键退回未设置态）——该载荷没有「删键」语义，去掉键需手改 settings.json。宿主侧同样不得给缺失键补默认值（否则「省略」会被宿主翻译成一次写入）。
* **用户偏好的层与来源（2026-09-10 追加）**：用户级 `~/.wave/settings.json` 是用户偏好的**落点**，但不是「谁最后说了算」——取值按 SDK 各解析链的层序，从高到低为 `AgentOptions / stdio initialize 覆盖项 > Remote 组织下发 > local（<workdir>/.wave/settings.local.json）> project（<workdir>/.wave/settings.json）> user（~/.wave/settings.json）> env（settings 快照 > OS 环境变量）> 默认值`。因此设置页的**初始值读取必须是生效值**：宿主经无会话的全局 RPC（`getUserSettings`）向会话所在进程查询，CLI 侧按上述层序归因，回包在生效值之外附 `preferenceSources`（每个键的来源层：`remote` / `user` / `env` / `default`；每个键恒有来源，缺省即 `default`）。**设置页的语义 = 显示用户偏好的值，并如实标注被更高层覆盖时的真实生效值**：来源为 `remote` 的键显示生效值 + 置灰（组织策略不可被本地覆盖）+「由组织配置管理」；来源为 `env` 的键同样显示**生效值**（不得回落成「未设置」占位符——否则页面显示 200K 而实际生效 64K 分叉）+ 一句来源说明（当前值来自系统环境变量、保存后以本页设置为准），但**保持可编辑**（用户级文件优先级高于 OS 环境变量，在此保存即写进该文件并覆盖环境变量的值）；来源为 `user` / `default` 的键与改造前一致，不显示任何来源说明。两个易错点须钉住：①**上下文长度**没有顶层键，它的落点是 `env.WAVE_CONTEXT_WINDOW`（见边界说明「上下文长度的落点」），因此它的来源层 = 该 **env 键**所在的那一层（Remote 的 `env` > 用户文件的 `env` > OS 环境变量）；②**自动记忆开关**每层有两条路径——顶层标量 `autoMemoryEnabled` 与 `env.WAVE_DISABLE_AUTO_MEMORY`，且顶层标量**整层**优先于 env 路径（`resolveAutoMemoryEnabledFrom` 先看合并后的配置、再看 env），归因时不得把「Remote 的 env 键」错误地排在「用户文件的顶层键」之前。**值的语义与改造前完全一致**（键缺失 = 没有任何提供者，设置页按 SDK 默认展示未设置态/「开」），`preferenceSources` 是**纯增量**字段：老宿主/未实现该字段的宿主回包缺少它时，设置页全部按键可编辑（向后兼容）。**归因范围（本 PR 只做进程级可达的层）**：①`override` / `options`（`AgentOptions` 构造参数、stdio `initialize` / `updateConfig` 的覆盖项）**不做**——三端宿主已不再给这些键下发覆盖项、`AgentOptions.contextWindow` 也无宿主使用，该层当前**恒空**，故设置页不展示它的归因，也不为恒空分支另造会话级通道；②`project`（`<workdir>/.wave/settings.json`）/ `local`（`<workdir>/.wave/settings.local.json`）**留后续**——归因它们需要 workdir，而 `getUserSettings` 是刻意的无会话全局 RPC，故被 project/local 覆盖时设置页可能仍显示用户级值（本 PR 不归因、不置灰），待查询入口能带 workdir（或在项目设置视图内做项目级归因）后补齐；③**按模型上限的遮蔽不归因（既有语义，非本次回归）**——`resolveContextWindow` 链里 `currentConfiguration.models[被解析模型].contextWindow` 位于 `env.WAVE_CONTEXT_WINDOW` **之上**，因此「在该模型上配了上限」的用户，其「上下文长度」行显示的是 user/env 的具体数字，而真正生效的是模型配置；该行的可见说明「全局默认；当前模型自带上下文上限时以模型配置为准」已覆盖这层歧义，本 PR 不为它引入 per-model 展示或改判定（否则设置页要跟随当前模型变化，超出「进程级可达层」的范围）。**视图落点**：归因本身是全局的（不依赖 workdir），任何渲染这些偏好控件的视图共用同一回包判定置灰与来源说明；当前这些键的控件只出现在「全局设置」（语言 / 上下文长度 / 补全模型与补全开关——后者见 [code-completion.md](../editor/code-completion.md)）与「个性化」（自动记忆开关 / 轮次）两个视图，「项目设置」视图只有 SDD 插件开关、不含用户偏好控件，故本 PR 的落点即前两个视图。本场景的验证落点见「验证分层」表的「被更高层覆盖的键如实显示（场景 9）」行。
* **企业知识库开关的落点（2026-10-26 追加）**：设置页「企业知识库」行读写 SDK **既有顶层键** `enableKnowledgeBase`（`WaveConfiguration`，缺省关），**不新增键、不改默认值、不改三个 `kb_*` 工具本身的行为**。本规格只做两件事：把它纳入用户偏好子集（`UserPreferenceSettings`，于是自动获得 `preferenceSources` 归因与「由组织配置管理」置灰态）、给一个设置页控件。因此托管配置自动具备优先级（`mergeRemoteSettings` 里 `remote.enableKnowledgeBase` 赢本地各层），不需要另设管理开关。**项目级配置照旧可写**（`<workdir>/.wave/settings.json` 的 `enableKnowledgeBase` 仍参与合并），只是 `project` / `local` 层不在设置页归因范围内——与其余偏好键同一残差（见上条边界说明 ②）。
* **工具开关的重载时机（2026-10-26 追加）**：`kb_*` 三个工具原先在 `Agent` 创建时一次性求值（挂在 `customTools` 上），改配置必须新开会话；本次改为与 Artifact / Exec **同一套**「实时求值的 feature-gated 内置工具」——注册在 `ToolManager.initializeBuiltInTools()` 内按 `enableKnowledgeBase` 判定，`reloadFeatureGatedTools()` 在实时重载时先摘掉再重新求值（每个 Agent 的 `LiveConfigManager.onReload` 已经调用它，且 CLI 的 `updateUserSettings` RPC 完成后会显式对所有会话 `reloadConfiguration()`）。因此设置页保存后**下一轮对话即生效**，不需要重建会话——与「配置变更不再需要重建会话」故事一致。判定用的目录是容器的 `SessionWorkdir`（会话打开目录，不随 `cd` 移动），与改动前的闭包 `workdir` 同义。
* **企业知识库开关的宿主范围（2026-10-26 追加）**：三端设置页（VS Code 扩展 / JetBrains 插件 / 桌面端）都渲染该行——知识库不是编辑器专属能力，与「补全设置行的宿主范围」（见 [code-completion.md](../editor/code-completion.md) 边界情况）不同。CLI 没有设置页，手改配置文件仍是它唯一的开启方式。
* **设置页没有「保存」按钮（2026-11-04 拍板）**：所有用户偏好控件都在**改动发生时**提交自己那一键（载荷只含该键，走既有的 `updateConfiguration` → `updateUserSettings` 链路，无新 RPC、无新键、无新宿主命令），提交颗粒与「一行一个键」一致——这样「改了没生效」在结构上不可能发生。这个决定有规范依据而不是口味：WinUI《Toggle switches》把「要按 submit 才生效」与「开关」判为互斥（要求改用复选框），《Guidelines for app settings》要求「改了就立即反映」；Carbon《Toggle》同样把开关定义为「flip 之后立即发生」。**不做「保存按钮 + 未保存徽标 + 放弃 + 关闭确认」**（2026-11-04 上午曾按此设计，当天下午改判作废）：那是把风险从「忘了点保存」转成「忘了看徽标」，且开关本来就没有可撤销的中间态，让用户为一个不存在的提交步骤负责没有意义。**提交时机按控件的失败代价分**：开关与下拉是离散值，选中即写；数字输入（上下文长度、记忆轮次）在**失焦或回车**时提交——边打字边写会把输入途中的 `2` 当成真实值写进文件，而 VS Code 用 1s 防抖来解决同一问题（`SETTING_UPDATE_SLOW_DEBOUNCE`），我们的写盘要走 RPC，宁可等一次明确的「改完了」信号。**乐观显示 + 失败回滚**：改动立刻反映在界面上，失败回执（`configurationError`）到达时按上一次生效值强制回填，界面永不停在「界面是开、文件里是关」。**回执不回拨**：界面对本次挂载内用户改过的键以本地乐观值为准（否则滞后回执会把开关拨回去，正是客户看到的「自动关闭」），用户没碰过的键仍随回执刷新；正在输入、尚未提交的草稿在任何回执到达时都不被冲掉。**未加载时不可交互**：`configurationData` 为 null 时全部偏好控件置灰，避免回包覆盖用户的改动（「自动关闭」的第二条路径）。**写盘必须串行**：CLI 侧 `updateUserSettings` 排队执行——它是「同步读 → `await` 异步写」的读-改-写，stdio 请求又是并发处理的（`jsonRpcConnection` 不 await `handleLine`），两个并发写会互相覆盖丢键。「恢复默认 / 删键」仍**不做**（载荷没有删键语义；VS Code 的做法是写 `undefined` 删键，我们要补的是 RPC 语义，单列一条，不在本次范围）。
* **升级用户的一次性迁移：不做（2026-09-10 追加拍板）**：改造前用户偏好（`language` / `contextLength` / `autoMemoryEnabled` / `autoMemoryFrequency`）存放在**宿主私有存储**（VSCE `globalState` / JB `wave.xml` / 桌面 `wave-desktop.json`）。本规格生效后这些键不再被读作用户偏好的**落点**，且**不做一次性迁移**（不把旧值搬进 settings.json）：旧 `language` 值大多是改造前各端**注入的默认串**（`Chinese`），既无法与用户显式选择区分，词汇也与设置页的 `zh-CN` / `en-US` 不同——迁进 settings.json 会把旧默认固化成「用户显式设置」，并制造「设置页显示中文、settings.json 写 `Chinese`」的新分叉（正是本规格要消除的那类分叉）。**代价（升级影响，须随发布说明告知）**：升级用户此前显式选过的语言 / 上下文长度 / 自动记忆开关与频率会**静默回落默认值**（语言 → `zh-CN`，其余 → SDK 既有默认），需在设置页重选一次；此后用户偏好的**落点**只有 settings.json 一处（不再有第二处宿主私有存储），但它**不是「唯一真源」**——它是取值链里的一层，更高层的 Remote 组织下发与机器环境变量仍会盖过它（见边界说明「用户偏好的层与来源」）。
* **重建确认框已废止（2026-09-18 拍板）**：2026-09-10 曾规定「重建确认框（立即重启 / 稍后重启）为桌面端专属」——理由是只有桌面端的保存回执会等待会话重建完成，用户才会感知到「保存很久才恢复」，故需要弹框让用户选择时机（VS Code 扩展 / JetBrains 插件的保存回执本就 fire-and-forget、无此症状）。**2026-09-18 起插件变更改为就地重载、重建路径整体废弃**（见 [plugin.md](../ecosystem/plugin.md)「插件变更的就地重载」与「配置变更不再需要重建会话」故事），该确认框与「桌面端专属」这个例外一并取消；三端「用户偏好一律走 settings.json 实时重载」的同语义要求不变（见「插件端配置入口」故事场景 6）。
* **`contextWindow` 的语义 = 一次请求输入与输出共享的总预算（2026-11-06 改名）**：`contextWindow`（`AgentOptions.contextWindow` / `models[<id>].contextWindow` / `env.WAVE_CONTEXT_WINDOW`，统一沿用 Claude Code 的 `contextWindow` 叫法）表示模型的上下文窗口总量，**不是「输入上限」**——单次请求的 `输入 token + 声明输出 token` 共享这同一份预算（服务端按 `prompt_tokens + max_tokens ≤ contextWindow` 校验）。因此它不参与任何「输入是否超限」的判定：系统里没有任何代码拿它跟输入比大小，它只作为**自动压缩阈值的基数**与**用量百分比的基数**（`contextWindow − min(maxOutputTokens, 20000) − 13000`，见 [message-compact.md](message-compact.md)「自动压缩阈值」「用量百分比口径」）。改名前的 `maxInputTokens` / `WAVE_MAX_INPUT_TOKENS` 会让读者（以及读 builtin settings 技能的模型）误以为这是输入上限，进而误以为「压缩时预留输出空间」没有必要——实际正是因为输入与输出共享窗口，压缩请求自身的 `max_tokens` 才必须被预留。**不保留旧名兼容**（2026-11-06 用户拍板）：旧字段名与旧 env 键一律不再读取。
* **上下文长度的落点 = 全局 `env.WAVE_CONTEXT_WINDOW`（2026-09-10 用户拍板）**：设置页的「上下文长度」读写**同一个全局键** `env.WAVE_CONTEXT_WINDOW`（K×1000；用户原话「那本来就是全局的上下文设置」），**不新增 per-model 落点、不改动 SDK 既有解析优先级**。因此存在一个刻意保留的语义：该键是**全局默认**，当**当前模型**自带上下文上限（服务端下发的 `models[<id>].contextWindow`，即 `resolveContextWindow` 链中 `constructorLimit > options.contextWindow > models[resolvedModel].contextWindow > envSnapshot.WAVE_CONTEXT_WINDOW > 默认` 的靠前项）时，**以模型配置为准**、全局值被遮蔽——这是既有 SDK 行为，本规格不修改它。为防「改了不生效」的新投诉，设置页「上下文长度」行须给出一句可见说明（文案从轻，如「全局默认；当前模型自带上下文上限时以模型配置为准」；不新增区块、不新增状态源、不引入 per-model 展示）。**与来源归因的关系（2026-09-10 追加）**：模型上限在取值链里位于 env 之上，而它不在「进程级可达的层」范围内，故该行的 `preferenceSources` 仍按 user/env 归因——即「配了按模型上限的用户，该行显示 user/env 的数字、生效值是模型配置」这一残差**如实保留并记录在案**（见边界说明「用户偏好的层与来源」③）。
* **settings.json `env` 与 OS 环境变量的关系**：settings.json `env` 存入会话级快照，优先级高于 OS 环境变量，但**不写入 `process.env`**（`WAVE_SERVER_URL` 例外，见下条）。优先级从高到低：显式构造参数 / stdio `initialize` 参数 > settings.json `env`（快照）> OS 环境变量 > 默认值。
* **`WAVE_SERVER_URL` 支持从 settings.json `env` 读取（镜像到 `process.env`）**：`AuthService` 与 `remoteSettingsService` 是进程级单例，不持有 per-session 快照，因此 settings `env` 里的 `WAVE_SERVER_URL` 经 `setEnvironmentVars` 特例**镜像写入 `process.env`**，使这些单例能读到。优先级：`options.serverUrl` / stdio `initialize` 参数 > settings.json `env`（镜像到 `process.env`）> OS 环境变量 > 默认值。由于 `AuthService` 是进程单例（一个进程一个 server），同进程多会话的 `WAVE_SERVER_URL` 应为同值，镜像不造成跨会话污染。启动 401 竞态已通过 init 顺序消除（`loadCacheFromDisk` → `loadMergedConfiguration` 写入 `process.env` → `startBackgroundFetch` 读取）。
* **服务端地址不经设置页配置（2026-11-04 移除该行）**：服务端地址的唯一入口是 `env.WAVE_SERVER_URL`——用户级 `~/.wave/settings.json` 的 `env` 块，或 OS 环境变量（镜像机制与解析链见上两条）。设置页**不再渲染「服务端地址」行**，`UserPreferenceSettings` / `preferenceSources` 不含该键，三端宿主也不再转发它；因此「设置页保存用户偏好」的批次语义（省略键 = 不改该键、未设置态占位符、来源归因与置灰）**不适用于**该地址。改动它仍需手改配置文件或改 OS 环境变量，生效时机仍为下一次 `reloadConfiguration`（重启进程最稳）。桌面端本地缓存的服务地址（仅用于拼更新 feed，经 `getAuthStatus` 更新，见 [desktop-settings.md](../desktop/desktop-settings.md)）与宿主的 `authStatusResponse.serverUrl`（已登录时的生效地址，用于「接收 Beta 版更新」开关的可用性判断）是**另一条链路**，不随本变更移除。
* **SSO 认证 / 远端设置轮询为进程级单例**：`authService` 与 `remoteSettingsService` 是进程级单例（一个 `auth.json` / 远端设置缓存——按用户）。因此对于后台 token 刷新/远端设置轮询，**每个进程只有一个 `serverUrl`**；AI 网关/模型/密钥的解析本身仍是按会话进行的。
* **基础设施子进程保持 OS-env-only**：git/worktree/LSP 等基础设施子进程只读取 OS 环境变量（`PATH`/`HOME`/`LC_ALL` 等），不合并会话快照。`WAVE_PLUGIN_GIT_TIMEOUT_MS`、`WAVE_SHELL`、`WAVE_GIT_BASH_PATH` 等"基础设施级"变量不从 settings.json `env` 读取，需通过 OS 环境设置。

### 边界说明：验证分层（单测 / e2e / 真 host）

本规格与 [desktop-settings.md](../desktop/desktop-settings.md)「用户偏好的保存路径与重建时机」的验收要求按三层测试落地（2026-09-10 追加拍板：两层新测试基建建成后，本需求的每个场景都必须标明落在哪一层）。

三层职责边界：**单测**（`packages/*/tests`，vitest，宿主/组件被 mock）验证「本端自己发出的东西对不对」；**e2e**（`packages/webview/e2e`，Playwright 真浏览器 + 真 webview bundle，宿主被消息注入替代）验证「真 UI 在真实浏览器里的交互与渲染」；**真 host**（`packages/desktop/tests/integration`，真 `DesktopHost` + 真 `wave --stdio` 子进程 + 真文件系统，`pnpm -F wave-desktop test:realhost`）验证「跨进程接缝」——单测永远看不到的那一层。

#### 保存路径热生效（PR-2）

| 场景                                                                                                                                                                   | 单测                                                                                                                                                                                                                                                                                                                                                                                                                | e2e                                                                                                                                                | 真 host                                                                                                                                                    |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 保存后真 CLI 进程未被重启（sessionId 不变）+ 会话不丢（「设置实时重载」场景 1/2、「插件端配置入口」场景 6）                                                            | 宿主侧「不再调 updateConfig」                                                                                                                                                                                                                                                                                                                                                                                       | 保存后未弹重建确认框、未发重建回执（未触发重建）                                                                                                   | **主验**：保存前后 `sessionId` 一致、转录文件唯一且连续                                                                                                    |
| 自动记忆开关/频率在保存路径上真的离开 webview（[memory-management.md](./memory-management.md)「自动记忆开关」场景 1–6 的入口；#2115 回归网）                           | `settingsMemory.test.tsx`（`SettingsPage.onSave` 载荷 + ChatApp `updateConfiguration.configurationData` 两键，开关拨动即写 / 轮次失焦或回车写）                                                                                                                                                                                                                                                                     | **主验**：`packages/webview/e2e/desktop-settings-auto-memory-save.e2e.ts`（真 bundle 拨开关/填轮次 → 真报文断言两键；另验证宿主回包回填开关/轮次） | — （host→CLI 的 stdio 透传由对应透传测试负责，不在此重复）                                                                                                 |
| 补全模型/补全开关在保存路径上真的离开 webview（[code-completion.md](../editor/code-completion.md) 故事 4 场景 2–3、故事 5 场景 1；含未设置态不写该键、失效值行内提示） | **主验**：`settingsGlobal.test.tsx`（`SettingsPage.onSave` 载荷两键、未改动/未设置态不出现在载荷、失效模型名保留显示 + 提示；**以及宿主门禁：桌面端 / 远端网页宿主下两行不渲染**）＋ SDK `userSettings.test.ts`（顶层键读写、来源归因：`completionModel` 有 `env` 层、`completionEnabled` 只有 `user`）                                                                                                             | `packages/webview/e2e/plugin-settings-completion.e2e.ts`（真 bundle：两行渲染 + 打开下拉拉候选 + 改动断言报文两键）                                | —（host→CLI 的 stdio 透传由对应透传测试负责，不在此重复）                                                                                                  |
| settings.json 被真实写入且下一轮读到新值（`language` 一例，场景 4）                                                                                                    | SDK `LiveConfigManager` 解析                                                                                                                                                                                                                                                                                                                                                                                        | —                                                                                                                                                  | **主验**：真 CLI 进程 + 真文件：写文件后下一轮行为变化                                                                                                     |
| 自动记忆开关：关闭后自动记忆目录写入被权限层拒绝、重开恢复（场景 6）                                                                                                   | 白名单幂等增删                                                                                                                                                                                                                                                                                                                                                                                                      | —                                                                                                                                                  | —（本 PR 未覆盖：驱动权限层需要模型发出工具调用，而真 host 的本地假模型只回文本）                                                                          |
| 会话启动时 settings.json 不存在，首次创建/保存后也生效（场景 7；`LiveConfigManager` `existsSync` 门禁回归）                                                            | **主验**：watcher 门禁单测（不存在但父目录存在 → 监视该路径 / 无父目录 → 上溯）                                                                                                                                                                                                                                                                                                                                     | —                                                                                                                                                  | —（「启动时文件不存在」在真 host 构造不出：真 CLI 启动即自建 `~/.wave/settings.json`（插件市场引导）；对应的**可观察**形态＝文件存在但没有该键，见下一行） |
| 保存回执即已重载、紧接的下一轮生效（场景 7 的「不得只依赖监视」；显式 reload）                                                                                         | **主验**：bridge 单测断言写完文件后对进程内全部 live 会话各调一次 `reloadConfiguration`（空载荷不调）                                                                                                                                                                                                                                                                                                               | —                                                                                                                                                  | **主验**：真 host——文件里没有 `language` 时保存一次，**紧接**的下一轮即带新语言指令（不设「等 watcher」重试循环）                                          |
| 被更高层覆盖的键如实显示（场景 9：生效值 + `remote` 置灰「由组织配置管理」/ `env` 标注来源但可编辑；边界说明「用户偏好的层与来源」）                                   | **主验**：SDK `readUserPreferenceView` 层序/来源单测（各键 × 各层：上下文长度的 env 键归因、自动记忆开关的两条 Remote 路径、顶层标量整层优先于 env、文件损坏只丢用户层）＋ bridge 单测断言两个 RPC 回包 = 生效值 + `preferenceSources` ＋ `settingsGlobal` / `settingsMemory` 断言 `remote` 键置灰与提示、`env` 键显示生效值 + 来源说明 + 改得动（载荷含该键）、`user` / `default` 键无任何来源说明、缺字段向后兼容 | **主验**：`settings-org-managed-keys.e2e.ts`（真 bundle：`remote` 置灰 + 提示；`env` 显示生效值 + 来源说明且可编辑）                               | **主验**：真 host——机器环境变量（`WAVE_CONTEXT_WINDOW`）提供的上下文长度按生效值回给设置页且来源层标为 `env`                                               |
| 轮内快照不抖动（场景 5）                                                                                                                                               | 快照读取时机                                                                                                                                                                                                                                                                                                                                                                                                        | —                                                                                                                                                  | —（本 PR 未覆盖：需要同一轮内的第二次模型调用，假模型只回文本、无法在轮中再次发请求）                                                                      |
| 上下文长度落 `env.WAVE_CONTEXT_WINDOW`（边界说明「上下文长度的落点」）                                                                                                 | 解析链与换算单测                                                                                                                                                                                                                                                                                                                                                                                                    | —                                                                                                                                                  | —（本 PR 未覆盖：假模型不回 usage，宿主的 `contextUsage.percent` 无从观察取值；落盘值本身已在真 host 断言）                                                |
| 插件变更不自动生效、只提示，敲 `/reload-plugins` 后七类能力就地换装（「配置变更不再需要重建会话」场景 3–4）                                                            | 断言不发起任何重建                                                                                                                                                                                                                                                                                                                                                                                                  | **主验**：真 bundle——敲 `/reload-plugins` 后的提示与能力生效                                                                                       | **主验**：真 host——变更真落盘、重载后同一 CLI 进程 sessionId 不变                                                                                          |
| 未敲命令时新开对话按磁盘新状态装载、既有对话保持原状（「插件变更的就地重载」场景 9）                                                                                   | —                                                                                                                                                                                                                                                                                                                                                                                                                   | —                                                                                                                                                  | **主验**：真 host——新建会话按新状态装载、既有会话能力不变                                                                                                  |

#### 凭据链路下线（PR-1，已随本规格同批落地）

| 场景                                                                                                                                 | 单测                                                                                                                                                                                                                                                     | e2e                                                                                                                                                                            | 真 host                                                                                                                                                                                                                                      |
| ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| IDE 未认证时欢迎页显示登录入口、遗留直连配置不再关掉它（[sso-auth.md](../enterprise/sso-auth.md)「插件端更多菜单与欢迎页」场景 5/7） | `welcomeViewFlash.test.tsx`                                                                                                                                                                                                                              | **主验**：`packages/webview/e2e/plugin-welcome-login.e2e.ts`                                                                                                                   | —                                                                                                                                                                                                                                            |
| 插件端设置页无任何凭据输入控件（sso-auth 场景 5 / 边界情况「插件端不再有直连免登录旁路」）                                           | 四视图渲染断言                                                                                                                                                                                                                                           | **主验**：`packages/webview/e2e/plugin-settings-credentials-removed.e2e.ts`（7 视图全扫 + 带遗留凭据的 `configurationResponse` 不产生控件）                                    | —                                                                                                                                                                                                                                            |
| 宿主→CLI 真报文不含 `apiKey` / `baseURL` / `defaultHeaders`（同上边界情况）                                                          | 宿主测试断言不透传                                                                                                                                                                                                                                       | —                                                                                                                                                                              | **主验**：`packages/desktop/tests/integration/realHostCredentialWire.integration.test.ts`（tee 抓真字节：`initialize` / `updateConfig` / `sendMessage` 双向报文）                                                                            |
| 未认证时发送入口禁用、禁用原因写在输入框占位文案（[sso-auth.md](../enterprise/sso-auth.md)「插件端更多菜单与欢迎页」场景 8）         | `unauthenticatedInputDisabled.test.tsx`（未登录）＋ `desktopApp.test.tsx`（桌面未选目录「should disable the input area when no workdir is selected」两条原因独立 ＋「shows the login reason, not the workdir reason, when both reasons apply」登录优先） | **主验**：`packages/webview/e2e/plugin-welcome-login.e2e.ts`、`packages/webview/e2e/desktop-unauthenticated-input-disabled.e2e.ts`（真 bundle 下两条原因互不覆盖、登录后恢复） | —                                                                                                                                                                                                                                            |
| 未登录时宿主如实回带未认证、且不发模型请求（同上边界情况）                                                                           | —                                                                                                                                                                                                                                                        | 欢迎页登录入口可见 + 输入框禁用                                                                                                                                                | **主验**：同一真 host 用例（`setInitialState.isAuthenticated === false` 驱动 webview 门禁；无凭据回合 `model.requests` 为空、无 `endStreaming`）。**注**：仍不新增「未登录即拒绝新建对话/恢复历史」的宿主或 CLI 硬门禁，若将来要求须另立规格 |

**main-only CI 说明**：真 host 层挂在两个 main-only job——Linux `real-host-e2e`，以及 Windows `check-windows`（含 desktop 单元套件 + 真 host，平台差异面）；e2e 层 `test:e2e` 在 PR CI 中为跳过项（非门禁）。本地执行命令：`pnpm -F wave-webview test:e2e`、`pnpm -F wave-desktop test:realhost`。
