Skip to content

feat(windows): 接通共享 lithe-agent-host 的 Agent 连接桥 - #960

Draft
puppyben1 wants to merge 18 commits into
1lck:previewfrom
puppyben1:codex/issue-956-windows-agent-host
Draft

puppyben1 wants to merge 18 commits into
1lck:previewfrom
puppyben1:codex/issue-956-windows-agent-host

Conversation

@puppyben1

@puppyben1 puppyben1 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

这个 PR 做了什么

Windows 端此前没有 Agent 后端:backendCapabilities.agent 是 false,platform_invoke 里没有任何 agent 分支(translate() 未命中的命令直接返回 Windows platform command is not implemented),windows/tauri/src/features/ai/ 是不接后端的历史代码(#440 已判定不作为参考)。而 macOS 的 Agent 侧栏已经建在共享的 rust/lithe-agent-host 上。

这个 PR 把 Windows 接到同一个 crate,先把连接桥做出来,为后续面板 UI 打底。Refs #956(该 issue 的一期)。

复核意见已处理:P1(退出前不等已移出注册表的连接)和 P2(send/close 未校验调用窗口)在 2caa6ce2 修掉,两条行内线程都有回复;2db71928 补上用受控假适配器验证整棵进程树回收的确定性验收。详细说明见 PR 末尾评论。

改动点

  • windows/tauri/src-tauri/src/agent.rs(新增):按连接 ID 登记 AgentHandle,只提供 agent_open / agent_send / agent_close 三个命令,转发的就是 shared/fixtures/agent/acp-events-v1.json 那套 JSON,协议、会话、取消语义仍只有共享 crate 一份。
    • 连接 ID 由界面生成:界面先订阅 agent_event 再开连接,因此不存在“事件早于连接标识到达”的竞态;重复或为空的 ID 直接拒绝,不会静默顶掉在用的连接。
    • 事件只发给打开它的那个窗口,不广播给其他项目;agent_send / agent_close 也注入 Webview,在注册表锁内比对连接归属窗口,被拒的调用既不发送也不移除连接,窗口 B 拿到窗口 A 的 ID 也驱动不了它。
    • AgentHandle::close 的等待放到阻塞池,不占住异步运行时。
    • 项目窗口销毁和应用退出都会释放连接与进程树。移出连接与「正在关闭」计数在同一个临界区完成(take_connections),退出先在同一次加锁里停止接受新连接并取走剩余连接、同步关闭它们,再以 15 秒上限等待仍在阻塞池里的关闭,超时打印告警后放弃。
  • windows/tauri/src-tauri/src/bin/fake_acp_adapter.rs(新增):受控假适配器,让“整棵进程树被回收”有确定性证据。它按 NDJSON 应答 ACP 握手(initialize 返回 authMethods: [{ id: "gateway" }],所以能走完 initialize → authenticate → Ready),记录自身 PID 后派生一个继承 stdout 的孙进程模拟 codex-acp 的 app-server,并把两个 PID 写进 pid 文件;孙进程带 120 秒自活上限,测试被中断也不会把 fixture 进程泄漏给后续 CI。它注册成普通 bin 而不是 test-support 目标——后者没有任何测试套件启用,fake_dap_adapter 的测试因此在 CI 里从不运行;它也不会被打包,tauri.conf.json 只打包应用二进制。
  • agent::tests:13 个用例。除原有的非法启动配置、空或重复连接 ID、启动失败上报、未知连接与非法命令、按窗口释放连接、跨窗口 send/close 被拒、关闭计数在移出连接时即生效、出口等待的挂起/唤醒/超时、退出 drain 后拒绝新连接之外,新增两条进程树断言:closing_a_live_connection_reclaims_the_whole_agent_tree(agent_close 路径)与 destroying_a_window_reclaims_the_agent_tree_it_opened(窗口销毁路径),都先等到 ready 事件确认握手完成再关闭,断言外层进程与孙进程都已退出,失败时先杀掉幸存者再断言。
  • windows/tauri/src-tauri/src/main.rs:注册三个命令;WindowEvent::Destroyed 释放该窗口的连接;RunEvent::Exit 关闭全部连接。
  • windows/tauri/src-tauri/Cargo.toml / Cargo.lock:直接依赖 lithe-agent-host。
  • windows/tauri/src/platform/tauri-core.ts:把三个命令登记为原生命令,invoke 边界直接把长连接命令交给 Tauri 主机,而不是塞进共享命令信封。
  • windows/tauri/src/platform/tauri-core.test.ts:新增路由断言。
  • rust/lithe-agent-host/src/lib.rs:Windows 上以 CREATE_NO_WINDOW 启动适配器。Windows 外壳是 GUI 进程、没有控制台,不加这个标志每次起 Agent 都会弹出一个控制台窗口。
  • .agents/notes/.../2026-09-25-shared-acp-agent-conversation.md:记录 Windows 连接桥的所有权、事件寻址、退出清理、CREATE_NO_WINDOW,以及进程树验收的覆盖范围与仍待真机验收的边界,更新适用范围和验证命令。
  • shared/platform-feature-matrix.json 与生成的 docs/development/platform-parity-matrix.*:把连接桥加入四个 Agent 条目的 Windows 证据,Windows 验证状态仍是待验证。

明确不做

验证

  • cargo test --manifest-path windows/tauri/src-tauri/Cargo.toml agent:: → 13 passed
  • ./.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 -Scope WindowsRust → 211 passed,0 failed(含新增 3 个用例,最慢两条约 0.95 s,上限 15 s;报告 .artifacts/test-stability/index.html)
  • cargo test -p lithe-agent-host → 47 passed
  • ./.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 -Scope Frontend -FrontendTestPath src/platform/tauri-core.test.ts → 5 passed
  • bun run typecheck → 干净
  • cargo fmt --manifest-path rust/Cargo.toml --all -- --check、cargo fmt --manifest-path windows/tauri/src-tauri/Cargo.toml -- --check → 干净
  • ./.agents/skills/write-stable-tests/scripts/verify-test-stability.ps1、node scripts/verify-agent-notes.mjs、node scripts/generate-platform-feature-matrix.mjs --check → 通过
  • scripts/verify-windows-boundaries.ps1:本机因未跟踪的 windows/build-*、windows/winui 构建目录触发 “must not restore the retired Qt/C++ implementation”,与本次改动无关(git ls-files 里没有任何 .cpp/.h),该脚本由 CI 在干净检出上运行(.github/workflows/ci-windows.yml:204)。

测试都在本机 Windows 上跑过,不依赖真实 Agent、网络或已安装的适配器。除“能解析但起不来”的假启动之外,进程树用例用的是本仓库自带的 fake_acp_adapter 假适配器,不会碰用户环境。

已知限制

  • 连接桥还没有用真实适配器在 Windows 上跑过端到端(装 @agentclientprotocol/codex-acp、node/npm 探测、.cmd shim 启动、以及配合本机 Codex CLI 或真实 Key 的完整对话)。平台矩阵的 Windows 验证状态保持“待验证”,等真机验收后再改。
  • 面板 UI 落地前,这几个命令没有产品入口,只有测试在用。

Windows 端此前没有 Agent 后端:backendCapabilities.agent 为 false,
platform_invoke 不认识任何 agent 命令,features/ai 是不接后端的遗留代码。
macOS 的 Agent 侧栏早已使用共享的 rust/lithe-agent-host,本提交把 Windows
接到同一个 crate,为后续面板 UI 打底(Refs 1lck#956)。

- windows/tauri/src-tauri/src/agent.rs(新增):按连接 ID 登记 AgentHandle,
  只提供 agent_open / agent_send / agent_close 三个命令;连接 ID 由界面生成,
  界面先订阅 agent_event 再开连接,避免事件早于标识到达;重复或空 ID 直接
  拒绝,不会顶掉在用的连接;事件只发给打开它的窗口,不广播给其他项目;
  显示关闭走阻塞池,避免 AgentHandle::close 的等待卡住异步运行时;项目窗口
  销毁和应用退出都会释放连接与进程树,退出路径同步等待。
- windows/tauri/src-tauri/src/main.rs:注册三个命令,在 WindowEvent::Destroyed
  里释放该窗口的连接,在 RunEvent::Exit 里关闭全部连接。
- windows/tauri/src-tauri/Cargo.toml、Cargo.lock:直接依赖 lithe-agent-host。
- windows/tauri/src/platform/tauri-core.ts:把三个命令登记为原生命令,让前端
  的 invoke 边界直接把长连接命令交给 Tauri 主机,而不是走共享命令信封。
- windows/tauri/src/platform/tauri-core.test.ts:新增路由断言。
- rust/lithe-agent-host/src/lib.rs:Windows 上以 CREATE_NO_WINDOW 启动适配器,
  否则 GUI 外壳每开一次 Agent 都会弹出一个控制台窗口。
- .agents/notes/.../2026-09-25-shared-acp-agent-conversation.md:记录 Windows
  连接桥的所有权、事件寻址和退出清理,并更新适用范围与验证命令。
- shared/platform-feature-matrix.json 与生成的 docs/development/platform-parity-matrix.*:
  把连接桥加入四个 Agent 条目的 Windows 证据,并注明 UI 仍待接入。

未包含:Agent 面板 UI、供应商切换、订阅额度展示,以及 backendCapabilities.agent
的开关(留给面板 PR,避免顺带放开 features/ai 里那些没有后端的历史命令)。

验证:
- cargo test --manifest-path windows/tauri/src-tauri/Cargo.toml agent:: → 5 passed
- test-stability-windows.ps1 -Scope WindowsRust → 202 passed,0 failed(新增 5 个
  用例 80–926 ms,报告 .artifacts/test-stability/index.html)
- cargo test -p lithe-agent-host → 47 passed
- test-stability-windows.ps1 -Scope Frontend -FrontendTestPath src/platform/tauri-core.test.ts → 5 passed
- bun run typecheck → 干净;cargo fmt --check(两个 crate)→ 干净
- verify-test-stability.ps1、verify-agent-notes.mjs、generate-platform-feature-matrix.mjs --check → 通过
- verify-windows-boundaries.ps1 在本机因未跟踪的 windows/build-* 与 windows/winui
  构建目录报「must not restore the retired Qt/C++ implementation」,与本次改动无关
  (git ls-files 中没有任何 .cpp/.h);其余检查项逐条通过。

已知限制:连接桥还没有用真实适配器在 Windows 上跑过端到端,平台矩阵的 Windows
验证状态仍是待验证。
@ghfind-review ghfind-review Bot added the review: medium ghfind author score; see https://ghfind.com label Sep 28, 2026

@1lck 1lck left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lithe Review

结论:存在 1 个 P1 阻塞问题,另有 1 个 P2 建议。
范围: preview ← PR #960,共同基点 130c39f485983d9691e017b9c346747dfb58845d 至 f107358eaf1a8e979803c336f595f12c6160e42d。

Findings

  1. P1:窗口关闭时从注册表移除连接,却把清理留在未登记的后台任务中。 随后的应用退出无法等待这些任务,Agent 进程树可能残留。
  2. P2:agent_send / agent_close 未校验调用窗口。 其他窗口只要传入已有 connection ID,就能操作原窗口的连接。

位置、触发时序及修复建议见两条行内意见,均为本次新增连接桥引入。

Scope Check

已核对 #956 的一期范围:本 PR 复用共享 lithe-agent-host,新增 open/send/close、按窗口事件投递、销毁与退出钩子、CREATE_NO_WINDOW 及原生命令路由;没有另写 ACP 协议实现。面板 UI、供应商与额度展示仍未接入,不能视为整个 #956 完成。继续关闭旧 agent 能力开关有明确原因:旧命令尚无后端,不把这一点作为缺陷。

Verification

  • 本地 diff、程序包只读、功能矩阵及变更门禁、Windows 边界、Agent Notes 和跨平台测试稳定性静态检查通过。
  • 当前 head 的已有 GitHub checks 无失败或待完成项,但分支与 preview 存在合并冲突;应解决冲突后重新验证。
  • 检查了新增 403 行连接桥、Tauri main 事件钩子、共享 AgentHandle::open/send/close/Drop 与进程终止路径、路由测试、Note 和矩阵。锁定 Tauri 2.11.5 的本地依赖源码确认 App::run 退出不会等待任意未登记清理任务。
  • 现有连接桥测试主要以无法启动的假命令验证注册表和 stopped 事件,不能证明存活 Agent 的进程树在窗口销毁/退出竞争下已被回收。
  • 本次未运行完整构建、测试或原生 UI;没有生成新的编译缓存。代码缺陷依据当前 head 的调用链分析,未声称已在 Windows/macOS 实机复现。

建议先修复 P1,并用可控关闭 gate 覆盖“窗口销毁移出注册表→清理未结束→应用退出”的顺序;真实适配器端到端验证仍需 Windows。

Comment thread windows/tauri/src-tauri/src/agent.rs Outdated
/// for its bounded stop window.
pub fn close_window_connections(window: &str) {
for connection in take_window_connections(window) {
tauri::async_runtime::spawn_blocking(move || connection.handle.close());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 退出前也要等待已从注册表移出的连接清理

take_window_connections 先把连接从全局 map 移除,这里再把 AgentHandle::close() 丢到 blocking pool,并丢弃任务句柄。随后 shutdown() 只 drain map 中剩下的连接,因此看不到这些正在关闭的 Agent。close_connection() 也有先移除、再异步等待的同类窗口。

触发时序:存在一个仍运行的 Agent,销毁其项目窗口,在后台关闭任务执行前或尚未完成时退出应用。退出钩子调用 agent::shutdown() 时注册表已空,可以立即返回;Tauri 的 App::run 最终直接退出进程,后台线程和 AgentHandle 的 Drop 不能作为完成保证,外部 Agent/子进程可能残留。现有用“缺失可执行文件”构造的测试没有存活进程,无法覆盖这个路径。

这是本次新增桥接层的生命周期缺口。建议让 registry 同时拥有 active 和 closing 连接/任务,退出时禁止新连接并有界等待两类清理完成;不要仅等待仍在 active map 中的 handle。补充使用可控 gate 的销毁→退出竞争测试,并以受控假适配器验证整棵进程树退出。

证据为当前代码时序与锁定的 Tauri 2.11.5 退出路径,未在 Windows 实机运行该复现。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已按建议修掉,见本分支新增 commit 2caa6ce:

  • 注册表移出连接与「正在关闭」计数现在在同一个临界区完成(take_connections),所以退出 drain 不可能看不到已被取走的连接;agent_close 命令路径也不再是「先移除、后异步等待」。
  • shutdown() 先在同一次加锁里停止接受新连接(Registry.accepting)并取走剩余连接、同步关闭它们,再以 CLOSING_WAIT(15 秒)有界等待仍在阻塞池里的关闭,超时打印告警后放弃。
  • 可控 gate 覆盖:exit_wait_returns_once_the_last_close_is_released(有关闭挂起时退出等待阻塞、释放后返回)、exit_wait_gives_up_at_its_deadline_while_a_close_is_pending(超时放弃)、taking_a_connection_for_a_close_counts_it_before_any_task_runs(移出连接时计数立即生效)、an_exit_drain_takes_every_connection_and_refuses_new_ones(drain 后拒绝新连接)。

仍缺的验证与你指出的一致:这些用例证明的是计数与等待语序,没有用存活的真实 Agent 验证整棵进程树在「窗口销毁→退出」竞争下已被回收,那一步仍需要 Windows 真机加真实适配器。

分支已合入 preview f53f65d。cargo test --manifest-path windows/tauri/src-tauri/Cargo.toml agent:: 11 passed;test-stability-windows.ps1 -Scope WindowsRust 208 passed(最慢用例 2.45s,低于 15s 上限);Agent Note 与功能矩阵 --check 通过。

Comment thread windows/tauri/src-tauri/src/agent.rs Outdated

/// Queues one shared `AgentCommand` on an open connection.
#[tauri::command]
pub fn agent_send(connection_id: String, command: Value) -> Result<(), String> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] send/close 应校验调用者仍是连接所属窗口

agent_open 从注入的 Webview 记录 owner,但 agent_send 和 agent_close 均只接受前端提供的 connection ID,没有注入调用窗口,也没有与 Connection.window 比较。全局 map 的查找/移除只按 ID,因此窗口 B 使用窗口 A 已打开的 ID,就能给 A 发 prompt/cancel/permission 命令,或关闭 A 的连接;仅把输出事件定向给 A 不能保证命令侧隔离。

可构造复现:A 用 ID connection-a 打开连接,B 调用 agent_close({ connectionId: 'connection-a' });当前代码会移除并停止 A 的连接。该行为属于本次新增命令的归属边界,按 P2 提出。

建议 send/close 也注入 Webview,并在锁内查找/移除前校验 owner;内部窗口销毁/退出清理可保留独立的受信路径。补充 A 可操作 A、B 操作 A 被拒绝且连接仍在的双窗口测试。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修:agent_send 和 agent_close 现在同样注入 tauri::Webview,在注册表锁内用 verify_owner 比对连接归属窗口。close_connection 的 select 在移除之前就会返回错误,所以被拒绝的调用既不发送也不移除连接;窗口销毁/退出的内部清理走独立的受信路径。

新增测试 a_window_cannot_drive_or_close_another_windows_connection:A 打开连接后,B 的 send 与 close 都返回 "belongs to another window",随后 A 的连接仍在(send 只报 payload 非法)。Tauri 命令的 JS 参数形状没有变化,前端无需改动。

修复 review 提出的两个问题:

- P1 退出前必须等待已从注册表移出的连接清理:窗口销毁时连接先被移出
  注册表、清理却留在未登记的阻塞任务里,随后的应用退出只 drain 注册表,
  看不到这些连接,可能残留 Agent 进程树。现在注册表的移出与「正在关闭」
  计数在同一个临界区完成;退出先在同一次加锁里停止接受新连接并取走剩余
  连接、同步关闭它们,再有界等待(CLOSING_WAIT,15 秒)仍在阻塞池里的
  关闭,超时打印告警后放弃(AgentHandle::close 自带停止窗口并强杀进程树,
  这个上限只兜底任务尚未被调度的情况)。
- P2 agent_send / agent_close 未校验调用窗口:其他窗口只要知道 connection
  ID 就能驱动或关闭别人的连接。现在两个命令都注入 Webview,并在注册表锁
  内比对连接归属窗口;被拒绝的调用不会移除也不是关闭连接。

改动点:

- windows/tauri/src-tauri/src/agent.rs:注册表改为 Registry{connections,
  accepting},新增 open_in / drain_in / take_connections(移出并计数),
  queue_close、wait_for_closing、verify_owner;关闭计数用 Condvar 通知,
  退出等待有界。
- windows/tauri/src-tauri/src/agent.rs 测试从 5 个增加到 11 个:新增跨窗口
  send/close 被拒且连接仍在、移出连接时计数立即生效、退出等待在有关闭时
  挂起、超时后放弃、最后一个关闭结束时返回、退出 drain 后拒绝新连接;按
  窗口释放连接的用例同时断言计数归零。
- .agents/notes/implemented/architecture/2026-09-25-shared-acp-agent-conversation.md:
  补记窗口归属校验、退出停止接受新连接与有界等待,以及「未用存活 Agent
  验证整棵进程树回收」的覆盖边界。

验证:cargo test --manifest-path windows/tauri/src-tauri/Cargo.toml agent::
(11 passed);test-stability-windows.ps1 -Scope WindowsRust(208 passed,
最慢用例 2.45s,低于 15s 上限);rustfmt;node scripts/verify-agent-notes.mjs。
…ndows-agent-host

合入 preview 到 f53f65d。冲突只在由 shared/platform-feature-matrix.json 生成的
三个文件:保留本分支的 Windows Agent 连接桥 evidence、verify 文案与
lastReviewed=2026-09-28,同时保留 preview 新增的图标资源验收句子;随后用
node scripts/generate-platform-feature-matrix.mjs 重新生成 md/csv,
`--check` 通过。
@puppyben1

puppyben1 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

已按 review 修复,并合入最新 preview

新提交:2caa6ce2(修复)+ a93558c9(合并 preview f53f65d,冲突只在 shared/platform-feature-matrix.json 及其生成的 md/csv,已按 node scripts/generate-platform-feature-matrix.mjs 重新生成并通过 --check)。

P1 退出等待(windows/tauri/src-tauri/src/agent.rs)

  • 注册表改为 Registry { connections, accepting };take_connections 在同一个临界区内移出连接并累加「正在关闭」计数,退出 drain 不可能漏掉已取走的连接。agent_close 命令路径也不再是「先移除、后异步等待」。
  • shutdown() 先在同一次加锁里停止接受新连接并取走剩余连接、同步关闭,再以 CLOSING_WAIT(15 秒)有界等待仍在阻塞池里的关闭,超时打印告警后放弃;AgentHandle::close 自带停止窗口并强杀进程树,这个上限只兜底「任务还没被调度」。

P2 命令归属(同文件)

  • agent_send / agent_close 注入 tauri::Webview,在锁内用 verify_owner 校验连接归属窗口;被拒绝的调用既不发送也不移除连接。窗口销毁与退出的内部清理仍走受信路径。Tauri 命令的 JS 参数形状不变,前端无需改动。

测试(5 → 11 个)

新增跨窗口 send/close 被拒且连接仍在、移出连接时计数立即生效、退出等待在有关闭时挂起、超时后放弃、最后一个关闭结束时返回、退出 drain 后拒绝新连接;按窗口释放连接的用例补了计数归零断言。

验证

  • cargo test --manifest-path windows/tauri/src-tauri/Cargo.toml agent:: → 11 passed
  • test-stability-windows.ps1 -Scope WindowsRust → 208 passed(最慢 2.45s,低于 15s 上限)
  • test-stability-windows.ps1 -Scope Frontend -FrontendTestPath src/platform/tauri-core.test.ts → 5 passed;bun run typecheck 干净
  • rustfmt --check、node scripts/verify-agent-notes.mjs、功能矩阵 --check 通过
  • verify-windows-boundaries.ps1 本机仍因未跟踪的构建产物(windows/build-windows、windows/build-winui、windows/winui)失败,与本次改动无关:git ls-files windows 中 .cpp/.h 数量为 0,CI 干净检出不受影响。

仍未覆盖:面板 UI、供应商切换与额度展示未接入;计数与等待语序有测试,但没有用存活的真实 Agent 验证整棵进程树在「窗口销毁→退出」竞争下已被回收,这一步仍需 Windows 真机加真实适配器。

连接桥的关闭语义此前只用假启动失败和可控 gate 覆盖,没有用存活的 Agent
验证“整棵进程树”是否真的被回收。Windows 适配器(如 codex-acp)会派生出
独立的 app-server 并继承 stdout,只杀掉外层进程会留下它并占住输出管道。

- 新增 `fake_acp_adapter` 假适配器:以 NDJSON 应答 ACP 握手(initialize
  返回 gateway authMethod,session/new 返回会话),派生一个继承 stdout 的
  grandchild 模拟 app-server,并把两个进程 ID 写进 pid 文件;孙进程带 120
  秒自活上限,避免测试失败时泄漏进程。
- 它是普通 bin 而不是 `test-support` 特性下的目标:后者没有任何测试套件
  启用(`fake_dap_adapter` 的测试因此在 CI 中从不运行)。它也不会被打包,
  `tauri.conf.json` 只打包应用二进制。
- agent::tests 新增两条端到端路径断言:`agent_close` 关闭连接、以及窗口
  销毁触发的 `close_window_connections`,都必须让外层进程和继承 stdout 的
  孙进程同时退出,失败时先杀掉幸存者再断言。
- 顺带修掉测试噪音:RecordingSink 对已结束测试的迟到事件不再 panic,等
  待中的测试仍以自己的 recv_timeout 失败。
- Agent Note 更新进程树回收的验证状态与仍待真机验收的边界。
@puppyben1

Copy link
Copy Markdown
Contributor Author

再看这次 review 的结论“现有连接桥测试主要以无法启动的假命令验证注册表和 stopped 事件,不能证明存活 Agent 的进程树在窗口销毁/退出竞争下已被回收”,补了 commit 2db7192(test(windows): 用受控假适配器验证 Agent 进程树回收)。它把这条验证从“代码存在”推进到“本机可复现的确定性证据”,不再需要真实适配器也能覆盖整棵树。

新增假适配器 windows/tauri/src-tauri/src/bin/fake_acp_adapter.rs:

  • 以 NDJSON 应答 ACP 握手,initialize 返回 authMethods: [{id: "gateway"}],所以连接桥会走完 initialize → authenticate → Ready 全部步骤;
  • 记录自身 PID 后派生一个继承 stdout 的孙进程,模拟 codex-acp 那个会晚几秒退出、并占住输出管道的 app-server;
  • 把两个 PID 写进 pid 文件,测试据此在操作系统层面断言存活;
  • 孙进程带 120 秒自活上限:测试失败被中断时不会把 fixture 进程泄漏给后续 CI。

为什么是普通 bin 而不是 test-support 目标:test-support 现在没有任何脚本或 CI 启用,fake_dap_adapter 的测试因此在 CI 里从不运行。它也不会被打包,tauri.conf.json 只打包应用二进制。

新增两条端到端断言(都在 agent::tests,不需要网络和真实 Agent):

  • closing_a_live_connection_reclaims_the_whole_agent_tree:agent_close 关闭连接后,外层进程与孙进程都已退出,注册表也不再持有该连接;
  • destroying_a_window_reclaims_the_agent_tree_it_opened:close_window_connections 走窗口销毁路径,同样回收整棵树。

两条都先 wait_for_event(..., "ready") 确认真实握手完成再关闭,断言失败时会先杀掉幸存者再 panic,避免把残留进程带进后面的测试。

顺带修掉一个测试噪音:RecordingSink 原会对“测试已结束、连接 worker 才迟到上报”的事件 panic(closing_a_window_releases_only_its_own_connections 用缺失二进制时会命中),现在改为丢弃;仍在等待的测试依旧会以自己的 recv_timeout 失败。

本地验证:

  • cargo test --manifest-path windows/tauri/src-tauri/Cargo.toml agent:: → 13 passed;
  • ./.agents/skills/write-stable-tests/scripts/test-stability-windows.ps1 -Scope WindowsRust → 211 条逐测试计时通过,最慢的两条新测试约 0.95 s(上限 15 s);
  • node scripts/verify-agent-notes.mjs、node scripts/generate-platform-feature-matrix.mjs --check 通过;
  • Agent Note 里的验证小节和“后果”边界已同步:进程树回收现在有确定性证据,仍然缺的是真实 codex-acp 在 Windows 上的端到端验收;退出与窗口销毁并发的时序继续由注册表计数和 gate 测试覆盖。

P1/P2 的修复没有回退,面板 UI、供应商与额度展示仍未接入,本期范围没变。

cargo test --no-run 只产出 libtest harness,不会在 target/debug 下生成平名的
fake-acp-adapter.exe,干净检出上两条进程树测试因此以 "The system cannot find
the file specified" 失败。

fixture_path() 改为在平名产物缺失时按需执行
cargo build --bin fake-acp-adapter,并在调用方传入 --target-dir 时跟随其
profile 目录;构建失败时把 cargo stderr 带进 panic,避免只看到 os error 2。
- platform_invoke 增加第 6 个参数 agent_events;Tauri 会把缺失的尾部
  Option 参数注入为 None,所以旧的前端调用点不需要改动。
- agent.install / agent.installCli 的 SharedEvent 投递到 agent 通道,
  其余事件(含 git.*)仍走原来的 git 通道。
- 安装类命令不再套用 30 秒默认信封超时:npm 下载可能持续几分钟,
  共享 host 自己以 15 分钟为界,macOS 也不设信封超时。
- 前端 installProgressEvent 先校验 kind 与数值字段再上报,避免面板
  渲染出 undefined。
面板(对齐 macOS 的右侧 Agent 侧栏)
- features/agent 新增面板本体:会话标签、消息流、工具与权限提示、输入框、
  上下文用量与订阅额度、历史会话列表与恢复、适配器状态/安装/卸载。
- 连接是「一个窗口一个项目」的模块单例:隐藏面板保留运行中的回合,
  切换项目停掉旧进程树,窗口销毁交给 Tauri 宿主回收。
- 传输层直接复用 `@tauri-apps/api/event` 的 listen,并把 `listen` 从
  tauri-core 里移除,恢复 tauri-core.test.ts 只 mock Channel/invoke 的前提。
- 可写目录统一走 platform/app-data-directory.ts(appDataDir),不从安装
  目录推导,避免破坏发行包基线与增量更新。

设置与入口
- 设置页新增 Agent 供应商配置:API Key 存安全存储
  (agent-provider-api-key/<agentId>),参数改为每行一个的 Textarea,
  与 macOS 的 agentArguments 按换行拆分保持一致。
- 侧栏活动栏、快捷键命令与视图动作新增 Agent 面板入口;settings.agentPanel
  记录面板可见性并参与归一化。
- 能力开关:backendCapabilities.agent 打开,新增 aiChat=false;tauri-core 把
  agent.status/install/uninstall/installCli 映射到 agent,旧的 acp_*/codex_*/
  ai_provider/_chat 仍映射到 aiChat,继续显示「待开发」而不是「未实现」。
- 功能矩阵 9 条 Agent 行补齐 Windows 证据与实现状态:入口发现、上下文用量、
  Codex 订阅与额度已实现;历史、管理、文件引用、供应商配置、ACP 对话为部分
  实现,verificationStatus 仍保持 pending(真机验证未做)。
- 同步重新生成 docs/development/platform-parity-matrix.md 与 .csv。
- Agent Note 补充本轮 4 条决策:右栏挂载与 settings.agentPanel、供应商 API Key
  的安全存储键、启动规则对齐 macOS 的 agentLaunchConfiguration、能力开关拆分,
  以及安装进度/安装超时的归属。
新增步骤通过计时 runner 显式枚举 `src/features/agent` 与
`src/config/backend-capabilities.test.ts`(本地 38 项通过,最慢 2 ms),
报告写入 `.artifacts/test-stability/windows-agent.json` 并随现有上传步骤归档。
此前这些用例不在任何 CI 步骤里,改动 `windows/*` 已由分类器选中前端 lane,
所以只补测试入口,不动分类逻辑。
对齐 macOS `AgentHistoryFeatureModel` 的 Lithe 侧标注能力:

- 新增 `agent-history-annotations.ts`(纯函数:作用域键、解析、重命名、标记)
  与 `agent-history-store.ts`(平台 store `agent-history.json` 的读写)。
  标注只属于 Lithe,按“Agent ID + 工作区路径”隔离,不进发行包,也不写
  Agent 自己的会话文件;每次修改前先重读再合并,两个窗口不会互相覆盖。
- 新增 `agent-history-view.ts`:筛选(全部/收藏/已移除)、按标题或会话 ID
  搜索、更新时间倒序(无时间戳的排最后、同刻按 ID 升序),以及已加载会话
  的消息数统计,语义与 macOS `AgentHistoryPresentation` 一致。
- 历史列表增加筛选、搜索框、复制会话 ID、收藏、重命名、单条移除与恢复,
  以及多选批量收藏/移除/恢复;批量操作只作用于当前可见行,移除前先确认,
  并说明 Agent 原始记录会保留。
- `useAgentHistory` 在面板内持有标注;面板把标注、已加载会话与动作传给列表。
- 新增 14 项纯函数测试(作用域键规范化、解析容错、空标题拒绝、标记合并、
  筛选/搜索/排序、消息数);标注逻辑与 store 分开,测试不加载 Tauri 运行时。
  本地 53 项通过(`src/features/agent` + `src/config/backend-capabilities.test.ts`)。
- 矩阵与 Agent Note 记录该增量:Markdown 导出与独立历史页仍未实现。
对齐 macOS `AgentHistoryFeatureModel.export`:

- 模型新增 `canExportTranscript` / `historyTranscript`:按需重放未打开的会话,
  导出用的加载不打开标签页、不改当前选中会话;重放串行执行,避免共享连接上
  两次回放交错。加载失败、连接中断或部分回放一律返回 null,绝不返回半份记录。
- `agent-history-export.ts` 复刻 macOS `AgentHistoryDocument.markdown`:一段
  一个会话标题与会话 ID,消息按角色分节,工具消息保留状态、输入、输出与内容块;
  标题里的换行会折叠,避免破坏标题层级。
- `agent-history-save.ts` 用原生保存对话框选定目标,只写用户选中的路径;取消
  对话框不写文件,超过 32 MiB 直接报错,平台错误不直接抛给用户。
- 面板与历史列表接入:行内导出与「选择」模式下的批量导出,导出中禁用按钮,
  全部加载成功后才写文件并显示失败原因。
- 新增 6 项模型测试(不打开标签页、失败返回 null、连接中断返回 null、已加载不
  重复加载、两次导出串行、不可重放时拒绝)与 3 项 Markdown 渲染测试;本地 62
  项通过。纯渲染与保存分开,测试仍不加载 Tauri 运行时。
@puppyben1

Copy link
Copy Markdown
Contributor Author

本 PR 的完整范围

原 scope(f107358e)是接通共享 lithe-agent-host 的 ACP 连接桥,并包含 review 修复(2caa6ce2)。在此基础上,按 #956 “Windows 与 macOS 对齐”的目标补齐了 Windows Agent 面板,全部落在本 PR:

  • 44016eb5 转发 agentInstallProgress;agent.install / agent.installCli 不再套 30s 信封超时
  • b4da1b22 Agent 侧栏面板:入口、会话、权限与工具确认、上下文用量与订阅额度、供应商与订阅设置、适配器安装与状态、能力开关(新增 agent: true,旧 AI 聊天仍为 aiChat: false)
  • 110f7190 功能矩阵记录 Windows Agent 面板实现状态
  • 5c62656e Windows 前端流水线增加 Agent 面板与能力开关测试
  • d8ffe226 历史会话:筛选、搜索、收藏、本地重命名
  • f8cb3f5e 导出历史会话为 Markdown(对齐 macOS AgentHistoryDocument.markdown)
  • 2db71928 + bc66c2ce 用受控假适配器验证 Agent 进程树回收,并修掉干净 target/ 下 Rust 测试的红灯

目前还差的部分

以下都还没进本 PR,我会在本 PR 上继续补(不新开 PR,原因见下):

  1. 供应商配置导入:解析已在共享 Rust(agent.parseProviderConfiguration),Windows 还缺设置表单与校验提示。
  2. agent.installCli 安装/更新入口:纯 UI,host 命令已实现且已过能力门控。
  3. 输入框文件引用:@ 与拖拽交互还缺;URI 解析和 prompt 附加管线已经有了。
  4. 真机端到端证据:需要在 Windows 上实际跑通 Node/npm 探测 → .cmd shim 启动 → 进程树回收 → 完整一轮对话,这部分只能在我的机器上做。

关于新开 PR

不支持把本 PR 之后的工作拆成“基于本 PR 分支的 stacked PR”:gh pr create --base codex/issue-956-windows-agent-host 会被拒(fork-only 分支不能作为 base,No commits between ... Base ref must be a branch)。因此后续提交继续追加在本 PR 上;如果 reviewer 认为本 PR 太大、希望缩小,我会按“先合 bridge、再从新 preview 拉面板”的顺序拆,而不是叠 PR。

@1lck

1lck commented Sep 29, 2026

Copy link
Copy Markdown
Owner

还在开发的话 可以把状态设置为草稿

@puppyben1
puppyben1 marked this pull request as draft October 1, 2026 05:34
- 从系统拖入文件即可引用:复用 file-system 的 extractDroppedFilePaths,与文件选择器
  走同一条 addFileReferences 校验、去重与数量上限;高亮只在真正携带 Files 时出现,
  在子元素之间移动不会闪烁。
- 输入 `@` 打开项目文件选择器并按已输入前缀过滤,选中后把 `@query` 文本替换为文件
  引用标签,草稿其余内容不变。
- 匹配规则抽成纯函数 services/agent-composer-mentions.ts:引用必须位于行首或空白
  之后且不含空白与第二个 `@`,因此邮箱地址和已完成的引用不会重新弹出选择器;不需要
  DOM 或真实适配器即可测试。
- i18n 新增 agent.composer.dropFiles,并顺带补齐本 PR 后续设置页要用的键。
- CLI 安装/升级入口(agent.installCli):设置页显示 PATH 上 CLI 的版本与安装来源;
  只有 host 明确给出 canUpdate,或根本没检测到 CLI 时才提供按钮,升级进度复用
  agentInstallProgress;成功后显示已验证版本,安装器带警告时单独一行展示,不把
  警告当成失败,也不在无法判断来源时让用户以为会自动覆盖。
- 供应商配置导入(agent.parseProviderConfiguration):粘贴 Codex TOML 或 Claude JSON,
  由共享 Rust 解析出 endpoint、model 与协议后填入供应商表单;只读元数据、不搬运
  密钥;chatCompletions 以及与所选适配器协议不匹配时给出明确提示,而不是猜一个协议。
- 服务层新增 installAgentCli / parseAgentCliUpdateResult 与 agent-provider-import,
  agent.install 与 agent.installCli 共用的进度 channel 抽成 installProgressChannel。
- tauri-core 能力门控加入 agent.parseProviderConfiguration,Agent 面板关闭时与其他
  agent.* 一样走统一不可用路径。
…sue-956-windows-agent-host

冲突解决:
- windows/tauri/src-tauri/src/main.rs:窗口销毁时同时保留上游的 core::close_ide_hosts
  与本分支的 agent::close_window_connections。
- windows/tauri/src/platform/tauri-core.ts:采用上游新的 isPlatformDispatcherCommand
  路由规则(取代原有 nativeCommands 白名单),并在其上保留本分支改动——
  platform_invoke 的 agentEvents 通道、agent.* 能力门控(含
  agent.parseProviderConfiguration);tauri-core.test.ts 的 agent 连接用例改为断言该规则。
- docs/development/platform-parity-matrix.csv/.md:生成物,按合并后的
  shared/platform-feature-matrix.json 重新生成。
- shared/platform-feature-matrix.json 自动合并成功:上游新增 13 行,本分支 agent 行的
  状态与证据保持不变,无重复 id。
- agent-management:Windows 说明改为已接入 CLI 安装/升级——只有 host 明确给出可更新来源
  (canUpdate)或完全没有检测到 CLI 时才显示按钮,升级后显示 host 验证过的版本,安装器报错
  但版本确实提升时单独一行展示警告;仍未接入的是取消安装、预检清单与本机 CLI 配置文件读取。
  证据补上 hooks/use-agent-management.ts。
- agent-provider-configuration:说明粘贴配置文本导入已接入(共享 Rust 解析出 endpoint、模型与
  协议,只读元数据、不搬运密钥,chatCompletions 或协议不匹配时报错);仍未接入的是一键读取
  本机 CLI 配置文件、多供应商列表与取消关联。证据补上 services/agent-provider-import.ts。
- agent-file-references:Windows 由 partial 改为 implemented。补上从系统拖拽文件与在输入框
  输入 @ 打开选择器(两者共用同一套去重、数量上限与可移除标签)的说明与证据;真实拖放体验与
  Claude 端到端仍待验证,所以 verificationStatus 保持 pending。
- Agent Note 同步以上实现、验证命令与遗留项。
- docs/development/platform-parity-matrix.csv/.md 由 node scripts/generate-platform-feature-matrix.mjs 重新生成。
Tauri 默认开启原生拖放(dragDropEnabled),WebView2 收到系统文件时只发原生事件,
DOM 的 HTML5 drop 拿不到文件;项目树用鼠标事件模拟拖动,也不会触发 drop。
此前输入区只挂了 HTML5 onDrop,真实窗口里两条路径都不会添加引用。

- 拖放路由新增 agent 目标:落点在 data-agent-file-drop-target 内时,原生拖放与
  项目树松手都派发 lithe-agent-file-drop 事件,交给输入区同一个 attach 入口
  (复用去重与 32 个上限);文件夹不作为引用
- 新增路由与派发单测;矩阵 agent-file-references 证据与说明、Agent Note 同步
Windows 真机端到端发现两个问题,都出在 npm 全局安装写的是前缀目录里的
<命令>.cmd 批处理启动器,而不是 Unix 上的符号链接:

- 新建会话报 spawn EINVAL:适配器不经 shell 启动 CLI_PATH 指向的程序,Node.js
  拒绝这样启动 .cmd。现在 cli_update::launch_executable 在包声明的 bin 是原生
  .exe 时(Claude Code)改传这个 .exe;脚本 bin(Codex 的 codex.js)仍传启动器,
  codex-acp 在 Windows 上自己用 shell 启动。API Key 与订阅两条路径共用
- CLI 来源被识别成 Unknown、不给升级按钮:.cmd 的规范路径不指向包,现在先按同
  目录 node_modules/<包>/package.json 找到声明的 bin 再做原有身份校验;另一个
  npm 前缀的启动器仍然只给手动指引

新增 launcher_tests(映射逻辑全平台运行,来源识别仅 Windows);矩阵
agent-management 与 Agent Note 同步。本机验证:Node 24 / npm 11,真实安装
claude-acp 0.81.2,.cmd 启动、ACP 握手、session/new 成功,关闭后进程树清空。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review: medium ghfind author score; see https://ghfind.com

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants