---
title: 给本地 AI 编码工作台造一块可靠的终端地基
description: 从重复的启动横幅到 sideload ConPTY——Cordy 终端可靠性攻坚中的五桩连环案，以及一套可迁移的调试方法论
date: 2026-07-12
author: DS-Dev
---

这是一篇关于"终端到底有多难做对"的实战复盘。

Cordy 是一款本地优先的个人 AI 编码工作台。它把 Infinite Scroll 那种"终端工作区 + 键盘优先"的体验，和 Superset 那种"并行托管 Claude Code / Codex 等外部 CLI coding agent"的能力，融合成一个跑在 Windows / macOS / Linux 上、不上云、不做团队协作的桌面应用。它的核心对象模型很简单：

- `Project`：仓库级根对象，回答"这是哪份代码"；
- `Workspace`：project 之下的持久工作上下文，回答"我此刻在哪个上下文里干活"；
- `CodeTarget`：workspace 实际操作的代码副本，可能是 project 根，也可能是一个 Git worktree。

在这套模型里，AI **不是**内置的、调云端 LLM 的聊天框，而是用户亲手拉起、跑在 workspace 终端里的 CLI agent 进程。Cordy 只负责托管和监控它们，不碰它们的 provider、prompt、context 或 approval。

这个定位有一个直接后果：**终端的正确性，就是产品的地基。**

为了让并行 workspace 之间的切换做到"瞬间、像所有任务都活着"，Cordy 把 terminal / TUI / agent 的生命周期从渲染进程里彻底剥离，放进一个独立的 Rust runtime（跨平台的 tmux-equivalent + CLI agent supervisor）。渲染层只是一个可以随时销毁、重连的视图：renderer reload、workspace 切换、甚至 Windows 上"关到托盘时销毁整个渲染进程"的瘦壳架构，都不能杀掉后台的 PTY。

当会话的寿命长到能跨越窗口销毁、当同一个终端要被反复 attach / detach，任何一个字节级的错误都会被放大成用户能一眼看到的糊画面、重复行、跳字。下面是这条地基上先后浮现、又被逐一了结的五桩案子。

---

## 第一案：重复的启动横幅

### 症状与那条决定性线索

每次从托盘恢复窗口、或者 reload 之后，终端顶部的 shell 启动横幅（那种 "PowerShell … took 2126ms" 的启动耗时提示）会**多出来一份**。看上去像是 shell 被重启了一次。

但真正破案的，是那个耗时数字：两条横幅上写的都是 **`took 2126ms`**，一毫秒不差。

> **决定性线索**：如果 shell 真的重启了，第二次的启动耗时不可能和第一次逐字节相同。两条一模一样的 `2126ms` 只能说明——这不是一次新的 shell 启动，而是**同一段旧字节被重放了两遍**。问题不在进程，在我们对字节流的处理。

### 根因：把绘屏协议当成可重放日志

顺着"字节被重放"往下挖，根因浮出水面：**Windows 的 inbox ConPTY 在每次 PTY resize 之后，会重绘整个视口**。而当时 `terminal.attach` 的实现，是把 runtime 缓冲的历史字节，当作"历史日志"原样重放进一个全新的 xterm 里。

这是一个范畴错误。PTY 字节流**不是**面向"内容"的可重放日志，它是面向**某个特定网格尺寸**的绘屏协议——里面全是"把光标移到 (r,c)、清到行尾、重画这一屏"这类相对于当时网格的指令。一旦重放时的网格和录制时的网格不一致，原本"就地重绘"的一屏，就会变成**追加在下面的重复行**。启动横幅正是这样被复制出来的。

更糟的是，我们曾经试图在渲染层用"前缀匹配去重"来救场——检测重放进来的 ConPTY 视口重绘、把重复的部分丢掉。但这类启发式在真实时序下**必然失效**，有两个结构性原因：

