# V3.1 Windows ConPTY 与终端可靠性开发记录

日期：2026-10-11。稳定基线：`origin/main` 的 `0b924f8`（V3.0.0 Web-first）。开发分支：`codex/v3.1-conpty-reliability`。本次不发布版本，npm 版本仍为 `3.0.0`，node-pty 仍为 `1.1.0`，没有生产依赖或 lockfile 变更。

> 2026-10-13 合并审查补录：以下策略结论和测试记录描述原开发分支，早于 PR #118。整合 `main` 后，Windows `auto` 沿用 #118 的隔离 Host 原生能力探测：验证成功使用 native，失败才 fallback；显式 `native` 严格运行，显式 `stdio` 不启动原生探测。保留 #118 的换行处理、异步有界清理、Host 单一元数据写入和退出确认。审查另修复失联状态轮询按历史 PID 杀进程的问题，真实独立进程回归已先复现误杀再验证修复。原始 node-pty worker 自然退出问题仍是历史探针的已知限制，不能把该探针视为已通过；产品 Host 退出释放资源的路径另行验收。本次用户已授权创建 PR 并合并，不发布新版本。

## 结论与验收边界

当前保持 Windows Node 22/24 的 **auto → stdio-fallback** 策略。新增 **进程级、严格 native 实验**，失败不降级。macOS arm64 Node 22.23.3 / 24.17.0 已通过真实 Native PTY 集成测试。授权手动 CI 已验证 Windows 22/24 的产品 Native Host 交互与生命周期；原始库自然退出的 worker 清理仍失败，因此不改变默认策略。Ubuntu Native 已通过。最终 Runner 明细见验收补录。

原来的 Windows 限制并不能证明 Node 22 的 ABI 不兼容。已读取的历史 CI 日志证明 Session/目录清理失败；没有足够的原生崩溃栈定位到 ConPTY 或 Node 的单一根因。本次不会把修复普通 Session 生命周期问题描述成已修复 Windows 原生崩溃。

## 历史证据与故障分类