- **8 KiB 的读块切分**：runtime 按 8 KiB 分块读 PTY，一次整屏重绘经常被拦腰切成两块，落单的尾巴就被当成新行重放了出来；
- **节流合帧**：为了不让 output flood 拖死 UI，输出会被节流合并，前缀被埋进合成帧的中段，任何"从帧首匹配前缀"的逻辑都对不上。

> **这一案最贵的一课**：这个 bug 类被打了**六次**启发式补丁，每一次都在下一个"分屏 / 窗格几何"特性上线时原样复发。当一个修复需要靠"猜字节流长什么样"来生效，它就永远差一个时序反例。**启发式跑在字节流上，迟早被时序击穿。**

### 解法：抄 tmux / VS Code 的作业——内嵌一个 headless VT 模拟器

真正的终端（tmux、VS Code 的 pty-host）从不重放原始历史。它们在后端**养一个屏幕状态**：把每个 PTY 字节喂给一个 headless 的 VT 模拟器，需要恢复时，直接把"当前这块屏幕的状态"序列化出来。重绘指令在模拟器里被**幂等地吸收**——resize 十次，屏幕还是那一屏，不会变成十屏。

Cordy 照这个架构重写了 attach（协议版本 `0.10.0`，attach-as-snapshot）：

- 每个 terminal session 内嵌一个 headless VT 模拟器（`avt`，2000 行回滚，实测约 **6.6 MB/session**），在 session 状态锁下逐字节喂入 PTY 输出，并与 PTY **同步 resize**——于是整屏重绘被幂等吸收；
- `terminal.attach` 不再返回"历史字节"，而是返回一个**带判别式的状态**：要么是 `delta`（缓冲环里的增量帧，仅当环完整覆盖了请求的 since 时——保住了"停靠式长回滚"重挂路径），要么是 `snapshot`（把模拟器在当前尺寸下的屏幕 + 回滚序列化出来，带 `snapshotSeq`）；
- 渲染层在 attach 往返期间**扣住**实时进来的 live 帧，拿到结果、应用完之后再按 seq 顺序放行——关掉了 bind-before-attach 窗口里的重复 / 乱序 / 丢帧三个竞态。

那些跑在渲染层的 ConPTY 重绘启发式（`isConptyViewportRepaint` / `dropSupersededViewportRepaints`）、主进程侧的镜像重放缓冲、四个从未被消费的 attach 遥测字段——**全部删掉**。

配套的还有一个"这个架构才做得出来"的回归测试：真 PowerShell，连做 3 次 ConPTY resize，断言快照把横幅**恰好渲染一次**——这是旧架构根本无法测试的接缝。

### 一个选型岔路：avt 还是 alacritty_terminal？

新架构立起来后，很快撞到 avt 的一堵墙：当一个全屏 TUI 占用**备用屏**（DEC 私有模式 `?1049h`）时拍快照，会丢掉**主缓冲的 scrollback**——stock 的 `avt::Vt` facade 只暴露"活动缓冲"的行，而备用屏模式下活动缓冲就是那块零回滚的 alt buffer，进 TUI 之前的 shell 历史够不着。

摆在面前有两条路：换掉 avt，或者给 avt 打补丁。我们真的去评估了换成 `alacritty_terminal`——读源码 + 写行为测试，结论是它**撞的是同一堵墙**（`grid` 和 `inactive_grid` 都是私有、没有访问器），而代价却更高：

| 维度                           | `avt` 0.18.0            | `alacritty_terminal`                    |
| ------------------------------ | ----------------------- | --------------------------------------- |
| alt-screen 主缓冲访问          | facade 私有（同样的墙） | `grid`/`inactive_grid` 私有（同样的墙） |
| 每 session 内存（2000 行回滚） | 约 **6.6 MB**           | 约 **9.9 MB**（多 ~50%）                |
| 序列化                         | 自带 `dump()`           | **无 `dump()` 等价物**，序列化器要重写  |
| 其它                           | —                       | blink / margin 状态回归                 |

一个佐证：连 Zed 都是通过**一个 tracked fork** 消费 alacritty_terminal 的——对这一类 crate，"vendor 加补丁"本就是正常的嵌入策略，不是歪门邪道。**决定：留在 avt。**

于是把 avt 0.18.0 vendored 进 `cordy-runtime/vendor/avt`，经 `[patch.crates-io]` 接入、并从 workspace `exclude`（这样 `cargo test --workspace` 不会去编它的测试和 dev-deps）。补丁是**纯 additive** 的五个访问器，每一处都带 `// CORDY-PATCH:` 标记，刻意做成可以直接提交上游的形状：`Terminal::primary_lines()` / `Vt::primary_lines()` / `Vt::active_buffer_type()`、从 crate root re-export `BufferType`、以及 `Line::wrapped()`（给软换行 reflow 的后续留的，暂未消费）。序列化器改从 `vt.primary_lines()` 取主缓冲 scrollback 前缀——已从 vendored 源码核实 `dump()` 永远先画主缓冲的 view（从不画 scrollback）再叠 alt 内容，所以这个切分是精确的，primary 模式下的输出和从前**逐字节一致**。`CORDY-PATCH.md` 记下了 base 版本、动机，以及移除条件（上游发布等价公开 API 后删掉 vendor 目录）。

---

## 第二案：中文 IME 之战

### "版本 pin 不是修复"

第二案是从一次**回归**开始的。此前 Cordy 为 xterm 的中文输入法打过一个补丁；后来某次升级 Electron 42 时，有人以"beta.220 已经修好了"为由，把那个 xterm IME 补丁、连同它的回归 harness flow 一起**删掉了**。

结果：agent TUI 里的中文 IME 从两条战线上同时崩了。

> **这一案的第一课**：把一个上游依赖 pin 到"据说修好了"的版本，**不等于**修复。没有一个能证明"修好了"的回归门禁在，任何"升级顺手删补丁"的操作都会悄悄把老 bug 放回来——而且是在你最没防备的时候。

### 合成一个 TSF forge，让缺陷现原形

要把这个 bug 钉死，得先能**稳定复现**它。真实的微软拼音很难在自动化里驱动，于是我们合成了一个 **TSF（Windows Text Services Framework）形状的事件流 forge**，模拟 IME 的提交序列。证据一下子就清楚了：

在**未打补丁**的构建上，用 forge 连续提交 3 次"临时"，xterm 实际发射了 **6 块**文本。3 → 6，字被翻倍了。

根因是两个独立缺陷：

- **(A) 双提交 / 吞字**：`_finalizeComposition` 让已提交的文本在隐藏 textarea 里**累积**（只在 Enter / Ctrl-C / blur 时才清），并靠 substring 切片去推导"这次提交了什么"；而在 TSF 形状的事件流下，`_inputEvent` 会把同一段文本**再同步发射一遍**。累积 + 二次发射，就是 3 变 6 的来源。
- **(B) 候选窗跳位**：xterm 在**每一帧 render** 都把 IME 的 textarea 和 `.composition-view` 重新锚定到真实的 buffer 光标格。而全屏 TUI（比如 Claude Code）会把硬件光标停在**非输入位置**、还高频重绘——于是候选窗落在错误的单元格上，并随着每一帧重绘满屏乱跳（实测锚点在 `80,16px` 与 `232,32px` 之间反复横跳）。

### 修复：单次发射 + 冻结锚点

补丁（`pnpm patch` 打在 `@xterm/xterm@6.1.0-beta.220` 的 `lib/xterm.mjs` 上）对症下两刀：

- composition 期间让 `_inputEvent` **让位**，`_finalizeComposition` 成为唯一发射者，每次 finalize 后清空 textarea 和记账——单次发射，3 就是 3；
- 在 `compositionstart` 时**冻结**候选窗的锚点格，composing 期间跳过逐帧重锚。哪怕 TUI 用 DECTCEM 把硬件光标藏了、或者把它停在漂移的位置，候选窗也稳稳锚在组合开始时的那一格。