限制最早出现在 `500c752` / [PR #17](https://github.com/hogancv/coordinate-agents/pull/17)，最后一个子提交说明为 `fix: stabilize Windows Node 22 session cleanup`。

通过 `gh` 获取了以下日志：

- [32867508239](https://github.com/hogancv/coordinate-agents/actions/runs/32867508239)，`2e0f39a3`，尚未限制 Windows native：external Adapter 注册 fixture 最后 `rmSync` 出现 `EBUSY`。
- [32868040172](https://github.com/hogancv/coordinate-agents/actions/runs/32868040172)，`3aa8b166`，尚未限制 Windows native：同样的目录 `EBUSY`，另有 MCP persistent Session 布尔断言失败。普通 Session open/write/read/close 测试在同一 Windows Node 22 作业中通过。
- `4cb639cd` 随后引入 Windows Node 22+ fallback；历史成功不等于真实 ConPTY 验收。

| 分类 | 已有证据 / 目前处理 |
| --- | --- |
| 加载失败 | Windows 的 `require('node-pty')` 仅加载 JS 入口，`conpty.node` 在 `WindowsPtyAgent` 构造时才加载。独立探针显式检查绑定；缺少依赖的独立 checkout 测试证明 auto 可诊断 fallback、native 必须失败。历史日志未证明 ABI 加载失败。 |
| 初始化失败 | 公开的 `spawn` 同时完成 pseudo console 和进程创建，不能仅凭调用失败精确划分。已按 node-pty 的已知错误模板分类；原始探针把调用前阶段与绑定阶段分开。严格 Windows 实验拒绝不支持 ConPTY 的旧 build，防止 WinPTY 冒充 ConPTY。 |
| 启动失败 | 旧 Manager 超过 5 秒后仍返回 `starting`。未绑定 endpoint 的 Host fixture 已复现，并修复为明确失败与进程回收。稳定性窗口仍保留。 |
| 运行时错误 | 已注入真实 PTY 对象的 EPIPE 事件，验证错误监听、脱敏固定原因及退出；这是故障注入，不是 Windows 自然崩溃复现。正常 native keyboard / ANSI / UTF-8 / resize 使用真实 PTY。 |
| 关闭清理失败 | 历史 Windows EBUSY 已确认。新测试同时检查 CLI、后代、Host 存活和目录删除；macOS 已通过。忽略 SIGTERM 的未就绪 Host 在本机复现泄漏，现加入有界 SIGKILL 升级。Windows 句柄释放仍需真实 Runner 验收。 |
| 测试/启动误判 | 同一历史作业已有 native Session 测试通过，但后续 fixture 清理失败；清理异常可能覆盖实际断言。V3.0 的 Windows 独立 executable 文件、stdio close/drain、IPC init 消费顺序修复全部保留。新 raw probe 比较 attached 与 detached，避免仅测库 import 或 HTTP 200。 |

node-pty 1.1.0 的源码使用 N-API，并且 Windows exit callback 有 `Napi::Error::Fatal` / assert 路径。源码存在这些路径不能证明本次历史失败经过它们。没有证据要求升级 node-pty，因此暂不升级。

## 实现范围

| 文件 | 改动 |
| --- | --- |
| `pty-runtime.mjs` | 严格实验模式、固定的脱敏 failure stage/code、平台/Node 信息；native stream 错误处理；stdio UTF-8 decoder；UTF-8 安全尾部截断；截断事实；清理定时器；Windows tree kill 顺序。 |
| `session-host.mjs` | 将 backend、diagnostics、cols/rows 持久化；close 返回 active 时不提前关闭 endpoint，允许 Manager 升级清理。 |
| `session-manager.mjs` | 公共记录与 durable 记录保留诊断/尺寸；捕获 owned fork 的异常退出；启动超时明确失败并回收；未就绪 Host 的有界强制退出。旧 terminal 记录的 PID 不用于强制杀进程，避免 PID 复用伤及其他进程。 |
| `workspace-task-runtime.mjs` | 双终端的既有 Session view 添加 backend、diagnostics、cols/rows。任务/角色/协议保持不变。 |
| `session-service.mjs` | 无需修改；既有 forwarding 自动带出新增 Session 字段。 |
| `app.js` / `terminal-model.mjs` | 现有 footer 显示 Native PTY / stdio fallback / unknown；tooltip 提供脱敏诊断。Resize 仅在后端确认后记为同步，允许失败后相同尺寸重试，并修复 pending A→B→A。 |
| `.github/workflows/web-ci.yml` | 增加仅手动的 `native_pty_regression`，不扩大日常矩阵；保留 `windows_regression`。 |

诊断不复制 argv、env、Cookie、Token、登录凭据或会话内容。新增的失败原因均为固定代码。已有会话输出仍沿用既有脱敏和有界读取；没有新日志收集系统。

API 只添加字段，不改变现有操作或 schemaVersion。旧记录缺少这些字段时返回 null，UI 显示 unknown，不能据此声称 native。`nativeStatus` 区分 pending/running/unavailable/disabled/exited/failed，failure 包括 selection/load/initialization/spawn/runtime/exit/host/cleanup 阶段。

## 最小复现与运行方法

先在独立开发 checkout 执行 `npm ci`。探针只启动仓库内离线 Node fixture，不调用 Codex、Antigravity 或模型服务，不读取登录凭据。

```sh
# 原始 node-pty：显式绑定加载、TTY、输入、中文/ANSI、尺寸、自然退出和目录释放
node scripts/diagnose-native-pty.mjs
node scripts/diagnose-native-pty.mjs --attached

# 正常日常回归：真实 fallback、缺少绑定、错误注入、UTF-8、前端尺寸队列
npm run test:pty

# 必须是 native，绝不把 fallback 的通过记成 native 通过
npm run test:pty:native
```

`diagnose-native-pty.mjs` 在独立 worker 中调用原始 node-pty，阶段通过 IPC 回传，忽略普通 stdio 时仍可观察进度。20 秒监督超时、worker exit code/signal、最后阶段、CLI 存活和临时目录删除都进入 JSON。成功需要 worker **自然退出**，不能用 `process.exit(0)` 隐藏 native worker/handle 泄漏。绑定、TTY、尺寸和子进程事实都来自实际库或实际子进程，不来自产品 backend 字符串。

`scripts/pty-probes/terminal-probe.cjs` 在 raw mode 接收 Enter、Backspace、四个方向键、Ctrl+C，并计算大粘贴内容 SHA-256；使用 alternate screen、clear/home/color 重绘。子进程查询真实 console window size；Windows 不一定发送 Node resize event，因此还轮询 `getWindowSize()`。正常 `/exit` 和异常 `/crash` 分别退出 0 / 9；`/spawn` 创建后代，验证进程树释放。

实验开关：

```sh
# POSIX：仅本次启动进程及其 owned Host 使用
COORDINATE_AGENTS_PTY_MODE=native node bin/coordinate-agents.mjs web --root <offline-test-root>
```

```powershell
# PowerShell：当前进程环境；结束测试后清除
$env:COORDINATE_AGENTS_PTY_MODE = 'native'
node bin/coordinate-agents.mjs web --root <offline-test-root>
Remove-Item Env:COORDINATE_AGENTS_PTY_MODE
```

支持 `auto`（默认、保持平台政策）、`native`（Node >=20、严格无 fallback）、`stdio`（显式兼容后端）；拼写错误直接拒绝。不要写入用户 config，不设置系统环境变量。已发布的 V3.0.0 不含此开关；本开发分支不做任何发布操作。实验应使用独立 root，因为已有 Session 持久化与复用契约不会自动重启一个历史 fallback Session。

浏览器离线 QA 入口：`node scripts/pty-probes/native-browser-server.mjs`。它输出临时 URL/directory，建立独立用户配置、真实 executable 和双终端 fixture；SIGTERM/SIGINT 会关闭 owned Sessions 并删除临时目录。此入口仅供人工/ego-browser QA，不进入 npm 白名单或日常自动测试发现。

## 测试结果

本机：macOS arm64，OS release `27.0.0`。运行记录的摘要见 [v3.1-local-evidence.json](v3.1-local-evidence.json)。

- `npm ci`：通过，node-pty 1.1.0。
- `npm run check:web`：Web 57 项 + Shared 81 项通过。
- `npm run test:package`：7 项通过；真实 pack 后独立 consumer 安装生产依赖，运行离线双终端、角色提示、重连、重启、归档和清理。精简 files 白名单、Plugin 缺省与版本独立测试通过。
- `npm run test:legacy`：114 项通过。
- `npm run test:adapters`：33 项通过。
- `npm run test:pty:native`：macOS Node 22/24 各 6 项通过，零 skip；增加真实 Host uncaught error 的固定原因与资源释放测试。
- 真实 ego-browser/xterm：中文与绿色 ANSI 输出、alternate screen 重绘、Backspace/方向键、Ctrl+C、大粘贴 53,248 字节（子进程 SHA-256 验证）、尺寸 61×32 → 76×47（xterm/API/子进程一致）、浏览器刷新后原 Session IDs 保留，通过。这里只代表 macOS 浏览器。
- `npm run test:full`：最终安全清理实现 `c12933c` 的完整回归 408/408、零 skip，通过。后续只增加初始化前平台诊断和调整专用 fixture，另以 Session 9/9、Native 6/6 和 tarball 7/7 验证。初次扩大回归主动停止，不计为通过；另一次 407 项回归中，`obsolete benchmark, performance report, and test-support paths are absent from repository root` 失败（406/407），原因是临时新建了受既有布局约束禁止的根目录。改放 `scripts/pty-probes` 后保留原断言并通过，未放宽布局测试。Native 接受测试通过显式入口运行，支持 Node 18 的 full suite 不会误启动严格 native 实验或常驻 QA server。

Session 集成测试不仅检查输出，还检查真实 backend、三个标准流的 TTY、initial 120×30、resize 103×31、Session Manager 重建后的重连/复用、CLI 0/9、Host SIGKILL 后不能复用、强制关闭后代，以及 Host/CLI 退出后临时 root 可删除。普通 shell 子进程不是模型代理；退出测试不推断 HTTP 200 为成功。

## 手动 CI 与默认策略的闸门

授权后使用 CLI：

```sh
git push -u origin codex/v3.1-conpty-reliability
gh workflow run web-ci.yml --ref codex/v3.1-conpty-reliability -f native_pty_regression=true
```

矩阵只在手动输入为 true 时启用：Windows Node 22/24、Ubuntu Node 24、macOS Node 22 的严格 native；Windows Node 20 同时验证 native 与默认既有行为；Windows Node 18 验证稳定 fallback 与严格 native 拒绝。Node 18 没有“跳过失败的 native 断言”：它执行相应兼容策略断言，不属于 native 验收平台。

## 交付状态（首次本地验收）

**已完成且有测试证据**：历史回溯；两个独立启动方式的 raw MRP；严格模式和诊断贯穿 Session/Workspace；macOS Node 22/24 native 键盘/ANSI/UTF-8/resize/生命周期；本机 xterm QA；UTF-8、尺寸队列、Host 启动超时缺陷修复；四组要求的回归；独立 tarball 安装。

**已实现但缺少真实平台验证**：Windows 的真实浏览器/xterm 端到端显示与高输出长期压力；不同 Windows build、架构与第三方 TUI 的长期稳定性。Windows ConPTY Host 与 Ubuntu native 的 Runner 证据已取得，见补录。

**尚未完成**：定位历史 Windows 原生崩溃的确定根因（历史日志不足）；Windows Node 22/24 原始库资源释放的完整验收；长时间高输出/多次重启压力验收；将 Windows native 设为默认的决策。仅在真实平台结果充分且没有未解决故障时才考虑默认切换。当前不具备默认切换条件。

没有修改 npm/Plugin 版本、Tag、Release、dist-tag、日常 CI 矩阵或 Web-first 架构；没有创建 PR、合并 main、发布 npm、引入新协议/框架或恢复旧分发包。


## Runner 验收补录

第一轮 [38138450785](https://github.com/hogancv/coordinate-agents/actions/runs/38138450785)，提交 `aaaf21d`；第二轮 [38139026280](https://github.com/hogancv/coordinate-agents/actions/runs/38139026280)，提交 `726630a`。两轮 overall 均为 failure，未把失败平台当作通过。

Windows Node 22.23.3 / 24.21.0，build 10.0.26100、x64：原始探针真实 `conpty.node` 加载、TTY、输入、中文、ANSI、resize 和 CLI 0 退出全部到达 complete，但独立 worker 20 秒后仍活着，`timedOut:true`、`childAlive:false`、`failureStage:cleanup`。同一作业的产品 Native Session Host open/write/read/resize、重连、正常/异常退出、Host SIGKILL、后代关闭和目录删除通过。因此不能把问题归为“绑定加载失败”或“Node 22 无法创建 ConPTY”。Node 20.20.2 的 raw probe 出现同样资源清理故障，进一步排除了它仅特有于 Node 22/24 的假设。

node-pty 1.1.0 源码中的实际路径：`WindowsPtyAgent._$onProcessExit()` → `_flushDataAndCleanUp()` → `_cleanUpProcess()` 只销毁 outSocket；`ConoutConnection` 的 worker 在 `conoutSocketWorker.js` 中创建并监听一个 server，自然退出路径没有调用 worker dispose。这解释了“CLI 退出、TTY 交互正常、独立库进程仍留存”的复现。它是目前有源码和运行证据的资源释放障碍；它不能证明历史 PR #17 的每一个 EBUSY 或断言错误都来自同一原因。

第二轮的 `pty.kill()` after onExit 对比能令 raw worker 自然退出。然而 `WindowsPtyAgent.kill()` 会异步启动 console-list helper，再向返回的 PIDs 发信号；helper 超时 5 秒后退回原 PID，且没有已退出/身份校验。已退出 PID 可能被重用，因此该方法不能作为安全 disposal。实验选项和 CI 对比步骤已移除，不进入生产修复。当前 stable registry 的 node-pty 仍为 1.1.0，没有经证明可替代的稳定升级。

生产路径保留独立 Host：在 drained terminal fact 持久化之后退出 Host，释放其 ConPTY worker/文件句柄。Windows force-close 和 stream-failure 使用当前 owned process tree；不在 taskkill 完成后再调用会延迟按旧 PID 发信号的 node-pty kill。另有固定码 `uncaughtExceptionMonitor`，保留 Node 默认崩溃行为，不吞掉异常、不写原始栈或私密错误值。

Ubuntu Node 24 第一轮因为新增的 error observer 与 node-pty 对 POSIX 正常 EIO/errno 5 的处理不一致而失败。修正为同样的正常读端结束分类后，第二轮完整 Native suite、原有 Session 回归与 raw 两种启动方式都通过；没有通过跳过断言处理。Windows 18 fallback 和 macOS 22 Native 两轮均通过。Windows 20 的第二、三轮原有 Session 回归通过，但专门的 unready fixture 提前退出。仅限制 Unix SIGTERM hook 没有修复早退。随后 fixture 改为与真实 Host 一样 await init 消费后再断开 IPC，并打开专门 fixture 的诊断；聚焦作业验证通过。此前早退的具体机制没有足够日志，不能将其归为生产 ConPTY 故障。

最后一轮安全实现验证为 [38139799801](https://github.com/hogancv/coordinate-agents/actions/runs/38139799801)，提交 `c12933c`，overall 为 failure：Windows 22/24 各 5/6；Windows 20 为 4/6（raw 清理和专用 fixture），Ubuntu/macOS 各 6/6；原有 Session/前端回归每个平台 14/14。`Existing platform policy and Session regressions` 使用 `always()` 独立执行，确保 raw probe 失败不会阻止既有行为回归。Windows raw 的资源释放断言依然保留，预期会阻止 native 完整验收变绿。

独立 tarball 在 macOS Node 22/24 下还以 `COORDINATE_AGENTS_PTY_MODE=native` 强制模式完成了两组 7/7 安装验收，包括 Web 进程停止后新 Web 进程重连原 Session。它们不是 fallback 验收。Windows 上的独立 tarball native 安装未单独执行，不能外推为已验证。


最终产品验收与默认策略：

| 环境 | 实验中的实际 backend / 结果 | 默认 auto / 决策 |
| --- | --- | --- |
| Windows 22.23.3 | node-pty / 真实 ConPTY；产品 Host 5 项通过，raw 自然资源释放 1 项失败 | stdio-fallback；不切换 |
| Windows 24.21.0 | node-pty / 真实 ConPTY；产品 Host 5 项通过，raw 自然资源释放 1 项失败 | stdio-fallback；不切换 |
| Windows 20.20.2 | 原有 Session/前端回归 14/14；Native Host 实测通过；raw 清理失败。超时 fixture 后续聚焦验证通过 | 原有 node-pty 策略保留 |
| Windows 18 | fallback 与严格 native 拒绝的 9 项策略、14 项原有回归通过 | stdio-fallback 保留 |
| Ubuntu 24.21.0 | 原生 PTY 6/6 + 14/14，通过 | node-pty 保留 |
| macOS Node 22 | 原生 PTY 6/6 + 14/14，通过；本机 22/24 和 native tarball 另有证据 | node-pty 保留 |

聚焦 [38140458643](https://github.com/hogancv/coordinate-agents/actions/runs/38140458643)（`cc7ad99`）只筛选 Windows 20 unready Host 一项，1 项通过、其余 5 项未选中；它不是完整 Native acceptance，不能抵消整套矩阵的失败。新增 `host_startup_diagnostic=true` 入口仅运行该一项，避免为 fixture 诊断反复启动完整矩阵，不影响日常 CI。

最终代码审查无剩余重要安全问题。新增初始平台/Node 诊断在 Host 模块尚未加载时就落盘，故 native abort 早于 Runtime 构造也能保留环境事实；Host 的失败记录继承它。错误标识采用有限白名单，未知标识统一为 UNKNOWN，测试包含秘密样式的大写 error.code，避免用字母正则伪称安全脱敏。

**已完成且有证据**：受控真实 ConPTY 恢复入口、实际 backend 可观测性、真实交互与 Host 生命周期的 Windows 22/24 验证、macOS/Linux 回归、Windows 18/20 既有行为、Native tarball 本机独立安装、代码审查与技术报告。

**已实现但缺少真实平台验证**：Windows 浏览器实机/xterm 完整端到端重绘；Windows 独立 native tarball 安装；不同 build/ARM64、长时间高输出和大量重启压力。

**尚未完成**：原始 node-pty 自然退出后的完整 worker/handle disposal；历史 PR #17 原生崩溃的确定根因；Windows Node 22/24 完整 native gate 变绿与默认切换。当前**不具备**默认切换条件，开发成果保留在分支，不执行发布或合并。

最终补充说明：`e0a065f` 只补充 Runtime 尚未构造时的平台诊断。其后本机 Session 9/9、Native 6/6、tarball 7/7 通过；完整 408 项与跨平台全套矩阵的执行提交仍明确记录为 `c12933c`，不将其说成重新运行了每个后续诊断字段变更。

初始化前诊断的最终 Windows Node 20 聚焦验证：[38142027731](https://github.com/hogancv/coordinate-agents/actions/runs/38142027731)（`e0a065f`），通过。该作业仍只验证超时、环境诊断与 owned Host 回收，不计为完整 Native acceptance。