这里有个诚实的边界要交代：真实的 Claude Code（v2.1.207）在输入提示符处**关掉了硬件光标**（DECTCEM off），还把它停在随重绘时机漂移的位置。所以我们的补丁能保证候选窗**位置稳定**，但它的绝对精度受制于 TUI 自己的光标纪律——这是外部程序的行为，不是我们能在终端层根治的。

### 把回归 flow 变成常驻门禁

补丁修好只是一半。这一案真正的产出，是**工程文化**上的：那个曾被删掉的 `ime-composition` harness flow 被**复活成一条常驻门禁**，里面就是那个 TSF forge + 锚点跳变场景——在未打补丁的构建上实测 **FAIL**（6 块发射 + 锚点跳动），打补丁后 **PASS**。

> **这一案的第二课**：**没有门禁的修复，等于没修。** 从今往后，任何人想"升级顺手删补丁"，都会先被这条门禁拦下来。合成 TSF forge ≠ 真实微软拼音，所以真人拼音手测仍然是最终验收步骤（本轮已于 2026-07-12 实测通过：长句连打干净、无吞字无重复）——但更重要的是，静默地把补丁丢掉这条路，被焊死了。

上游跟踪有一个后来才发现的曲折：这个缺陷最初被归因到 xtermjs/xterm.js#5887 与 #5894，但深挖后发现那两个其实是 macOS WKWebView 环境的 bug，与我们这个 Windows TSF 缺陷并非同源——**我们修的缺陷在上游至今没有对应的 issue**，把 forge 复现提交上游是一个待办。删补丁的时机：上游某个版本经 forge 验证确实修复了 finalize 缺陷、且真实拼音在 CLI-agent TUI 内手测通过之后（回归门禁永久保留）。

---

## 第三案：Codex 的纯黑输入条，与 ConPTY 的真相

### 症状：浅色主题下，一条突兀的黑输入条

Codex CLI 在 Cordy 的**浅色**主题下，底部输入条渲染成了深色底——在一片浅色里格外扎眼。

Codex 靠 **OSC 10/11**（查询终端前景 / 背景色）来决定输入条配色。所以问题变成：这个查询，为什么没得到正确答复？

### 双端插桩，锁定 inbox ConPTY

我们在**两端同时插桩**：子进程发出 OSC 11 查询的那一刻，和 Cordy 的 PTY master 收到回包的那一刻。实证结论很干脆——**Windows 的 inbox ConPTY 在查询到达 Cordy 的管线之前，就把它吞掉了**：既不转发、也不作答。Codex 探测超时，按深色兜底，而且在 Windows 上**不再重探测**。

作为对照组，同一个 Codex 跑在 **Windows Terminal** 里，OSC 查询能在 **1–2ms** 内被应答，Codex 就正确适配了。差别不在 Codex，在**谁在提供那个伪终端**。

我们先做了一个能立刻见效的旁路：让终端的 `COLORFGBG` 随主题下发（协议 `0.11.0`，`terminal.create` 携带 `appearance`，浅色 spawn `"0;15"`、深色 `"15;0"`，语义和真实终端一致——只影响新建 session，不影响运行中的）。这条通道让**读 `COLORFGBG`** 的 TUI（vim / neovim 的背景自动探测、delta 等）正确判定了背景。**但它对 Codex 无效**——Codex 只认 OSC 探测，不读 `COLORFGBG`。要真正根治，得让 OSC 查询能穿透。

### VS Code 的答案：直接捆一个现代 conpty

这一类问题，VS Code 早就趟过了。它的解法是**随应用捆绑 Windows Terminal 的 ConPTY 构建**（开关 `windowsUseConptyDll`，自 2026-05 起默认开启）；wezterm 也是同样的做法。而 `portable-pty` 恰好**优先** `LoadLibraryW("conpty.dll")`——Windows 的 DLL 搜索顺序会先查可执行文件自己的目录——只有在这个 dll（或它那三个无前缀导出）解析不到时，才回退到 kernel32 的 inbox ConPTY。

也就是说，对我们而言，这个修复**几乎是纯打包 + 可观测性**的事。

### 我们的移植：三重防线

我们把微软签名的**匹配对** `conpty.dll` + `OpenConsole.exe`（版本 `1.24.260512001`，取自 microsoft/terminal `v1.24.11321.0` release 附带的 `Microsoft.Windows.Console.ConPTY` nupkg，MIT 许可）vendored 到 `cordy-runtime/vendor/conpty/win-x64`，配了三重防线：

1. **portable-pty 原生 sideload**：dev 构建把这对文件复制到 debug/release 的 `cordy-runtime.exe` 旁；打包构建经 electron-builder 的 `extraResources` 一起 ship 到 `resources/runtime/win32/`——不需要改一行 runtime 代码，靠的就是 DLL 搜索顺序。
2. **微软签名的成对产物**：两个文件都经 `Get-AuthenticodeSignature` 验证为 Microsoft 签名、`Valid`、PE machine `0x8664`，SHA256 记录在 `PROVENANCE.md`。
3. **静默回退的 tripwire**：runtime 每进程一次在 `tracing::info` 记录活动后端（`sideloaded conpty.dll` vs `inbox`）。这一条是专门为一个尖角设的——`conpty.dll` 在、但 `OpenConsole.exe` 漏带时，portable-pty 会**静默**退回 inbox 的老坏行为，不报任何错。这条日志就是抓这种打包失误的探针。

这里有一条不能违反的**匹配对纪律**：`conpty.dll` 和 `OpenConsole.exe` 是一起构建的一对，`conpty.dll` 会把 `OpenConsole.exe` 作为控制台宿主拉起，版本错配会**崩溃**（wezterm 就踩过这个坑——新 dll 配旧 exe）。规则很硬：永远在同一个 commit 里、从同一个 nupkg 版本、成对替换两个文件。

端到端的回归门 `conpty-osc-roundtrip` 证明了整条链路：子进程发 OSC 11 查询 → xterm 用实时主题作答 → 回包抵达子进程 stdin——在 inbox ConPTY 下实测 **FAIL**（隐藏 dll 时子进程报 `CORDY_OSC11_MISS`），sideload 后 **PASS**。

### 意外的红利：第一案的始作俑者，从源头消失了

现代 conpty 还带来一个**没预料到的红利**：它发出的是一条更干净的 VT 流——**不再有整屏的 `ESC[2J ESC[H` 视口重绘**。

这正是第一案里那个"每次 resize 就整屏重绘"的元凶行为。换句话说，当初逼着我们去内嵌 VT 模拟器的那个源头，在 sideload 现代 conpty 之后，从根上消失了。VS Code 也正是把这条"更干净的流"归功于它最佳的 IME 表现。两案在此闭环。

> **收尾**：合成的 OSC 回环 ≠ 真实 Codex 的观感，所以最终仍以真人验收为准——2026-07-12 实测通过：浅色主题下 Codex 输入条随主题正确渲染。上游脉络：openai/codex#2020、#16411、#18942（Codex 侧的探测行为）；microsoft/terminal#3718（ConPTY 不透传查询的原始 issue）→ microsoft/terminal#17729（2024-08 合入的 OSC 查询透传修复，正是捆绑版携带的能力）。

---

## 第四组：两桩收尾小案

### 主动 workspace 恢复：问题在"写侧没有路径"

从托盘恢复（或 reload）时，Cordy 总是落到**最后创建**的那个 workspace，而不是用户离开时**正在用**的那个。

顺着直觉，很容易去怀疑"读侧被覆写"。但真正的根因更根本：**选择一个 workspace，这个动作从来就没有持久化路径。** `activeWorkspaceId` 只被生命周期操作（create / open / close）写过；点击 / 滚动激活只改了渲染层的 store，suspend 交接时也从没把它 flush 下去。所以快照里存的，永远是"最后创建的 id"。

> **这一案的一课**："**写侧没有路径**"比"读侧被覆写"更根本。当一个值总是错的，先别急着查读取逻辑——先问它到底有没有被**正确地写下来过**。

修复让"选择"搭上既有的 debounced layout flush，作为一个顶层 `active` 补丁（latest-wins 合并；存在 `active` 就把补丁标记为非空，于是 suspend 交接的 `flushNow` 会把它排空，快关竞态也被覆盖）。启动 hydration 用一个不触发 flush 的路径回放持久值，避免和并发的生命周期写抢；store 在写入时重新校验陈旧 id，让迟到的 flush 也没法把指针卡死。写侧回归测试在修复前失败、修复后通过；一个端到端 store 测试覆盖了原样上报的场景（3 个 workspace，陈旧 active = 最后一个，选第一个，冷 reload → 落到第一个）。

### WebGL 重取时缺了一次 refresh：光标消失了

切回一个终端窗格（workspace / tab 切回来）时，偶尔会看到一帧**陈旧**画面——最明显的是**光标 / 插入符不见了**——直到 TUI 下一次自我重绘才恢复。

两处缺口：`acquireWebglAddon` 加载了 addon 并做了 fit，却**从没 refresh**（它的 dispose 兄弟函数做了）——于是一个在隐藏期间 WebGL addon 被驱逐的窗格，重取时进了一块陈旧的 canvas；而 `isActive` 返回路径只重取 + focus，从不强制重绘，隐藏期间攒下的任何陈旧都活过了激活。

修复让重取路径**镜像** dispose 路径、做一次全视口 refresh；激活时通过一个 disposed-guarded 的 `refreshViewportRef` 调度一次 frame-settled 的重绘。这里也守住了不过度设计的边界：context-loss 的顺序经核实本就正确（renderer swap 先于 refresh），之前观察到的"无恢复"其实是 SwiftShader 的假象，所以没有加任何投机的机器。两个回归测试把行为钉住：驱逐后重取会重绘、变为活动会重绘。

> **附带一提**，同一波清理里还顺手了结了一条 dev 期噪音：`reader thread exiting` 的裸 WARN 每创建一个终端就响一次，读起来像"会话死了"。根因是 React StrictMode 把 bootstrap effect 挂了两遍，第一次挂载创建的 session 在 disposal 之后才 resolve、随即被杀——一个真实存在的短命 session。日志现在总是带 `session_id`，并按（退出原因、was_running）分级：已 Exited 会话的 EOF 和拆除属 debug，只有仍 Running 会话上的读错误才留在 warn。有意思的是，这条 ConPTY 构建**从不在 master 上投递 EOF**（实测：子进程退出 40s 后仍无 EOF——这正是 `spawn_exit_watcher` 存在的理由），所以被杀的短命 session 表现为 broken-pipe 读错误，而不是 EOF。

---

## 方法论：给同类构建者的可迁移经验

五桩案子背后，是一套反复起作用的方法。如果你也在给自己的工具造终端 / PTY 地基，这几条大概率能直接搬走：

**1. 差分诊断——把成熟工具当对照组。** 每一案都有一个"它在别处不这样"的参照：Superset 的 terminal daemon、VS Code 的 pty-host、Windows Terminal 的 OSC 应答。对照组不只是灵感来源，它是**假设检验的实验对照**——"同一个 Codex 在 Windows Terminal 里 1–2ms 就应答了"这一句，直接把嫌疑从 Codex 洗到了 inbox ConPTY。

**2. 证据先于修复——每个根因都得有一个实验数字。** 两条 `2126ms`、3 → 6 块、6.6 vs 9.9 MB、OSC 应答的 1–2ms 对照。这些数字不是装饰，它们是"根因确实是它"的证明。拿不到数字之前，不要动手改代码——先列出未知项，再找能证伪假设的最小反例。

**3. 根治 vs 补丁的判据。** 一个可操作的判据：**当一个修复要靠猜字节流的形状（前缀、长度、分块边界）才能生效，它就是补丁，迟早被时序击穿。** 第一案的六次启发式复发就是反面教材。根治是换一个不依赖时序假设的模型——后端养屏幕状态、拍快照，而不是在渲染层猜历史。

**4. 防回归三件套。** 修复交付时，配齐三样：**双向的 harness 证据**（未打补丁 FAIL、打补丁 PASS，两个方向都测——单测"打补丁能过"证明不了"没补丁会挂"）、**后端的 tripwire 日志**（每进程一次记录活动后端，抓静默回退）、**成对更新纪律**（匹配对文件、协议 + TS bindings 必须同 commit 动）。IME 那条被复活的门禁，就是"没有门禁的修复等于没修"的直接体现。

**5. 当一类 bug 在 VS Code / wezterm 里不存在时，先看它们捆绑了什么。** 这是最省时间的一条。Codex 的黑输入条、乃至第一案的整屏重绘，答案都藏在"VS Code / wezterm 随应用捆了一个现代 conpty"这件事里。成熟工具用**打包**解决的问题，往往不需要你去发明新算法——照着捆一份就好。

---

## 附：关键提交索引与上游引用

这轮攻坚落地的提交（cordy-desktop，monorepo 内含 `cordy-runtime/` Rust workspace）：

| 提交      | 主题                                                                     |
| --------- | ------------------------------------------------------------------------ |
| `48c321d` | `fix(runtime)`：reader-exit 日志带 session id，预期退出降级              |
| `8ea0059` | `fix(runtime)`：用 VT 模拟器快照取代 attach 重放（协议 0.10.0）          |
| `7976873` | `fix(runtime)`：alt-screen 快照保留主缓冲 scrollback（vendored avt）     |
| `5fb3365` | `fix(workspace)`：主动 workspace 选择跨 restore/reload 持久化            |
| `d9007ea` | `fix(terminal)`：恢复 xterm IME 补丁——单次 finalize + 冻结锚点           |
| `1d4033d` | `fix(terminal)`：session spawn 时下发主题感知的 COLORFGBG（协议 0.11.0） |
| `bca9eb6` | `fix(terminal)`：WebGL 重取与窗格激活时重绘视口                          |
| `fdab67f` | `feat(runtime)`：sideload Windows Terminal ConPTY 以透传 OSC 主题查询    |

上游引用（均可回溯到仓库内的 commit / `PROVENANCE.md` / 维护轨道）：

- xterm.js IME：我们修复的 TSF finalize 缺陷**在上游尚无对应 issue**（提交 forge 复现是待办）；早先误归因的 xtermjs/xterm.js **#5887**、**#5894** 实为 macOS WKWebView 问题，留此存照；
- Codex OSC 主题探测：openai/codex **#2020**、**#16411**、**#18942**；
- ConPTY OSC 透传：microsoft/terminal **#3718**（原始 issue）→ **#17729**（透传修复，2024-08）；VS Code 默认启用捆绑 conpty：microsoft/vscode **#315951**（2026-05）；捆绑的 conpty 取自 microsoft/terminal release **v1.24.11321.0** 的 `Microsoft.Windows.Console.ConPTY` nupkg（`1.24.260512001`，MIT）；
- xterm WebGL context-loss 回退（避免空白终端）：microsoft/vscode **#263159**；keep-alive 中间档提案被拒：microsoft/vscode **#113507**、**#47653**。

内部权威源：`cordy-runtime/vendor/avt/CORDY-PATCH.md`、`cordy-runtime/vendor/conpty/PROVENANCE.md`、`docs/Tracking/v1.x-maintenance-tracking.zh-CN.md`（已知限制表）、`docs/architecture/cordy-local-agent-runtime-design.zh-CN.md`、`docs/architecture/workspace-keepalive-design.zh-CN.md`。

---

终端很老，老到人们默认它"早就做对了"。但当你把它放进一个要跨窗口销毁存活、要托管别人家 CLI agent、要在浅色主题下正确应答一个 40 年历史的转义序列的本地工作台里，每一处"默认对"都会变成一桩要亲手侦破的案子。地基之所以是地基，正因为它得先扛住这些。
