diff --git a/AGENTS.md b/AGENTS.md index e1779324..aeb2647f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,11 @@ # Working on TermForge - Read README.md, docs/PLAN.md, docs/FEATURES.md and the active milestone before making architecture decisions. +- Follow docs/adr/0006-confirmed-project-direction.md: fully recreate WindTerm 2.7.0, with Windows 11 x64 as the first complete-version target and Windows/Linux/macOS as the final platform goal. Prioritize terminal foundations and performance before SSH/SFTP and the remaining features. +- Preserve Alacritty as a candidate, not an approved product-core selection. Keep investigating the original code and mechanisms; internal implementation need not be identical. +- Default to observed WindTerm 2.7.0 behavior when it conflicts with standards, third-party behavior or existing project policy. Unknown behavior requires investigation; important deviations and original-bug fixes require user confirmation. +- The user confirms scope changes, major architecture/dependency choices, acceptance criteria and important behavior differences. Agents may autonomously research, experiment, implement, test and fix within confirmed scope. Record proposal/confirmed status and confirmation evidence in ADRs and issues; merged code or closed issues do not imply user approval. +- Performance tolerances have no approved numeric values yet. Propose them from reliable baseline evidence and explicitly agree them with the user before using them for acceptance; keep existing diagnostic guards separate. - The project has a limited Windows local-shell prototype. Do not claim a daily-use release, Linux/macOS client, complete WindTerm parity or measured performance equivalence. - Do not lock the stack or add a large application skeleton before benchmark evidence supports it. - Every performance claim needs workload identity, environment, correctness checks, raw samples and limitations. @@ -17,5 +22,5 @@ - Keep build configurations separate in claims: default core/system backend, main candidate/system backend, and an explicit experimental backend need their own source and validation identity. Do not combine their independent successes into a joint acceptance result. - Source comments explain responsibilities, invariants and policy. Put changing status, batch names and future work in the owning issue; retain fixed experiment/protocol identities when they define behavior or evidence. A TODO must point to a current task rather than duplicate the roadmap. - Start only an issue whose real blockers are resolved. Record source observations separately from TermForge policies, implement independently, and validate the complete user-visible behavior. An unknown target behavior is an investigation item, not a passed comparison. -- Before expanding terminal modes and common TUI implementation in R2, complete ADR-0003's actual external-core comparison; candidate build and adaptation feasibility can proceed during R1. Before product backend selection, revisit ADR-0004. Do not silently turn experimental choices into permanent architecture. +- ADR-0003's historical external-core comparison remains evidence for continued candidate work, not user confirmation of a permanent route. Before final product core/backend/GUI selection, revisit ADR-0003/0004 under ADR-0006 and obtain the user's confirmation of a concrete evidence-backed proposal. Do not silently turn experimental choices into permanent architecture. - Documentation is primarily Chinese; technical identifiers and concise English summaries are welcome. diff --git a/CMakeLists.txt b/CMakeLists.txt index bed67f32..2454a7b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -163,6 +163,9 @@ if(WIN32) target_compile_definitions(shell_query_client PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN UNICODE _UNICODE) add_dependencies(termforge_shell shell_query_client) add_executable(shell_queue_probe experiments/shell/queue_probe.cpp) + add_executable(sender_fence_pipe_test experiments/shell/sender_fence_pipe_test.cpp) + termforge_warnings(sender_fence_pipe_test) + target_compile_definitions(sender_fence_pipe_test PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN) add_executable(shell_size_client experiments/shell/size_client.cpp) add_executable(shell_input_client experiments/shell/input_client.cpp) add_executable(shell_text_client experiments/shell/text_client.cpp) @@ -244,6 +247,16 @@ if(BUILD_TESTING) add_test(NAME candidate_title COMMAND candidate_title_test) set_tests_properties(candidate_title PROPERTIES TIMEOUT 35 ENVIRONMENT "TERMFORGE_TEST_ARTIFACTS=${PROJECT_BINARY_DIR}/test-artifacts") + if(TERMFORGE_CONPTY_BACKEND_DIR) + add_executable(candidate_reply_close_test experiments/shell/candidate_reply_close_test.cpp experiments/shell/shell_launch.cpp) + termforge_warnings(candidate_reply_close_test) + target_compile_definitions(candidate_reply_close_test PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN UNICODE _UNICODE) + target_link_libraries(candidate_reply_close_test PRIVATE termforge_core d2d1 dwrite gdi32 user32 imm32 ole32 oleaut32 uuid bcrypt) + termforge_candidate_shell(candidate_reply_close_test) + add_dependencies(candidate_reply_close_test termforge_shell_assets) + add_test(NAME candidate_reply_close COMMAND candidate_reply_close_test) + set_tests_properties(candidate_reply_close PROPERTIES TIMEOUT 30) + endif() endif() add_dependencies(runtime_observation_test termforge_shell_assets) add_test(NAME runtime_observation COMMAND runtime_observation_test) @@ -299,6 +312,10 @@ if(BUILD_TESTING) add_test(NAME paste COMMAND paste_test) add_test(NAME sender_fence COMMAND sender_fence_test) set_tests_properties(sender_fence PROPERTIES TIMEOUT 10) + if(WIN32) + add_test(NAME sender_fence_pipe COMMAND sender_fence_pipe_test) + set_tests_properties(sender_fence_pipe PROPERTIES TIMEOUT 10) + endif() add_test(NAME clipboard_policy COMMAND clipboard_policy_test) add_test(NAME keyboard COMMAND keyboard_test) add_test(NAME text_action_policy COMMAND text_action_policy_test) @@ -343,6 +360,7 @@ if(BUILD_TESTING) termforge_add_python_test(workflow_system_runner -m unittest research.ime.test_workflow_system_runner -v) set_tests_properties(workflow_system_runner PROPERTIES TIMEOUT 30) termforge_add_python_test(copy_error_frame -m unittest experiments.shell.test_copy_error_frame -v) + termforge_add_python_test(paste_fence_evidence -m unittest experiments.shell.test_paste_checks -v) termforge_add_python_test(resize_trace -m unittest experiments.shell.test_resize_trace -v) if(WIN32) termforge_add_python_test(research_guards -m unittest discover -s research/tests -v) @@ -366,7 +384,7 @@ endif() if(WIN32) foreach(target termforge_conpty_probe conpty_ctrl_c_probe conpty_margin_probe conpty_backend_probe real_app_capture termforge_shell workflow_fixture_test shell_queue_probe shell_paste_probe - runtime_observation_test candidate_title_test raw_console_receiver_test) + runtime_observation_test candidate_title_test candidate_reply_close_test raw_console_receiver_test) if(TARGET ${target}) termforge_use_conpty_backend(${target}) endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74da831d..6b156746 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ R1 基础文字交互已按限定合同验收,当前推进 R2 的联合配置、兼容性、输入与交付验收,并行处理性能和环境缺口。 -开始前阅读[总体计划](docs/PLAN.md)、[当前能力](docs/FEATURES.md)、[R2 交付路线与验收](docs/milestones/R2-local-terminal.md)和[任务跟踪](docs/TRACKING.md)。已验基础合同见 [R1](docs/milestones/R1-local-text.md);先核对 issue 的真实阻塞、目标构建配置和工作树,再选取范围明确的任务。PR 说明问题、最终行为、验证方法和限制,并更新对应能力行。大型依赖或技术栈变更需要记录 ADR 和实验依据。 +开始前阅读[总体计划](docs/PLAN.md)、[当前能力](docs/FEATURES.md)、[R2 交付路线与验收](docs/milestones/R2-local-terminal.md)和[任务跟踪](docs/TRACKING.md)。已验基础合同见 [R1](docs/milestones/R1-local-text.md);先核对 issue 的真实阻塞、目标构建配置和工作树,再选取范围明确的任务。PR 说明问题、最终行为、验证方法和限制,并更新对应能力行。按 [ADR-0006](docs/adr/0006-confirmed-project-direction.md),目标范围、主要技术路线、验收门槛和重要差异需用户确认;ADR 记录提议、证据和确认依据。研究、候选实验及已确认方向内的实现和修复可自主推进。 规划文件各维护一种事实:能力在 FEATURES、路标范围在 PLAN、交付顺序和验收在当前路标、实时进度和依赖在 GitHub。TRACKING 和 EVIDENCE 分别提供任务与来源导航。源码注释说明机制与不变量,TODO 指向责任 issue;不要把“这一批已完成”“下一波”等动态进度写进实现。固定实验、协议和历史结果的身份应保留,不机械改名。 @@ -10,6 +10,8 @@ R1 基础文字交互已按限定合同验收,当前推进 R2 的联合配置 提供具体应用版本/构建、输入 SHA-256、环境、配置、重复样本和正确性检查。说明时间端点是生产完成、消费完成还是画面呈现;不能混为一谈。报告退化和波动,不只挑最佳结果。 +关键指标逐项对标 WindTerm 2.7.0,首个完整版本环境为 Windows 11 x64。先测可靠基线,再提出容差并与用户明确;未经确认的数值只作提案,不用于宣布性能验收通过。终端行为默认原版优先;与规范、第三方核心或项目策略冲突时保留原版证据和差异,明确缺陷的修正由用户确认。 + ## 来源与隐私 遵守 [PROVENANCE](docs/PROVENANCE.md)。使用自生成输入与隔离测试配置,上传前检查用户名、路径、主机名、地址和凭据。第三方实现单独说明许可证;项目 MIT 许可不覆盖未获授权的第三方材料。 diff --git a/README.md b/README.md index 1d748c26..6dedbe96 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # TermForge -**独立、完整开源地复刻 WindTerm 2.7.0,以可复现证据验证功能、交互和性能。** +**以完整开源、可独立构建的实现复刻 WindTerm 2.7.0,优先完成 Windows 11 x64,最终覆盖 Windows、Linux、macOS。** -An independent, fully open-source terminal project targeting WindTerm 2.7.0 feature, interaction and performance parity. Not affiliated with WindTerm or KingToolbox. +An independent, fully open-source terminal project targeting WindTerm 2.7.0 feature, behavior, UI and performance parity. Windows 11 x64 first; Windows, Linux and macOS ultimately. Not affiliated with WindTerm or KingToolbox. -当前是有限 Windows 本地终端原型,**R1 基础文字交互已按限定合同验收,正在推进 R2 可靠本地终端与开发版**。主线已有候选核心路线、首批流式字素、有限多会话和配置恢复;候选核心与实验 ConPTY 后端的联合配置、完整 TUI/输入、真实不同 DPI、性能预算和开发包仍需闭合。现状见[能力矩阵](docs/FEATURES.md),接续顺序见 [R2 交付路线与验收](docs/milestones/R2-local-terminal.md)。SSH/SFTP 属于 R3,完整 UI 与 WindTerm 性能等价均未验收。`TermForge` 是暂名。 +当前是有限 Windows 本地终端原型,**R1 基础文字交互已按限定合同验收,正在推进 R2 可靠本地终端与开发版**。主线已有 Alacritty 候选实验、首批流式字素、有限多会话和配置恢复;候选核心与实验 ConPTY 后端的联合配置、完整 TUI/输入、真实不同 DPI、性能预算和开发包仍需闭合。现状见[能力矩阵](docs/FEATURES.md),接续顺序见 [R2 交付路线与验收](docs/milestones/R2-local-terminal.md)。SSH/SFTP 属于 R3,完整 UI 与 WindTerm 性能等价均未验收。`TermForge` 是暂名。 + +方向已经用户确认,依据见 [ADR-0006](docs/adr/0006-confirmed-project-direction.md):原版行为优先,内部结构不强求一致,但认真研究、借鉴原版;Alacritty 保留为候选,正式选型待证据和用户确认。关键性能指标分别验收,具体允许波动范围后续与用户明确。 ## 开始使用 @@ -24,16 +26,17 @@ An independent, fully open-source terminal project targeting WindTerm 2.7.0 feat |---|---| | 已经做了什么、还缺什么 | [当前能力与目标矩阵](docs/FEATURES.md) | | 最终目标与五个路标(从远到近) | [总体计划](docs/PLAN.md) | +| 哪些方向已确认、哪些决定仍需确认 | [方向与决策权限](docs/adr/0006-confirmed-project-direction.md) | | 官方发行平台、架构与声明的最低系统边界 | [2.7.0 平台范围台账](research/windterm-2.7.0-platform-scope.md) | | R2 分几步交付,每步还缺什么 | [R2 交付路线与验收](docs/milestones/R2-local-terminal.md) | | 已验收的基础文字交互合同 | [R1 基础文字交互](docs/milestones/R1-local-text.md) | | GitHub milestones、issues 和执行顺序 | [任务跟踪](docs/TRACKING.md) | | 结论依据、复现命令和全部文档 | [证据索引](docs/EVIDENCE.md) · [文档导航](docs/README.md) | -交付主线:**基础文字交互 → 可靠本地终端 → SSH/SFTP 远程日常版 → 高级功能/跨平台/性能 → 完整 2.7.0 复刻验收**。性能工程和定向逆向贯穿各路标,不把远期整块调查当作近期开发的前置。 +交付主线:**终端基础能力与性能 → SSH/SFTP 远程工作流 → Windows 高级功能与性能收敛 → Windows 11 x64 完整复刻验收 → Linux/macOS 扩展与全平台验收**。保留 R1–R5 编号,R5 分 Windows 和全平台两个出口;性能工程、原版研究和平台可行性调查持续推进。 ## 工程原则与来源 -功能先通过正确性验证;性能同时衡量吞吐、交互延迟、CPU、内存、稳定性和多会话公平性。不以丢数据、无界缓冲或积压呈现换取跑分,不预设某种语言或框架必然达标。优先复用许可明确的组件,以测量和可维护性决定产品架构。 +功能先通过正确性验证;性能同时衡量吞吐、交互延迟、CPU、内存、稳定性和多会话公平性。不以丢数据、无界缓冲或积压呈现换取跑分,不预设某种语言或框架必然达标。研究原版公开代码、运行行为与关键内部机制,并比较自主实现和许可明确的组件;主要技术路线、验收门槛和重要差异经用户确认后生效。 原创代码和文档采用 [MIT](LICENSE),第三方组件保留自身许可。实现不依赖 WindTerm 二进制或专有资源;观察、静态线索和推断分别记录。详见[来源规则](docs/PROVENANCE.md)与[性能规范](benchmarks/README.md)。 diff --git a/benchmarks/PROTOCOL.md b/benchmarks/PROTOCOL.md index f5a9f80c..02b85f3c 100644 --- a/benchmarks/PROTOCOL.md +++ b/benchmarks/PROTOCOL.md @@ -109,4 +109,4 @@ H01 搜索固定首部/中部/尾部命中及不存在目标各 10 次,冷/重 吞吐和资源报告中位数、最小/最大、IQR、所有样本与失败率;原始批次保留。异常样本只能按预先记录的外部故障理由排除,报告排除前后结果。校准运行不能混入正式样本。 -M0 不给出“追平”结论。完成基线后,按每个关键场景制定绝对/相对预算和可接受差异,冻结后用于 M1/M2 对照。吞吐提高但尾延迟或正确性退化不能视为整体达标;没有证据的组件成本(如黑盒布局/解析耗时)必须保持未知。 +本协议的历史 M0 基线阶段不给出“追平”结论;当前执行使用 R1–R5 路标。按 [ADR-0006](../docs/adr/0006-confirmed-project-direction.md),先建立 WindTerm 2.7.0 的可靠基线,提出每个关键场景的绝对/相对预算和允许波动范围,与用户明确并记录确认后冻结,用于后续验收。当前没有已批准的数值容差,采样噪声或自有回归护栏不能替代该确认,也不得根据待验结果反填门槛。首个完整版本对照环境为 Windows 11 x64,其他平台分别留证。吞吐提高但尾延迟或正确性退化不能视为整体达标;没有证据的组件成本(如黑盒布局/解析耗时)必须保持未知。 diff --git a/benchmarks/README.md b/benchmarks/README.md index 31e86d05..29a3edc0 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -1,5 +1,7 @@ # 性能基准 +当前优先建立 Windows 11 x64 上 WindTerm 2.7.0 的终端基础能力对照,吞吐、响应、CPU、内存及多会话分别留证。按 [ADR-0006](../docs/adr/0006-confirmed-project-direction.md),具体允许波动范围在可靠基线后与用户明确,当前没有已批准的数值容差;阶段性诊断与性能验收分开。 + > M0/M1/M2 是协议 v0.1 的历史阶段名称;场景和测量约束继续有效,当前交付路标与执行顺序以 [R1–R5 计划](../docs/PLAN.md)及 [#3/#4 跟踪](../docs/TRACKING.md)为准。 已定义 [M0 实验协议 v0.1](PROTOCOL.md)、[机器可读场景参数](cases.json) 与 [记录模板](run-template.json)。[工具使用说明](tools/README.md) 包含生成器、播放器、Windows 资源采样与自建窗口视觉诊断。已有[真实 WindTerm 诊断烟测](results/2026-10-21-windterm-smoke/README.md),#3 的实际终端端点适配及正式开销校准仍未完成,尚无正式 WindTerm 性能基线。执行以 PROTOCOL.md 为准,本页为导航摘要。 diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index ceb2cd6d..2614b33b 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -4,6 +4,8 @@ 每条证据仅支持其固定配置,不能把历史成功、局部模块、未合并分支拼成当前产品验收: +产品方向和确认依据见 [ADR-0006](adr/0006-confirmed-project-direction.md)。历史候选比较及限定合同保留事实身份,不构成用户批准正式选型或重要差异;关键性能容差仍待可靠基线后与用户明确。 + | 配置 | 证据定位 | |---|---| | main 默认:自研核心 + 系统 ConPTY | R1 限定总验收和有限 VT/Unicode 子集;默认构建不需要 Rust | @@ -51,11 +53,13 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | 原生 margin 逐字段合同与修复候选(#56 仍开放) | [合同、源码与路径](research/conpty-native-margin-path.md)、[独立 oracle](../experiments/conpty/margin_contract.py)、[固定运行](../experiments/conpty/results/2026-10-26-native-margin-path/validation.json) | 四阶段文字/格属性/光标独立判据;系统与官方原生 final 仍红,官方80列两次原5秒生命周期失败保留。共享单 margin 源码支持小 MIT fork 候选;patch 仅可应用,未编译、未运行、不计修复。 | | ConPTY 系统/固定官方后端的同源 margin A/B(#41、#47 未关闭) | [合同与第一方来源](research/conpty-margin-backends.md)、[固定 175B 与 16 case 结果](../experiments/conpty/results/2026-10-26-margin-backends/validation.json) | 相同 175B、两网格/两 mode/两 fresh:系统末端 margin 错,固定官方后端的末端正确;CHAR_INFO 仍错。禁用 VT 由窄屏反例拒绝;仅诊断可行性,不证明 PR #50 或 main 联合整合通过。 | | 实验输入 worker 持块与停止(#62) | [sender fence 合同](research/sender-fence.md)、[固定源码与实际字段](../experiments/shell/results/2026-10-26-sender-fence/validation.json) | `519a3f3` 的系统/官方各两进程、每次 stop/cancel/竞争,共 12 条实际 worker 记录,均 join/Job0;另有各一次原 focus 单测和各 2/2 纯 CTest。明确修正旧 never-read/output-first 不能保证 16KiB 的夹具假设,同 EXE 旧顺序仍红。实现仅在 PR #50 实验整合,main 未包含;probe 未链接候选 VT,纯 EOF 为模拟,#53 UI/3MB/768B/八会话及完整官方矩阵未验收。 | +| 实验 UI 未完成粘贴(#53) | [机制与边界](research/ui-paste-fence.md)、[旧固定记录](../experiments/shell/results/2026-10-26-ui-paste-fence/validation.json)、[组合修复](research/resize-replay-cost.md)、[最终同源码记录](../experiments/shell/results/2026-10-26-replay-cost-and-close/validation.json) | `eae4b6a` 单窗口两轮、真实EOF/无GO负例及两轮原八会话data-only通过;行键复用及关闭期间不回复分别有诊断/因果回归,原completed/stage45/退出错误保留。完整CTest54/56仍有Vim59、resize43、main7/ui52与shell180秒期限;未计真实CF/IME、完整后端或性能通过。 | | Native Vim 捕获目录隔离 | [原因与复验](research/vim-fixture-isolation.md)、[固定原件审计](../experiments/conpty/results/2026-10-26-vim-fixture-isolation/validation.json) | 实验分支同旧 EXE 的兄弟运行一绿一红;修正新目录隔离后完整模块9/9。仅闭合 #52 native 夹具原因;GUI stage59 仍开放,未知与受扰动探针分列。 | | 工作区几何与交互的固定记录 | [#38](https://github.com/JavaZeroo/termforge/issues/38)、[工作区几何与输出验证](../experiments/shell/results/2026-10-26-workspace-geometry/validation.json)、[resize 限定证据](../experiments/core/results/2026-10-26-resize-replay/validation.json) | 固定 27f376d 的客户区/布局/输出、默认/候选恢复及窄屏记录;其完整回归仍有 #36/#38 红项,不代表当前所有配置或 R2 通过。 | | 短窗口选区夹具与 hosted 四 shell 前置(#38) | [固定锚点回归](../experiments/shell/test_shell.py)、[准备入口](../tools/prepare_hosted_shells.py)、[夹具收尾合同](research/hosted-wsl-fixture-cleanup.md)、[云端结果与剩余阻塞](https://github.com/JavaZeroo/termforge/issues/38) | 短窗口保留相同选择断言;仅指定一次性 hosted VM 准备固定 Ubuntu。夹具强制收尾与产品自然退出分列,未知归属/回收仍失败。 | | 工作区恢复的运输完成端点(#38 夹具修正) | [限定合同](research/workspace-restore-transport-oracle.md)、[原失败及默认/候选验证](../experiments/shell/results/2026-10-26-workspace-transport-oracle/validation.json) | OSC 标题中的重复标记与 column-one CUP 误导旧端点;新判定保留完整 UTF-8、期限、负载及恢复断言。仅修夹具,不宣称修复产品恢复。 | | 工作区恢复的命令编辑前缀(#61) | [BS 因果反例及复验](../experiments/shell/results/2026-10-26-workspace-restore-bs/validation.json)、[运输完成合同](research/workspace-restore-transport-oracle.md) | 原失败中的 PowerShell 命令重绘 BS 污染当前记录;真实 LF 后仍须完整输出与新提示符,回显/拼接/非法封装拒绝保留。仅修完成 oracle,原 30/40 秒与产品断言不变。 | +| 工作区保存的空记录重绘(#72) | [原 122 B 因果回归与三配置结果](../experiments/shell/results/2026-10-27-workspace-empty-cr/validation.json)、[运输完成合同](research/workspace-restore-transport-oracle.md) | 两次 CI 失败记录逐字节相同;空且未编辑的记录在输出前允许裸 CR,仍须完整独立标记与新提示符。默认/system、候选/system、候选/官方各两轮保存/恢复/钳制共 18 次通过;最终完整回归、跳过和未闭合项独立记录。 | | 候选文字观测与回显端点(#38 夹具修正) | [同源码前后对照及反事实](../experiments/shell/results/2026-10-26-native-observation/validation.json)、[标准文本投影](../experiments/core/text_projection.hpp) | 观测改用标准文本投影,回显完成后才全选;原断言和负例保留。修正夹具时序/投影,不宣称产品选择策略已变或 R2 全绿。 | | 拒绝负例的读取前停止(#46 夹具修正) | [原记录、反事实及 fresh 验证](../experiments/shell/results/2026-10-26-receiver-stop/validation.json)、[公开空数据夹具](../experiments/shell/fixtures/rejected-order-before-read.json) | 旧 oracle 错误拒绝“首读前已停止”;同一空数据/零读原件红→绿,正常读取约束保留。限定夹具修正,不解释 #44 的退出。 | | 候选 GUI 合法空标题(#45) | [真实 GUI 前后对照](../experiments/shell/results/2026-10-26-empty-title/validation.json)、[转换入口](../experiments/presentation/render.hpp)、[持续回归](../experiments/shell/candidate_title_test.cpp) | 真实空 OSC 标题使旧 UTF 转换抛异常;修后两 fresh GUI 正常,非法非空反例保留。不回填旧 normal-1 未知异常的根因。 | @@ -67,7 +71,7 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 |---|---|---| | 构建与 CI 入口 | [贡献指南](../CONTRIBUTING.md)、[根 CMake](../CMakeLists.txt)、[CI workflow](../.github/workflows/build.yml)、[Actions 39930814167](https://github.com/JavaZeroo/termforge/actions/runs/39930814167) | 历史 2dc8d6d 的 Windows/Ubuntu 构建记录;当前入口由根 CMake/CI 定义。Linux 核心 CI 不代表 Linux 客户端。 | | 核心有限子集与恢复 | [核心支持合同](../experiments/core/README.md)、[Recovery 合同](../experiments/core/RECOVERY.md)、[resize 决策](adr/0005-physical-row-resize.md) | 自研有限 VT 子集、恢复与物理行裁剪合同;手写预期和拆包不能替代完整 VT/Unicode/TUI 认证。 | -| R2 核心路线决定 | [ADR-0003](adr/0003-core-reference-and-reuse.md)、[平台/维护/接口审查](research/2026-10-25-core-route-review.md) | ADR-0003 决定 R2 新能力复用 Term/Processor,同时保留传输/呈现/自研差分。历史平台/维护评估不等于当前客户端支持;路线决定不等于整项交付。 | +| R2 候选比较与历史路线决定 | [ADR-0003](adr/0003-core-reference-and-reuse.md)、[平台/维护/接口审查](research/2026-10-25-core-route-review.md) | 历史 Term/Processor 实验及传输/呈现/自研差分证据保留。按 ADR-0006,Alacritty 当前为待评估候选,正式核心选择由 #34 提交用户确认;历史平台/维护评估不等于客户端支持或产品选型批准。 | | 初始外部核心可行性(历史) | [固定版本/许可与复现入口](../experiments/candidates/alacritty/README.md)、[本地 Windows 摘要](../experiments/candidates/alacritty/results/2026-10-24-windows/summary.json)、[构建验证](../experiments/candidates/alacritty/results/2026-10-24-windows/validation.json)、[Windows/Ubuntu CI](https://github.com/JavaZeroo/termforge/actions/runs/40117504346) | 初始 crates.io 0.26.0 的固定来源、许可、构建及 headless 可行性;不是当前补丁版或产品路线验收。 | | 独立语义参考 | [差分入口](../experiments/differential/README.md)、[固定依赖](../experiments/differential/requirements.txt)、[2026-10-25 摘要](../experiments/differential/results/2026-10-25/summary.json) | 固定 pyte/wcwidth 的共同轨迹、精确裁决与负例;参考差异不算兼容通过,不用于性能比较。 | | 初始共同轨迹(历史) | [差分说明](../experiments/differential/README.md)、[逐项裁决](../experiments/differential/decisions.md)、[2026-10-25 Windows 摘要](../experiments/differential/results/2026-10-25-candidate-common-v3/summary.json) | 旧候选共同矩阵及精确参考差异;semantic_acceptance=false,保持原版本身份。 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index c65269f1..90d01e56 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -4,6 +4,8 @@ 比较目标固定为 **WindTerm 2.7.0**。功能范围依据[固定提交的官方 README](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/README.md)、[2.7.0 release](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0)及仓库内的发行物/运行时研究。官方声明说明目标包含什么,不证明已经在本机验证了其行为,更不证明 TermForge 已实现。各平台、版本限制和具体交互仍需逐项形成可重复场景。 +用户确认的目标和权限见 [ADR-0006](adr/0006-confirmed-project-direction.md):首个完整版本验收 Windows 11 x64,最终覆盖 Windows/Linux/macOS;原版实际行为优先。Alacritty 保留为候选,正式核心和 GUI/后端选型待确认;关键性能指标逐项达标,具体波动范围后续与用户明确。以下历史限定验收描述实现证据,不自动表示用户批准了最终策略或已达到原版一致性。 + 状态定义:**限定验收**指表中明确范围已有实现与独立回归;**实现中,未验收**指已有实现,但整项目标场景或整合验收未完成;**未实现**指尚无相应产品能力;**待取证**指目标行为或比较结论未知;**限定测量**仅说明已有指定范围的观测,不代表性能预算通过。每行的“路标”表示主要交付位置,不表示之前的路标已完成。 ## 构建配置与证据边界 @@ -11,8 +13,8 @@ | 配置 | 当前定位 | 可以引用的证据与限制 | |---|---|---| | 默认 main:自研核心 + 系统 ConPTY | 默认构建与已发布 Alpha 的基础路线,不需要 Rust | 已有 R1 限定验收;VT/Unicode 能力仍为有限子集,不能引用候选核心的字素或 TUI 结果 | -| 显式候选 main:`termforge.4` + 系统 ConPTY | `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON`;按 [ADR-0003](adr/0003-core-reference-and-reuse.md)推进 R2 新终端能力 | [固定来源](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)与 [#39 字素合同](research/streaming-graphemes.md);候选正确性通过不关闭系统 ConPTY 或 R2 父项 | -| 实验分支:`termforge.4` + 固定官方 ConPTY | [PR #50(Draft)](https://github.com/JavaZeroo/termforge/pull/50),联合源码 `3e5b96d` 已重建并[冻结真实基线](research/joint-conpty-baseline.md),尚未合并或通过产品整体验收 | [#47](https://github.com/JavaZeroo/termforge/issues/47) 跟踪后端整合;终端 margin 与原生状态分列。[resize 残尾 #51](https://github.com/JavaZeroo/termforge/issues/51)、[Console-width Vim #52](https://github.com/JavaZeroo/termforge/issues/52)、[未完成粘贴的确定性取消夹具 #53](https://github.com/JavaZeroo/termforge/issues/53)仍开放,现有 completed 观察不能证明部分发送取消通过或产品 Cancel 故障;worker gate [#62](https://github.com/JavaZeroo/termforge/issues/62) 已有[独立夹具修正与双后端实际 worker 验证](research/sender-fence.md),默认产品路径不启用 fence,也不替代 #53 的 UI 取消/关闭;工作区 [#63](https://github.com/JavaZeroo/termforge/issues/63) 回归仍开放 | +| 显式候选 main:`termforge.4` + 系统 ConPTY | `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON`;按 [ADR-0003](adr/0003-core-reference-and-reuse.md)保留为可评估实验,正式核心选型待用户确认 | [固定来源](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)与 [#39 字素合同](research/streaming-graphemes.md);候选正确性通过不关闭系统 ConPTY 或 R2 父项 | +| 实验分支:`termforge.4` + 固定官方 ConPTY | [PR #50(Draft)](https://github.com/JavaZeroo/termforge/pull/50),联合源码 `3e5b96d` 已重建并[冻结真实基线](research/joint-conpty-baseline.md),尚未合并或通过产品整体验收 | [#47](https://github.com/JavaZeroo/termforge/issues/47) 跟踪后端整合;终端 margin 与原生状态分列。[resize 残尾 #51](https://github.com/JavaZeroo/termforge/issues/51)、[Console-width Vim #52](https://github.com/JavaZeroo/termforge/issues/52)、[未完成粘贴 #53](https://github.com/JavaZeroo/termforge/issues/53)的[UI fence](research/ui-paste-fence.md)与[行键复用/关闭回复修复](research/resize-replay-cost.md)已在同源码验证单窗口两轮、真实EOF/无GO负例及两轮原八会话data-only;完整CTest仍54/56,resize43、Vim59、main7/ui52与shell180秒期限保留,不能据此验收整个后端;原completed与组合失败记录保留。worker gate [#62](https://github.com/JavaZeroo/termforge/issues/62) 已有[独立夹具修正与双后端实际 worker 验证](research/sender-fence.md),默认产品路径不启用 fence,也不替代 #53 的 UI 取消/关闭;工作区 [#63](https://github.com/JavaZeroo/termforge/issues/63) 回归仍开放 | 以下能力默认指已合并实现;某个配置的通过结果不能转用于另一个配置。R1 的真实系统 CF/拼音验收属于固定自研核心配置,不外推到候选 `.4` 或实验后端联合配置。 @@ -43,7 +45,7 @@ | 基础窗口操作 | **限定验收**:新建/切换/关闭/重命名、五条分隔线、面板显隐、底部焦点模式 | 完整会话动作/快捷键、命令面板、目标 Focus/SyncInput 行为 | R1 / R4 | | 工作区外观 | **实现中,未验收**:两种逻辑尺寸下部分外框/层级依据直接取证;UI 字体来自独立 OFL 来源,私有加载、实际 GDI face/em/行高与缺失资产失败有自动检查 | 不是像素等价;图标、主题/透明度、多语言及所有状态仍缺。Explorer/Filer/Outline 当前为空区域 | R2 / R3 / R4 | | DPI 与文字缩放 | **限定验收**:每会话 50%–200%,受控 96→144→192→96;[字体/显示合同](milestones/R2-font-display.md)明确 12×26 基准网格。两台真实 96-DPI 显示器间往返两次保持窗口、rename 焦点、隐藏标签和网格,[证据](../experiments/shell/results/2026-10-24-r2-monitor-font/validation.json)明确 `distinct_dpi=false`;v2 会话恢复另覆盖 100%/150%/75% zoom | 不同 DPI 的真实跨屏、实际呈现和真实 IME 跨屏仍未验收;恢复证据为同一 96-DPI 私有 desktop,目标字形/逐字形 fallback 未等价 | R2 | -| 配置与会话持久化 | **限定验收**:有界 v2、原子替换、v1 迁移、空会话与损坏/未知版本保护;[多会话恢复](../experiments/shell/results/2026-10-25-workspace-restore-multi/validation.json)覆盖 shell 类型/目录/标题/zoom/split/窗口,[迁移回退](../experiments/shell/results/2026-10-25-workspace-recovery-guards/validation.json)及[写失败与关闭策略](../experiments/shell/results/2026-10-25-workspace-write-failure/validation.json)均有真实验证。[#61 的合法编辑 BS 前缀](../experiments/shell/results/2026-10-26-workspace-restore-bs/validation.json)已在默认/候选 system 限定修正;目录/版本完成按[独立输出与新提示符合同](research/workspace-restore-transport-oracle.md)核对;只重建白名单 shell,不恢复原进程/历史;保存失败保留窗口,显式放弃保存才跳过 | 仅当前单工作区、正常窗口和白名单本地会话;偏好、分组/搜索、最大化/多窗口与文字标记恢复归后续。真实 DPI、呈现和 #38 等独立回归不因此视为通过,R2 出口未宣告完成 | R2 / R3 / R4 | +| 配置与会话持久化 | **限定验收**:有界 v2、原子替换、v1 迁移、空会话与损坏/未知版本保护;[多会话恢复](../experiments/shell/results/2026-10-25-workspace-restore-multi/validation.json)覆盖 shell 类型/目录/标题/zoom/split/窗口,[迁移回退](../experiments/shell/results/2026-10-25-workspace-recovery-guards/validation.json)及[写失败与关闭策略](../experiments/shell/results/2026-10-25-workspace-write-failure/validation.json)均有真实验证。[#61 的合法编辑 BS 前缀](../experiments/shell/results/2026-10-26-workspace-restore-bs/validation.json)与 [#72 的空记录 CR 重绘](../experiments/shell/results/2026-10-27-workspace-empty-cr/validation.json)已限定修正;后者同源码三配置共 18 次保存/恢复/钳制通过;目录/版本完成按[独立输出与新提示符合同](research/workspace-restore-transport-oracle.md)核对;只重建白名单 shell,不恢复原进程/历史;保存失败保留窗口,显式放弃保存才跳过 | 仅当前单工作区、正常窗口和白名单本地会话;偏好、分组/搜索、最大化/多窗口与文字标记恢复归后续。真实 DPI、呈现和 #38 等独立回归不因此视为通过,R2 出口未宣告完成 | R2 / R3 / R4 | | 文本增强与辅助面板 | **未实现** | 高亮/时间戳/折叠/outline、补全与 Fish 增强、命令历史、Sender/QuickBar、Free Type Mode(输入模式,行为待取证)、vim 本地/远程键位、锁屏、Sysmon | R4 | ## 远程、文件与协议 @@ -66,11 +68,11 @@ | 组合验收基础 | **限定验收**:[固定源码审计](../experiments/shell/results/2026-10-24-r1-workflow-foundation/validation.json)覆盖基础数据与快照;[受控夹具](../experiments/shell/WORKFLOW.md)分离编号日志、ConPTY传输、原始接收与阶段日志;固定输入前缀保持4096 UTF-16容量,旧IME模式仍要求空前缀 | 该基础证据不单独验收八会话或真实Copy→Paste→IME;组合出口见 #18 独立验收 | R1 | | 八会话文字工作流 | **限定验收**:[R1 固定源码整体验收](../experiments/shell/results/2026-10-24-r1-system-workflow/validation.json)覆盖同次 8 会话/拒绝第 9 个、后台有限洪流、历史/Find/实际选择与 CF 复制/确认粘贴/真实拼音、切换缩放、淘汰、取消关闭和重复回收;实际字节、目标隔离、动作与 Job/desktop 回收有独立审计 | 限定 Windows 固定拼音、本地 cmd 与有界负载;这是固定 R1 配置的组合验收,不能替代 R2 候选/新后端组合。实际呈现、常用 TUI、长期稳定及 UI/性能等价仍未验收 | R1 | | 活体资源与滚动端点 | **限定测量**:普通启动默认关闭;[R1 组合审计](../experiments/shell/results/2026-10-24-r1-system-workflow/validation.json)绑定实际接收、state 与同 revision 的 WM_PAINT/BitBlt/GdiFlush 提交,保留跨时钟校准与阶段资源,可离线重算 | launcher-tree 不等于 UI 归因;观察进程成本不等于增量采样成本,短命进程及退出 CPU 可能漏采。短驻留不证明长期无泄漏,`presented_done=null`,不构成性能通过 | R1;扩展 R4 / R5 | -| 自动正确性与构建 | **限定验收**:Windows shell/ConPTY/呈现及 Windows/Linux 核心 CI、差分/恢复/队列/生命周期场景 | 产品打包、干净机器使用、依赖升级、长期回归和更广平台矩阵;#40 的 CI 纯夹具超时,以及后续保存 stage2 #72 / 窄窗口选择 #73 的独立失败仍未闭合 | R2 / R4 | +| 自动正确性与构建 | **限定验收**:Windows shell/ConPTY/呈现及 Windows/Linux 核心 CI、差分/恢复/队列/生命周期场景 | 产品打包、干净机器使用、依赖升级、长期回归和更广平台矩阵;#40 的 CI 纯夹具超时,以及窄窗口选择 #73 等独立失败仍未闭合;#72 保存完成判据的限定修正不代替完整回归 | R2 / R4 | | Windows 产品交付 | **实现中,未验收**:首个 [v0.1.0-alpha.1 预发布](releases/v0.1.0-alpha.1.md)提供 Windows x64、静态 MSVC 运行库、附资源许可和构建身份的免安装 ZIP;未签名,已验环境限 Windows 11 本机与 Server 2025 CI,布局仍写入 LOCALAPPDATA | 这是 R1 体验包,尚非 R2 日常开发版;干净机器与更广平台矩阵、签名/安装、配置升级及持续运行仍待完成。Windows 10 未验收,Server 2022 已知资源门槛未过 | R2 / R3 | -| Linux/macOS 与其他架构 | **未实现**客户端;Linux 核心 CI 已有。[官方平台范围台账](../research/windterm-2.7.0-platform-scope.md)已固定 2.7.0 的 Windows x86/x64、macOS x64、Linux x64 资产及声明下限;这不是 TermForge 支持或实测结论 | 各平台 PTY/输入/字体/UI/安装与架构覆盖。Windows 7/8/10、macOS 10.13–12、Linux glibc 2.17+ 和未列出的 ARM/旧新系统等按台账分别验收或保留未知,不能由核心 CI 推断产品支持 | R4 / R5 | -| 吞吐、响应、内存与稳定性 | **实现中,未验收**:已有机制实验、连续 CPU 区间校准与合成回归;候选字素改动的受限诊断中 project/interactive 吞吐中位数约下降 16%/9%,由 [#54](https://github.com/JavaZeroo/termforge/issues/54) 跟踪 | 共享宿主、allocator 包装与缺少真实呈现/输入时延端点使该诊断不构成正式预算。仍需同负载/同正确性、无插桩对照、观测开销、长期/多会话公平性和可复核预算;与 WindTerm 的实际差距**待取证** | 持续工作;R4 / R5 收敛 | +| Linux/macOS 与其他架构 | **未实现**客户端;Linux 核心 CI 已有。[官方平台范围台账](../research/windterm-2.7.0-platform-scope.md)已固定 2.7.0 的 Windows x86/x64、macOS x64、Linux x64 资产及声明下限;这不是 TermForge 支持或实测结论 | 最终需真实 Linux/macOS 客户端;平台调查可提前进行,整合及完整验收排在 Windows 11 x64 完整版本之后。其他系统版本、最低版本和架构由用户后续明确;旧系统及 ARM 等保持调查记录,不自动承诺或排除,也不能由核心 CI 推断产品支持 | 持续调查;R5 全平台出口 | +| 吞吐、响应、内存与稳定性 | **实现中,未验收**:已有机制实验、连续 CPU 区间校准与合成回归;候选字素改动的受限诊断中 project/interactive 吞吐中位数约下降 16%/9%,由 [#54](https://github.com/JavaZeroo/termforge/issues/54) 跟踪 | 共享宿主、allocator 包装与缺少真实呈现/输入时延端点使该诊断不构成正式预算。仍需同负载/同正确性、无插桩对照、观测开销和长期/多会话公平性;可靠原版基线后提出逐项预算及允许波动范围,**由用户明确确认**。与 WindTerm 的实际差距**待取证** | 当前优先;贯穿 R1–R5 | | 大历史与动态内存压缩 | **未实现**产品能力;WindTerm 压缩属于官方性能声明,内部机制尚未证实 | 1/10/100 万行等明确规模下的内存/滚动/搜索/淘汰实验;根据瓶颈决定独立存储/压缩实现 | R4 | -| 完整目标验收 | **待取证**:已有固定版本和局部观察,没有完整行为/性能对照矩阵 | 全部目标行均有实现、验收与来源;剩余差异有明确裁决,支持平台可从源码复建;不依赖 WindTerm 二进制或专有资源 | R5 | +| 完整目标验收 | **待取证**:已有固定版本和局部观察,没有完整行为/性能对照矩阵 | 先在 Windows 11 x64 完整验收功能/行为/UI/性能及独立构建,再完成 Linux/macOS 全平台出口;重要差异和性能容差须有用户确认,Windows 通过不代表全平台完成;不依赖 WindTerm 二进制或专有资源 | R5 分平台出口 | -官方已知限制属于对照边界,不自动变成必须新增的功能或必须重现的缺陷。例如 tmux 嵌套及其与 Sender/SyncInput 的限制、Windows ZMODEM 非 ASCII 文件名问题,应记录实际差异。项目完整开源不要求复制 WindTerm 的私有实现、资源或错误;行为兼容与独立改进须明确标注。 +官方已知限制属于对照边界,默认以固定原版实际行为为准。例如 tmux 嵌套及其与 Sender/SyncInput 的限制、Windows ZMODEM 非 ASCII 文件名问题,应记录原版证据和差异;明确缺陷的修正或重要独立改进先提交用户确认。项目完整开源不要求内部实现一致,但不能以规范、第三方核心或现有项目策略自动豁免行为差距。 diff --git a/docs/PLAN.md b/docs/PLAN.md index 540721ef..bc582c29 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -4,6 +4,10 @@ 目标是以完整开源、可维护、可独立构建的实现,复刻官方 WindTerm 2.7.0 的可观察功能、操作行为、界面和性能。用户应能完成相同的工作,而不仅是看到相似布局。完整复刻不要求恢复私有源码;它要求固定目标矩阵中的每一项都具有独立实现和验收证据。 +用户已确认上述第一目标及 [ADR-0006](adr/0006-confirmed-project-direction.md) 的决策边界。首个完整版本以 **Windows 11 x64** 为验收环境,最终覆盖 Windows、Linux、macOS。其他系统版本、最低版本和架构列为待明确范围;官方历史支持声明是调查输入,不自动成为首个 Windows 版本的验收承诺。 + +默认以 WindTerm 2.7.0 的实际行为为准。与通用规范、第三方核心或现有项目策略冲突时,记录原版证据并按原版对齐;明确缺陷的修正方案及重要差异由用户确认。认真研究、借鉴原版公开代码与关键机制,内部结构不要求一致。 + 主要对照固定为官方 **2.7.0 Windows x64** 发行包,EXE SHA-256 为 `7b10531dccc7cd614ae559f36f1ef518e0c96335637431b4646354823b2804d0`。原安装目录中的不同 EXE 不混入该基准。其他平台、架构及版本的支持范围也必须列入目标矩阵并逐项处理,不能把 Windows 原型或 Linux 核心 CI 当成完整跨平台产品。 官方 `2.7.0` tag 指向提交 `a8336c1d1e3dd1a981500dae8ef9eb42066de95f`。目标清单的公开来源是[固定提交 README](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/README.md)与[2.7.0 发布说明](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0)。官方声明、发行物静态事实、实际运行观察和本项目实现结果分别记录,前一类不能替代后一类。 @@ -14,23 +18,23 @@ **R1 基础文字交互已按限定合同验收,当前推进 R2,尚未达到开发版出口。** 下一次产品交付是在明确支持的 Windows 环境中可靠运行本地 shell 和固定 TUI、恢复工作配置,并提供可复建的开发包。当前各构建配置已做什么见 [FEATURES](FEATURES.md);R2 的接续顺序、并行工作和出口检查只在 [R2 交付路线与验收](milestones/R2-local-terminal.md)维护,实时进度由 [GitHub 执行项](TRACKING.md)承接。 -[平台范围](../research/windterm-2.7.0-platform-scope.md)与 [ADR-0003](adr/0003-core-reference-and-reuse.md)已为 R2 确定候选核心的实施路线。最终产品核心/GUI/平台边界仍由 #34 使用实际工作流与性能证据定案;局部路线决定、原型或 Alpha 均不代表可日常替代 WindTerm。 +[平台范围](../research/windterm-2.7.0-platform-scope.md)与 [ADR-0003](adr/0003-core-reference-and-reuse.md)保存已有调查和候选实验。Alacritty 代码保留为候选,与自主实现路线继续比较;#20/#26 的历史完成不代表用户确认采用。最终核心/GUI/平台边界由 #34 提交实际工作流、WindTerm 对标、成本及维护取舍,用户确认后定案;原型或 Alpha 均不代表可日常替代 WindTerm。 “完整复刻”同时包含功能、行为、UI、性能和交付,不能用单一完成比例概括。全量目标台账必须提前建立;性能测量和定向逆向从现在持续进行,远期路标表示验收位置,而不是推迟开始的日期。 ## 五个路标 -按用户价值划分五个路标,下面从远到近展开;实际主线按 R1 → R2 → R3 → R4 → R5 推进。远期只拆功能域,进入执行前再形成可验收的小任务。 +保留五个路标编号,下面从远到近展开;主线优先对齐终端基础能力和性能,再推进 SSH/SFTP 及其余功能。R1 → R2 → R3 → R4 → R5 首先完成 Windows 11 x64;R5 内先验收 Windows 完整版本,再完成 Linux/macOS 扩展和全平台出口。远期只拆功能域,进入执行前再形成可验收的小任务。 | 路标 | 达成后的能力 | 主要验收边界 | |---|---|---| -| R5 完整 2.7.0 复刻验收 | 在固定目标范围内用 TermForge 完成 WindTerm 2.7.0 的工作 | 全功能、行为、UI、平台、性能和交付矩阵逐项闭合 | -| R4 高级工作流、多协议与跨平台 | 使用专业终端的高级文本、自动化、多协议和跨平台工作流 | 高级功能域完成整合;实际客户端跨平台验证;性能差距收敛 | +| R5 完整 2.7.0 复刻验收 | 先在 Windows 11 x64 完整替代,再扩展 Linux/macOS | Windows 和全平台分别验收全功能、行为、UI、性能及交付;Windows 通过不关闭全平台总项 | +| R4 高级工作流、多协议与性能收敛 | 在 Windows 完成高级文本、自动化、多协议和完整工作区 | Windows 高级功能整合及性能收敛;跨平台可行性调查可提前推进 | | R3 Windows 远程日常版 | 可靠使用 SSH、文件传输和多会话完成远程工作 | 认证与身份、传输完整性、异常恢复、会话管理及持续运行 | | R2 可靠本地终端与开发版 | 日常使用本地 shell 和常见 TUI,重启后恢复工作配置 | 终端兼容、复杂文字、实际 DPI、持久化、真实应用与可分发构建 | | R1 基础文字交互 | 浏览历史、选择复制、粘贴、搜索,并完成基础中文输入 | 文字与输入正确,多个会话隔离,持续输出时仍可操作 | -性能与自动调查贯穿所有路标。R3 的协议库与测试环境调查、R4 的平台可行性调查可以提前并行;这不改变各路标的验收条件,也不要求先完成整个远期功能域才交付近期能力。 +性能与原版研究贯穿所有路标,终端基础能力及性能是当前优先工作。R3 协议库、受控环境及 Linux/macOS 可行性调查可以提前并行;完整 Linux/macOS 客户端不作为 Windows 完整版本的前置,仍是最终必需交付。 ## 逆向工作的安排 @@ -41,7 +45,7 @@ | R5 | 全量目标是否遗漏;各平台和异常状态是否仍有未知差异 | 每个目标 ID 对应来源、实现、复验与差异裁决;完整矩阵审计 | | R4 | 高级文本/自动化、其他协议、tmux 整合如何交互;已测瓶颈对应什么机制 | 按功能域的状态与动作合同;有测量支持的历史存储/调度等机制实验 | | R3 | 连接/断线/重连、身份提示、文件操作与冲突反馈如何变化 | 受控本地服务下的会话状态转换、文件操作和故障场景 | -| R2 | TUI 切换、屏幕恢复、resize、设置保存及实际 DPI/字体如何表现 | 固定应用轨迹、前后状态与配置恢复合同;规范和独立参考裁决 | +| R2 | TUI 切换、屏幕恢复、resize、设置保存及实际 DPI/字体如何表现 | 固定应用轨迹、前后状态与配置恢复合同;原版行为优先,规范与独立参考用于解释差异 | | R1 | 历史跟随、选择单位、复制文本、粘贴确认、查找与焦点如何工作 | 两次无人值守运行的输入/动作/结果记录,形成 R1-01 合同 | 优先比较可观察行为;需要解释默认值、调用分支或性能瓶颈时再定位二进制路径。每个调查都有待回答问题和停止条件;产物进入对应任务和证据索引,不另建一条无法验收的“恢复全部私有源码”主线。 @@ -50,19 +54,21 @@ **用户结果:** 不再需要因目标矩阵中的功能、交互或性能缺口回到 WindTerm 2.7.0;源码、依赖、资源、测试和构建流程均可获得。 +分两个出口管理:**先完成 Windows 11 x64 的完整复刻验收**,允许独立交付该平台的完整版本;随后实现和验收 Linux/macOS 客户端及已确认的扩展平台矩阵。Windows 出口通过时,全平台总项仍保持未完成。后续系统版本/架构的范围须由用户明确,不从官方资产清单或当前 CI 环境自动推导。 + 工作块: - **功能与行为闭合。** 对固定目标清单逐项验收,包括不在首批日常路径中的设置、菜单、会话状态、错误恢复和边界行为。清理仍为“未知”“仅静态线索”“只有布局”的项目。 - **UI 与操作闭合。** 在固定状态、字体、DPI、窗口尺寸和输入上下文下比较布局与操作结果;覆盖弹窗、面板、工作区、快捷键和持久化后的状态。视觉误差与功能差异分别验收。 -- **性能与稳定性闭合。** 在共同功能配置下验证吞吐、交互响应、CPU、内存、历史规模、传输、多会话公平性和长时间运行;给出原始样本身份、正确性、误差与适用范围。 -- **平台与交付闭合。** 固定平台、系统版本、架构及安装/便携方式的支持矩阵;完成真实客户端验证、可复建发布、依赖来源和许可、升级/迁移与故障恢复。 +- **性能与稳定性闭合。** 在共同功能配置下逐项验证吞吐、交互/滚动/搜索响应、CPU、内存/长期增长、历史规模、传输和多会话公平性;给出原始样本身份、正确性、误差与适用范围。各指标门槛及允许波动范围由用户在可靠基线后确认;未确认或未测项不能计为完整复刻通过。 +- **平台与交付闭合。** 首先在 Windows 11 x64 核对完整矩阵、可复建发布、依赖来源和许可、升级/迁移与故障恢复。随后完成 Linux/macOS 的 PTY、输入法、字体、窗口、剪贴板、会话和打包整合及真实客户端验证;按用户确认的系统/架构范围逐平台复验,核心 CI 不代替客户端验收。 - **维护闭合。** 将已确认的参考行为、协议兼容性和性能预算变成持续回归,使后续开发不依赖重新阅读过程记录或个人环境。 **出口条件:** 每个必需矩阵项都有目标依据、实现入口、验证方法和结果,全部达到既定合同。记录了差异不等于消除了差异;缺少环境、未知端点或尚未验证的功能仍是未完成。不得通过减少测试、改变负载、移除功能或静默缩小平台范围宣布完整复刻。确需改变目标时,应明确形成新范围版本,不能用新范围证明原目标已经完成。 -## R4:高级工作流、多协议、跨平台与性能收敛 +## R4:Windows 高级工作流、多协议与性能收敛 -**用户结果:** 从日常远程终端扩展到 WindTerm 的完整专业工作流,各面板和动作背后都有真实功能。 +**用户结果:** 在 Windows 从日常远程终端扩展到 WindTerm 的完整专业工作流,各面板和动作背后都有真实功能。 工作块: @@ -71,10 +77,10 @@ - **高级文本与自动化。** 补齐高亮、折叠、标记/大纲、补全、命令历史、命令面板、Sender、QuickBar、Free Type Mode(用户可见的自由输入模式,具体行为待取证)、同步输入、自动执行及其他已确认的工作流;后台分析必须有预算并可取消。 - **完整工作区。** 完成剩余面板、对话框、模式、主题/颜色、快捷键上下文、多语言、日志及会话操作;分清参考版本支持、参考版本已知限制和本项目额外能力。 - **tmux 深度整合。** 按 2.7.0 的会话、窗口、面板与操作合同实现整合,而不是把“能在终端里运行 tmux”作为完成。参考版本已知不支持的组合不自动升级为本路标必须实现的增强。 -- **跨平台客户端。** 在已记录的产品架构决策上完成 Linux/macOS 等目标平台的真实应用;分别验证 PTY、输入法、字体、窗口、剪贴板、会话和打包。复用核心测试之外仍需平台集成测试。 +- **跨平台准备。** 提前调查 Linux/macOS 的 PTY、输入法、字体、窗口、剪贴板和打包适配,识别影响长期选型的障碍。完整客户端整合与验收在 R5 的 Windows 出口之后完成,不以放弃跨平台换取当前选型便利。 - **性能收敛。** 对大历史、后台搜索、多会话、慢连接、大量小文件及持续输出识别瓶颈,再比较数据布局、缓存、压缩、批处理和调度方案。采用能保持正确性与维护成本的方案,不以恢复 WindTerm 的私有内部结构为前提。 -**出口条件:** 每个声明支持的功能域和平台都有可重复集成环境、失败路径、资源回收及真实工作流验证;功能与性能差距集中到明确的最终验收清单。R5 做的是全矩阵验收和剩余缺口关闭,不重新开始发现基础范围。 +**出口条件:** Windows 的每个声明支持功能域都有可重复集成环境、失败路径、资源回收及真实工作流验证;功能与性能差距集中到明确的最终验收清单。R5 的 Windows 出口核对全矩阵并关闭剩余缺口;Linux/macOS 的整合和验收继续单列,不把其尚未完成视作 Windows 出口失败。 ## R3:Windows 远程日常版 @@ -100,7 +106,7 @@ | 工作包 | 具体事项 | 完成条件 | |---|---|---| -| 核心路线重审 | 依据 ADR-0003 实际构建许可明确的候选核心;用同一输入、状态检查点和真实应用比较继续自研/复用;对六类 pyte 已知差异结合规范与第二参考逐项裁决,保留持续 fuzz 和最小失败回归;记录维护成本与可测量性 | 在 R2 扩大终端模式与常用 TUI 实现投入前形成可重审结论,不能只比较文档、接口名或语言 | +| 核心路线重审 | 保留 ADR-0003 的实际候选及历史比较;继续对照原版行为、关键性能与维护成本,并研究原版公开代码和机制;规范与其他参考协助解释差异,原版未知项待取证 | 实验可继续;正式选型由 #34 提供具体方案并经用户确认,不能由历史 issue 关闭或候选存在自动确定 | | 备用屏与滚动区域 | 主/备用屏切换、光标/样式恢复、历史隔离;局部滚动、边界、原点/换行模式及保存恢复 | 分块输入、重复切换、区域外保持、resize 交叉用例及参考差分通过 | | TUI 与终端回复 | 按固定真实应用补查询回复、插删、输入/输出模式和必要样式;定义模式与 UI 选择/粘贴的关系 | 应用可进入、操作、退出并恢复主屏;维护明确应用/协议支持矩阵 | | resize 与历史语义 | 核对逻辑行、软硬换行、裁剪/重排、宽字符边界及历史关系;处理与备用屏/滚动区交叉 | 当前物理行 resize 的限制被明确保留或由经验证的新合同取代;状态与真实子进程尺寸一致 | @@ -108,10 +114,10 @@ | 字体与真实 DPI | 明确终端字体来源、网格度量、缺字处理;完成真实显示设置和跨显示器切换 | 实际环境中布局、焦点、编辑框、输入命中和隐藏标签正确;受控消息测试不冒充跨屏验收 | | 本地会话与工作区 | shell 类型、目录/参数、启动失败、退出;真正分屏、标签操作和焦点;核对参考动作上下文 | 固定本地 shell 与多会话真实任务通过;布局存在不等于面板业务完成 | | 持久化 | 会话配置、工作区布局、标题/比例等状态的明确保存范围;原子替换、版本迁移、损坏回退 | 新进程恢复与异常配置场景通过;不会把当前仅保存布局说成完整会话恢复 | -| 产品架构决策 | 根据已实现工作流与测量结果,确定产品 GUI/平台边界、核心与历史接口、传输/呈现隔离、依赖和维护策略 | 在扩大跨平台/远程产品整合前形成 ADR;现有 Win32/DirectWrite 实验不自动锁定最终架构,也不因规划而盲目重写 | +| 产品架构决策 | 根据已实现工作流、原版对标和测量结果,提出核心、GUI/平台、历史接口、传输/呈现隔离、依赖和维护方案 | 在扩大跨平台/远程产品整合前提交具体 ADR,经用户确认后定案;原型投入不锁定架构,性能容差另经用户明确 | | 开发版构建与交付 | 清理实验与产品入口、可分发构建、字体/依赖来源、使用说明、诊断与长时回归 | 干净机器可以构建和使用;功能声明对应可下载产物与真实测试;重复开关会话资源稳定 | -**前置与架构门槛:** [平台切片 #23](https://github.com/JavaZeroo/termforge/issues/23)和[核心关卡 #20](https://github.com/JavaZeroo/termforge/issues/20)已有明确结论,新终端能力按 [ADR-0003](adr/0003-core-reference-and-reuse.md)实施。[全量目标台账 #19](https://github.com/JavaZeroo/termforge/issues/19)的其余调查继续并行。#34 必须使用平台边界、真实工作流、依赖维护成本及性能证据重审产品架构;现有 Win32/DirectWrite 或实验后端不自动成为最终选择。 +**前置与架构门槛:** [平台切片 #23](https://github.com/JavaZeroo/termforge/issues/23)和[核心比较 #20](https://github.com/JavaZeroo/termforge/issues/20)的限定证据保留,候选实验可按 [ADR-0003](adr/0003-core-reference-and-reuse.md)继续。[全量目标台账 #19](https://github.com/JavaZeroo/termforge/issues/19)的调查并行推进。#34 按 [ADR-0006](adr/0006-confirmed-project-direction.md)重审核心与 GUI/后端,提交证据和取舍供用户确认;#20/#26 已关闭不替代该确认,也不要求重做已有实验。 **出口条件:** 固定本地真实任务、TUI、文字输入、配置恢复、真实 DPI 和干净环境发布全部通过;实际支持与已知差异进入功能矩阵。详细验收、证据边界与近期执行顺序见 [R2 出口检查表](milestones/R2-local-terminal.md)。仅普通命令可运行不等同于可靠本地终端。 @@ -138,7 +144,7 @@ 1. 先提出具体问题和合成场景。优先公开文档、许可明确的源码、发行物声明与运行时行为;需要时再做定向二进制调查。 2. 将观察转成输入、状态、动作、结果和边界的合同,分别标记事实、推断、未知与本项目设计选择。静态符号不能代替运行结果。 -3. 使用公开规范和许可明确的组件独立实现。协议、核心状态、历史、会话与呈现应可分别测试;不复制私有源码、资源或未授权资产。 +3. 研究、借鉴许可明确的原版源码和关键机制,选择独立实现或开源组件。协议、核心状态、历史、会话与呈现应可分别测试;不复制私有源码、资源或未授权资产。参考规范用于理解行为,冲突时仍以固定原版为默认依据,明确缺陷的修正另由用户确认。 4. 对相同场景比较状态、文字、几何、动作结果或协议数据,再进行视觉检查。没有参考运行证据的项仍标未知,不用本项目自身测试补成“参考匹配”。 5. 发布精简事实、原创探针与可公开结果;原始二进制、私有提取物及敏感数据留在受控本地。具体规则见 [PROVENANCE](PROVENANCE.md)。 @@ -159,7 +165,8 @@ - 分开接收、解析/状态、历史、布局/绘制、提交与实际呈现;未知端点保留未知。 - 同时覆盖吞吐、交互延迟、CPU、内存、长期增长和多会话公平性。一个数字不能代表整体达标。 - 自动 WindTerm 对照尚不能成立时,继续自有成本分层和回归,但不能据此宣布已经追平。 -- 在可靠基线和共同功能范围明确后确定逐场景预算,按测量瓶颈选择优化;不预设 C++、某个 GUI 框架或恢复同样内部结构就必然达到同样性能。 +- 在可靠原版基线和共同功能范围明确后提出逐场景预算及允许波动范围,与用户明确并记录确认后用于验收;当前没有批准任何数值容差。采样波动、既有诊断护栏和候选间回归数字不自动成为等价标准,不能根据待验结果反填门槛。 +- 按测量瓶颈选择优化;不预设 C++、某个 GUI 框架或恢复同样内部结构就必然达到同样性能。阶段版本如实报告差距,完整复刻必须满足用户确认的各项门槛。 ## 文档与任务保持一致 @@ -168,3 +175,4 @@ - [任务跟踪](TRACKING.md)连接当前 GitHub 任务与依赖;近期任务写清可验收结果,远期保持功能域级别。 - 当前里程碑文档保存详细执行合同;历史研究、评审和样本保留可追溯证据,不再重复充当当前待办清单。 - 路标完成时更新矩阵并核对出口条件;不得只按 issue 关闭数量或代码量报告完成比例。 +- ADR 与 issue 中区分提议、实验实现及用户已确认决定;记录确认日期和依据。目标范围、主要技术路线、验收门槛、重要差异的变更先按 ADR-0006 确认;本轮方向确认不追认全部旧设计或未定细节。 diff --git a/docs/PROVENANCE.md b/docs/PROVENANCE.md index 150127a5..c99fff4f 100644 --- a/docs/PROVENANCE.md +++ b/docs/PROVENANCE.md @@ -10,6 +10,12 @@ 研究报告分别标记观察、推断和未知;公开结果应删除个人路径、账户、主机信息。测试优先采用隔离配置及合成数据。 +## 核心代码来源的记录口径 + +按 [ADR-0006](adr/0006-confirmed-project-direction.md) 认真研究、借鉴原版公开代码及关键机制。模块的实现来源与行为依据分开记录:直接复用 WindTerm 公开源码须注明固定版本、许可和修改;根据原版运行观察或逆向研究形成的实现须链接具体行为/机制证据;按功能合同独立开发须注明合同;Alacritty 等其他开源实现及其修改单独记录上游来源,不能归为 WindTerm 代码。 + +“逆向研究指导的原创实现”和“直接复制反编译输出”是不同来源,不用同一个代码占比混算。无法确定来源时标为待核实,不能自动归入自研。后续核心代码统计须固定提交、构建配置、代码范围及计数方法,并将第三方代码、生成数据与测试/研究工具分列;模块受到原版行为启发不等于其中每行都是原版源码。 + ## 已使用的第三方数据 - `assets/fonts/RobotoMono-Regular.ttf`:从 Google Fonts 的 RobotoMono 官方仓库固定提交独立取得,未修改;适用 SIL Open Font License 1.1,完整版权与许可证随字体分发。字体源码、固定版本、原始链接、字节数和 SHA-256 见[字体来源](../assets/fonts/README.md)。只在 TermForge 进程内作为工作区 UI 字体加载;不是从 WindTerm 提取的资源,不受项目 MIT 许可证覆盖。 diff --git a/docs/README.md b/docs/README.md index ca254be8..96a4f996 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,7 @@ |---|---| | 已经能做什么,离目标差在哪里? | [功能与差异矩阵](FEATURES.md) | | 最终目标是什么,如何分阶段完成? | [五个路标,远到近](PLAN.md) | +| 用户确认了什么,哪些决策仍待确认? | [ADR-0006:方向与决策权限](adr/0006-confirmed-project-direction.md) | | R2 分几步交付,当前先做什么、怎样验收? | [R2 交付路线与验收](milestones/R2-local-terminal.md) | | 已验收的基础文字交互合同是什么? | [R1 基础文字交互](milestones/R1-local-text.md) | | 字体、网格和真实显示如何验收? | [R2 字体与显示合同](milestones/R2-font-display.md) | diff --git a/docs/STATUS-AND-BACKLOG.md b/docs/STATUS-AND-BACKLOG.md index c9572a6b..3ab47671 100644 --- a/docs/STATUS-AND-BACKLOG.md +++ b/docs/STATUS-AND-BACKLOG.md @@ -4,6 +4,7 @@ - 已实现能力、未实现功能及证据边界:[FEATURES](FEATURES.md)。 - 从完整 WindTerm 2.7.0 复刻倒推的五个路标:[PLAN](PLAN.md)。 +- Windows 11 x64 优先、原版行为优先、候选保留及确认权限:[ADR-0006](adr/0006-confirmed-project-direction.md)。 - 当前路标的交付顺序、剩余事项和验收:[R2 本地终端与开发版](milestones/R2-local-terminal.md)。 - 已验收的基础文字交互合同:[R1 基础文字交互](milestones/R1-local-text.md)。 - 正在等待实施的任务及真实状态:[TRACKING](TRACKING.md)。 diff --git a/docs/TRACKING.md b/docs/TRACKING.md index 6a03f833..10e239ea 100644 --- a/docs/TRACKING.md +++ b/docs/TRACKING.md @@ -2,12 +2,14 @@ 本页只导航到 GitHub 任务,不维护第二份进度表。能力与构建配置见 [FEATURES](FEATURES.md),R1–R5 范围见 [PLAN](PLAN.md),当前接续顺序与完成条件见 [R2 交付路线与验收](milestones/R2-local-terminal.md)。实时状态、原生父子/阻塞关系和验收记录以各 issue 为准。 +执行方向按 [ADR-0006](adr/0006-confirmed-project-direction.md):优先终端基础能力与性能,首个完整版本为 Windows 11 x64,随后完成 Linux/macOS;原版行为优先。#34 负责提交核心/GUI/后端正式选型供用户确认,#4/#54 提供基线和预算提案,性能容差经用户明确后生效。候选实验和既有关闭任务均不自动构成用户确认。 + ## 产品路标 | 路标(远到近) | GitHub milestone | 总项 | |---|---|---| -| R5 · WindTerm 2.7.0 完整复刻验收 | [milestone 6](https://github.com/JavaZeroo/termforge/milestone/6) | [完整目标验收](https://github.com/JavaZeroo/termforge/issues/10) | -| R4 · 高级功能、跨平台与性能收敛 | [milestone 5](https://github.com/JavaZeroo/termforge/milestone/5) | [高级工作流](https://github.com/JavaZeroo/termforge/issues/9) | +| R5 · 完整复刻验收:先 Windows,后全平台 | [milestone 6](https://github.com/JavaZeroo/termforge/milestone/6) | [Windows / 全平台两个出口](https://github.com/JavaZeroo/termforge/issues/10) | +| R4 · Windows 高级功能与性能收敛 | [milestone 5](https://github.com/JavaZeroo/termforge/milestone/5) | [高级工作流与跨平台准备](https://github.com/JavaZeroo/termforge/issues/9) | | R3 · SSH/SFTP 远程日常版 | [milestone 4](https://github.com/JavaZeroo/termforge/milestone/4) | [远程日常版](https://github.com/JavaZeroo/termforge/issues/8) | | R2 · 可靠本地终端与开发版 | [milestone 3](https://github.com/JavaZeroo/termforge/milestone/3) | [当前路标](https://github.com/JavaZeroo/termforge/issues/7) | | R1 · 基础文字交互 | [milestone 2](https://github.com/JavaZeroo/termforge/milestone/2) | [限定验收](https://github.com/JavaZeroo/termforge/issues/6) | @@ -28,7 +30,7 @@ | 查询回复、应用模式与真实 TUI | [固定应用矩阵 #28](https://github.com/JavaZeroo/termforge/issues/28) | #27;完成时核对 #29 的 resize 合同及目标配置 | | 完整 Unicode、IME 与选择/键盘 | [输入工作流 #30](https://github.com/JavaZeroo/termforge/issues/30) | #26/#39 已验;首批字素基础不替代最终配置的真实输入与同次八会话 | | 字体、真实不同 DPI 与跨屏 | [真实显示 #31](https://github.com/JavaZeroo/termforge/issues/31) | 当前为实际环境缺口;自动环境准备可并行,不反复运行相同 96-DPI 场景 | -| 字素吞吐成本定位与预算 | [受控性能 #54](https://github.com/JavaZeroo/termforge/issues/54) | 独立并行;固定负载/端点/重复方法与预算依据,再优化,不等 #34 最后才研究 | +| 字素吞吐成本定位与预算提案 | [受控性能 #54](https://github.com/JavaZeroo/termforge/issues/54) | 独立并行;固定负载/端点/重复方法并归因,预算提案需用户确认,不等 #34 最后才研究 | ## R2 可靠性与聚合关卡 @@ -43,7 +45,7 @@ | Windows 工作流可靠性 | [回归汇总 #38](https://github.com/JavaZeroo/termforge/issues/38) | #40/#44/#57/#72/#73;已闭合切片和旧失败保留在该总项,不再排入当前队列 | | 显式官方 ConPTY 整合 | [后端汇总 #47](https://github.com/JavaZeroo/termforge/issues/47) | #51/#52(GUI)/#53/#63;#62 worker 夹具与 #52 native 捕获已独立修正,#55 已固定实验联合基线,实验可选不等于产品默认或日常验收 | | 每缓冲区 margin 端到端合同 | [margin 汇总 #41](https://github.com/JavaZeroo/termforge/issues/41) | #47/#56;原生 API、transport 和末端分别验证 | -| 产品 GUI/平台与核心边界 | [架构定案 #34](https://github.com/JavaZeroo/termforge/issues/34) | #28–#31/#54,结合已验 #23/#26/#32/#33;维护正式 ADR | +| 产品核心、GUI/平台与呈现边界 | [选型提案与用户确认 #34](https://github.com/JavaZeroo/termforge/issues/34) | #28–#31/#54,结合 #23/#26/#32/#33 和原版对标证据;重审 ADR-0003/0004,记录用户确认 | | 最终 Windows 开发版 | [产物与持续运行 #35](https://github.com/JavaZeroo/termforge/issues/35) | #27–#34/#36/#38;在最终产物上执行完整合同后核对 #7 出口 | ## 已交付基础与历史入口 @@ -52,19 +54,19 @@ |---|---| | R1 基础文字与同次八会话限定验收 | [R1 总项 #6](https://github.com/JavaZeroo/termforge/issues/6)、[组合验收 #18](https://github.com/JavaZeroo/termforge/issues/18)、[合同](milestones/R1-local-text.md) | | Alpha 1 Windows x64 体验包 | [发布切片 #22](https://github.com/JavaZeroo/termforge/issues/22)、[发布说明](releases/v0.1.0-alpha.1.md)、[下载与验证](releases/v0.1.0-alpha.1-validation.json) | -| R2 平台与核心前置 | [平台 #23](https://github.com/JavaZeroo/termforge/issues/23)归属 [全量台账 #19](https://github.com/JavaZeroo/termforge/issues/19);[候选 #24](https://github.com/JavaZeroo/termforge/issues/24)、[差分 #25](https://github.com/JavaZeroo/termforge/issues/25)、[成本/路线 #26](https://github.com/JavaZeroo/termforge/issues/26)归属 [核心关卡 #20](https://github.com/JavaZeroo/termforge/issues/20) | +| R2 平台调查与候选比较证据 | [平台 #23](https://github.com/JavaZeroo/termforge/issues/23)归属 [全量台账 #19](https://github.com/JavaZeroo/termforge/issues/19);[候选 #24](https://github.com/JavaZeroo/termforge/issues/24)、[差分 #25](https://github.com/JavaZeroo/termforge/issues/25)、[成本/历史路线 #26](https://github.com/JavaZeroo/termforge/issues/26)归属 [历史比较 #20](https://github.com/JavaZeroo/termforge/issues/20);限定结项保留,正式选型待 #34 用户确认 | | 有限本地会话与配置恢复 | [本地会话 #32](https://github.com/JavaZeroo/termforge/issues/32)、[持久化 #33](https://github.com/JavaZeroo/termforge/issues/33),能力边界见 [FEATURES](FEATURES.md) | | 首批流式字素、宽度与 glyph | [字素合同 #39](https://github.com/JavaZeroo/termforge/issues/39)、[来源与合同](research/streaming-graphemes.md),完整输入由 #30 继续验收 | | 已闭合的回归/夹具与参考捕获 | #38 的原生子项及 [EVIDENCE](EVIDENCE.md)保留 #37/#42/#45/#46 等固定原因和证据,不再次当作待做功能 | | 旧 M0/G1、批次规格与审查 | [归档说明与迁移表](archive/README.md);历史记录不作为当前 TODO | -## 可提前推进的远期调查 +## 当前优先及可并行的调查 | 工作 | 责任任务与边界 | |---|---| | WindTerm 2.7.0 全量目标与平台台账 | [全量目标 #19](https://github.com/JavaZeroo/termforge/issues/19),R5 验收基础;#23 平台切片完成不代表其余行为已取证 | -| 自动观测、扰动校准与完整 Schema | [观测工具 #3](https://github.com/JavaZeroo/termforge/issues/3),R4 收敛;停止的手工批次不默认重启 | -| 正式 WindTerm 性能基线 | [受控对照 #4](https://github.com/JavaZeroo/termforge/issues/4),依赖 #3;不能用自有微基准替代 | +| 自动观测、扰动校准与完整 Schema | [观测工具 #3](https://github.com/JavaZeroo/termforge/issues/3),当前优先支持 Windows 本地终端对照,扩展覆盖持续至 R4;停止的手工批次不默认重启 | +| 正式 WindTerm 性能基线 | [受控对照 #4](https://github.com/JavaZeroo/termforge/issues/4),依赖 #3;优先 Windows 11 x64 本地终端,给用户提供容差提案,不能用自有微基准替代 | | Server 2022 ConPTY 稳态句柄 | [兼容性 #21](https://github.com/JavaZeroo/termforge/issues/21),R4;严格门槛与已知失败保留,不扩大支持声明 | 任务完成时更新责任 issue、能力行与证据索引。修改合同时同步规格和原生关系;记录未知或整理文档本身不解除产品出口。维护规则见 [issue tracker 约定](agents/issue-tracker.md)。 diff --git a/docs/adr/0001-evidence-before-stack.md b/docs/adr/0001-evidence-before-stack.md index d7456d4d..1f111a5f 100644 --- a/docs/adr/0001-evidence-before-stack.md +++ b/docs/adr/0001-evidence-before-stack.md @@ -1,11 +1,11 @@ # ADR-0001:性能证据先于技术栈 -- 状态:性能证据原则保持接受;先完成完整基线再开始原型的顺序由 [ADR-0002](0002-automation-first-investigation.md) 修订 +- 状态:证据原则由 [ADR-0006](0006-confirmed-project-direction.md) 确认;原型执行顺序由 [ADR-0002](0002-automation-first-investigation.md) 修订,具体栈与性能容差仍待用户确认 - 日期:2026-10-19 ## 背景 -项目需要接近 WindTerm 的速度和资源效率,同时易于维护。仅凭语言、GUI 框架或渲染 API 无法预测完整应用表现。早期考虑的现成客户端/Electron 路线尚未进行性能验证,不能作为已定方案。 +项目以 WindTerm 2.7.0 关键指标达到同等性能水平为目标,同时易于维护;允许波动范围在可靠基线后与用户明确。仅凭语言、GUI 框架或渲染 API 无法预测完整应用表现。早期考虑的现成客户端/Electron 路线尚未进行性能验证,不能作为已定方案。 ## 决策 @@ -15,4 +15,4 @@ ## 后果与重审 -短期交付以测量工具和规格为主,应用功能启动较晚,但避免围绕错误假设积累大量代码。M0/M1 结束后用新的 ADR 固定首个实现方案;重大负载变化或回归可以触发重审。 +本 ADR 最初要求先完整测量再启动应用;该顺序已由 ADR-0002 修订为自动调查与独立原型并行。当前按 R1–R5 推进,先对齐终端基础能力与性能;主要技术路线和性能门槛按 ADR-0006 提交证据供用户确认。历史 M0/M1 名称及早期候选保留原义,不构成现行选型。 diff --git a/docs/adr/0002-automation-first-investigation.md b/docs/adr/0002-automation-first-investigation.md index 1a6390ee..7f9bbfb3 100644 --- a/docs/adr/0002-automation-first-investigation.md +++ b/docs/adr/0002-automation-first-investigation.md @@ -2,7 +2,7 @@ > 本决策继续有效。当前能力见 [FEATURES](../FEATURES.md),任务见 [TRACKING](../TRACKING.md);旧工作包与工具安装过程见[历史版本](https://github.com/JavaZeroo/termforge/blob/c386494587656faa3d8621f7ddbf43a6aa678255/docs/adr/0002-automation-first-investigation.md)。本页只维护决策和执行约束。 -- 状态:接受 +- 状态:自动执行原则保持接受;重大决定的确认权限由 [ADR-0006](0006-confirmed-project-direction.md) 明确 - 日期:2026-10-22 - 修订 ADR-0001 的执行顺序,保留其性能证据与正确性要求。 @@ -16,7 +16,7 @@ 1. 默认交付可由代理执行的 CLI、脚本或测试。测试循环不要求用户新建会话、输入 y、复制结果或确认每一轮。仅确实需要用户作出的产品选择或外部权限决定才请求介入。 2. 不再扩展现有手工校准流程,也不把补齐该批次作为项目推进前提。旧记录保留,旧工具供可选人工诊断使用,不是默认入口。 -3. 调查与小型原型可以交替推进;正式性能选型和等价结论仍依赖充分证据。缺少完整 WindTerm 基线时,可以验证自己的正确性、各层成本与候选设计,预算明确暂定。原 M0/M1 名称已由当前 R1–R5 路线接续。 +3. 调查与小型原型可以交替推进;正式性能选型和等价结论仍依赖充分证据。缺少完整 WindTerm 基线时,可以验证自己的正确性、各层成本与候选设计;测试护栏或预算提案不等于验收标准。具体性能波动范围必须在可靠基线后与用户明确,主要技术路线也由用户确认。原 M0/M1 名称已由当前 R1–R5 路线接续。 4. 定向逆向优先使用无界面的分析与导出。初期研究范围为接收/线程协作、屏幕/历史存储、绘制调度;2026-10-23 路线整理后,范围由 [PLAN 的逆向安排](../PLAN.md#逆向工作的安排)和当前路标合同接续,包含文字操作、UI、会话及其他需要复刻的行为。每个问题输出事实、推断、未知、下一项独立实验,不以恢复整个私有源码为目标。 5. WindTerm 动态自动化先验证单次闭环,再建设批量运行。使用测试专用 profile 和受控 producer/服务端,每种场景单独验证建连、结果和回收。不得把“可以启动 EXE”当成“可以自动建立会话、验证画面并完成测试”;已验证范围从证据索引查阅。 6. 自动化入口不可用时,记录具体缺口并转向静态分析或自有原型;不回退为反复要求用户操作。遵守工具实际支持范围和操作规则,不通过换一套按键注入规避限制。 @@ -28,7 +28,7 @@ - 输出:机器可读原始数据、自动一致性检查、简短报告、明确测量端点。 - 自有原型可暴露解析完成、状态摘要、帧提交等测试接口,但这些端点不等价于 WindTerm 的黑盒端点;帧提交也不等价于显示完成。 - 需要可见桌面的渲染实验另列环境要求;没有画面证据时只完成可验证部分,不降低标准或把 producer_done 改名为 presented_done。 -- 在已授权的仓库工作中,代理完成代码、验证、脱敏报告、提交和推送;用户无需审批每个小步骤。外部必要权限仍按实际环境处理,不承诺自动绕过权限。 +- 在已授权的仓库工作中,代理完成代码、验证、脱敏报告、提交和推送;用户无需审批每个小步骤。目标范围、主要技术路线、验收门槛和重要行为差异依 ADR-0006 单独确认;自动执行授权不等于最终选型授权。外部必要权限仍按实际环境处理,不承诺自动绕过权限。 ## 结果与执行入口 diff --git a/docs/adr/0003-core-reference-and-reuse.md b/docs/adr/0003-core-reference-and-reuse.md index ded88186..e7e8afdc 100644 --- a/docs/adr/0003-core-reference-and-reuse.md +++ b/docs/adr/0003-core-reference-and-reuse.md @@ -1,11 +1,19 @@ # ADR-0003:差分参考与可重审的核心路线 -> 本文保留 C1/G1 等旧代号和当时的比较结果。2026-10-25 已由 [#20](https://github.com/JavaZeroo/termforge/issues/20)/[#26](https://github.com/JavaZeroo/termforge/issues/26) 完成 R2 重审;现状见 [FEATURES](../FEATURES.md)。 +> 本文保留 C1/G1 等旧代号和当时的比较结果。2026-10-25 由 [#20](https://github.com/JavaZeroo/termforge/issues/20)/[#26](https://github.com/JavaZeroo/termforge/issues/26) 完成的候选比较继续作为证据,正式选型按下方当前决定重新评估;能力现状见 [FEATURES](../FEATURES.md)。 -- 日期:2026-10-22;重审:2026-10-25 -- 状态:已接受 R2 复用候选核心的路线;产品整合尚未验收 +- 日期:2026-10-22;历史重审:2026-10-25;用户方向确认:2026-10-26 +- 状态:保留 Alacritty 作为候选;正式核心选型待用户确认。2026-10-25 的路线决定已被 [ADR-0006](0006-confirmed-project-direction.md) 的候选定位替代 - 对应任务:C1;R2-00 至 R2-03 +## 当前决定与确认边界 + +用户已确认保留 Alacritty 的代码和实验材料,重新按 WindTerm 2.7.0 的行为、关键性能及长期维护成本评估;不强求原版内部结构一致,但应认真研究、借鉴原版公开代码和机制。现有自研 C++ 核心与外部候选的证据均保留,允许继续可替换的实验和兼容修复。 + +正式核心选择由 [#34](https://github.com/JavaZeroo/termforge/issues/34) 提供具体方案、原版对标、性能及维护取舍,用户确认后生效。#20/#24–#26 已完成的是其限定研究和实验;关闭状态、过去的“Accepted”或已有适配器不能替代用户确认。性能允许波动范围同样待与用户明确,旧候选成本不等于 WindTerm 等价预算。 + +行为冲突默认以 WindTerm 2.7.0 实际行为为准;pyte、Alacritty、规范及现有项目合同是比较材料,未知原版行为继续取证。下面保留历史比较和当时的工程选择,供复验与重审,不作为强制采用某核心的当前指令。 + ## 问题 现有自研核心已经能回放有限文本、样式和历史,但单靠自己写预期容易把实现行为误当协议。若继续逐项扩展到完整终端,错误恢复、模式交互、Unicode、resize 和应用兼容性的维护成本会增长。完整开源允许复用开源核心;语言本身不构成性能证据。 @@ -24,14 +32,14 @@ 维护负担列为具体缺口,未虚构工时或使用代码行数衡量质量。 -## 决定 +## 初始实验决定(历史) 1. **保留现有核心用于受限实验和 G1 的有限集成,pyte 只作为测试依赖。** 不将这一临时安排升级为长期自研承诺,不发布性能等价声明。 2. 将可替换边界固定在“字节 feed → 可检查状态/变更 → 独立呈现”之间。参考适配不得依赖 GUI;自研核心、传输、呈现分别可测。当前 trace 是测试协议,未来产品状态接口仍需针对一致性/增量更新设计。 3. 共同子集每次变更执行差分;已知差异单独精确检查。错误恢复等不在共同子集的功能先定义行为,再增加负面与差分测试。参考结果与规范或产品策略冲突时保留复现并裁决,不机械追随任何一个实现。 4. C2 可在现有核心上补有界恢复,T1/V1 可继续独立推进。进入完整 TUI 功能扩张前,启动一个可构建的外部核心适配切片,再作产品路线决定。 -## 必须重审的关卡 +## 外部候选比较关卡(历史要求,结果继续复用) 当前关卡对应 R2:扩大终端模式与常用 TUI 的实现投入前,使用同一批字节轨迹至少比较自研和一个实际构建的外部候选;仅有库名或 README 不满足此条件。候选构建与共同子集/历史投影适配可以在 R1 期间并行进行,不要求先把自研核心补成完整终端,也不等待正式 WindTerm 基线。具体补齐: @@ -46,10 +54,12 @@ 当时完成固定参考、共同子集逐检查点/随机拆包差分、失败复现、策略差异记录与临时路线 ADR;产品级选择、外部原生候选与性能评估保留到下列 R2 重审。 -## R2 重审决定(2026-10-25) +## R2 重审决定(2026-10-25,正式选型效力已被替代) 完整的平台、维护、兼容修复与接口证据见 [R2 原生核心路线重审](../research/2026-10-25-core-route-review.md)。 +以下七项记录当时为何推进该实验,并保留其版本、接口和限制。当前继续哪些实验、最终保留/改造/替换哪条路线,遵循上方“当前决定与确认边界”,不再沿用下文的排他选型要求。 + 1. **R2 新终端能力复用 `alacritty_terminal` 的 `Term` / `Processor`,不再沿自研核心扩展完整 VT/TUI。** 原始比较版本为 0.26.0,后续整合的精确版本与兼容补丁以[来源台账](../../experiments/candidates/alacritty/PROVENANCE.md)为准。该次重审的备用屏、滚动区、回复、resize、真实应用与共同 feed 计量支持复用路线;这不是后续版本的性能预算通过证明,新增字素成本另由 [#54](https://github.com/JavaZeroo/termforge/issues/54)处理。 2. **只复用核心,不锁定候选 PTY/event loop。** 保留现有 ConPTY/传输、OutputQueue/InputQueue 背压、会话生命周期和 UI 所有权。终端 `PtyWrite` 经有界输入队列发送;其他事件按产品合同处理。 3. **保留自研 C++ 核心作为 R1 回退和独立差分参考。** 候选的显式原型接入不自动替换默认会话;替换须通过相同文字、选择、搜索、IME、多会话和资源回归。#27–#30 按选定路线实施,#34/#35 的产品与路标验收不变。 diff --git a/docs/adr/0004-presentation-backend.md b/docs/adr/0004-presentation-backend.md index 5505a4cd..082d874c 100644 --- a/docs/adr/0004-presentation-backend.md +++ b/docs/adr/0004-presentation-backend.md @@ -3,9 +3,11 @@ > A1/G1/G2 为历史代号;首个本地窗口已进入当前原型。后端与平台边界的重审归入 [R2 产品架构决策](../PLAN.md),须在扩大远程和跨平台产品整合前完成,不能以本 ADR 将实验后端永久定案。 - 日期:2026-10-23 -- 状态:临时接受;仅覆盖 Windows 的 A1/G1 实验,不锁定产品后端或跨平台栈。 +- 状态:历史实验选择保留;仅覆盖 Windows 的 A1/G1 实验,产品 GUI/呈现/后端选型待用户确认。 - 前置:[V1 窗口探针](../../experiments/presentation/README.md)、[ADR-0001](0001-evidence-before-stack.md)、[ADR-0002](0002-automation-first-investigation.md)。 +当前按 [ADR-0006](0006-confirmed-project-direction.md) 优先实现 Windows 11 x64 的完整复刻,最终覆盖 Windows/Linux/macOS。#34 必须给出原版行为、性能、跨平台可行性和维护取舍的具体方案,经用户确认后定案;保留本实验不等于用户选定 Win32、DirectWrite 或 Direct2D。具体性能容差尚待用户明确。 + ## 问题与候选 需要把现有终端状态可靠地画进窗口,并让接收、解析和画面可以独立验证。先在 GDI `DrawText` 与 DirectWrite layout + Direct2D 软件 DC target 两个小候选中比较正确性可观测性、资源复用和接入成本。不据语言标签、主观速度或一次 draw 时长决定产品栈。 diff --git a/docs/adr/0005-physical-row-resize.md b/docs/adr/0005-physical-row-resize.md index 31716f39..bdc2b748 100644 --- a/docs/adr/0005-physical-row-resize.md +++ b/docs/adr/0005-physical-row-resize.md @@ -2,11 +2,13 @@ > 本文保留 C5 当时的物理行合同与实现记录。窗口章节中的固定度量和尺寸约束已由后续 DPI/zoom 实现扩展;现行窗口行为见 [shell 说明](../../experiments/shell/README.md),完整现状见 [FEATURES](../FEATURES.md)。 -日期:2026-10-23。状态:C5 / G1 实验采用,产品布局定案前重审。 +日期:2026-10-23。状态:默认/候选实验行为记录保留;原版一致性与最终产品策略由 #29 / #34 重审,重要差异待用户确认。 + +按 [ADR-0006](0006-confirmed-project-direction.md),WindTerm 2.7.0 实际行为优先。本页的裁剪、历史回填、8192 字节重放边界等是现有实现合同;其自身回归通过不构成原版匹配,也不代表用户批准内容丢失等差异。#29 补原版对照并区分匹配、未知和差异;明确原版缺陷的修正或其他重要差异提交用户确认。历史样本和当前回归护栏保留。 ## 决策与范围 -`Screen::resize(rows, columns)` 接受 1–200 行、1–500 列。该阶段的自研解析器采用物理行裁剪,不推断逻辑段落,不重排软换行或硬换行;此默认策略继续保留。缩窄造成的信息丢失是公开的功能限制,不用作吞吐优化依据。候选的软换行元数据与独立补充合同见下文;完整 reflow、备用屏、滚动区交叉语义由 #29 验收。 +`Screen::resize(rows, columns)` 接受 1–200 行、1–500 列。该阶段的自研解析器采用物理行裁剪,不推断逻辑段落,不重排软换行或硬换行;这是默认构建的当前行为。缩窄造成的信息丢失是公开的功能限制,不用作吞吐优化依据,也不因此成为最终目标策略。候选的软换行元数据与独立补充合同见下文;原版对照、完整 reflow、备用屏、滚动区交叉语义由 #29 验收。 - 尺寸未变是严格 no-op,保留待换行和组合字符附着。 - 实际尺寸变化时,保留左侧单元格;右侧被截去的内容不会在增宽后恢复。宽字符若在右边界被截断,整个字符及组合标记清空为默认样式空格。可见行和历史行遵循同一列宽规则,完整保留的字符保持文字、组合标记和样式。 diff --git a/docs/adr/0006-confirmed-project-direction.md b/docs/adr/0006-confirmed-project-direction.md new file mode 100644 index 00000000..b1bb98e7 --- /dev/null +++ b/docs/adr/0006-confirmed-project-direction.md @@ -0,0 +1,36 @@ +# ADR-0006:完整复刻目标、Windows 优先与决策确认 + +- 状态:已确认 +- 日期:2026-10-26 +- 决策者:项目负责人(用户) +- 确认依据:本次 `$grill-me` 逐项讨论及最终确认“可以的 包括issues等等”。下表记录确认的含义;不将此前 ADR 的状态、提交作者或 issue 关闭解释为用户确认。 +- 范围与执行入口:[PLAN](../PLAN.md)、[FEATURES](../FEATURES.md)、[TRACKING](../TRACKING.md)。本文保存决策依据和权限边界,不维护第二份路线或进度表。 + +## 已确认的方向 + +| 决策 | 确认内容与依据 | +|---|---| +| 第一目标 | 用户明确“将windterm开源出来”是第一目标。交付完整开源、可独立构建的实现,完整复刻功能、操作行为、界面和性能。 | +| 内部实现 | 用户选择外部行为等价,不强求内部结构一比一,同时要求尽可能研究、借鉴原版代码与关键机制。公开源码复用、逆向研究指导的实现、自主开发及其他开源组件均可作为手段;具体来源仍遵守 [PROVENANCE](../PROVENANCE.md)。 | +| 固定版本 | 用户明确“固定windterm2.7.0”;版本升级另行确认,当前任务不追随最新发行版扩大目标。 | +| 平台与首个完整版本 | 用户明确“最终目标是全平台,但是首要目标是我们的windows”,并确认以 Windows 11 x64 为首个完整版本验收环境。最终覆盖 Windows、Linux、macOS;其他系统版本、最低版本和架构仍需单独明确。 | +| 推进顺序 | 先对齐终端基础能力与性能,再逐步接入 SSH、SFTP 和其余功能。平台可行性及独立调查可并行;Linux/macOS 的完整客户端验收不阻塞首个 Windows 完整版本。 | +| Alacritty | 用户确认“可以保留”。保留已有代码与证据,重新作为待评估候选;比较目标行为、性能及维护成本后,再确认正式核心选型。 | +| 性能标准 | 吞吐、交互/滚动/搜索延迟、CPU、内存/长期增长及多会话关键指标分别达到原版同等水平。用户明确“具体的波动范围需要后续与我明确”;当前没有获得任何数值容差的批准。 | +| 行为裁决 | 用户明确“原版优先”。与通用规范、第三方核心或既有项目策略冲突时,默认以固定原版的实际行为为准;明确缺陷的修正方案另由用户确认。未知目标行为继续调查,不计为匹配。 | +| 决策权限 | 目标范围、主要技术路线、验收门槛和重要差异由用户确认;已确认方向内的研究、逆向调查、候选实验、具体实现、测试和修复由代理自主推进。 | + +## 对现有路线的处理 + +- [ADR-0003](0003-core-reference-and-reuse.md) 的既有 Alacritty 实验及结果保留;2026-10-25 的路线选择不再作为当前正式选型依据。最终决定由 [#34](https://github.com/JavaZeroo/termforge/issues/34)提交证据和方案供用户确认。 +- [ADR-0004](0004-presentation-backend.md) 的 Win32/DirectWrite/Direct2D 路线继续作为实验实现,GUI、呈现及产品后端尚未正式选定。 +- [ADR-0005](0005-physical-row-resize.md)、字体/网格、字素、选择等现有合同仍用于准确描述实现和防止回归。它们与原版的匹配情况须另验;重要差异不因测试通过或写成合同而自动获准。 +- 已完成的构建、实验和限定验收保留原来源、结果和历史状态。当前问题由现有责任 issue 接续;不因方向重审重开全部已关闭任务,也不关闭真实缺口。 + +## 后续决定如何生效 + +重大决定先以“提议 / 待用户确认”记录,包含问题、候选及取舍、证据、未测项、影响的目标和后续工作。用户确认后才标为“已确认”,注明日期及确认依据;被替代的结论保留历史标记。实验实现状态与产品决策状态分开记录。 + +性能工作先建立可靠的原版基线,给出逐场景指标、重复样本、噪声和容差提案,再与用户明确并记录确认。确认后的门槛用于后续验收;不得根据待验结果反填一个刚好能通过的数值。既有诊断、测试超时和资源护栏保留原含义,不自动成为用户认可的 WindTerm 等价标准。 + +候选比较、证据准备和无依赖的实现工作可以自主继续;将候选正式采用、改变目标范围或接受重要行为差异前,提交具体可审查方案。源码可用、CI 通过、issue 关闭及 ADR 的旧“Accepted”文字均不替代这一步确认。 diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md index da10bdd8..f47e9c56 100644 --- a/docs/agents/issue-tracker.md +++ b/docs/agents/issue-tracker.md @@ -9,4 +9,8 @@ - 使用原生 sub-issue 和 blocked-by 关系;任务正文也列出依赖,便于源码环境阅读。修改依赖时同步正文和关系,不以大路标的顺序制造不必要串行。 - 开工先核对 issue、当前 milestone 和工作树,固定本次审查基点;提交验收结果后更新对应能力行并关闭任务。不要把完整产品工作长期塞进旧研究 issue。 - 改范围先改规格,再同步 issue;移动到归档的记录是当时的证据,不能作为当前待办。原始结果/已知失败不因文档整理而改写。 +- 按 [ADR-0006](../adr/0006-confirmed-project-direction.md) 区分调查/实验实现与正式产品决定。范围、主要技术路线、验收门槛和重要差异先写提案、证据及影响,由用户确认后生效;记录确认日期和依据。历史 ADR、代码合并及 issue 关闭不代替用户确认。 +- 方向变化时更新受影响总项和执行项的当前合同;已完成的限定实验可保留关闭状态,在正文标明当前决定由谁接续。新发现的原版差异保持未完成,不改写历史结果或批量重开已验证工作。 +- #10 分别跟踪 Windows 11 x64 完整出口和后续 Linux/macOS 全平台出口;后者未完成时总项保持开放。其他系统版本/架构范围待用户明确,不从官方资产清单自动推导。 +- #3/#4 的原版基线和 #54 的候选成本研究可以自主执行;数值预算及允许波动范围作为提案,与用户明确后才用于性能验收。提案/待确认不写作性能通过。 - 规划整理不自动算功能交付;功能开始实施时另行记录源码基点与验收。旧任务和新路标的映射见 [归档说明](../archive/README.md)。 diff --git a/docs/milestones/R1-local-text.md b/docs/milestones/R1-local-text.md index 207c0169..c73769c8 100644 --- a/docs/milestones/R1-local-text.md +++ b/docs/milestones/R1-local-text.md @@ -1,5 +1,7 @@ # R1:基础文字交互复刻 +> 下文保留已完成的限定实现合同。按 [ADR-0006](../adr/0006-confirmed-project-direction.md),当前完整复刻以原版行为为默认依据;未对照策略与重要差异继续在目标矩阵/责任任务中调查并提交用户确认,R1 关闭不表示全部行为或容量限制已获最终批准。 + **状态:已按本合同限定范围验收。** 固定源码`ce9b24d`的[整体验收](../../experiments/shell/results/2026-10-24-r1-system-workflow/validation.json)结合R1-01–07及同源码回归,验证了真实八会话完整文字工作流。当前不含常用TUI、全部输入法、真实跨屏、长期无泄漏或WindTerm性能等价;下一路标见[总体计划](../PLAN.md)。下文保留交付合同与验收边界。 目标:用户在两个区域的真实本地会话中完成“输出日志 → 回看历史 → 查找 → 选择复制 → 粘贴 → 中文输入 → 切换/缩放 → 退出”。这是一条能完整操作的工作流,不以增加菜单或空面板计完成。 diff --git a/docs/milestones/R2-font-display.md b/docs/milestones/R2-font-display.md index fffcaab9..e2968b2b 100644 --- a/docs/milestones/R2-font-display.md +++ b/docs/milestones/R2-font-display.md @@ -2,6 +2,8 @@ 对应 [#31](https://github.com/JavaZeroo/termforge/issues/31)。本合同区分工作区字体、终端绘制字体、固定网格和真实显示环境;受控消息、真实窗口跨屏、实际呈现是不同证据层级。复杂 Unicode/IME 由 #30 验收,产品后端决定由 #34 负责。 +本文记录当前实现与验证合同,不表示用户已确认最终字体/网格或与原版的差异。按 [ADR-0006](../adr/0006-confirmed-project-direction.md),#31 补固定原版的实际对照;12×26 网格、18 DIP、缩字适配及缺字体失败等策略保留明确来源与限制,重要差异提交用户确认。已有度量和回归结果继续有效于原限定范围。 + ## 字体来源与失败策略 - 工作区 UI 使用独立获取的 Roboto Mono Regular,9pt,SIL OFL 1.1;固定上游提交、原始字节和 SHA-256 见[字体来源](../../assets/fonts/README.md)。通过 `AddFontResourceExW(..., FR_PRIVATE, ...)` 仅对进程加载,缺失、注册失败或实际 `GetTextFaceW` 不是 Roboto Mono 时明确失败,不静默替换。`renderer_metrics` 独立复核实际 GDI face、em 高度与 `TEXTMETRIC.tmHeight`;96/120/144/192 DPI 的 em 为 12/15/18/24,行高为 16/20/24/32,不能把二者混为“字符高度”。 diff --git a/docs/milestones/R2-local-terminal.md b/docs/milestones/R2-local-terminal.md index 94e7e187..a1b81700 100644 --- a/docs/milestones/R2-local-terminal.md +++ b/docs/milestones/R2-local-terminal.md @@ -4,9 +4,11 @@ 用户结果:在明确支持的 Windows 环境中,用固定本地 shell、分页器和编辑器完成连续工作;复杂文字、输入和会话互相一致;重启后恢复已声明的配置;最终下载包能从公开源码复建。 +方向约束见 [ADR-0006](../adr/0006-confirmed-project-direction.md):优先对齐终端基础能力与性能,首个完整版本环境为 Windows 11 x64,R2 是其中的阶段性交付。原版行为优先;Alacritty 保留为候选,核心/GUI/后端正式方案及性能容差需用户确认。历史已验合同继续作为回归依据,原版未知和重要差异由相应任务取证及提交确认。 + ## 当前站位 -R2 的前期平台/核心路线、有限会话与恢复、首批流式字素已有各自限定交付。剩余工作集中在**联合配置、兼容与输入、环境、性能及最终产物**,不是重做这些前置,也不是只差修几个崩溃后打包。已发布 Alpha、主线显式候选、Draft 官方后端是不同配置,证据不得互借。 +R2 的前期平台调查/候选比较、有限会话与恢复、首批流式字素已有各自限定交付;这些研究不代表用户确认正式核心路线。剩余工作集中在**联合配置、原版对照、兼容与输入、环境、性能、用户选型确认及最终产物**。已有证据继续复用;已发布 Alpha、主线显式候选、Draft 官方后端是不同配置,证据不得互借。 联合源码 `3e5b96d` 的三配置[真实基线](../research/joint-conpty-baseline.md)已在实验分支冻结,包含原场景及完整回归的明确失败;近期转向这些失败的因果修复与复验。基线交付不选择默认后端或宣布兼容性通过,Draft #50 仍需通过原有回归。 @@ -18,7 +20,7 @@ R2 的前期平台/核心路线、有限会话与恢复、首批流式字素已 |---|---|---| | 1. 冻结联合基线 | 主线候选和显式官方后端来自同一份可重建源码;系统对照与联合配置各有实际结果 | [联合基线 #55](https://github.com/JavaZeroo/termforge/issues/55):固定 source/依赖/二进制/包/flags,验证 API 生命期、原 175B margin、Unicode/glyph、真实尺寸 ack、退出与回收;重放 #51–#53 原场景,分列 red、green 和未复现。默认路线与 #34 产品决定保持不变 | | 2. 闭合兼容性与输入 | 明确支持的 shell、分页器、编辑器、复杂文字与会话交互在联合配置中连续工作 | #47/#51–#53 处理后端;#41 处理原生 API/末端语义;#27/#29 核对模式与 resize,#28 验收固定 TUI/回复/输入字节,#30 验收完整 IME/选择/粘贴。实际不同 DPI 由 #31 并行提供 | -| 3. 定案产品架构与支持配置 | 能明确说明产品采用哪条核心/后端/呈现边界、为什么以及如何维护 | #34 使用 #23/#26、#28–#33 的真实结果与 #54 的受控成本,重审 ADR-0004;明确配置、平台下限、接口、并发/取消、依赖升级和暂不支持的范围,不静默缩小 R2 合同 | +| 3. 提交并确认产品架构与支持配置 | 提供可比较的核心/后端/GUI 方案、证据和取舍,经用户确认后定案 | #34 使用 #23/#26、#28–#33 的真实结果与 #54 的受控成本,重审 ADR-0003/0004;明确 Windows 11 x64 优先、未来平台障碍、接口、并发/取消和维护成本;范围及重要差异依 ADR-0006 确认 | | 4. 验收最终开发包 | 用户从公开源码复建或解压下载包,在声明环境中完成固定工作流 | #35 在最终产物上复验本页全部门槛、配置升级/恢复、错误/回收及预先冻结的持续运行与资源合同;#36/#38 的可靠性出口必须闭合。来源、许可、说明与产物身份一致后,才结项 #7 | 关键依赖如下;实线为硬前置,虚线为联合复验条件,省略已关闭前置和冗余边。完整原生关系在 GitHub。 @@ -42,8 +44,8 @@ flowchart LR ## 下一批的任务边界 -1. **在已冻结基线上逐项闭合后端回归。** #55 已交付含红的真实基线;接续 #51–#53 与 #63 的独立失败。#62 的[真实 writer 持块、取消与停止夹具](../research/sender-fence.md)不降低原 4MiB/16KiB/期限,下一步把默认关闭的 fence 接到 #53 原 UI/3MB/八会话入口;这不代表这些 UI 路径已通过。#51–#53 可以用原冻结输入独立定位,最终修复验收须在联合配置重建;一次未复现不自动构成因果修复。 -2. **并行处理平台语义与性能。** [原生语义处理路径 #56](https://github.com/JavaZeroo/termforge/issues/56)先明确 #41 原合同需要什么、哪些路径可支持;保留必要的上游或独立实现工作,不用“平台不同”豁免缺口。#54 现在就冻结重复方法、完成端点和预算决定程序,再定位吞吐成本;不等到 #34 才补性能。 +1. **在已冻结基线上逐项闭合后端回归。** #55 已交付含红的真实基线;接续 #51–#53 与 #63 的独立失败。#62 的[真实 writer 持块、取消与停止夹具](../research/sender-fence.md)不降低原 4MiB/16KiB/期限,#53 已接入默认关闭的 fence,[单窗口 UI/3MB/768B 与控制故障切片](../research/ui-paste-fence.md)及[同源码两轮原八会话data-only](../research/resize-replay-cost.md)已通过;完整官方回归仍保留resize/Vim/工作区失败和shell聚合期限,真实CF/IME组合独立验收。#51–#53 可以用原冻结输入独立定位,最终修复验收须在联合配置重建;一次未复现不自动构成因果修复。 +2. **并行处理原版对照、平台语义与性能。** [原生语义处理路径 #56](https://github.com/JavaZeroo/termforge/issues/56)先明确 #41 原合同需要什么、哪些路径可支持;保留必要的上游或独立实现工作,不用“平台不同”豁免缺口。#3/#4 优先建立原版 Windows 本地终端基线;#54 现在冻结重复方法与完成端点并定位吞吐成本,数值预算作为提案交用户确认,不等到 #34 才补性能。 3. **在主线推进独立行为合同。** #29 核对极窄/极短、软硬行、历史/淘汰、选择/Find、连续 resize 和 child ack;#30 核对 IME 组合/提交/取消与输出/resize/切换、应用键盘/粘贴接收、实际复制及同次八会话。已验收的 #39 只提供首批字素基础,最终配置的交叉验收仍需执行。 4. **补终端模式和应用矩阵。** #27 明确 DECOM/DECAWM、pending-wrap、保存恢复、异常序列与 margin×resize;#28 先固定应用版本/入口/动作及协议字节预期,再逐应用完成进入、操作、resize、退出、主屏恢复。Vim 的一个修复不替代完整 TUI 验收。 5. **独立推进真实显示与可靠性。** #31 当前缺实际不同 DPI 环境;优先自动探测或准备隔离测试宿主,不改个人显示设置、不反复测试同两台 96-DPI 屏幕。#36/#40/#44 及[旧 normal-1 事件 #57](https://github.com/JavaZeroo/termforge/issues/57)按有停止条件的诊断批次推进,原失败和未知保留,不阻塞其他独立工作。 @@ -53,7 +55,7 @@ flowchart LR | 工作 | 每次执行必须产出 | 遇到缺口如何处理 | |---|---|---| | 联合基线后的后端修复 | 对同一 source/负载/期限的反例、最小原因与完整动作结果;native、transport、核心、glyph 和回收分列 | 有因果 red→green 才写修复;暂未复现保留旧输入和未知,不改 oracle 或等待预算 | -| #54 受控性能 | 正确性、固定负载/环境/配置、无观察或观察成本边界、重复样本、分层归因与预算依据 | 当前约 16%/9% 的吞吐成本只是共享宿主诊断;没有正式 WindTerm 基线或实际端点就保持未知 | +| #3/#4 原版基线及 #54 受控性能 | 正确性、固定负载/环境/配置、无观察或观察成本边界、重复样本、分层归因与预算提案 | 当前约 16%/9% 的吞吐成本只是共享宿主诊断;没有正式 WindTerm 基线或实际端点就保持未知;具体容差由用户明确后生效 | | #31 真实不同 DPI | 可识别的实际环境、跨屏输入/焦点/网格/隐藏会话和真实 child 尺寸记录 | 无环境则保留环境阻塞,继续隔离宿主自动化准备;消息模拟或受控 DIB 不计真实跨屏 | | 历史可靠性 #36/#40/#44 与 #38 未知事件 | 一次预先限定的假设/样本批次,包含事件阶段、原期限、源码/宿主身份和清理 | 未复现就报告该批未复现,更新下一可证伪假设;不无限重跑取绿,也不由规划整理解除 #35 的出口 | @@ -66,7 +68,7 @@ flowchart LR | resize 与真实 TUI:#29、#28 | 逻辑/物理行、宽字符、历史与淘汰有明确策略;窗口、核心和真实子进程尺寸一致;固定应用逐一完成进入、操作、resize、退出、主屏恢复;回复、输入模式和 bracketed paste 字节经独立预期验证 | 离线回放、单独 Vim 成功或默认核心通过,不能代表整个候选产品矩阵;未声明应用不计完成 | | 复杂文字与输入:#30(复用已验 #39) | 保留首批字素合同,在最终配置核对原始标量、宽度/光标、实际 glyph、复制与子进程接收;完成固定 IME 与同次八会话交叉回归 | #39 的字素与单列 resize 成功不代表完整 IME、应用模式或 R1 系统输入组合在另一配置已通过;未知 WindTerm 行为仍保留 | | 真实显示:#31 | 按[字体/显示合同](R2-font-display.md)记录实际不同 DPI、真实跨屏、焦点、IME/Find/rename、命中、隐藏会话及子进程尺寸;环境身份和重复结果可核对 | 相同 96 DPI 的两台显示器、受控消息、DIB 或实际提交时刻不能代替不同 DPI 或实际上屏证据;不得修改个人显示设置制造验收 | -| 产品架构:#34 | 基于上述实际工作流与 #23/#26、已验收的 #32/#33,重审 ADR-0004;明确产品核心选择、GUI/平台边界、历史/快照、传输/呈现、背压/取消、依赖升级与维护策略 | 原型已采用某技术或候选已有接口,不构成最终产品选择;未测性能仍保持未知 | +| 产品架构:#34 | 基于实际工作流、原版对标及 #23/#26/#32/#33/#54 的证据,重审 ADR-0003/0004;提出核心、GUI/平台、历史/快照、传输/呈现、背压/取消和维护方案,记录用户确认依据 | 原型已采用某技术、历史 issue 关闭或候选已有接口,不构成最终产品选择;未测性能和未确认容差保持未完成 | | 开发版交付:#35 | 最终下载产物通过干净构建、依赖/字体许可和清单审计、解压运行、shell/TUI/文字/配置升级/真实显示工作流;按预先冻结的负载和资源合同完成重复会话与持续运行,逐项核对本表 | Alpha 1、源码构建成功、短驻留或某一 CI 通过,不代表 R2 日常开发包、长期稳定或 WindTerm 性能等价 | #32 本地 shell/分屏标签、#33 会话与配置恢复已有各自限定合同;最终集成仍须复验这些合同。R2 不扩大为 SSH/SFTP、任意嵌套分屏、完整 WindTerm UI 或跨平台客户端;这些任务继续按 PLAN 的对应路标推进。 @@ -76,5 +78,5 @@ flowchart LR - 每个结果保留源码/二进制、系统/后端、输入/负载身份,以及实际通过、失败和跳过;不同源码或补充运行不拼成一次“全绿”。 - 控制台、模型、DWrite 布局、DIB、窗口提交和实际呈现各自留证;未观测端点保持 unknown/null。 - 只公开经审核的合成输入和脱敏摘要;运输原件、用户路径、桌面身份等按现有来源规则处理。真实原生会话的宿主和子进程须在自有 Job 中启动、超时和回收;离线模型回放另列实际期限与监督范围,不能由进程 timeout 推断 Job 回收。 -- 单项完成更新其 issue、FEATURES 与 EVIDENCE;原失败仍保留。若真实平台能力迫使改变合同,先明确提出范围/架构决定,不能用新合同证明旧合同已完成。 +- 单项完成更新其 issue、FEATURES 与 EVIDENCE;原失败仍保留。若需改变范围、主要路线、验收门槛或接受重要差异,先提交证据和具体方案供用户确认,不能用新合同证明旧合同已完成。 - #7 由 #35 对所有出口逐项核对后结项;正式 WindTerm 性能基线继续独立推进,不由本表推断已经等价。 diff --git a/docs/research/2026-10-25-core-route-review.md b/docs/research/2026-10-25-core-route-review.md index acd4b352..3cd71b20 100644 --- a/docs/research/2026-10-25-core-route-review.md +++ b/docs/research/2026-10-25-core-route-review.md @@ -1,5 +1,7 @@ # R2 原生核心路线重审(#26) +> 历史研究记录:2026-10-26 用户已确认保留 Alacritty 为候选并重新评估,见 [ADR-0006](../adr/0006-confirmed-project-direction.md)。本文的样本、接口及当时结论保留原义;下文的“决定”不再作为当前正式选型或用户批准依据。最终核心方案由 #34 提交用户确认。 + 调查与决定日期:2026-10-25。固定候选为 `alacritty_terminal 0.26.0`,对照为当前自研 C++ 核心。本文记录 [#26](https://github.com/JavaZeroo/termforge/issues/26) 的平台、维护、接口和路线证据;[ADR-0003](../adr/0003-core-reference-and-reuse.md) 保存决定,[FEATURES](../FEATURES.md) 保存实际能力。没有启动 WindTerm、GUI 或截图探针,也没有把 headless 轨迹写成产品呈现。 ## 决定 diff --git a/docs/research/r1-action-comparison.md b/docs/research/r1-action-comparison.md index 0843aa8a..114ce1cc 100644 --- a/docs/research/r1-action-comparison.md +++ b/docs/research/r1-action-comparison.md @@ -1,5 +1,7 @@ # R1 文字动作对照 +> 本页保留 R1 限定观察、实现策略与验证。当前方向按 [ADR-0006](../adr/0006-confirmed-project-direction.md) 原版优先;未经原版对照的项目策略继续列为差异或未知,重要差异及原版缺陷修正需用户确认。历史验收不自动成为最终差异获准记录。 + 本表用于 [R1-07 / #17](https://github.com/JavaZeroo/termforge/issues/17) 的逐项验收。参考观察、产品策略和实际验证分别记录;有实现或纯模型测试不等于真实入口通过。本表已按 R1 合同完成限定验收,执行状态只在 issue 维护。 ## 固定依据 diff --git a/docs/research/resize-replay-cost.md b/docs/research/resize-replay-cost.md new file mode 100644 index 00000000..9352fbab --- /dev/null +++ b/docs/research/resize-replay-cost.md @@ -0,0 +1,36 @@ +# 八会话的 resize 重放成本与关闭回复 + +接续 [UI sender fence](ui-paste-fence.md) 的 [#53](https://github.com/JavaZeroo/termforge/issues/53)。候选 `.4` × 显式官方 ConPTY `1.25.260930003`、flags24 在源码 `eae4b6a4ef3e044ebfebbbbfeccf9aec74cd7f0d` 上通过两轮原八会话 **data-only** 合同。修复保留原 3MB 汉字、768B checkpoint、16KiB sender 门槛、后台输出量、UI70秒和 fixture55秒期限。[固定身份与最终回归](../../experiments/shell/results/2026-10-26-replay-cost-and-close/validation.json)与[全部诊断样本](../../experiments/shell/results/2026-10-26-replay-cost-and-close/)分列,不把真实剪贴板/IME、其他构建或整个 R2 算作通过。 + +## 成本定位与有限优化 + +旧源 `8ff112e` 的两次真实八会话插桩仍在 stage45 失败,Job0、desktop 已回收。第一次把约64秒定位到 parser feed;第二次进一步分开内部阶段:parser feed 63.987秒、Rust feed 0.560秒、synchronize 63.420秒,其中 cell export 2.031秒、reconcile_replay 60.905秒。计数是主线程累计的嵌套 wall time,不能相加,也不是实际呈现或输入延迟。两次均关闭资源采样,仅作诊断;[完整计时、源码/二进制身份与原始记录哈希](../../experiments/shell/results/2026-10-26-replay-cost-and-close/ui-profiles.json)保留。 + +普通无 resize 的 headless F1 输入未复现高成本:[2,048,000B 的六样本](../../experiments/shell/results/2026-10-26-replay-cost-and-close/headless-ordinary.json)均满足逐行文字、光标和历史预期,未插桩三次 wall 为0.446–0.486秒。缩小到204,800B后,只在计时前加入一次同尺寸 `resize(17,74)`,未插桩两次从无 resize 的0.046/0.044秒变成0.929/0.969秒;六次最终 screen JSON 哈希全部相同。[全部样本与冻结输入](../../experiments/shell/results/2026-10-26-replay-cost-and-close/headless-resize.json)、[诊断驱动及计时边界](../../experiments/shell/results/2026-10-26-replay-cost-and-close/probe/README.md)可审查。 + +观察与源码共同支持一个有限原因:resize 保留了重放起始屏幕,后续每次同步的 `reconcile_replay` 会反复序列化同一行,查找 origin 时还会再次生成行键。`06bf6b5` 让同次同步生成的 expected/actual 键向量保留空行位置,并直接按索引复用;计数仍跳过空键。键内容、重复行处理、匹配顺序、行 ID/version 映射、重放生命期和历史策略保持原合同。缓存只在一次调用内存在。 + +这些样本来自共享 Windows 11 build26200 宿主,8逻辑核,MSVC19.42 Release x64;headless 只输入 producer 字节,没有 ConPTY 变换或 UI。短样本与插桩均有扰动,不据此计算产品提速比例,也不替代 [#54](https://github.com/JavaZeroo/termforge/issues/54) 的预冻结 `.3/.4` 正式 A/B、用户性能预算或 WindTerm 对标。 + +## 关闭期间仍发送回复的独立错误 + +只完成键复用后的原八会话在27.467秒完成后台消费,35.974秒完成粘贴关闭和后续输入,但退出时因 `terminal reply rejected by input queue` 返回2。额外诊断的两轮分别成功与失败;失败现场为 `closing=1, stopping=1, queued=0, failed=0, paste_busy=0`,回复是 `ESC[4;4R`。这些成功/失败均保留在[中间运行记录](../../experiments/shell/results/2026-10-26-replay-cost-and-close/intermediate-workflows.json),一次成功不消除退出错误。 + +原因是关闭已停止输入队列,剩余输出中的光标位置查询仍经 `route_terminal_events` 发送回复。新增真实 ConPTY 回归先将查询留在 OutputQueue,明确要求尚未消费输出,再关闭并确认输入停止,最后才泵窗口消息。旧代码0.19秒即因输入拒绝失败;`eae4b6a` 仅在 `closing_` 时不再发送回复,两次新进程均实际观察查询、消费65B、解析标记文字、零输入拒绝并完成回收。输出仍解析、排空和发布;正常会话的回复失败仍严格报错。 + +[红绿身份](../../experiments/shell/results/2026-10-26-replay-cost-and-close/closing-reply.json)明确记录夹具变化:绿色版本增加了已解析标记与消费字节断言,未放宽旧条件。测试只注册于候选+显式后端组合,不能据此声明默认/system 配置已验收。 + +## 最终结果与剩余边界 + +最终源码两轮八会话包含资源采样,后台完成时间分别为26.268、27.312秒,粘贴关闭后输入验证分别在34.675、35.808秒完成。每轮26个检查点、五轮6↔8会话、拒绝第9个会话、精确接收字节和18个会话关闭均通过,Job0、desktop已删除。两轮 cancel/close 的动作前均为 sent17391 / accepted18414 / held1023;取消接收18414B,关闭恰768B,in_flight0、retained15。close 的实际唤醒为 cancelled,最终状态为 stopped,二者分列。 + +同源码完整 CTest 为54/56,465.08秒:`candidate_shell_tui` 的六项中 Vim stage59 失败,其余五项通过;`shell` 聚合触及原180秒期限,已记录 resize/resize-close43、main第三轮7、ui第三轮52。聚合中单窗口粘贴两轮和 EOF/无GO反例均已完成并通过;负例的产品运行仍返回2、接收恰768B、Job0/desktopgone,未因清理转绿。未结束的 shell 项不能算通过。 + +内部 skip 也保留:可选 Rust 对比/real-app 工具未构建、托管实际 CF/IME 保护、原版失败注入 opt-in 及私有站点 OpenClipboard 权限失败。CTest 条目通过不表示其中被跳过的场景通过。#51/#52/#63、真实系统剪贴板/IME、实际呈现、长期资源、正式性能及产品核心/后端选型仍需各自完成。自动化使用自有非输入桌面,不要求操作用户窗口。 + +```powershell +cmake --build build/candidate --config Release +ctest --test-dir build/candidate -C Release --output-on-failure +``` + +复验前按贡献指南显式选择 `.4` 与固定官方后端,准备固定 Vim 的测试进程 PATH;结果同时记录二进制/依赖哈希。单独复验关闭查询可用 `ctest --test-dir build/candidate -C Release -R '^candidate_reply_close$' --output-on-failure`。完整回归的失败不会因局部修复通过而被忽略。 diff --git a/docs/research/streaming-graphemes.md b/docs/research/streaming-graphemes.md index 67541cd8..ce36ce9f 100644 --- a/docs/research/streaming-graphemes.md +++ b/docs/research/streaming-graphemes.md @@ -1,5 +1,7 @@ # R2 流式字素、宽度与固定 emoji 合同(#39) +> 当前合同与规范回归描述候选实现,未完成的原版匹配由 #30 继续取证。按 [ADR-0006](../adr/0006-confirmed-project-direction.md),WindTerm 2.7.0 实际行为优先;Unicode 版本、宽度或事件边界策略与原版有重要差异时提交用户确认。#39 的关闭和正确性结果不构成该确认,也不确定最终核心选型。 + 本合同属于显式候选 `Term/Processor` 路线,不扩展默认自研解析器。固定首批样本为 `e\u0301`、`🙂`、`👩‍💻`、`1️⃣`;要求原始标量、不经 NFC 的原文、单元格、光标、选择复制和实际绘制一致。WindTerm 2.7.0 的对应运行时行为仍未知,不能由静态 QString 测量路径推断。 ## 依据与路线 diff --git a/docs/research/ui-paste-fence.md b/docs/research/ui-paste-fence.md new file mode 100644 index 00000000..704589d5 --- /dev/null +++ b/docs/research/ui-paste-fence.md @@ -0,0 +1,50 @@ +# 未完成 UI 粘贴的确定性取消与关闭 + +对应 [#53](https://github.com/JavaZeroo/termforge/issues/53),属于 [#50](https://github.com/JavaZeroo/termforge/pull/50) 的实验后端整合。实现源 `a15eb7f67315fb53c9d66b9c5a4424e13511a311` 的单窗口两轮取消/关闭及控制故障检查通过,**该源码的完整八会话仍失败**。后续在 `eae4b6a` 上通过两轮原 data-only 合同,见[行键成本与关闭回复修复](resize-replay-cost.md);本页保留修复前证据。[固定源码、二进制、原始记录哈希与逐字段结果](../../experiments/shell/results/2026-10-26-ui-paste-fence/validation.json)分列原 red、针对性验证与整体验证,不能互相替代。 + +## 原失败与修改 + +在 `.4` 候选与官方 ConPTY `1.25.260930003`、Console width flags24 的联合构建上,原场景两次被正确拒绝:一次到动作前已 `sent=accepted=total=3000000`,另一次在 receiver gate 前就 completed。这说明原夹具不能稳定观察未完成期,不能据此认定无栅栏的产品取消失效。 + +`ShellWindow` 现在允许自有测试预配置单次 generation 与绝对期限。真实 `PasteTransfer` 创建后、`InputQueue::begin_paste` 唤醒 writer 前完成 fence 绑定;普通产品请求仍默认关闭。writer 沿原取块、实际发送、写确认和释放路径工作,达到原 16KiB 门槛后只持住一个尚未提交的真实块。 + +先只增加栅栏、保持原 UI 直接 `transfer.cancel()` 时,场景在原 35 秒期限失败;改为真实 Escape 调用 `input_.cancel_paste()` 后,同一测试两轮通过。该入口通知等待中的 writer,允许已领取块完成真实发送;取消前没有成功 GO。关闭仍走既有 cancel、stop、Session 回收路径。 + +原 100 万个汉字、3,000,000B、768B 接收检查点和 flood 不变。只有同时观察到 held、实际文件 checkpoint、实际输出 flood 和另一会话继续响应,才执行动作。单窗口的另一会话在 held 后执行独立 echo;八会话检查另一后台会话在 held 后继续消费输出。两者的响应证据不同,不称为同一种输入延迟测量。 + +## 控制协议和证据 + +测试控制端通过真实匿名 Win32 管道发送 generation/GO、读取应答;两个端点在同一进程,不是外部监督进程断连验证。UI 正例先验证错误 generation 被拒绝且仍 held,随后执行真实 Escape 或关闭。纯协议测试另覆盖未 held、匹配 GO、重复 GO、八条请求/应答积压及第九条拒绝。 + +EOF 负例实际关闭写端,并由 `ReadFile` 确认 `ERROR_BROKEN_PIPE`;无 GO 负例使用单独的 2 秒故障期限。两种真实 UI 场景均保持失败返回 2,writer 释放 payload,receiver 仍恰为 768B;Job0 与 desktop 回收不会转化为成功粘贴。正例原期限仍为 UI 35 秒、外层 40 秒、receiver gate 15 秒;八会话仍为 UI 70 秒和 fixture 55 秒。 + +`CreatePipe` 的请求大小只是建议,因此构造时通过 `GetNamedPipeInfo` 检查实际 incoming server buffer 足以容纳整个 128B 请求/192B 应答预算,不能直接把 4096 请求值当作实测容量。见 Microsoft 的 [CreatePipe](https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe) 与 [GetNamedPipeInfo](https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-getnamedpipeinfo) 合同。 + +关闭可能先由 cancel 唤醒,再由 stop 改变最终状态。现在在 CV 返回时固定实际退出原因,并分别记录 `exit` 与 `final_state`;最新两轮 close 为 `exit=cancelled / final_state=stopped`。旧 `62226af` 记录的 `exit=stopped` 只是最终状态,不作为实际唤醒原因证据。 + +| 动作 | 动作前 sent / accepted / held bytes | 动作后与独立 receiver | +|---|---|---| +| Escape,两轮 | 17391 / 18414 / 1023 | cancelled;sent=accepted=received=18414;in_flight0、retained15 | +| 关闭,两轮 | 17391 / 18414 / 1023 | receiver 恰为768;in_flight0、retained15;已提交块仍按真实计数记录 | + +## 未完成的组合出口 + +两次带采样八会话都完成了上述两项粘贴动作,但后台整体消费完成分别约为 51.158、52.546 秒;close 前分别约为 61.806、63.096 秒。前台 B fixture 已按原 55 秒期限退出,最后一个 `!` 无法收到,UI 在 stage45 期限失败。原始状态、字节和回收结果均保留。 + +同一最终代码关闭资源采样的诊断仍失败:后台完成 50.262 秒、close 前 60.948 秒。因此不能把采样器认定为这一失败的唯一原因;该单变量运行也不替代带采样组合验收。候选适配、消息循环和宿主负载成本仍需分别定位,不延长原期限或减少后台负载。 + +`62226af` 的完整 CTest 为52/55:Vim 不在当时 PATH、八会话 stage45、shell 聚合180秒期限分别保留;shell 中另有 resize43、main7、ui52 的失败,不归因于粘贴修复。最终唤醒原因与容量修正后,3/3 针对性 CTest、两次完整单窗口正例及 EOF/无 GO 负例通过。随后仅在测试进程 PATH 补齐固定 Vim9.1,原入口再次在 stage59 失败,resize 命令进入插入文本;这与环境准备失败分列,#52 继续开放。其他配置、托管系统剪贴板/IME、呈现与 WindTerm 性能等价均未由这些结果验收。 + +## 复验 + +按贡献指南准备依赖,显式选择候选及固定官方后端后: + +```powershell +cmake --build build/candidate --config Release --target termforge_shell shell_test_launcher sender_fence_test sender_fence_pipe_test +ctest --test-dir build/candidate -C Release -R '^(sender_fence|sender_fence_pipe|paste_fence_evidence)$' --output-on-failure +$env:TERMFORGE_BIN_DIR = (Resolve-Path build/candidate/bin/Release).Path +python -m unittest experiments.shell.test_shell.ShellTests.test_paste_confirmation_and_hidden_target_preserve_visible_focus experiments.shell.test_shell.ShellTests.test_paste_control_eof_remains_failure_after_cleanup experiments.shell.test_shell.ShellTests.test_paste_missing_go_remains_failure_after_cleanup -v +python -m unittest experiments.shell.test_workflow_sessions -v +``` + +最后一项保留真实失败出口。测试使用自有非输入桌面,不要求操作用户窗口;完整 R2 与产品后端选择仍由各自合同约束。 diff --git a/docs/research/workspace-restore-transport-oracle.md b/docs/research/workspace-restore-transport-oracle.md index 7aeb7cfd..7d8a17b2 100644 --- a/docs/research/workspace-restore-transport-oracle.md +++ b/docs/research/workspace-restore-transport-oracle.md @@ -10,16 +10,21 @@ [#61](https://github.com/JavaZeroo/termforge/issues/61) 的两份默认/system 新失败具有不同原因:PowerShell 命令编辑重绘在实际输出之前带一个裸 BS(`0x08`)。旧 oracle 因它直接拒绝整个运输,即使随后已有真正 CRLF、完整目录/版本输出和新提示符。用原字节独立重放,两个 PowerShell 条件均为 false,而 cmd/底部条件为 true;仅移除前缀 BS 的诊断输入使旧条件转为 true,证实拒绝分支。修正不模拟退格或删除回显,而是把该前缀记录标记为不可信,直到真正 LF 才允许后续独立输出;[限定复验](../../experiments/shell/results/2026-10-26-workspace-restore-bs/validation.json)保留原始失败及各配置身份。这是测试完成端点修正,不是产品输出丢失或恢复功能缺陷的修复。 +[#72](https://github.com/JavaZeroo/termforge/issues/72) 的两次默认/system CI 保存 stage2 失败记录逐字节相同。PowerShell 的 122 B 原运输在真实 LF 后、完整输出之前含 `CSI 63X / CSI 63C / CR / CSI ?25h`;旧 helper 对空记录的裸 CR 永久拒绝。保留完整原字节的 C++ 回归先红后绿,仅移除这一 CR 的诊断对照在旧 helper 下已通过。修正只允许下述受限空记录情形,不删除运输字节或改变产品输出。源码 `4daf992` 下默认/system、候选/system、候选/官方 flags24 各两轮保存、普通恢复、钳制恢复共 18 次真实运行通过;[身份与结果](../../experiments/shell/results/2026-10-27-workspace-empty-cr/validation.json)分别保留因果回归、原始失败及各配置的实际断言与回收。局部通过不替代最终完整回归或 CI。 + +同源码官方配置完整 CTest 为 54/56、449.38 秒;工作区保存/恢复方法也在完整 shell 套件中通过。剩余为 Vim stage59,以及 shell 的 resize/resize-close stage43、main stage7、workspace stage52,分别由 #51/#52/#63 承接。此次 shell 套件实际完成 40 方法,4 error、3 skip,用时 174.65 秒;旧批次的 180 秒聚合超时仍保留。内部可选依赖、hosted 系统输入和私有剪贴板跳过逐项记录,不写成这些能力通过。PR #78 的两条 CI 均通过,其中 Windows 默认/system 为 51/51、273.64 秒;其实际合并 checkout 与本机源码分别列明。 + ## 限定 oracle 合同 - 独立扫描 7-bit ESC 形式的完整 OSC(BEL 或 ST 终止)和 CSI。OSC 内容不参与正文匹配,UTF-8 的所有高位正文 bytes 原样保留,包括 `目` 中的 `0x9b`。 - 标记必须匹配完整正文记录,前后不可附加命令、其它正文或任意子串。LF 结束记录;显式正行号、第 1 列 CUP/HVP 仅可结束完整标记或提示符,不能清除命令前缀或拼接分散的标记片段。其它非空内容遇 CUP 保持未知,直到真实 LF;SGR 和其它 CSI 不会凭空结束标记。 -- 在首次匹配输出之前,命令编辑中的 BS 只会污染当前记录;CUP 不能解除该污染,也不把退格两侧文字拼起来。真实 LF 后可验证新的完整记录。已经匹配输出后的 BS 仍拒绝,不能借编辑过的提示符证明命令完成;其它裸控制字符的拒绝保持不变。 +- 在首次匹配输出之前,命令编辑中的 BS 只会污染当前记录;CUP 不能解除该污染,也不把退格两侧文字拼起来。真实 LF 后可验证新的完整记录。已经匹配输出后的 BS 仍拒绝,不能借编辑过的提示符证明命令完成。 +- CRLF 的 CR 不改变正文。裸 CR 仅在已观察到记录边界、当前记录为空且未受 BS 污染、尚未匹配输出时允许经过;它不清除文字、不结束记录,也不替代之后的完整标记和新提示符。初始裸 CR、非空或受编辑污染记录中的裸 CR、已匹配输出后的裸 CR,以及其它裸控制字符仍拒绝。 - 该记录之后必须出现独立的 `TF>` 或 `TF> ` 记录,才证明 shell 返回;输入命令回显、标题内标记、旧提示符和输出行前缀都不满足条件。 - 完整 CSI 仅按语法封装剥离,不解释或验收其执行语义;其中受限 column-one CUP 的记录边界另按上条处理。不完整或格式错误的封装、未知 ESC 引导形式、DCS 等其它控制字符串保持未证明状态。这只覆盖固定持久化命令已输出和返回提示符的运输证据,不扩展为第二个通用 VT 核心;产品屏幕及恢复断言仍独立保留。 - 保留原有 8192-byte 观测上限、30 秒总期限、真实目录/版本、返回提示符、恢复成员/标题/缩放/分屏/几何及尺寸检查和真实 Job/desktop 回收断言。 -`transport_completion_test` 独立覆盖标题重复遮蔽、BEL/ST、SGR、CUP、UTF-8、命令编辑前缀 BS,以及命令回显、BS 拼接/提示符编辑、OSC/DCS payload、不完整控制、缺失或旧提示符等反例。它修正的是测试 oracle,不构成产品恢复故障修复或完整 R2 验收。 +`transport_completion_test` 独立覆盖标题重复遮蔽、BEL/ST、SGR、CUP、UTF-8、命令编辑前缀 BS、空记录裸 CR,以及命令回显、CR/BS 拼接或提示符编辑、OSC/DCS payload、不完整控制、缺失或旧提示符等反例。它修正的是测试 oracle,不构成产品恢复故障修复或完整 R2 验收。 ```powershell ctest --test-dir build/default -C Release -R '^transport_completion$' --output-on-failure diff --git a/experiments/candidates/alacritty/README.md b/experiments/candidates/alacritty/README.md index 1d452710..c6c711a9 100644 --- a/experiments/candidates/alacritty/README.md +++ b/experiments/candidates/alacritty/README.md @@ -1,6 +1,6 @@ # Alacritty 固定核心与候选适配工具 -本目录包含候选构建、状态适配、固定兼容补丁、C ABI 与核心成本实验。[ADR-0003](../../../docs/adr/0003-core-reference-and-reuse.md)决定 R2 新能力复用候选 Term/Processor;C++ 拥有值适配接入显式候选 `ShellWindow`,默认 CMake 保留自研回退。能力统一见[功能矩阵](../../../docs/FEATURES.md),联合验收和依赖见 [R2 合同](../../../docs/milestones/R2-local-terminal.md),本说明不维护逐批进度。 +本目录包含候选构建、状态适配、固定兼容补丁、C ABI 与核心成本实验。[ADR-0003](../../../docs/adr/0003-core-reference-and-reuse.md)及 [ADR-0006](../../../docs/adr/0006-confirmed-project-direction.md)保留 Alacritty 为待评估候选,正式核心选型待原版对标和用户确认;C++ 拥有值适配接入显式候选 `ShellWindow`,默认 CMake 保留自研回退。能力统一见[功能矩阵](../../../docs/FEATURES.md),联合验收和依赖见 [R2 合同](../../../docs/milestones/R2-local-terminal.md),本说明不维护逐批进度。 ## 固定来源与许可 diff --git a/experiments/core/CANDIDATE.md b/experiments/core/CANDIDATE.md index 3863ddaa..a7f81954 100644 --- a/experiments/core/CANDIDATE.md +++ b/experiments/core/CANDIDATE.md @@ -1,5 +1,7 @@ # 候选 C++ 终端适配器 +按 [ADR-0006](../../docs/adr/0006-confirmed-project-direction.md),用户确认保留本候选代码继续评估,尚未确认最终采用。下述实现、来源与回归结果继续保留;与 WindTerm 2.7.0 的重要差异、性能容差及正式选型分别提交用户确认。 + `CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.4` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 ## 构建与验证 diff --git a/experiments/core/candidate_terminal.cpp b/experiments/core/candidate_terminal.cpp index cc12246e..9bb1fab8 100644 --- a/experiments/core/candidate_terminal.cpp +++ b/experiments/core/candidate_terminal.cpp @@ -86,12 +86,11 @@ std::string replay_key(const Line& line) { template std::vector replay_keys(const History& history, const Lines& lines) { std::vector result; - const auto append = [&](const Line& line) { - auto key = replay_key(line); - if (!key.empty()) result.push_back(std::move(key)); - }; - for (const auto& line : history) append(line); - for (const auto& line : lines) append(line); + result.reserve(history.size() + lines.size()); + // Keep empty entries aligned with their rows so reconciliation can reuse + // each serialization while scanning the original and incoming order. + for (const auto& line : history) result.push_back(replay_key(line)); + for (const auto& line : lines) result.push_back(replay_key(line)); return result; } } // namespace @@ -150,17 +149,18 @@ std::unordered_set CandidateTerminal::reconcile_replay(std::vector{}, retained); std::unordered_map old_counts, new_counts; - for (const auto& key : expected) ++old_counts[key]; - for (const auto& key : actual) ++new_counts[key]; + for (const auto& key : expected) if (!key.empty()) ++old_counts[key]; + for (const auto& key : actual) if (!key.empty()) ++new_counts[key]; std::vector origin; origin.insert(origin.end(), source.history.begin(), source.history.end()); origin.insert(origin.end(), source.lines.begin(), source.lines.end()); std::unordered_set mapped; size_t origin_index = 0; - for (auto& line : retained) { - const auto key = replay_key(line); + for (size_t index = 0; index < retained.size(); ++index) { + auto& line = retained[index]; + const auto& key = actual[index]; if (key.empty() || old_counts[key] != 1 || new_counts[key] != 1) continue; - while (origin_index < origin.size() && replay_key(origin[origin_index]) != key) ++origin_index; + while (origin_index < origin.size() && expected[origin_index] != key) ++origin_index; if (origin_index == origin.size()) break; line.id = origin[origin_index].id; line.version = origin[origin_index].version; diff --git a/experiments/shell/candidate_reply_close_test.cpp b/experiments/shell/candidate_reply_close_test.cpp new file mode 100644 index 00000000..1a5e0149 --- /dev/null +++ b/experiments/shell/candidate_reply_close_test.cpp @@ -0,0 +1,110 @@ +// Exercise the real receive -> parser -> reply route while closing the input +// side. The existing owned desktop/Job supervises the window and console child. +#define WinMain included_shell_entry +#include "shell.cpp" +#undef WinMain +#include "ime_desktop_process.hpp" + +namespace { +template +void reply_wait(Predicate predicate, const char* detail) { + const auto deadline = GetTickCount64() + 3000; + while (!predicate()) { + require(GetTickCount64() < deadline, detail); + Sleep(1); + } +} + +int reply_client(const wchar_t* name) { + BOOL owned = FALSE; + require(IsProcessInJob(GetCurrentProcess(), nullptr, &owned) && owned, "reply client requires owned Job"); + const std::wstring prefix(name); + const auto open = [&](const wchar_t* suffix) { + return tf_conpty::Handle(OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, (prefix + suffix).c_str())); + }; + auto ready = open(L"-ready"), release = open(L"-release"), written = open(L"-written"); + require(ready.get() && release.get() && written.get(), "reply client gates unavailable"); + DWORD mode = 0; + const auto output = GetStdHandle(STD_OUTPUT_HANDLE); + require(GetConsoleMode(output, &mode) && SetConsoleMode(output, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING), + "reply client output mode failed"); + require(SetEvent(ready.get()), "reply client ready failed"); + require(WaitForSingleObject(release.get(), 3000) == WAIT_OBJECT_0, "reply client release deadline"); + constexpr char bytes[] = "TF_REPLY_CLOSE_READY\r\n\x1b[6n"; + DWORD count = 0; + require(WriteFile(output, bytes, sizeof(bytes) - 1, &count, nullptr) && count == sizeof(bytes) - 1, + "reply client query write failed"); + require(SetEvent(written.get()), "reply client written signal failed"); + // The parent owns cancellation; do not let a natural child exit precede it. + Sleep(10000); + return 2; +} + +int reply_window_case() { + require(tf_shell::is_ime_desktop_child(), "reply test requires owned non-input desktop"); + BOOL owned = FALSE; + require(IsProcessInJob(GetCurrentProcess(), nullptr, &owned) && owned, "reply test requires owned Job"); + require(SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2), "reply test DPI setup failed"); + const auto prefix = L"Local\\TermForgeReply-" + std::to_wstring(GetCurrentProcessId()) + L"-" + std::to_wstring(GetTickCount64()); + const auto create = [&](const wchar_t* suffix) { + const auto event = CreateEventW(nullptr, TRUE, FALSE, (prefix + suffix).c_str()); + const auto error = GetLastError(); + if (!event || error == ERROR_ALREADY_EXISTS) { + if (event) CloseHandle(event); + throw std::runtime_error("reply gates must be fresh"); + } + return event; + }; + tf_conpty::Handle ready(create(L"-ready")), release(create(L"-release")), written(create(L"-written")); + ShellWindow window("", ""); + window.enable_runtime_observation(); window.begin_output_observation(); + tf_shell::ShellLaunch launch; + wchar_t path[32768]{}; + const auto path_size = GetModuleFileNameW(nullptr, path, 32768); + require(path_size && path_size < 32768, "reply fixture executable path unavailable"); + launch.id = L"reply-close"; launch.executable.assign(path, path_size); + launch.arguments = L"--reply-client \"" + prefix + L"\""; + window.create(nullptr, launch); + require(WaitForSingleObject(ready.get(), 3000) == WAIT_OBJECT_0, "reply console ready deadline"); + const auto before = window.runtime_snapshot(); + require(SetEvent(release.get()), "reply release failed"); + require(WaitForSingleObject(written.get(), 3000) == WAIT_OBJECT_0, "reply console write deadline"); + reply_wait([&] { return window.runtime_snapshot().output.queued_bytes > before.output.queued_bytes; }, + "reply transport receive deadline"); + require(window.runtime_snapshot().consumed_bytes == 0, "query must remain unparsed before close"); + window.close(); + reply_wait([&] { return window.runtime_snapshot().input.stopping; }, "reply input stop deadline"); + const auto deadline = GetTickCount64() + 5000; + while (IsWindow(window.handle())) { + require(GetTickCount64() < deadline, "reply window cleanup deadline"); + MSG message{}; + for (int count = 0; count < 256 && PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE); ++count) { + TranslateMessage(&message); DispatchMessageW(&message); + } + MsgWaitForMultipleObjects(0, nullptr, FALSE, 1, QS_ALLINPUT); + } + const bool query = window.observed_output().find("\x1b[6n") != std::string::npos; + const auto final = window.runtime_snapshot(); + std::cout << "{\"query_consumed_after_close\":" << (query ? "true" : "false") + << ",\"consumed_bytes\":" << final.consumed_bytes + << ",\"input_stopping\":" << (final.input.stopping ? "true" : "false") + << ",\"input_rejected\":" << final.input.rejected << "}\n"; + const auto result = window.collect(); + require(query && window.live_contains_line("TF_REPLY_CLOSE_READY") && final.consumed_bytes > 4 && + final.input.stopping && final.input.rejected == 0 && result.complete && result.active_children == 0, + "closing input must drain the real query without attempting a reply"); + return 0; +} +} + +int main(int argc, char** argv) { + try { + if (argc == 3 && std::string(argv[1]) == "--reply-client") return reply_client(utf16(argv[2]).c_str()); + if (argc == 2 && std::string(argv[1]) == "--owned-desktop-child") return reply_window_case(); + require(argc == 1, "unexpected reply test arguments"); + for (int round = 0; round < 2; ++round) + require(tf_shell::run_ime_desktop_child(12000) == 0, "owned candidate closing reply failed"); + std::cout << "{\"closing_reply\":\"passed\",\"owned_job_zero\":true,\"desktop_deleted\":true}\n"; + return 0; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/experiments/shell/paste_checks.py b/experiments/shell/paste_checks.py index 8a2cbf90..3de07a7a 100644 --- a/experiments/shell/paste_checks.py +++ b/experiments/shell/paste_checks.py @@ -3,6 +3,19 @@ from pathlib import Path +def validate_paste_fence(row: dict, generation: int) -> None: + fence = row.get("fence") + if (not isinstance(fence, dict) or type(fence.get("generation")) is not int or + fence["generation"] != generation or fence.get("held") is not True or + fence.get("wait_finished") is not True or fence.get("other_responsive") is not True or + fence.get("wrong_generation_rejected") is not True or + fence.get("exit") not in (("cancelled",) if row.get("name") == "cancel" else ("cancelled", "stopped")) or + fence.get("final_state") != ("cancelled" if row.get("name") == "cancel" else "stopped") or + type(fence.get("owned_bytes")) is not int or not 0 < fence["owned_bytes"] <= 1024 or + row["before_accepted"] - row["before_sent"] != fence["owned_bytes"]): + raise ValueError("window paste sender fence identity, hold, wake or isolation evidence is incomplete") + + def validate_paste_workflow(directory: Path) -> dict: directory = Path(directory) data = json.loads((directory / "paste-workflow.json").read_text(encoding="utf-8")) @@ -18,7 +31,7 @@ def validate_paste_workflow(directory: Path) -> dict: raise ValueError("window paste cancellation and close evidence is incomplete") expected = b"\xe6\xb1\x89" * 1000000 summary = {} - for row in cases: + for generation, row in enumerate(cases, 1): integers = ("target", "total", "before_sent", "before_accepted", "accepted", "sent", "in_flight", "retained_capacity", "checkpoint_bytes", "received") if any(type(row.get(key)) is not int or row[key] < 0 for key in integers): @@ -30,6 +43,7 @@ def validate_paste_workflow(directory: Path) -> dict: row["in_flight"] or row["retained_capacity"] > 32 or row["checkpoint_bytes"] != 768 or row.get("flood_observed") is not True or row.get("state") not in ("cancelled", "failed")): raise ValueError("window paste did not remain partial, bounded and terminal under output flood") + validate_paste_fence(row, generation) received = (directory / f"{row['name']}.bin").read_bytes() if (not received or len(received) != row["received"] or len(received) > row["sent"] or received != expected[:len(received)]): diff --git a/experiments/shell/paste_scenario_state.hpp b/experiments/shell/paste_scenario_state.hpp index 0c1165d0..77db7d43 100644 --- a/experiments/shell/paste_scenario_state.hpp +++ b/experiments/shell/paste_scenario_state.hpp @@ -2,6 +2,7 @@ #pragma once #include "../conpty/session.hpp" #include "paste.hpp" +#include "sender_fence_pipe.hpp" #include #include #include @@ -38,15 +39,20 @@ struct PasteWindowResult { std::string name; tf_shell::PasteProgress before, after; size_t checkpoint_bytes=0, received=0; - bool flood=false; + bool flood=false,other_responsive=false; + SenderFence::Snapshot fence_before,fence_after; + bool wrong_generation_rejected=false; }; struct PasteScenarioState { static constexpr size_t large_units=1000000; std::unique_ptr receiver; std::shared_ptr transfer; + std::shared_ptr fence; + std::unique_ptr control; std::vector results; PasteWindowResult pending; HWND target=nullptr; + size_t other_consumed=0; bool default_enter=false, escape_cancel=false, tab_enter=false; std::array original_keys{}; bool keys_saved=false; @@ -64,7 +70,7 @@ struct PasteScenarioState { } void start_receiver(const std::filesystem::path& capture,const std::string& name) { receiver=std::make_unique(static_cast(results.size())); - transfer.reset(); pending={}; pending.name=name; + control.reset(); transfer.reset(); fence.reset(); pending={}; pending.name=name; paste_check(!std::filesystem::exists(capture),"paste receiver capture must be new"); } std::wstring receiver_command(const std::filesystem::path& capture) const { @@ -77,7 +83,21 @@ struct PasteScenarioState { std::ifstream file(path,std::ios::binary); paste_check(file.good(),"paste evidence capture missing"); return {std::istreambuf_iterator(file),std::istreambuf_iterator()}; } + bool held() const { + paste_check(fence && transfer,"paste fence and transfer must be observed after admission"); + const auto state=fence->snapshot(fence->ticket()).state; + paste_check(state==SenderFence::State::armed || state==SenderFence::State::held,"paste fence exited before UI action"); + return state==SenderFence::State::held; + } void before_action(const std::filesystem::path& capture,bool flood) { + paste_check(held(),"paste action requires an actual held writer"); + paste_check(pending.other_responsive,"paste action requires another session response while held"); + control=std::make_unique(fence); + const auto generation=fence->ticket().generation; + control->send_go(generation+1);control->dispatch();const auto ack=control->acknowledgement(); + pending.wrong_generation_rejected=ack && ack->generation==generation+1 && !ack->accepted && held(); + paste_check(pending.wrong_generation_rejected,"wrong-generation pipe GO released the UI writer"); + pending.fence_before=fence->snapshot(fence->ticket()); paste_check(transfer!=nullptr && receiver->checkpoint(),"paste action requires a live transfer and receiver checkpoint"); pending.before=transfer->progress(); pending.flood=flood; pending.checkpoint_bytes=read(capture).size(); @@ -90,9 +110,28 @@ struct PasteScenarioState { << " total=" << pending.before.total << " state=" << static_cast(pending.before.state); throw std::runtime_error(details.str()); } + const auto& held_progress=pending.fence_before.held_progress; + paste_check(pending.fence_before.attached && pending.fence_before.ever_held && !pending.fence_before.wait_finished && + pending.before.sent==held_progress.sent && pending.before.accepted==held_progress.accepted && + pending.before.in_flight==pending.fence_before.owned_bytes,"held UI progress changed before action"); + std::ofstream out(capture.wstring()+L".before.json",std::ios::binary); + out<<"{\"sent\":"<progress(); const auto bytes=read(capture); pending.received=bytes.size(); + pending.fence_after=fence->snapshot(fence->ticket()); + const bool woke_cancelled=pending.fence_after.wait_exit_state==SenderFence::State::cancelled; + const bool woke_stopped=pending.fence_after.wait_exit_state==SenderFence::State::aborted && + pending.fence_after.wait_exit_reason==SenderFence::AbortReason::stop; + paste_check(pending.fence_after.wait_finished && + (cancelled?woke_cancelled:(woke_cancelled || woke_stopped)) && + (cancelled?pending.fence_after.state==SenderFence::State::cancelled: + pending.fence_after.state==SenderFence::State::aborted && pending.fence_after.abort_reason==SenderFence::AbortReason::stop), + "UI action did not wake its held writer through cancel/stop"); const auto& progress=pending.after; paste_check(progress.total==large_units*3 && progress.acceptedsnapshot(fence->ticket());const auto progress=transfer->progress(); + paste_check(state.wait_finished && progress.state==tf_shell::PasteState::failed && !progress.in_flight && + progress.retained_capacity<=32,"control failure did not release writer-owned payload"); + std::ofstream out(path,std::ios::binary); + out<<"{\"eof_observed\":"<<(control->eof_observed()?"true":"false") + <<",\"timed_out\":"<<(state.state==SenderFence::State::timed_out?"true":"false") + <<",\"wait_finished\":"<<(state.wait_finished?"true":"false") + <<",\"state\":\"failed\",\"in_flight\":"< +@@ -224,10 +225,12 @@ + + void CandidateTerminal::feed(std::string_view bytes) { + std::array error{}; +- check(tf_terminal_feed(impl_, reinterpret_cast(bytes.data()), bytes.size(), +- error.data(), error.size()), "candidate feed failed"); +- scan_replay(bytes); +- synchronize(); ++ { ProbeTimer timer(0); ++ check(tf_terminal_feed(impl_, reinterpret_cast(bytes.data()), bytes.size(), ++ error.data(), error.size()), "candidate feed failed"); } ++ { ProbeTimer timer(1); scan_replay(bytes); } ++ { ProbeTimer timer(2); synchronize(); } ++ ProbeTimer event_timer(3); + uint32_t kind = 0; + for (;;) { + size_t needed = 0; +@@ -295,6 +298,7 @@ + std::vector retained(meta.history + meta.rows); + std::vector raw_ids; + raw_ids.reserve(retained.size()); ++ { ProbeTimer export_timer(4); + for (size_t r = 0; r < retained.size(); ++r) { + TfRowMeta row{}; + check(tf_terminal_row_meta(impl_, r, &row), "candidate row metadata failed"); +@@ -313,6 +317,8 @@ + } + retained[r] = std::move(line); + } ++ } ++ ProbeTimer reconcile_timer(5); + const auto preserved = reconcile_replay(retained); + if (replay_show_) preserve_history_ = true; + if (preserve_history_ && !replay_origin_ && meta.history == 0 && meta.discarded == screen.discarded) diff --git a/experiments/shell/results/2026-10-26-replay-cost-and-close/probe/ordinary_probe.cpp b/experiments/shell/results/2026-10-26-replay-cost-and-close/probe/ordinary_probe.cpp new file mode 100644 index 00000000..7980cfd9 --- /dev/null +++ b/experiments/shell/results/2026-10-26-replay-cost-and-close/probe/ordinary_probe.cpp @@ -0,0 +1,53 @@ +#define NOMINMAX +#include +#include "candidate_terminal.hpp" +#include "text_projection.hpp" +#include "probe_timing.hpp" +#include +#include +#include +#include +#include + +uint64_t cpu_ns() { + FILETIME created{}, exited{}, kernel{}, user{}; + if (!GetProcessTimes(GetCurrentProcess(), &created, &exited, &kernel, &user)) + throw std::runtime_error("GetProcessTimes failed"); + const auto value = [](const FILETIME& ft) { return (uint64_t(ft.dwHighDateTime)<<32) | ft.dwLowDateTime; }; + return (value(kernel)+value(user))*100; +} +int main(int argc, char** argv) { + try { + if(argc!=3) throw std::runtime_error("usage: phase_probe input.bin state.json"); + std::ifstream input(argv[1], std::ios::binary); + if(!input) throw std::runtime_error("input missing"); + const std::string bytes((std::istreambuf_iterator(input)), {}); + if(bytes.size()!=2048000) throw std::runtime_error("unexpected workload"); + termforge::CandidateTerminal terminal(17,74,100); + probe_ns.fill(0); probe_count.fill(0); probe_max.fill(0); + const auto cpu_start=cpu_ns(); const auto start=ProbeTimer::Clock::now(); + for(size_t i=0;i(ProbeTimer::Clock::now()-start).count(); + const auto cpu_elapsed=cpu_ns()-cpu_start; + terminal.finish(); + const auto& screen=terminal.screen; + if(screen.row!=16 || screen.column!=0 || screen.history.size()!=100 || screen.retained_size()!=117 || screen.discarded!=31884) + throw std::runtime_error("final cursor/history mismatch"); + for(size_t r=0;r<116;++r) { + std::ostringstream expected; + expected<<"TF_R108_F1_"< +#include +#include +inline std::array probe_ns{}, probe_max{}, probe_count{}; +struct ProbeTimer { + using Clock = std::chrono::steady_clock; + size_t phase; + Clock::time_point start = Clock::now(); + explicit ProbeTimer(size_t value) : phase(value) {} + ~ProbeTimer() { + const auto elapsed = static_cast(std::chrono::duration_cast(Clock::now()-start).count()); + probe_ns[phase] += elapsed; ++probe_count[phase]; + if (elapsed > probe_max[phase]) probe_max[phase] = elapsed; + } +}; diff --git a/experiments/shell/results/2026-10-26-replay-cost-and-close/probe/resize_probe.cpp b/experiments/shell/results/2026-10-26-replay-cost-and-close/probe/resize_probe.cpp new file mode 100644 index 00000000..68fbf318 --- /dev/null +++ b/experiments/shell/results/2026-10-26-replay-cost-and-close/probe/resize_probe.cpp @@ -0,0 +1,54 @@ +#define NOMINMAX +#include +#include "candidate_terminal.hpp" +#include "text_projection.hpp" +#include "probe_timing.hpp" +#include +#include +#include +#include +#include + +uint64_t cpu_ns() { + FILETIME created{}, exited{}, kernel{}, user{}; + if (!GetProcessTimes(GetCurrentProcess(), &created, &exited, &kernel, &user)) + throw std::runtime_error("GetProcessTimes failed"); + const auto value = [](const FILETIME& ft) { return (uint64_t(ft.dwHighDateTime)<<32) | ft.dwLowDateTime; }; + return (value(kernel)+value(user))*100; +} +int main(int argc, char** argv) { + try { + if(argc!=4) throw std::runtime_error("usage: phase_probe input.bin state.json resize|plain"); + std::ifstream input(argv[1], std::ios::binary); + if(!input) throw std::runtime_error("input missing"); + const std::string bytes((std::istreambuf_iterator(input)), {}); + if(bytes.size()!=204800) throw std::runtime_error("unexpected workload"); + termforge::CandidateTerminal terminal(17,74,100); + if(std::string(argv[3])=="resize") terminal.resize(17,74); + probe_ns.fill(0); probe_count.fill(0); probe_max.fill(0); + const auto cpu_start=cpu_ns(); const auto start=ProbeTimer::Clock::now(); + for(size_t i=0;i(ProbeTimer::Clock::now()-start).count(); + const auto cpu_elapsed=cpu_ns()-cpu_start; + terminal.finish(); + const auto& screen=terminal.screen; + if(screen.row!=16 || screen.column!=0 || screen.history.size()!=100 || screen.retained_size()!=117 || screen.discarded!=3084) + throw std::runtime_error("final cursor/history mismatch"); + for(size_t r=0;r<116;++r) { + std::ostringstream expected; + expected<<"TF_R108_F1_"< +@@ -224,10 +225,11 @@ + + void CandidateTerminal::feed(std::string_view bytes) { + std::array error{}; +- check(tf_terminal_feed(impl_, reinterpret_cast(bytes.data()), bytes.size(), +- error.data(), error.size()), "candidate feed failed"); +- scan_replay(bytes); +- synchronize(); ++ { UiPhase phase_core(10); ++ check(tf_terminal_feed(impl_, reinterpret_cast(bytes.data()), bytes.size(), ++ error.data(), error.size()), "candidate feed failed"); } ++ { UiPhase phase_scan(11); scan_replay(bytes); } ++ { UiPhase phase_sync(12); synchronize(); } + uint32_t kind = 0; + for (;;) { + size_t needed = 0; +@@ -295,6 +297,7 @@ + std::vector retained(meta.history + meta.rows); + std::vector raw_ids; + raw_ids.reserve(retained.size()); ++ { UiPhase phase_export(13); + for (size_t r = 0; r < retained.size(); ++r) { + TfRowMeta row{}; + check(tf_terminal_row_meta(impl_, r, &row), "candidate row metadata failed"); +@@ -313,11 +316,16 @@ + } + retained[r] = std::move(line); + } +- const auto preserved = reconcile_replay(retained); ++ } ++ std::unordered_set preserved; ++ { UiPhase phase_reconcile(14); preserved = reconcile_replay(retained); } + if (replay_show_) preserve_history_ = true; + if (preserve_history_ && !replay_origin_ && meta.history == 0 && meta.discarded == screen.discarded) + preserve_history_ = false; ++ { UiPhase phase_merge(15); + merge_history(retained, raw_ids, meta.history, meta.discarded); ++ } ++ UiPhase phase_versions(16); + if (replay_show_) replay_origin_.reset(); + screen.normalize_identities(retained); + for (size_t r = 0; r + 1 < retained.size(); ++r) diff --git a/experiments/shell/results/2026-10-26-replay-cost-and-close/probe/ui-instrumentation.patch b/experiments/shell/results/2026-10-26-replay-cost-and-close/probe/ui-instrumentation.patch new file mode 100644 index 00000000..fefd5eac --- /dev/null +++ b/experiments/shell/results/2026-10-26-replay-cost-and-close/probe/ui-instrumentation.patch @@ -0,0 +1,103 @@ +--- experiments/shell/shell.cpp ++++ experiments/shell/shell.cpp +@@ -1,3 +1,4 @@ ++#include "local/r53-cost-probe/ui_timing.hpp" + // Local shell with physical-row resize. See README for the limited contract. + #include "../presentation/render.hpp" + #ifdef TERMFORGE_CANDIDATE_CORE +@@ -367,7 +368,7 @@ + closed_.store(true); + }); + } +- void publish(std::optional scroll_operation=std::nullopt,bool scroll_changed=false) { ++ void publish(std::optional scroll_operation=std::nullopt,bool scroll_changed=false) { UiPhase probe(3); + selection_.reconcile(termforge::TextDocument(parser_.screen)); + auto view = viewport_.snapshot(parser_.screen); + if(runtime_enabled_) runtime_published_viewport_=view.viewport; +@@ -617,7 +618,7 @@ + stage_ = 6; + #endif + } +- void scenario() { ++ void scenario() { UiPhase probe(5); + if (mode_.empty() || closing_) return; + require(ms(start_) < 25000, "shell scenario timeout"); + if (mode_ == "watchdog") { +@@ -778,7 +779,7 @@ + } + } + #endif +- void tick() { ++ void tick() { UiPhase probe(0); + const auto began = Clock::now(); ++result_.ticks; + poll_paste(); + selection_autoscroll(); +@@ -793,7 +794,7 @@ + if(observed_output_.size()>8192) observed_output_.erase(0,observed_output_.size()-8192); + } + resize_trace_.observe(tf_shell_test::ResizeTrace::feed, parser_.screen.height(), parser_.screen.width(), 0, stage_, bytes); +- parser_.feed(bytes); batch += bytes.size(); ++ { UiPhase probe(1); parser_.feed(bytes); } batch += bytes.size(); + #ifdef TERMFORGE_CANDIDATE_CORE + route_terminal_events(); + #endif +@@ -914,7 +915,7 @@ + } + if (message == WM_TIMER && (self->started_ || self->closing_)) { self->tick(); return 0; } + if (message == WM_ERASEBKGND) return 1; +- if (message == WM_PAINT) { ++ if (message == WM_PAINT) { UiPhase probe(6); + PAINTSTRUCT paint{}; HDC dc = BeginPaint(hwnd, &paint); + const int painted_revision=self->snapshot_->revision; + const bool observed=self->operation_observations_.paint_candidate(painted_revision); +--- experiments/shell/search_interactions.hpp ++++ experiments/shell/search_interactions.hpp +@@ -12,7 +12,7 @@ + find_->focus_query(); + } + +-inline void ShellWindow::search_changed() { ++inline void ShellWindow::search_changed() { UiPhase probe(2); + const termforge::TextDocument document(parser_.screen); + search_.notify_change(document); + if(find_resize_pending_ && !find_resize_anchor_.reconcile(document)) clear_find_resize_anchor(); +@@ -66,7 +66,7 @@ + publish(); + } + +-inline void ShellWindow::poll_find() { ++inline void ShellWindow::poll_find() { UiPhase probe(4); + if(closing_) return; + termforge::TextDocument document(parser_.screen); + if(find_) { +--- experiments/shell/workspace_workflow_scenario.hpp ++++ experiments/shell/workspace_workflow_scenario.hpp +@@ -44,7 +44,7 @@ + require(FlushFileBuffers(evidence.resource_phase_file.get())!=FALSE,"workflow resource phase flush failed"); + } + } +-inline void Workspace::workflow_checkpoint(const char* phase,unsigned cycle,const std::vector& names) { ++inline void Workspace::workflow_checkpoint(const char* phase,unsigned cycle,const std::vector& names) { UiPhase probe(8); + auto& evidence=*workflow_;evidence.reserve_observations(names.empty()?static_cast(session_count()):names.size()); + workflow_resource_mark(phase,cycle); + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION job{}; +@@ -169,7 +169,7 @@ + if(workflow_system_ && stage_==100 && workflow_->closed.size()==18) workflow_system_finish();return; + } + } +-inline void Workspace::scenario_workflow() { ++inline void Workspace::scenario_workflow() { UiPhase probe(7); + if(image_.empty() || closing_) return; + require(workflow_!=nullptr,"workflow scenario state is missing"); + if(ms(started_)>=70000) throw std::runtime_error("workflow sessions scenario timeout at stage "+std::to_string(stage_)); +--- experiments/shell/workspace.hpp ++++ experiments/shell/workspace.hpp +@@ -381,7 +381,7 @@ + if(!settings_error_.empty()) result+=L": "+utf16(settings_error_.c_str()); + return result; + } +- void paint(HDC dc) { ++ void paint(HDC dc) { UiPhase probe(9); + auto old=SelectObject(dc,font_); SetBkMode(dc,TRANSPARENT); + fill(dc,{0,0,width_,height_},background); + label(dc,{0,0,82,33},L"Session"); label(dc,{82,0,150,33},L"View"); diff --git a/experiments/shell/results/2026-10-26-replay-cost-and-close/probe/ui_timing.hpp b/experiments/shell/results/2026-10-26-replay-cost-and-close/probe/ui_timing.hpp new file mode 100644 index 00000000..9b576009 --- /dev/null +++ b/experiments/shell/results/2026-10-26-replay-cost-and-close/probe/ui_timing.hpp @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include +#include +#include +struct UiProfile { + std::array ns{}, count{}, maximum{}; + ~UiProfile() { + const char* path=std::getenv("TERMFORGE_DIAGNOSTIC_PHASE_OUT"); + if(!path) return; + std::ofstream out(path); + out<<"{\"phases\":["; + for(size_t i=0;i(std::chrono::duration_cast(Clock::now()-start).count()); + ui_profile.ns[phase]+=elapsed; ++ui_profile.count[phase]; + if(elapsed>ui_profile.maximum[phase]) ui_profile.maximum[phase]=elapsed; + } +}; diff --git a/experiments/shell/results/2026-10-26-replay-cost-and-close/ui-profiles.json b/experiments/shell/results/2026-10-26-replay-cost-and-close/ui-profiles.json new file mode 100644 index 00000000..ebc26ccf --- /dev/null +++ b/experiments/shell/results/2026-10-26-replay-cost-and-close/ui-profiles.json @@ -0,0 +1,699 @@ +[ + { + "run": "ui-run-2b18652f1a8841f5a097e28148ff0d34", + "sample": { + "mode": "ui-workflow-sessions", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "error_kind": "ProbeFailure" + }, + "data_only": true, + "system_clipboard": false, + "ime": false, + "checkpoints": [ + { + "phase": "eight-ready", + "elapsed_ms": 579, + "cycle": 0 + }, + { + "phase": "reject-ninth", + "elapsed_ms": 583, + "cycle": 0 + }, + { + "phase": "load-started", + "elapsed_ms": 857, + "cycle": 0 + }, + { + "phase": "reading-before-append", + "elapsed_ms": 881, + "cycle": 0 + }, + { + "phase": "reading-after-append", + "elapsed_ms": 1130, + "cycle": 0 + }, + { + "phase": "search-running", + "elapsed_ms": 1392, + "cycle": 0 + }, + { + "phase": "find-complete", + "elapsed_ms": 11461, + "cycle": 0 + }, + { + "phase": "foreground-input", + "elapsed_ms": 11678, + "cycle": 0 + }, + { + "phase": "zoom-75", + "elapsed_ms": 11894, + "cycle": 0 + }, + { + "phase": "switch-zoom", + "elapsed_ms": 12152, + "cycle": 0 + }, + { + "phase": "history-evicted", + "elapsed_ms": 12611, + "cycle": 0 + }, + { + "phase": "paste-cancel-pending", + "elapsed_ms": 13775, + "cycle": 0 + }, + { + "phase": "paste-cancelled", + "elapsed_ms": 14486, + "cycle": 0 + }, + { + "phase": "background-complete", + "elapsed_ms": 50071, + "cycle": 0 + }, + { + "phase": "cycle-six", + "elapsed_ms": 50552, + "cycle": 1 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 51912, + "cycle": 1 + }, + { + "phase": "cycle-six", + "elapsed_ms": 52683, + "cycle": 2 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 54041, + "cycle": 2 + }, + { + "phase": "cycle-six", + "elapsed_ms": 54799, + "cycle": 3 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 56168, + "cycle": 3 + }, + { + "phase": "cycle-six", + "elapsed_ms": 56961, + "cycle": 4 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 58253, + "cycle": 4 + }, + { + "phase": "cycle-six", + "elapsed_ms": 58889, + "cycle": 5 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 60200, + "cycle": 5 + }, + { + "phase": "paste-close-pending", + "elapsed_ms": 60724, + "cycle": 5 + } + ], + "paste_cases": [ + { + "name": "cancel", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 18414, + "flood_observed": true, + "fence": { + "generation": 1, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "cancelled", + "wrong_generation_rejected": true + } + }, + { + "name": "close", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 768, + "flood_observed": true, + "fence": { + "generation": 2, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "stopped", + "wrong_generation_rejected": true + } + } + ], + "raw_sha256": { + "cancel.bin": "bcc3593351da0137a27071da66416a7cbe02ca5605f49b14e5a6c85ebac894a6", + "cancel.bin.before.json": "a90746e62c8e815c76305806dfff25d55dd34d67f54308b0c865152693425663", + "close.bin": "61da7e55d911305206ae68ac162a0a73d2fc1433efae5f0cfda2b9d0c9b5363c", + "close.bin.before.json": "42d48c9b60c19059b0fd753b871521787ece886f5976aef804fe05baa7db1584", + "identity.json": "bd2605c691401cbaba684f8b3dbf8d5ec4d87004dc5cc2a99043dd7137e0bd05", + "resource-phases.jsonl": "eb0a3de3035663d0086328cc52f98dac18a5e553f64197057a516169a4be56f3", + "sample.json": "7159224e988be562a77cbad22953c4bb2ef47b7ccc19ac9593c037630bde513e", + "sessions.json": "6ef9405ea3055273463e9d72fa467f7df9e9c4459dffb7a203a061c7dbaa3e94", + "ui-phases.json": "6644c6534aa391b9bd4bf2a6c344c4e8d9d1548c4c2fc3656e589b8a61809997", + "workspace.layout": "9a96d62943f54727e7c50ce5785a571efbfa6cbece6248f6a6d144257c1e1d58" + }, + "observed_failure": "workflow sessions scenario timeout at stage 45", + "instrumentation": { + "phases": [ + "whole_tick", + "parser_feed", + "search_changed", + "publish", + "poll_find", + "shell_scenario", + "shell_paint", + "workflow_scenario", + "workflow_checkpoint", + "workspace_paint" + ], + "source": { + "experiments/shell/shell.cpp": { + "original": "b369968152bbff107c699181d4e23f271e62a5732f0c41793159b3efc3609321", + "instrumented": "0e0cfbd952c37d05a0333bd79662ead2777dd19db220f08993a762437cb9bb77" + }, + "experiments/shell/search_interactions.hpp": { + "original": "75f1ac520dc3070efba2d01812bf56c29e8683540196956423b0984c42d7b181", + "instrumented": "b337e56639d273b45faca154ed20830157991b6e158834e3c377a1a74c6f4b55" + }, + "experiments/shell/workspace_workflow_scenario.hpp": { + "original": "467571556440f4e89305bc1261d389bb8f24ba84f607b8cd36b8078a58314369", + "instrumented": "2a95c33f6b6212f61c8217fd55298b16ca90209f8b9cd972e7dc135e94fc4c57" + }, + "experiments/shell/workspace.hpp": { + "original": "5669a13701b2fe60e8bd567a122edb970bdc4aadcb98ceb3927905f0c000275e", + "instrumented": "9cbe9e5db3529e578eec705307d9cab23035623b3f46ac9d14d6838f7ab7c61e" + } + }, + "original_exe": "0a1fee632cb640dff4599fdcca277c959038d31ec88bfed8cff893b3271d26a6", + "instrumented_exe": "babd7b24d89ae970f614c6d4b6090054db9df3a40dfbcbdcadaf53b5b2e79f24", + "resources": false + }, + "phase_wall_times": { + "phases": [ + { + "id": 0, + "ns": 64631951400, + "max_ns": 55502600, + "count": 4896 + }, + { + "id": 1, + "ns": 64079457000, + "max_ns": 55183600, + "count": 1771 + }, + { + "id": 2, + "ns": 335900, + "max_ns": 24500, + "count": 1806 + }, + { + "id": 3, + "ns": 500625100, + "max_ns": 8645600, + "count": 1958 + }, + { + "id": 4, + "ns": 73427600, + "max_ns": 9092300, + "count": 4896 + }, + { + "id": 5, + "ns": 745500, + "max_ns": 20500, + "count": 4754 + }, + { + "id": 6, + "ns": 1053698100, + "max_ns": 10532400, + "count": 233 + }, + { + "id": 7, + "ns": 1137670500, + "max_ns": 268286300, + "count": 611 + }, + { + "id": 8, + "ns": 157240300, + "max_ns": 17637500, + "count": 25 + }, + { + "id": 9, + "ns": 160054100, + "max_ns": 40076700, + "count": 48 + } + ] + }, + "limitations": [ + "nested inclusive wall timers; do not add phases", + "instrumentation overhead", + "resource sampler disabled for diagnosis", + "shared host; not formal R54 samples", + "no actual presented endpoint; no user-approved performance tolerance" + ] + }, + { + "run": "ui-run-82b1582d98de4cc1bd659c481ee99cfa", + "sample": { + "mode": "ui-workflow-sessions", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "error_kind": "ProbeFailure" + }, + "data_only": true, + "system_clipboard": false, + "ime": false, + "checkpoints": [ + { + "phase": "eight-ready", + "elapsed_ms": 603, + "cycle": 0 + }, + { + "phase": "reject-ninth", + "elapsed_ms": 606, + "cycle": 0 + }, + { + "phase": "load-started", + "elapsed_ms": 839, + "cycle": 0 + }, + { + "phase": "reading-before-append", + "elapsed_ms": 870, + "cycle": 0 + }, + { + "phase": "reading-after-append", + "elapsed_ms": 1104, + "cycle": 0 + }, + { + "phase": "search-running", + "elapsed_ms": 1350, + "cycle": 0 + }, + { + "phase": "find-complete", + "elapsed_ms": 11017, + "cycle": 0 + }, + { + "phase": "foreground-input", + "elapsed_ms": 11226, + "cycle": 0 + }, + { + "phase": "zoom-75", + "elapsed_ms": 11434, + "cycle": 0 + }, + { + "phase": "switch-zoom", + "elapsed_ms": 11664, + "cycle": 0 + }, + { + "phase": "history-evicted", + "elapsed_ms": 12079, + "cycle": 0 + }, + { + "phase": "paste-cancel-pending", + "elapsed_ms": 13120, + "cycle": 0 + }, + { + "phase": "paste-cancelled", + "elapsed_ms": 14002, + "cycle": 0 + }, + { + "phase": "background-complete", + "elapsed_ms": 47021, + "cycle": 0 + }, + { + "phase": "cycle-six", + "elapsed_ms": 47482, + "cycle": 1 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 48797, + "cycle": 1 + }, + { + "phase": "cycle-six", + "elapsed_ms": 49478, + "cycle": 2 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 50760, + "cycle": 2 + }, + { + "phase": "cycle-six", + "elapsed_ms": 51410, + "cycle": 3 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 52749, + "cycle": 3 + }, + { + "phase": "cycle-six", + "elapsed_ms": 53355, + "cycle": 4 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 54707, + "cycle": 4 + }, + { + "phase": "cycle-six", + "elapsed_ms": 55349, + "cycle": 5 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 56623, + "cycle": 5 + }, + { + "phase": "paste-close-pending", + "elapsed_ms": 57079, + "cycle": 5 + } + ], + "paste_cases": [ + { + "name": "cancel", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 18414, + "flood_observed": true, + "fence": { + "generation": 1, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "cancelled", + "wrong_generation_rejected": true + } + }, + { + "name": "close", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 768, + "flood_observed": true, + "fence": { + "generation": 2, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "stopped", + "wrong_generation_rejected": true + } + } + ], + "raw_sha256": { + "cancel.bin": "bcc3593351da0137a27071da66416a7cbe02ca5605f49b14e5a6c85ebac894a6", + "cancel.bin.before.json": "a90746e62c8e815c76305806dfff25d55dd34d67f54308b0c865152693425663", + "close.bin": "61da7e55d911305206ae68ac162a0a73d2fc1433efae5f0cfda2b9d0c9b5363c", + "close.bin.before.json": "42d48c9b60c19059b0fd753b871521787ece886f5976aef804fe05baa7db1584", + "identity.json": "7dc04a9a23392081f4b638214f91063c0a4b1704727470d1d14394d37aa97cde", + "resource-phases.jsonl": "0ed18c34974f8bf7e8d3e20949384e52b911672723293df5a3acf8c036cfb646", + "sample.json": "4d3fdb3f0561f468199900f2dfaaf4ed980cc1bdcdb9a1fa2db249676c170bbc", + "sessions.json": "1197f7ec3928c0dd08d8497610fe95b5171aa96559e9058e4ce1669f2fdc86cf", + "ui-phases.json": "cf363bd31befa7c37a6495591d630b1f0b9b164c07b1a9baa7871f6b1032aae4", + "workspace.layout": "9a96d62943f54727e7c50ce5785a571efbfa6cbece6248f6a6d144257c1e1d58" + }, + "observed_failure": "workflow sessions scenario timeout at stage 45", + "instrumentation": { + "phases": [ + "whole_tick", + "parser_feed", + "search_changed", + "publish", + "poll_find", + "shell_scenario", + "shell_paint", + "workflow_scenario", + "workflow_checkpoint", + "workspace_paint", + "core_feed", + "scan_replay", + "synchronize", + "cell_export", + "reconcile_replay", + "merge_history", + "versions_copy" + ], + "source": { + "experiments/shell/shell.cpp": { + "original": "b369968152bbff107c699181d4e23f271e62a5732f0c41793159b3efc3609321", + "instrumented": "0e0cfbd952c37d05a0333bd79662ead2777dd19db220f08993a762437cb9bb77" + }, + "experiments/shell/search_interactions.hpp": { + "original": "75f1ac520dc3070efba2d01812bf56c29e8683540196956423b0984c42d7b181", + "instrumented": "b337e56639d273b45faca154ed20830157991b6e158834e3c377a1a74c6f4b55" + }, + "experiments/shell/workspace_workflow_scenario.hpp": { + "original": "467571556440f4e89305bc1261d389bb8f24ba84f607b8cd36b8078a58314369", + "instrumented": "2a95c33f6b6212f61c8217fd55298b16ca90209f8b9cd972e7dc135e94fc4c57" + }, + "experiments/shell/workspace.hpp": { + "original": "5669a13701b2fe60e8bd567a122edb970bdc4aadcb98ceb3927905f0c000275e", + "instrumented": "9cbe9e5db3529e578eec705307d9cab23035623b3f46ac9d14d6838f7ab7c61e" + }, + "experiments/core/candidate_terminal.cpp": { + "original": "0d0eaabe5ef896aa7b263c8151622f579f37c421bf9473fc3d69ada010a64e61", + "instrumented": "5138c797086f84f584a254894004286257a1434bf2903ffcafd3751acb60ebba" + } + }, + "original_exe": "0a1fee632cb640dff4599fdcca277c959038d31ec88bfed8cff893b3271d26a6", + "instrumented_exe": "23b92b9087d836947bf848da3de7e04f624bada0c6ca6284690ea63719b6668f", + "resources": false + }, + "phase_wall_times": { + "phases": [ + { + "id": 0, + "ns": 64458192200, + "max_ns": 56417700, + "count": 5278 + }, + { + "id": 1, + "ns": 63986560900, + "max_ns": 56035900, + "count": 1892 + }, + { + "id": 2, + "ns": 449600, + "max_ns": 17600, + "count": 1922 + }, + { + "id": 3, + "ns": 432217300, + "max_ns": 3678600, + "count": 2074 + }, + { + "id": 4, + "ns": 52400300, + "max_ns": 4067000, + "count": 5278 + }, + { + "id": 5, + "ns": 787000, + "max_ns": 88700, + "count": 5137 + }, + { + "id": 6, + "ns": 958451800, + "max_ns": 11927600, + "count": 233 + }, + { + "id": 7, + "ns": 1086472600, + "max_ns": 239174200, + "count": 671 + }, + { + "id": 8, + "ns": 149742300, + "max_ns": 18294300, + "count": 25 + }, + { + "id": 9, + "ns": 141821100, + "max_ns": 21889600, + "count": 48 + }, + { + "id": 10, + "ns": 559755900, + "max_ns": 3343000, + "count": 1892 + }, + { + "id": 11, + "ns": 4690100, + "max_ns": 51400, + "count": 1892 + }, + { + "id": 12, + "ns": 63419742700, + "max_ns": 55759500, + "count": 1892 + }, + { + "id": 13, + "ns": 2031090200, + "max_ns": 7882100, + "count": 1930 + }, + { + "id": 14, + "ns": 60905226900, + "max_ns": 54182500, + "count": 1930 + }, + { + "id": 15, + "ns": 317300, + "max_ns": 3000, + "count": 1930 + }, + { + "id": 16, + "ns": 363684200, + "max_ns": 3660300, + "count": 1930 + } + ] + }, + "limitations": [ + "nested inclusive wall timers; do not add phases", + "instrumentation overhead", + "resource sampler disabled for diagnosis", + "shared host; not formal R54 samples", + "no actual presented endpoint; no user-approved performance tolerance" + ] + } +] diff --git a/experiments/shell/results/2026-10-26-replay-cost-and-close/validation.json b/experiments/shell/results/2026-10-26-replay-cost-and-close/validation.json new file mode 100644 index 00000000..a575046f --- /dev/null +++ b/experiments/shell/results/2026-10-26-replay-cost-and-close/validation.json @@ -0,0 +1,1513 @@ +{ + "schema": "termforge-replay-cost-closing-reply-v1", + "date": "2026-10-26", + "issue": 53, + "source_commit": "eae4b6a4ef3e044ebfebbbbfeccf9aec74cd7f0d", + "source_tree": "b07036829c5c8bd61ede6a8aa46f655e541ada1b", + "source_files": { + "CMakeLists.txt": { + "git_blob": "2454a7b8dc34fa2bbd74b25b0ad77d19a71a5f18", + "checkout_sha256": "f5bb85e12b7a815c4feb7417da9ebc5298de58664bcd2792b277cb282729da73" + }, + "experiments/core/candidate_terminal.cpp": { + "git_blob": "9bb1fab8f6deac294d0ba1a44712b67c919d03c7", + "checkout_sha256": "3092ee2ea44ca0f897a7d3b6244c079ba8073dc109993204e435f065388a2ed1" + }, + "experiments/shell/shell.cpp": { + "git_blob": "90b3f166e8a50ca2488f749e8e188c52cb5e4a91", + "checkout_sha256": "ac4dd44861aa2928213fffe91d30cdaf3aedf19f08468aefeed22828352296cc" + }, + "experiments/shell/candidate_reply_close_test.cpp": { + "git_blob": "1a5e0149c553469645dbfe3ddb217908b1958d42", + "checkout_sha256": "453ab888fb57d4f918bcfa583da7167a21463ce185be2a3d67caec74505fc424" + }, + "experiments/shell/test_workflow_sessions.py": { + "git_blob": "de6cef3be06725d606b91ae68eb1cc45eb744fb5", + "checkout_sha256": "6c968a443c46b30e75a929f880228e6a23bf2b92d9221bbbf18816933babd6e7" + }, + "experiments/shell/test_shell.py": { + "git_blob": "66512ac1099cf477cac8f26f225b18c301c1d3d8", + "checkout_sha256": "a6b63c1754b0e0dc1cc01863891f4c7c9d989f818d351838a8cfe295edaf811c" + }, + "experiments/shell/run.py": { + "git_blob": "bbd4dfc05012c55956973d7e2c69be717b48b1a9", + "checkout_sha256": "45976b828eecb316881d698dedaef09ad304f72d991dfa360f1c552bbcbe9f1d" + }, + "experiments/shell/workflow_sessions_checks.py": { + "git_blob": "8369b412abe893393bf316430ccedd22e9eb75ba", + "checkout_sha256": "b976d3504837eef57ff13319daed254870727741f36f265412a6b14b4cb3faae" + } + }, + "environment": { + "os": "Windows-11-10.0.26200-SP0", + "architecture": "AMD64", + "compiler": "MSVC 19.42.34433", + "configuration": "Release x64", + "candidate": "alacritty_terminal 0.26.1-dev+termforge.4", + "backend": "Microsoft.Windows.Console.ConPTY 1.25.260930003", + "glyph_width_flags": 24 + }, + "request": { + "utf8_bytes": 3000000, + "utf16_units": 1000000, + "literal": "U+6C49 repeated 1000000", + "sha256": "0ba67970de9941c1bc33e97869b116a0e129c8a859e3b4c0087e20c51f6a404b", + "minimum_sent": 16384, + "receiver_checkpoint_bytes": 768 + }, + "deadlines_ms": { + "standalone_ui": 35000, + "standalone_outer": 40000, + "receiver_gate": 15000, + "eight_session_ui": 70000, + "workflow_fixture": 55000, + "missing_go_negative_only": 2000 + }, + "binaries": { + "alpha_normal_smoke.exe": "36ac6dbe36c942b9850d6d9f5b3538fbfe2fc251f38ac026ea12c343b0e9abc8", + "candidate_core_test.exe": "6e9df1abbe2271690a1480a1be9042e219a5947779b6a4b03f529a742fbd10ce", + "candidate_reply_close_test.exe": "662a331701e49885ce56328ae19ddc2166347c5f802d7faa2787eef46e25b000", + "candidate_resize_replay.exe": "3d91227adb37bba4defcef8f803f0f3c2916001223300b99d18e0e2ab695de94", + "candidate_title_test.exe": "a9591c36d5a54a1f54cf30ec8f8b0e7ff047096f5adf2e2a74169252ab041852", + "clipboard_policy_test.exe": "809815e9838cad5b3443c98620d23a4e6c7186b5304b524d0990988eb47a6da9", + "conpty_backend_probe.exe": "3c54962c20f04e688a0413bb822713e908582af20b0be8bcfd188fb436907ef8", + "conpty_ctrl_c_probe.exe": "ac5ae5ccdb77c82512a8a6d769fafbe74693bf6fc535ce4d22bf60b0c8df2bfa", + "conpty_margin_probe.exe": "5404b009d0e005cd940d8a65afeb0fb9caf9c9ce2a18f651a88146343c41c252", + "core_measure.exe": "df9df797b16239d2fb71707f8a042a1ed956ad30c407d884527cdf18d600e785", + "find_bar_test.exe": "030733a2396ca9b31bbd029c610f1b838ab52394338c08410274501747dd9982", + "history_test.exe": "035507366facfd6de00e0c2762775c8d3816f605da3e55d498995a74a3f447d2", + "ime_context_test.exe": "be0d71ca72aee3dbd1565c9ad2314f81f0cd0c5511ec54cb623bbd39eede9974", + "ime_input_expectation_test.exe": "a5ea4532a06b893d955299793b4859cb9ccf221fab64e51949e1af2ee6ba5d0b", + "keyboard_test.exe": "18a35c2b2ab8a6316e0c938d8c36d0b82bcc131989d683205888b08bf4d33e19", + "layout_state_test.exe": "307519003536ce16d7d6fc552b3a25e9de25356d50a80c2de0c01168a10784a9", + "paste_test.exe": "47e9a5ce33335370b6fb199b0ff11adc19e2ab55bf64657061a13c1c020a24b0", + "presentation_probe.exe": "b7043cace46f65b22c7295df2fc06b214f060c2fd6fe18ac5ff27e13d943e096", + "raw_console_receiver_test.exe": "9c05cdb80d87ff264b3275387eb01dabc8f57640adbc7ee3910b0fc44f378224", + "real_app_capture.exe": "011efaad94da74ce182e30a3ff8abb0c1e1f58235bc77fa8c347ec3d9dcb822e", + "receive.exe": "ef316e11ddcc764ddac57426fc37b8b69f599ddb28a0ffedf5c2e85a3004a9d4", + "recovery_probe.exe": "c84e7b1b1792773b993f2758d5cafa59b80495451c01d194c3afd6cc653f0344", + "renderer_metrics_test.exe": "4cebad62798ff0f8eac42641a1119c7b9725728dd5fd31befcdf7e1ecb686cfd", + "replay.exe": "674f96ebbf00497576951a6780a5cf5195e3b80cc4acdb7f296fa8910a6203b7", + "resize_test.exe": "f566c56a1fa34e2a8ff480900ac5ae5059e2ac6872554208d5dbf0e24673dde0", + "runtime_observation_test.exe": "b721e08397dc0a1b80cff3cc9b3354d5ba81c59d9ef3e0934a7ec27fb2f5eebe", + "scenario_tick_gate_test.exe": "06cd4e813f1034d1a8c7fea592979f49c3ef89117c97fc6636cdae5012017ce8", + "search_test.exe": "0f29f3ea3147cc45eb2e500e683f3bcc1db04f820470b2ff473fffcb31cdb4f4", + "selection_test.exe": "104576787b62c2c4bb2428e0786c7742e63a23ceb766df293c88a5ccc6a1b836", + "sender_fence_pipe_test.exe": "6a94ac84631434a81ae1098932813d7ceeb81f2bb33a2e4b2456834bd8972e92", + "sender_fence_test.exe": "ab4af3b0f148f6e6377e70c98936c6c34d0be0d3cee4b17b3f8554c39b2d3e52", + "sent_message_wait_test.exe": "bc3cff14f60e3f1b17b3fd6255ff1c5b507f506d897d528802efa9666be74dcf", + "shell_clipboard_reader.exe": "bacc4bff465ca644fae61f9d26184a7adc22df9793470feca647af1d823fece8", + "shell_ime_client.exe": "d787ebf702cf5895e1ad0d71f895a51b3dead34d02953b5e7deff086c2bf8eb7", + "shell_input_client.exe": "850c82fb8bb58de8170bf096cc6dd4f8ba34d9ba11dbd2addc41508cddc31d8c", + "shell_launch_test.exe": "ace9b63148848bf7b405edb4b8818ca23d52699ac2ce8ca03a90c5bc1e32622d", + "shell_paste_probe.exe": "1370c6381b990270dfd816f53758e5aa57c63deba050fa7cf0dbd0d5cbf4f315", + "shell_query_client.exe": "71419f7918bc58f4ee47d14f8e6de35aa1828ac838a73ed607797fc120f56886", + "shell_queue_probe.exe": "7f34d6ff66b3e00d56798e22a480fb20a5afa083cff65a9d689dcbca44dbe12d", + "shell_selection_text_client.exe": "dbad81a22ecce4508eada90ae445f9a75abfe1effd2571776ef31b32144c8ac4", + "shell_size_client.exe": "5394d9863df9a589012f0d9a31f7e4167e7bc4ea23e93f9711c0a67c540651f6", + "shell_test_launcher.exe": "dbde4249dc08c7df80120398ffd252aa5e4958e4ef640cc161ea4c2d28b9f291", + "shell_text_client.exe": "9485b03424e421a4018596cc9487e7feeb33c26d30820985aa45843fdcf315d9", + "termforge_conpty_probe.exe": "7dd59bc8d0309ad5b131cde1c3ed98819139b6bb70f505347c98aabe8396f618", + "termforge_ime_probe.exe": "788a186b5fafb6d155eeab74f78d4f7cb992a599937b3aa48c5efcf49affe993", + "termforge_shell.exe": "9c01fa0577d8fc922d239d8a711ac13d1e4989104aad68795230bfcd998b4eb6", + "terminal_trace.exe": "40e869753f4c00515ee0990f3732eac04f965fe2c3070f1bf772717476269f3d", + "text_action_policy_test.exe": "535b469c9aa0da7e77670578f75d04956d98e9f912d5c9a6c310ab796ab58d4f", + "transport_completion_test.exe": "b83b797cacd8aaa05bfe8697b54f86da4d0ffe618b37b9b7080dbe05a0e99ad9", + "workflow_fixture_test.exe": "a6624a6b1536c6f445f92e05ff72a5318ece47d455500f6adc467d94f9f04222", + "workspace_state_test.exe": "ac3fbc8a56d33efda33cc9040c731c183e0a8666e23fce48d368c1868797714d", + "conpty-backend/conpty.dll": "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a", + "conpty-backend/OpenConsole.exe": "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586" + }, + "vim": { + "version": "9.1 patches 1-532, MSYS2", + "sha256": "2e0fda63a89171adfc46c9417d5105cc39ed778cc69a890b83a4eabc34e7646a", + "scope": "test-process PATH only" + }, + "final_ctest": { + "started_at": "2026-10-26T15:52:21.7023013Z", + "completed_at": "2026-10-26T16:00:07.0924704Z", + "exit_code": 8, + "tests": [ + { + "number": 1, + "name": "shell_launch", + "result": "Passed", + "elapsed_seconds": 0.01 + }, + { + "number": 2, + "name": "candidate_title", + "result": "Passed", + "elapsed_seconds": 0.78 + }, + { + "number": 3, + "name": "candidate_reply_close", + "result": "Passed", + "elapsed_seconds": 0.36 + }, + { + "number": 4, + "name": "runtime_observation", + "result": "Passed", + "elapsed_seconds": 1.66 + }, + { + "number": 5, + "name": "ime_input_expectation", + "result": "Passed", + "elapsed_seconds": 0.01 + }, + { + "number": 6, + "name": "sent_message_wait", + "result": "Passed", + "elapsed_seconds": 0.3 + }, + { + "number": 7, + "name": "find_bar", + "result": "Passed", + "elapsed_seconds": 0.37 + }, + { + "number": 8, + "name": "renderer_metrics", + "result": "Passed", + "elapsed_seconds": 0.92 + }, + { + "number": 9, + "name": "ime_context", + "result": "Passed", + "elapsed_seconds": 0.35 + }, + { + "number": 10, + "name": "layout_state", + "result": "Passed", + "elapsed_seconds": 0.13 + }, + { + "number": 11, + "name": "workspace_state", + "result": "Passed", + "elapsed_seconds": 0.34 + }, + { + "number": 12, + "name": "transport_completion", + "result": "Passed", + "elapsed_seconds": 0.01 + }, + { + "number": 13, + "name": "scenario_tick_gate", + "result": "Passed", + "elapsed_seconds": 0.01 + }, + { + "number": 14, + "name": "candidate_core", + "result": "Passed", + "elapsed_seconds": 1.17 + }, + { + "number": 15, + "name": "resize_core", + "result": "Passed", + "elapsed_seconds": 0.02 + }, + { + "number": 16, + "name": "history_core", + "result": "Passed", + "elapsed_seconds": 0.02 + }, + { + "number": 17, + "name": "selection_core", + "result": "Passed", + "elapsed_seconds": 0.09 + }, + { + "number": 18, + "name": "search_core", + "result": "Passed", + "elapsed_seconds": 0.12 + }, + { + "number": 19, + "name": "paste", + "result": "Passed", + "elapsed_seconds": 0.01 + }, + { + "number": 20, + "name": "sender_fence", + "result": "Passed", + "elapsed_seconds": 1.03 + }, + { + "number": 21, + "name": "sender_fence_pipe", + "result": "Passed", + "elapsed_seconds": 0.38 + }, + { + "number": 22, + "name": "clipboard_policy", + "result": "Passed", + "elapsed_seconds": 0.01 + }, + { + "number": 23, + "name": "keyboard", + "result": "Passed", + "elapsed_seconds": 0.01 + }, + { + "number": 24, + "name": "text_action_policy", + "result": "Passed", + "elapsed_seconds": 0.01 + }, + { + "number": 25, + "name": "candidate_resize_replay", + "result": "Passed", + "elapsed_seconds": 0.56 + }, + { + "number": 26, + "name": "candidate_shell_tui", + "result": "***Failed", + "elapsed_seconds": 30.68 + }, + { + "number": 27, + "name": "core", + "result": "Passed", + "elapsed_seconds": 6.15 + }, + { + "number": 28, + "name": "recovery", + "result": "Passed", + "elapsed_seconds": 9.89 + }, + { + "number": 29, + "name": "measure", + "result": "Passed", + "elapsed_seconds": 4.03 + }, + { + "number": 30, + "name": "binary_paths", + "result": "Passed", + "elapsed_seconds": 0.18 + }, + { + "number": 31, + "name": "conpty_margin_contract", + "result": "Passed", + "elapsed_seconds": 0.36 + }, + { + "number": 32, + "name": "benchmark_tools", + "result": "Passed", + "elapsed_seconds": 5.57 + }, + { + "number": 33, + "name": "differential", + "result": "Passed", + "elapsed_seconds": 3.67 + }, + { + "number": 34, + "name": "ime_inventory", + "result": "Passed", + "elapsed_seconds": 50.6 + }, + { + "number": 35, + "name": "text_actions", + "result": "Passed", + "elapsed_seconds": 5.6 + }, + { + "number": 36, + "name": "workflow_fixture", + "result": "Passed", + "elapsed_seconds": 22.74 + }, + { + "number": 37, + "name": "workflow_sessions", + "result": "Passed", + "elapsed_seconds": 77.08 + }, + { + "number": 38, + "name": "workflow_resources", + "result": "Passed", + "elapsed_seconds": 0.6 + }, + { + "number": 39, + "name": "workflow_resource_phases", + "result": "Passed", + "elapsed_seconds": 4.35 + }, + { + "number": 40, + "name": "workflow_system_content", + "result": "Passed", + "elapsed_seconds": 0.16 + }, + { + "number": 41, + "name": "workflow_system_runner", + "result": "Passed", + "elapsed_seconds": 0.67 + }, + { + "number": 42, + "name": "copy_error_frame", + "result": "Passed", + "elapsed_seconds": 0.17 + }, + { + "number": 43, + "name": "paste_fence_evidence", + "result": "Passed", + "elapsed_seconds": 0.12 + }, + { + "number": 44, + "name": "resize_trace", + "result": "Passed", + "elapsed_seconds": 0.13 + }, + { + "number": 45, + "name": "research_guards", + "result": "Passed", + "elapsed_seconds": 5.06 + }, + { + "number": 46, + "name": "receive", + "result": "Passed", + "elapsed_seconds": 0.25 + }, + { + "number": 47, + "name": "conpty", + "result": "Passed", + "elapsed_seconds": 23.48 + }, + { + "number": 48, + "name": "conpty_ctrl_c", + "result": "Passed", + "elapsed_seconds": 4.42 + }, + { + "number": 49, + "name": "conpty_margin", + "result": "Passed", + "elapsed_seconds": 0.36 + }, + { + "number": 50, + "name": "conpty_backend", + "result": "Passed", + "elapsed_seconds": 1.24 + }, + { + "number": 51, + "name": "presentation", + "result": "Passed", + "elapsed_seconds": 4.05 + }, + { + "number": 52, + "name": "shell", + "result": "***Timeout", + "elapsed_seconds": 180.05 + }, + { + "number": 53, + "name": "selection_bytes", + "result": "Passed", + "elapsed_seconds": 3.65 + }, + { + "number": 54, + "name": "selection_context", + "result": "Passed", + "elapsed_seconds": 3.91 + }, + { + "number": 55, + "name": "raw_console_receiver", + "result": "Passed", + "elapsed_seconds": 0.41 + }, + { + "number": 56, + "name": "paste_system", + "result": "Passed", + "elapsed_seconds": 6.7 + } + ], + "ctest_log_sha256": "8aafbb86aff94166f27131a0730ae9f89047eabfd573df3009c1b2be7d60bef2", + "last_test_log_sha256": "c7c335df7b947320a6fcd0fdd22287d8bdb13aed18bc9ed95377b8c8dc2e64a2" + }, + "diagnostic_artifacts": [ + "headless-ordinary.json", + "headless-resize.json", + "ui-profiles.json", + "intermediate-workflows.json", + "closing-reply.json" + ], + "limitations": [ + "Only candidate .4 with explicit official backend validated here", + "Data-only eight-session contract; no hosted real clipboard or IME acceptance", + "Shared-host diagnostics are not R54 formal samples or WindTerm performance equivalence", + "No user-approved numeric budget; presentation endpoint unknown", + "Unrelated Vim, resize and workspace failures remain open" + ], + "eight_session_runs": [ + { + "run": "run-40c6fff18ecd453684ad2ee06a44c669", + "sample": { + "mode": "ui-workflow-sessions", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workflow-sessions", + "sessions_closed": 18, + "active_children_after": 0, + "presented_done": null + }, + "frame_sha256": "68fb528174c7f7edf2916ca1b6f1f7d2a6b80deeb4cec787539fb051bb344ee5", + "sessions_workflow": { + "schema": "termforge-workflow-sessions-identity-validation-v1", + "live_sessions": 8, + "checkpoints": 26, + "rejected_ninth": true, + "load_foreground_verified": true, + "data_workflow_verified": true, + "complete_workflow_verified": false, + "system_clipboard_verified": false, + "ime_verified": false + }, + "resources": { + "summary": "resource-summary.json", + "scope": "launcher-tree", + "performance_gate": "not_evaluated" + }, + "error_kind": null + }, + "data_only": true, + "system_clipboard": false, + "ime": false, + "checkpoints": [ + { + "phase": "eight-ready", + "elapsed_ms": 571, + "cycle": 0 + }, + { + "phase": "reject-ninth", + "elapsed_ms": 574, + "cycle": 0 + }, + { + "phase": "load-started", + "elapsed_ms": 731, + "cycle": 0 + }, + { + "phase": "reading-before-append", + "elapsed_ms": 749, + "cycle": 0 + }, + { + "phase": "reading-after-append", + "elapsed_ms": 877, + "cycle": 0 + }, + { + "phase": "search-running", + "elapsed_ms": 1021, + "cycle": 0 + }, + { + "phase": "find-complete", + "elapsed_ms": 6355, + "cycle": 0 + }, + { + "phase": "foreground-input", + "elapsed_ms": 6493, + "cycle": 0 + }, + { + "phase": "zoom-75", + "elapsed_ms": 6626, + "cycle": 0 + }, + { + "phase": "switch-zoom", + "elapsed_ms": 6775, + "cycle": 0 + }, + { + "phase": "history-evicted", + "elapsed_ms": 7020, + "cycle": 0 + }, + { + "phase": "paste-cancel-pending", + "elapsed_ms": 7606, + "cycle": 0 + }, + { + "phase": "paste-cancelled", + "elapsed_ms": 7975, + "cycle": 0 + }, + { + "phase": "background-complete", + "elapsed_ms": 26268, + "cycle": 0 + }, + { + "phase": "cycle-six", + "elapsed_ms": 26584, + "cycle": 1 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 27859, + "cycle": 1 + }, + { + "phase": "cycle-six", + "elapsed_ms": 28160, + "cycle": 2 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 29429, + "cycle": 2 + }, + { + "phase": "cycle-six", + "elapsed_ms": 29734, + "cycle": 3 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 31003, + "cycle": 3 + }, + { + "phase": "cycle-six", + "elapsed_ms": 31326, + "cycle": 4 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 32562, + "cycle": 4 + }, + { + "phase": "cycle-six", + "elapsed_ms": 32875, + "cycle": 5 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 34169, + "cycle": 5 + }, + { + "phase": "paste-close-pending", + "elapsed_ms": 34461, + "cycle": 5 + }, + { + "phase": "after-paste-close", + "elapsed_ms": 34675, + "cycle": 5 + } + ], + "paste_cases": [ + { + "name": "cancel", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 18414, + "flood_observed": true, + "fence": { + "generation": 1, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "cancelled", + "wrong_generation_rejected": true + } + }, + { + "name": "close", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 768, + "flood_observed": true, + "fence": { + "generation": 2, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "stopped", + "wrong_generation_rejected": true + } + } + ], + "raw_sha256": { + "cancel.bin": "bcc3593351da0137a27071da66416a7cbe02ca5605f49b14e5a6c85ebac894a6", + "cancel.bin.before.json": "a90746e62c8e815c76305806dfff25d55dd34d67f54308b0c865152693425663", + "close.bin": "61da7e55d911305206ae68ac162a0a73d2fc1433efae5f0cfda2b9d0c9b5363c", + "close.bin.before.json": "42d48c9b60c19059b0fd753b871521787ece886f5976aef804fe05baa7db1584", + "frame.bmp": "68fb528174c7f7edf2916ca1b6f1f7d2a6b80deeb4cec787539fb051bb344ee5", + "resource-observations.jsonl": "39a33bf36d3da8512f99e18c617bb8ae9d2ce18bd45d8fb923ea89e3bc6fd15c", + "resource-phases.jsonl": "3d4bed0243cafdcb17e067cf7a80fc0a8deab7b297a4976cd4ca3fab6595b100", + "resource-summary.json": "8084b1234602e26ee63f26be0067a9fc8845eef227e9db7277c7092ea0528273", + "sample.json": "efb0f88de0d82b21f38592e2858c3937c9e4c56a0f57c9cbfc128b8cdb5ef818", + "sample.public.json": "9507843aee6d9088fd93fb35fb925f64d9b854f0d0c307dc5bc6676422ff7604", + "sessions.json": "953d27af5461179b5cfa672022f9ab7d7f1c50670b20f3acba9fb9bb046f7f28", + "validation.json": "0449c92f202590a5255928fb7df1f5ab615821d26656a3675c9b78a50c7692ef", + "workspace.layout": "9a96d62943f54727e7c50ce5785a571efbfa6cbece6248f6a6d144257c1e1d58" + } + }, + { + "run": "run-529ed519a8f145f191c850d0218abb04", + "sample": { + "mode": "ui-workflow-sessions", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workflow-sessions", + "sessions_closed": 18, + "active_children_after": 0, + "presented_done": null + }, + "frame_sha256": "4443a91feb73693650c2a6f8ac4feb91fa9fe3fee9e1340086892c2257db1990", + "sessions_workflow": { + "schema": "termforge-workflow-sessions-identity-validation-v1", + "live_sessions": 8, + "checkpoints": 26, + "rejected_ninth": true, + "load_foreground_verified": true, + "data_workflow_verified": true, + "complete_workflow_verified": false, + "system_clipboard_verified": false, + "ime_verified": false + }, + "resources": { + "summary": "resource-summary.json", + "scope": "launcher-tree", + "performance_gate": "not_evaluated" + }, + "error_kind": null + }, + "data_only": true, + "system_clipboard": false, + "ime": false, + "checkpoints": [ + { + "phase": "eight-ready", + "elapsed_ms": 570, + "cycle": 0 + }, + { + "phase": "reject-ninth", + "elapsed_ms": 574, + "cycle": 0 + }, + { + "phase": "load-started", + "elapsed_ms": 730, + "cycle": 0 + }, + { + "phase": "reading-before-append", + "elapsed_ms": 752, + "cycle": 0 + }, + { + "phase": "reading-after-append", + "elapsed_ms": 881, + "cycle": 0 + }, + { + "phase": "search-running", + "elapsed_ms": 1038, + "cycle": 0 + }, + { + "phase": "find-complete", + "elapsed_ms": 6482, + "cycle": 0 + }, + { + "phase": "foreground-input", + "elapsed_ms": 6642, + "cycle": 0 + }, + { + "phase": "zoom-75", + "elapsed_ms": 6791, + "cycle": 0 + }, + { + "phase": "switch-zoom", + "elapsed_ms": 6946, + "cycle": 0 + }, + { + "phase": "history-evicted", + "elapsed_ms": 7233, + "cycle": 0 + }, + { + "phase": "paste-cancel-pending", + "elapsed_ms": 7992, + "cycle": 0 + }, + { + "phase": "paste-cancelled", + "elapsed_ms": 8387, + "cycle": 0 + }, + { + "phase": "background-complete", + "elapsed_ms": 27312, + "cycle": 0 + }, + { + "phase": "cycle-six", + "elapsed_ms": 27632, + "cycle": 1 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 28930, + "cycle": 1 + }, + { + "phase": "cycle-six", + "elapsed_ms": 29255, + "cycle": 2 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 30560, + "cycle": 2 + }, + { + "phase": "cycle-six", + "elapsed_ms": 30907, + "cycle": 3 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 32144, + "cycle": 3 + }, + { + "phase": "cycle-six", + "elapsed_ms": 32466, + "cycle": 4 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 33763, + "cycle": 4 + }, + { + "phase": "cycle-six", + "elapsed_ms": 34105, + "cycle": 5 + }, + { + "phase": "cycle-eight", + "elapsed_ms": 35332, + "cycle": 5 + }, + { + "phase": "paste-close-pending", + "elapsed_ms": 35604, + "cycle": 5 + }, + { + "phase": "after-paste-close", + "elapsed_ms": 35808, + "cycle": 5 + } + ], + "paste_cases": [ + { + "name": "cancel", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 18414, + "flood_observed": true, + "fence": { + "generation": 1, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "cancelled", + "wrong_generation_rejected": true + } + }, + { + "name": "close", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 768, + "flood_observed": true, + "fence": { + "generation": 2, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "stopped", + "wrong_generation_rejected": true + } + } + ], + "raw_sha256": { + "cancel.bin": "bcc3593351da0137a27071da66416a7cbe02ca5605f49b14e5a6c85ebac894a6", + "cancel.bin.before.json": "a90746e62c8e815c76305806dfff25d55dd34d67f54308b0c865152693425663", + "close.bin": "61da7e55d911305206ae68ac162a0a73d2fc1433efae5f0cfda2b9d0c9b5363c", + "close.bin.before.json": "42d48c9b60c19059b0fd753b871521787ece886f5976aef804fe05baa7db1584", + "frame.bmp": "4443a91feb73693650c2a6f8ac4feb91fa9fe3fee9e1340086892c2257db1990", + "resource-observations.jsonl": "4a24473a1c2c48d3f96098f18dd8c99b16e1e33c30856900727c20af6cd4a832", + "resource-phases.jsonl": "4ddef626506c4410572d43f8daaa521487ce6121bc26fadca51c17a3ed73dc50", + "resource-summary.json": "a1c3b5a9b1c224c2ee9bd398ee72edecfc0046aacd464998ed68d20171dd5a59", + "sample.json": "d8abba7266e09bfbfab499957e620b1fd2e111c0b26aab6994baf0222108f3cf", + "sample.public.json": "2a0f6f9d77f7a858d372dcc0ad139b74b6e5e27550af87acb11f099df85986ed", + "sessions.json": "29bea82e4c709a55704b9dc6491e7a9c9a1cba08dfb42e4ef0564e765128aacf", + "validation.json": "1cc474b79845172d40b6544689ef261a331cdd7ebb4760d908c55c8d21fa9c41", + "workspace.layout": "9a96d62943f54727e7c50ce5785a571efbfa6cbece6248f6a6d144257c1e1d58" + } + } + ], + "internal_skips": [ + "setUpClass (test_candidate_compare.CandidateComparisonTests) ... skipped 'optional Alacritty binary is not built; core-candidate workflow runs these checks'", + "setUpClass (test_capability_matrix.CapabilityMatrixTests) ... skipped 'optional Alacritty binary is not built; core-candidate workflow runs these checks'", + "setUpClass (test_fuzz_matrix.FuzzMatrixTests) ... skipped 'optional Alacritty binary is not built; core-candidate workflow runs these checks'", + "setUpClass (test_real_app_matrix.RealApplicationMatrixTests) ... skipped 'optional real application capture dependencies are not built'", + "test_actual_system_copy_paste_and_shortcuts_twice (experiments.shell.test_actions.ActionWindowTest.test_actual_system_copy_paste_and_shortcuts_twice) ... skipped 'actual system Copy/Paste requires a disposable hosted VM'", + "test_suspended_target_is_reclaimed_if_job_assignment_fails (test_ui_r1.EvidenceBoundaryTests.test_suspended_target_is_reclaimed_if_job_assignment_fails) ... skipped 'opt-in pinned reference failure injection'", + "test_mouse_selection_to_real_system_clipboard (experiments.shell.test_shell.ShellTests.test_mouse_selection_to_real_system_clipboard) ... skipped 'system copy UI gate requires a disposable hosted VM'", + "test_system_clipboard_product_read_errors_cancel_and_close (experiments.shell.test_shell.ShellTests.test_system_clipboard_product_read_errors_cancel_and_close) ... skipped 'product clipboard read gate requires a disposable hosted VM'", + "test_system_clipboard_shortcut_to_window_confirmation_and_receiver (experiments.shell.test_shell.ShellTests.test_system_clipboard_shortcut_to_window_confirmation_and_receiver) ... skipped 'real system clipboard UI gate requires a disposable hosted VM'", + "test_real_system_copy_shortcuts_for_styled_fixture_twice (experiments.shell.test_selection_bytes.SelectionBytesWindowTest.test_real_system_copy_shortcuts_for_styled_fixture_twice) ... skipped 'system CF requires a disposable hosted VM'", + "test_real_system_copy_and_edit_context_twice (experiments.shell.test_selection_context.SelectionContextWindowTest.test_real_system_copy_and_edit_context_twice) ... skipped 'real CF/EDIT Copy requires a disposable hosted VM'", + "test_real_system_clipboard (experiments.shell.test_paste.PasteTests.test_real_system_clipboard) ... skipped 'Private window station OpenClipboard denied (5); system API gate remains unverified." + ], + "shell_unit_status": [ + { + "name": "test_clipboard_evidence_rejects_leaks_missing_phases_and_false_cleanup", + "status": "ok" + }, + { + "name": "test_hosted_guards_reject_missing_or_partial_identity_before_any_work", + "status": "ok" + }, + { + "name": "test_native_clipboard_mode_refuses_non_hosted_before_window_creation", + "status": "ok" + }, + { + "name": "test_actual_client_bounds_drive_geometry", + "status": "ok" + }, + { + "name": "test_native_copy_return_highlight_cf_and_target_are_independent_gates", + "status": "ok" + }, + { + "name": "test_occupied_product_copy_keeps_original_error_sentinel_target_and_strict_join", + "status": "ok" + }, + { + "name": "test_old_v3_is_readable_but_never_claims_the_new_copy_popup_gate", + "status": "ok" + }, + { + "name": "test_assignment_failure_reaps_gated_launcher_and_preserves_failure", + "status": "ok" + }, + { + "name": "test_close_during_continuous_output", + "status": "ok" + }, + { + "name": "test_close_while_resizing_continuous_output", + "status": "ERROR" + }, + { + "name": "test_commands_unicode_editing_interrupt_and_normal_exit", + "status": "ok" + }, + { + "name": "test_desktop_creation_failure_closes_job_and_keeps_evidence", + "status": "ok" + }, + { + "name": "test_find_continuous_output_partial_navigation_replace_cancel_and_target_close", + "status": "ok" + }, + { + "name": "test_find_real_edit_navigation_unicode_output_isolation_and_cancel", + "status": "ok" + }, + { + "name": "test_invalid_arguments_create_no_session", + "status": "ok" + }, + { + "name": "test_keyboard_message_path_to_real_console_bytes", + "status": "ok" + }, + { + "name": "test_launch_failure_is_visible_and_recoverable", + "status": "ok" + }, + { + "name": "test_local_shell_launch_working_directory_and_cleanup", + "status": "ok" + }, + { + "name": "test_main_and_bottom_sessions_focus_geometry_and_cleanup", + "status": "ERROR" + }, + { + "name": "test_main_split_background_restore_close_and_focus", + "status": "ok" + }, + { + "name": "test_mouse_selection_copy_model_zoom_sessions_and_eviction", + "status": "ok" + }, + { + "name": "test_mouse_selection_to_real_system_clipboard", + "status": "skipped" + }, + { + "name": "test_paste_confirmation_and_hidden_target_preserve_visible_focus", + "status": "ok" + }, + { + "name": "test_paste_control_eof_remains_failure_after_cleanup", + "status": "ok" + }, + { + "name": "test_paste_missing_go_remains_failure_after_cleanup", + "status": "ok" + }, + { + "name": "test_per_session_zoom_and_controlled_dpi_transitions", + "status": "ok" + }, + { + "name": "test_queues_apply_bounded_backpressure_and_reject_input", + "status": "ok" + }, + { + "name": "test_real_history_scroll_anchor_eviction_and_hidden_sessions", + "status": "ok" + }, + { + "name": "test_real_monitor_round_trip_preserves_workspace", + "status": "ok" + }, + { + "name": "test_resize_child_dimensions_and_interrupt", + "status": "ERROR" + }, + { + "name": "test_selection_autoscroll_with_short_and_tall_workspace", + "status": "ok" + }, + { + "name": "test_system_clipboard_product_read_errors_cancel_and_close", + "status": "skipped" + }, + { + "name": "test_system_clipboard_shortcut_to_window_confirmation_and_receiver", + "status": "skipped" + }, + { + "name": "test_watchdog_contains_real_shell_tree", + "status": "ok" + }, + { + "name": "test_workspace_rename_drag_and_fresh_process_restore", + "status": "ok" + }, + { + "name": "test_workspace_sessions_resize_and_cleanup", + "status": "ERROR" + }, + { + "name": "test_workspace_v1_empty_and_invalid_recovery", + "status": "ok" + }, + { + "name": "test_workspace_v2_empty_session_roundtrip", + "status": "ok" + } + ], + "shell_module_complete": false, + "shell_module_limitation": "CTest killed aggregate at original 180s; listed method statuses do not make the complete module pass", + "standalone_paste_runs": [ + { + "run": "paste-0", + "sample": { + "mode": "ui-paste", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 12, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 12, + "terminated": 0 + }, + "desktop_gone": true + }, + "raw_sha256": { + "cancel.bin": "bcc3593351da0137a27071da66416a7cbe02ca5605f49b14e5a6c85ebac894a6", + "cancel.bin.before.json": "a90746e62c8e815c76305806dfff25d55dd34d67f54308b0c865152693425663", + "close.bin": "61da7e55d911305206ae68ac162a0a73d2fc1433efae5f0cfda2b9d0c9b5363c", + "close.bin.before.json": "42d48c9b60c19059b0fd753b871521787ece886f5976aef804fe05baa7db1584", + "confirmation.bmp": "2b63b1d079427584b5bb4be1739dc221535af2291ef0b062994c3e5903a7c6a8", + "frame.bmp": "6e947f9d4d723f67da816440e3182629b4c4321a9ccb5876c87a4fa2ea06b9ce", + "input.bin": "d4acd50607214fe6614644e1f5502d7f1043dc3e0d38ca87bbdaf66f12e527b2", + "paste-workflow.json": "6c971df3ae4711bcbf07ecf4f5336211e7da3256a0ffcf694c7a9b06574f464b", + "sample.json": "f14a5f6d1977734e06a599aeed4b01da1f9ff17e523953f8035cf4b1544726f1", + "workspace.layout": "4c271282879f651f87a060c325ff62287a9ddd86cda103c86855a3133b209b13" + }, + "workflow": { + "schema": "termforge-window-paste-v2", + "keyboard": { + "default_enter_accept": true, + "default_escape_cancel": true, + "tab_enter_accept": true + }, + "cases": [ + { + "name": "cancel", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 18414, + "flood_observed": true, + "fence": { + "generation": 1, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "cancelled", + "wrong_generation_rejected": true + } + }, + { + "name": "close", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 768, + "flood_observed": true, + "fence": { + "generation": 2, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "stopped", + "wrong_generation_rejected": true + } + } + ] + } + }, + { + "run": "paste-1", + "sample": { + "mode": "ui-paste", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 12, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 12, + "terminated": 0 + }, + "desktop_gone": true + }, + "raw_sha256": { + "cancel.bin": "bcc3593351da0137a27071da66416a7cbe02ca5605f49b14e5a6c85ebac894a6", + "cancel.bin.before.json": "a90746e62c8e815c76305806dfff25d55dd34d67f54308b0c865152693425663", + "close.bin": "61da7e55d911305206ae68ac162a0a73d2fc1433efae5f0cfda2b9d0c9b5363c", + "close.bin.before.json": "42d48c9b60c19059b0fd753b871521787ece886f5976aef804fe05baa7db1584", + "confirmation.bmp": "2b63b1d079427584b5bb4be1739dc221535af2291ef0b062994c3e5903a7c6a8", + "frame.bmp": "6e947f9d4d723f67da816440e3182629b4c4321a9ccb5876c87a4fa2ea06b9ce", + "input.bin": "d4acd50607214fe6614644e1f5502d7f1043dc3e0d38ca87bbdaf66f12e527b2", + "paste-workflow.json": "6c971df3ae4711bcbf07ecf4f5336211e7da3256a0ffcf694c7a9b06574f464b", + "sample.json": "d52d098a6e8be6cf6e415bee3659f3add3d6d9998e3ed1bc702ceeda70da81c1", + "workspace.layout": "4c271282879f651f87a060c325ff62287a9ddd86cda103c86855a3133b209b13" + }, + "workflow": { + "schema": "termforge-window-paste-v2", + "keyboard": { + "default_enter_accept": true, + "default_escape_cancel": true, + "tab_enter_accept": true + }, + "cases": [ + { + "name": "cancel", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 18414, + "flood_observed": true, + "fence": { + "generation": 1, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "cancelled", + "wrong_generation_rejected": true + } + }, + { + "name": "close", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 768, + "flood_observed": true, + "fence": { + "generation": 2, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "stopped", + "wrong_generation_rejected": true + } + } + ] + } + }, + { + "run": "paste-control-eof", + "sample": { + "mode": "ui-paste-control-eof", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "job_after": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "desktop_gone": true + }, + "raw_sha256": { + "cancel.bin": "61da7e55d911305206ae68ac162a0a73d2fc1433efae5f0cfda2b9d0c9b5363c", + "cancel.bin.before.json": "a90746e62c8e815c76305806dfff25d55dd34d67f54308b0c865152693425663", + "confirmation.bmp": "2b63b1d079427584b5bb4be1739dc221535af2291ef0b062994c3e5903a7c6a8", + "input.bin": "d4acd50607214fe6614644e1f5502d7f1043dc3e0d38ca87bbdaf66f12e527b2", + "paste-control-failure.json": "b0fd2d17e5d7fa2d635cae3416293e4a2c1d11e240a48f829b5da4552b51d68c", + "sample.json": "2f9a3caf3ad18d64dd7d6f542c465c52db071c462dfbac552096ed01d127a1f5", + "workspace.layout": "a26afdba5f7a4c7a7056bab7530fc98f0866419b5d87087cd85142d4243b163c" + }, + "observed_failure": "paste control EOF rejected", + "control_failure": { + "eof_observed": true, + "timed_out": false, + "wait_finished": true, + "state": "failed", + "in_flight": 0, + "retained_capacity": 15 + }, + "receiver_bytes": 768 + }, + { + "run": "paste-control-timeout", + "sample": { + "mode": "ui-paste-control-timeout", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "job_after": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "desktop_gone": true + }, + "raw_sha256": { + "cancel.bin": "61da7e55d911305206ae68ac162a0a73d2fc1433efae5f0cfda2b9d0c9b5363c", + "cancel.bin.before.json": "a90746e62c8e815c76305806dfff25d55dd34d67f54308b0c865152693425663", + "confirmation.bmp": "2b63b1d079427584b5bb4be1739dc221535af2291ef0b062994c3e5903a7c6a8", + "input.bin": "d4acd50607214fe6614644e1f5502d7f1043dc3e0d38ca87bbdaf66f12e527b2", + "paste-control-failure.json": "e296e5fa38603e2d8818e9332d52248bb5a68c32c42c0593b84cfb311d473ec1", + "sample.json": "fbf107cba73ca6b72f1f78ae14cdbc94c3809b2cd5c919a3002c9a5c6828ee3c", + "workspace.layout": "a26afdba5f7a4c7a7056bab7530fc98f0866419b5d87087cd85142d4243b163c" + }, + "observed_failure": "paste control missing GO rejected", + "control_failure": { + "eof_observed": false, + "timed_out": true, + "wait_finished": true, + "state": "failed", + "in_flight": 0, + "retained_capacity": 15 + }, + "receiver_bytes": 768 + } + ], + "other_shell_failures": [ + { + "run": "resize-0", + "stage": 43, + "sample": { + "mode": "resize", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "job_after": { + "active": 0, + "total": 8, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 8, + "terminated": 0 + }, + "desktop_gone": true + }, + "expected_exact_line": "TF_SIZE_100x28", + "observed_line": "TF_SIZE_100x28OW", + "local_raw_sample_sha256": "ac6f650ce3bd87b934755ca47072f65b689be5662ea3396cb7939c41e847d7da" + }, + { + "run": "resize-close-0", + "stage": 43, + "sample": { + "mode": "resize-close", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "job_after": { + "active": 0, + "total": 8, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 8, + "terminated": 0 + }, + "desktop_gone": true + }, + "expected_exact_line": "TF_SIZE_100x28", + "observed_line": "TF_SIZE_100x28OW", + "local_raw_sample_sha256": "6489fa48ad6bb446e5c5395f3b133829d9f8852479f352f15926a126a8308c2f" + }, + { + "run": "main-2", + "stage": 7, + "sample": { + "mode": "ui-main", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "job_after": { + "active": 0, + "total": 16, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 16, + "terminated": 0 + }, + "desktop_gone": true + }, + "expected_exact_line": null, + "observed_line": null, + "local_raw_sample_sha256": "8ca836c302b4bdf21a81dbfd1668381978d7c55c45fb05140bbcbad596712926" + }, + { + "run": "ui-2", + "stage": 52, + "sample": { + "mode": "ui", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "job_after": { + "active": 0, + "total": 12, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 12, + "terminated": 0 + }, + "desktop_gone": true + }, + "expected_exact_line": null, + "observed_line": null, + "local_raw_sample_sha256": "de287a2da956ccb3a75985ec690146d8097ca61da63517c9fd61f33b02bc99d4" + } + ] +} diff --git a/experiments/shell/results/2026-10-26-ui-paste-fence/validation.json b/experiments/shell/results/2026-10-26-ui-paste-fence/validation.json new file mode 100644 index 00000000..3e990195 --- /dev/null +++ b/experiments/shell/results/2026-10-26-ui-paste-fence/validation.json @@ -0,0 +1,689 @@ +{ + "schema": "termforge-ui-paste-fence-validation-v1", + "date": "2026-10-26", + "issue": 53, + "status": "partial_issue_remains_open", + "source_commit": "a15eb7f67315fb53c9d66b9c5a4424e13511a311", + "source_tree": "b2a2d7b5b5e1f74de1be4aa3ccb8b692cb4c9fec", + "source_files": { + "CMakeLists.txt": { + "git_blob": "4f29e60930e0ce5f5aed359a6ee94c2b178dbdd2", + "checkout_sha256": "fa0bc11bd7821cab83dc0076d2d1fd305410febca7ccba96dd14c7bfa4b39fcf" + }, + "experiments/shell/paste_checks.py": { + "git_blob": "3de07a7a49726e1365daba541054ae94ad609e7e", + "checkout_sha256": "3e565ed0c297c94573589230bcff57c5e6f4a043d5b63ac605a79cd46a0ae18e" + }, + "experiments/shell/paste_scenario_state.hpp": { + "git_blob": "77db7d43dc22777596e4597ec4081078acd994e6", + "checkout_sha256": "52fdef856fe365b3a39aa27360d31c9d88979366b39de7d618c49942b0fc5433" + }, + "experiments/shell/sender_fence.hpp": { + "git_blob": "56e092f0397d6052e278b464cfc08add2cbee8ec", + "checkout_sha256": "fefdd87fff5840b7a0e03f1c682a2ca626249b48536b483fac8a12e8cf882987" + }, + "experiments/shell/sender_fence_pipe.hpp": { + "git_blob": "aca1566dfd9224c9c4e798c4d7435aced680c3b1", + "checkout_sha256": "ee3b862a6496b060ad2312c234221570ac081ebbf2e24cfccbec3ced57bdeb03" + }, + "experiments/shell/sender_fence_pipe_test.cpp": { + "git_blob": "64580c0c831eab4e52ded57a36bfd7d2b07c5368", + "checkout_sha256": "424c5645246dec064bfc76129553a273f1ae8c3cb0104be91b19269c69b8a59a" + }, + "experiments/shell/shell.cpp": { + "git_blob": "cd112860aa377857cdbd749cc2ef44973c80360b", + "checkout_sha256": "b369968152bbff107c699181d4e23f271e62a5732f0c41793159b3efc3609321" + }, + "experiments/shell/test_paste_checks.py": { + "git_blob": "0c53e0532c74bb5e936389f24eb002c818489894", + "checkout_sha256": "fdb2a4ceff178d07e18facaa8e30226670d1b0c4a4c0b69b2fd21fd2f0327d1c" + }, + "experiments/shell/test_shell.py": { + "git_blob": "66512ac1099cf477cac8f26f225b18c301c1d3d8", + "checkout_sha256": "a6b63c1754b0e0dc1cc01863891f4c7c9d989f818d351838a8cfe295edaf811c" + }, + "experiments/shell/test_workflow_sessions.py": { + "git_blob": "de6cef3be06725d606b91ae68eb1cc45eb744fb5", + "checkout_sha256": "6c968a443c46b30e75a929f880228e6a23bf2b92d9221bbbf18816933babd6e7" + }, + "experiments/shell/workflow_sessions_checks.py": { + "git_blob": "8369b412abe893393bf316430ccedd22e9eb75ba", + "checkout_sha256": "b976d3504837eef57ff13319daed254870727741f36f265412a6b14b4cb3faae" + }, + "experiments/shell/workspace.hpp": { + "git_blob": "7d4fa607b4878add7e0b5c411c711cc3e60abe3d", + "checkout_sha256": "5669a13701b2fe60e8bd567a122edb970bdc4aadcb98ceb3927905f0c000275e" + }, + "experiments/shell/workspace_paste_scenario.hpp": { + "git_blob": "2a22a036696ecf3ce6d2ff2a0b7aed3decad49b4", + "checkout_sha256": "d55b7568f345373784c288f496e97e0b3bfc336c47f4b59378d4c51409e0b128" + }, + "experiments/shell/workspace_workflow_scenario.hpp": { + "git_blob": "029ef4bcf9393be4ab6194bbbe95d145e98ff70b", + "checkout_sha256": "467571556440f4e89305bc1261d389bb8f24ba84f607b8cd36b8078a58314369" + } + }, + "environment": { + "os": "Windows-11-10.0.26200-SP0", + "architecture": "AMD64", + "compiler": "MSVC 19.42.34433", + "configuration": "Release x64", + "candidate": "alacritty_terminal 0.26.1-dev+termforge.4", + "backend": "Microsoft.Windows.Console.ConPTY 1.25.260930003", + "glyph_width_flags": 24 + }, + "binaries": { + "termforge_shell.exe": "0a1fee632cb640dff4599fdcca277c959038d31ec88bfed8cff893b3271d26a6", + "shell_test_launcher.exe": "dbde4249dc08c7df80120398ffd252aa5e4958e4ef640cc161ea4c2d28b9f291", + "shell_input_client.exe": "850c82fb8bb58de8170bf096cc6dd4f8ba34d9ba11dbd2addc41508cddc31d8c", + "shell_text_client.exe": "9485b03424e421a4018596cc9487e7feeb33c26d30820985aa45843fdcf315d9", + "sender_fence_test.exe": "ab4af3b0f148f6e6377e70c98936c6c34d0be0d3cee4b17b3f8554c39b2d3e52", + "sender_fence_pipe_test.exe": "6a94ac84631434a81ae1098932813d7ceeb81f2bb33a2e4b2456834bd8972e92", + "conpty-backend/conpty.dll": "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a", + "conpty-backend/OpenConsole.exe": "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586" + }, + "request": { + "utf8_bytes": 3000000, + "utf16_units": 1000000, + "literal": "U+6C49 repeated 1000000", + "sha256": "0ba67970de9941c1bc33e97869b116a0e129c8a859e3b4c0087e20c51f6a404b", + "minimum_sent": 16384, + "receiver_checkpoint_bytes": 768 + }, + "deadlines_ms": { + "standalone_ui": 35000, + "standalone_outer": 40000, + "receiver_gate": 15000, + "eight_session_ui": 70000, + "workflow_fixture": 55000, + "missing_go_negative_only": 2000 + }, + "controls": [ + { + "case": "paste-0", + "sample_sha256": "aa28ff969ef47debda3f78e9d5eeb2e414492e30ecea5d354cfbb2d7857997a9", + "status": "passed", + "exit_code": 0, + "outer_timed_out": false, + "job_after": { + "active": 0, + "total": 12, + "terminated": 0 + }, + "desktop_gone": true, + "paste_cases": [ + { + "name": "cancel", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 18414, + "flood_observed": true, + "fence": { + "generation": 1, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "cancelled", + "wrong_generation_rejected": true + } + }, + { + "name": "close", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 768, + "flood_observed": true, + "fence": { + "generation": 2, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "stopped", + "wrong_generation_rejected": true + } + } + ], + "captures": { + "cancel.bin": { + "bytes": 18414, + "sha256": "bcc3593351da0137a27071da66416a7cbe02ca5605f49b14e5a6c85ebac894a6" + }, + "close.bin": { + "bytes": 768, + "sha256": "61da7e55d911305206ae68ac162a0a73d2fc1433efae5f0cfda2b9d0c9b5363c" + }, + "input.bin": { + "bytes": 25021, + "sha256": "d4acd50607214fe6614644e1f5502d7f1043dc3e0d38ca87bbdaf66f12e527b2" + } + } + }, + { + "case": "paste-1", + "sample_sha256": "d769fff8294f3b933520464be2a710863cbc1c2cbc4d48b09ce04fdb09fc9d27", + "status": "passed", + "exit_code": 0, + "outer_timed_out": false, + "job_after": { + "active": 0, + "total": 12, + "terminated": 0 + }, + "desktop_gone": true, + "paste_cases": [ + { + "name": "cancel", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 18414, + "flood_observed": true, + "fence": { + "generation": 1, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "cancelled", + "wrong_generation_rejected": true + } + }, + { + "name": "close", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 768, + "flood_observed": true, + "fence": { + "generation": 2, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "stopped", + "wrong_generation_rejected": true + } + } + ], + "captures": { + "cancel.bin": { + "bytes": 18414, + "sha256": "bcc3593351da0137a27071da66416a7cbe02ca5605f49b14e5a6c85ebac894a6" + }, + "close.bin": { + "bytes": 768, + "sha256": "61da7e55d911305206ae68ac162a0a73d2fc1433efae5f0cfda2b9d0c9b5363c" + }, + "input.bin": { + "bytes": 25021, + "sha256": "d4acd50607214fe6614644e1f5502d7f1043dc3e0d38ca87bbdaf66f12e527b2" + } + } + }, + { + "case": "paste-control-eof", + "sample_sha256": "577e1a5125a10f42ba176cafcc8bd4b72d92271be33ae8ec6b4f0e1ba61a71dc", + "status": "failed", + "exit_code": 2, + "outer_timed_out": false, + "job_after": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "desktop_gone": true, + "expected_failure": { + "eof_observed": true, + "timed_out": false, + "wait_finished": true, + "state": "failed", + "in_flight": 0, + "retained_capacity": 15 + }, + "captures": { + "cancel.bin": { + "bytes": 768, + "sha256": "61da7e55d911305206ae68ac162a0a73d2fc1433efae5f0cfda2b9d0c9b5363c" + }, + "input.bin": { + "bytes": 25021, + "sha256": "d4acd50607214fe6614644e1f5502d7f1043dc3e0d38ca87bbdaf66f12e527b2" + } + } + }, + { + "case": "paste-control-timeout", + "sample_sha256": "2a5b36750f15d093e34ad81a29363e33ea84c4ea21e2798af96dc5702960e19b", + "status": "failed", + "exit_code": 2, + "outer_timed_out": false, + "job_after": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "desktop_gone": true, + "expected_failure": { + "eof_observed": false, + "timed_out": true, + "wait_finished": true, + "state": "failed", + "in_flight": 0, + "retained_capacity": 15 + }, + "captures": { + "cancel.bin": { + "bytes": 768, + "sha256": "61da7e55d911305206ae68ac162a0a73d2fc1433efae5f0cfda2b9d0c9b5363c" + }, + "input.bin": { + "bytes": 25021, + "sha256": "d4acd50607214fe6614644e1f5502d7f1043dc3e0d38ca87bbdaf66f12e527b2" + } + } + } + ], + "eight_session_runs": [ + { + "source_commit": "62226af2d1f636cef1920ac0ef9998935a7dd243", + "resource_sampling": true, + "diagnostic_only": false, + "sample_sha256": "93be8809499aa33081e411df03e20c879d63e4c4bf4940f85fcab8e1d992059f", + "sessions_sha256": "063739d6e17ff15a2861fd13f247dffd40fe7d1f92a4835e040f9a04b0d61106", + "exit_code": 2, + "outer_timed_out": false, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "failure": "workflow sessions scenario timeout at stage 45", + "foreground_fixture_failure": "workflow control deadline expired", + "foreground_fixture_sha256": "48a40aec11a95182dd5a55b5f7c3e5eceb58b093f36ad02674aae6ba103325d5", + "phases": [ + { + "phase": "paste-cancel-pending", + "elapsed_ms": 14371 + }, + { + "phase": "paste-cancelled", + "elapsed_ms": 15111 + }, + { + "phase": "background-complete", + "elapsed_ms": 51158 + }, + { + "phase": "paste-close-pending", + "elapsed_ms": 61806 + } + ], + "paste_cases": [ + { + "name": "cancel", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 18414, + "flood_observed": true, + "fence": { + "generation": 1, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "wrong_generation_rejected": true + } + }, + { + "name": "close", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 768, + "flood_observed": true, + "fence": { + "generation": 2, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "stopped", + "wrong_generation_rejected": true + } + } + ], + "wake_evidence": "legacy_exit_is_final_state_not_actual_wake" + }, + { + "source_commit": "62226af2d1f636cef1920ac0ef9998935a7dd243", + "resource_sampling": true, + "diagnostic_only": false, + "sample_sha256": "6bcd382eca3ec3e09bdd3ab59f1aba530e1fe9670fb3423d9adda2a941920635", + "sessions_sha256": "dd918bc9fc472bfa6ff8bd851fc13467143185f740a9d5bca6348e5492c8a26a", + "exit_code": 2, + "outer_timed_out": false, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "failure": "workflow sessions scenario timeout at stage 45", + "foreground_fixture_failure": "workflow control deadline expired", + "foreground_fixture_sha256": "48a40aec11a95182dd5a55b5f7c3e5eceb58b093f36ad02674aae6ba103325d5", + "phases": [ + { + "phase": "paste-cancel-pending", + "elapsed_ms": 14766 + }, + { + "phase": "paste-cancelled", + "elapsed_ms": 15730 + }, + { + "phase": "background-complete", + "elapsed_ms": 52546 + }, + { + "phase": "paste-close-pending", + "elapsed_ms": 63096 + } + ], + "paste_cases": [ + { + "name": "cancel", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 18414, + "flood_observed": true, + "fence": { + "generation": 1, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "wrong_generation_rejected": true + } + }, + { + "name": "close", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 768, + "flood_observed": true, + "fence": { + "generation": 2, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "stopped", + "wrong_generation_rejected": true + } + } + ], + "wake_evidence": "legacy_exit_is_final_state_not_actual_wake" + }, + { + "source_commit": "a15eb7f67315fb53c9d66b9c5a4424e13511a311", + "resource_sampling": false, + "diagnostic_only": true, + "sample_sha256": "e762d5d0caf2d016959572ec2c60272ba791a7d994b0ff006627406776d3250d", + "sessions_sha256": "5762dc068ce78233fdc40d06ebfa28734d263f0b4557c258f0146147fd6b25ef", + "exit_code": 2, + "outer_timed_out": false, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "failure": "workflow sessions scenario timeout at stage 45", + "foreground_fixture_failure": "workflow control deadline expired", + "foreground_fixture_sha256": "48a40aec11a95182dd5a55b5f7c3e5eceb58b093f36ad02674aae6ba103325d5", + "phases": [ + { + "phase": "paste-cancel-pending", + "elapsed_ms": 14071 + }, + { + "phase": "paste-cancelled", + "elapsed_ms": 14785 + }, + { + "phase": "background-complete", + "elapsed_ms": 50262 + }, + { + "phase": "paste-close-pending", + "elapsed_ms": 60948 + } + ], + "paste_cases": [ + { + "name": "cancel", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 18414, + "flood_observed": true, + "fence": { + "generation": 1, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "cancelled", + "wrong_generation_rejected": true + } + }, + { + "name": "close", + "target": 1, + "total": 3000000, + "before_sent": 17391, + "before_accepted": 18414, + "accepted": 18414, + "sent": 18414, + "in_flight": 0, + "retained_capacity": 15, + "state": "cancelled", + "checkpoint_bytes": 768, + "received": 768, + "flood_observed": true, + "fence": { + "generation": 2, + "held": true, + "owned_bytes": 1023, + "wait_finished": true, + "other_responsive": true, + "exit": "cancelled", + "final_state": "stopped", + "wrong_generation_rejected": true + } + } + ], + "wake_evidence": "immutable_wait_exit_and_final_state" + } + ], + "original_red": { + "source_commit": "abe5a850343cdeb96606961a7d1bfc062fc52d2a", + "binaries": { + "build\\candidate\\bin\\Release\\termforge_shell.exe": { + "sha256": "62d57a551746f6c9379fba73af87230f1cb1edc9b74de9bc8136a38441fd33c6", + "bytes": 1741312 + }, + "build\\candidate\\bin\\Release\\conpty-backend\\manifest.json": { + "sha256": "e75e522a3355c0a66c6d60cafa02413a83d8757262747f650b77c959c09ed439", + "bytes": 1467 + }, + "build\\candidate\\bin\\Release\\conpty-backend\\conpty.dll": { + "sha256": "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a", + "bytes": 119608 + }, + "build\\candidate\\bin\\Release\\conpty-backend\\OpenConsole.exe": { + "sha256": "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586", + "bytes": 1089376 + } + }, + "samples": [ + { + "sample_sha256": "27341653ab0979ffc5a6feb9904a9abaa146bc37a956780c5a4b63b33d92ba01", + "exit_code": 2, + "outer_timed_out": false, + "job_after": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "desktop_gone": true, + "failure": "paste action did not occur during a partial transfer with consumed bytes and output flood; checkpoint=768 flood=1 sent=3000000 accepted=3000000 total=3000000 state=3" + }, + { + "sample_sha256": "16213745702fcc944287229eafc9c4029de21dd4c2a4651f244eb983e93848ff", + "exit_code": 2, + "outer_timed_out": false, + "job_after": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "desktop_gone": true, + "failure": "cancel transfer completed before receiver gate" + } + ] + }, + "fence_without_ui_notify": { + "base": "abe5a850343cdeb96606961a7d1bfc062fc52d2a", + "patch_sha256": "fef9501e555c5ba103da297e320df903cbd100f4864e9847e38844aa1f410cd0", + "exe_sha256": "6f5c95e32ff688768ac02383735a4864b5acf84f38b49faeaab04392a8f3ba03", + "status": "failed at original 35000ms UI deadline; original one-line UI cancel changed after this run" + }, + "full_suite": { + "source_commit": "62226af2d1f636cef1920ac0ef9998935a7dd243", + "ctest_passed": 52, + "ctest_total": 55, + "failed": [ + "candidate_shell_tui: Vim absent from process PATH", + "workflow_sessions: stage45, original B fixture deadline expired", + "shell: 180s CTest aggregate deadline; resize stage43 and workspace main7/ui52 failures retained" + ], + "log_sha256": "4295f3d895b289cb326b182fa3f1d30e09ea521d096ea95c768cb49aff746c07" + }, + "reviewed_targeted_checks": { + "ctest": [ + "sender_fence", + "sender_fence_pipe", + "paste_fence_evidence" + ], + "ctest_passed": 3, + "ui_positive_processes": 2, + "ui_expected_failure_processes": 2, + "go_ack": "actual anonymous Win32 pipes with same-process endpoints; positive GO uses unit-level payload simulation only", + "pipe_capacity": "actual inbound server buffer checked against whole128B request/192B reply budgets; saturation8+8 and ninthrejection tested" + }, + "limitations": [ + "Full eight-session acceptance still failed; issue53 and parent47 remain open.", + "Candidate/system and default/system have no success claimed from these official-backend samples.", + "No hosted system-clipboard or IME acceptance is claimed here.", + "Held payload is not measured OS backpressure; workload has synchronization and is not a performance benchmark.", + "UI observed producer, output, state and cleanup; actual presented endpoint is unknown.", + "Raw logs and captures remain local; only synthetic byte counts, hashes and whitelisted records published." + ], + "vim_path_followup": { + "source_commit": "a15eb7f67315fb53c9d66b9c5a4424e13511a311", + "vim_version": "9.1 patches1-532 MSYS2", + "vim_sha256": "2e0fda63a89171adfc46c9417d5105cc39ed778cc69a890b83a4eabc34e7646a", + "sample_sha256": "5f35275f20697c8559b0b2e1b7a460b38032b9549fe0bbb60b3ccfb0ad18da01", + "status": "failed", + "failure": "candidate-tui stage59; resize command appeared in inserted text", + "exit_code": 2, + "outer_timed_out": false, + "job_after": { + "active": 0, + "total": 7, + "terminated": 0 + }, + "desktop_gone": true, + "issue": 52 + } +} diff --git a/experiments/shell/results/2026-10-27-workspace-empty-cr/validation.json b/experiments/shell/results/2026-10-27-workspace-empty-cr/validation.json new file mode 100644 index 00000000..1f029aff --- /dev/null +++ b/experiments/shell/results/2026-10-27-workspace-empty-cr/validation.json @@ -0,0 +1,3195 @@ +{ + "source_commit": "4daf99250a2740e1943635b6d46293d633427163", + "platform": "Windows-11-10.0.26200-SP0", + "python": "3.12.10", + "started_utc": "2026-10-26T16:35:57.098152+00:00", + "source_sha256": { + "CMakeLists.txt": "f5bb85e12b7a815c4feb7417da9ebc5298de58664bcd2792b277cb282729da73", + "experiments/shell/transport_completion.hpp": "535ff47bfbdd1038dca22820b467d1ee0d1839a29be38cd750e4de49439b602f", + "experiments/shell/transport_completion_test.cpp": "c9bb02bd739f916e78530590ee0f3b29bc952c58e445d31bc047ee69d301dcee", + "experiments/shell/workspace_persistence_scenario.hpp": "7f70d755549718d0fddb94b43cce12f3955727fcc51599a8bb124bf3e59ec263", + "experiments/shell/test_shell.py": "a6b63c1754b0e0dc1cc01863891f4c7c9d989f818d351838a8cfe295edaf811c", + "experiments/shell/run.py": "45976b828eecb316881d698dedaef09ad304f72d991dfa360f1c552bbcbe9f1d", + "experiments/shell/shell.cpp": "ac4dd44861aa2928213fffe91d30cdaf3aedf19f08468aefeed22828352296cc", + "experiments/shell/test_launcher.cpp": "19cab4a960f47f252c02f92a36316d43eed2481da1f72b5a7e97003c4fa1c2ed", + "experiments/core/candidate_terminal.cpp": "3092ee2ea44ca0f897a7d3b6244c079ba8073dc109993204e435f065388a2ed1", + "experiments/candidates/alacritty/Cargo.lock": "896c6e60909d55d61fb26d277b1a98daf9245e8aee811caff37fc3e9d06ca627" + }, + "source_git_blobs": { + "CMakeLists.txt": "2454a7b8dc34fa2bbd74b25b0ad77d19a71a5f18", + "experiments/shell/transport_completion.hpp": "e038584c5d719d2890ce58b292d5c2ac27c94ca2", + "experiments/shell/transport_completion_test.cpp": "70a412afaa644413460ada14f8c28c91b206d509", + "experiments/shell/workspace_persistence_scenario.hpp": "f7f73dbd771f559384d9b4e569d815d2ef0792da", + "experiments/shell/test_shell.py": "66512ac1099cf477cac8f26f225b18c301c1d3d8", + "experiments/shell/run.py": "bbd4dfc05012c55956973d7e2c69be717b48b1a9", + "experiments/shell/shell.cpp": "90b3f166e8a50ca2488f749e8e188c52cb5e4a91", + "experiments/shell/test_launcher.cpp": "6da0882255cd9587111aa4536b1f0b5a93b193bf", + "experiments/core/candidate_terminal.cpp": "9bb1fab8f6deac294d0ba1a44712b67c919d03c7", + "experiments/candidates/alacritty/Cargo.lock": "6501189a98bfe0610ebcb5e3c31d3abc2ae3f132" + }, + "configurations": [ + { + "build_config": "system-r72", + "candidate": false, + "backend": "system", + "binaries_sha256": { + "shell_clipboard_reader.exe": "6afbaba725490cc3856df97b56a8a16190251b27fa5c34024c63132a6c6bde41", + "shell_ime_client.exe": "2f032970045e33b80b615e0ffebe06e29ebe7b18698b7609bffc129af9b4d7df", + "shell_input_client.exe": "0471578cce2959b200c2e174514cbd8663e2b51db1bb973f8932ebe5a866f1b7", + "shell_query_client.exe": "d92be37207a6a7a4c03d4b08dd1f704ccddc2e86b7ef00085e028168d1a915e1", + "shell_selection_text_client.exe": "0ec9d66d66e672e98487bf38ee4db4313ac7bdad8b6852ebdc8fcd6309fd4358", + "shell_size_client.exe": "323583d4127d4aca27026cbe3ef9328a80390a269e99be763580f706c1cb4115", + "shell_test_launcher.exe": "3e72c4d5bd418eaa22062606f39891f0412acaf46a2841a3f9d219f24bc33014", + "shell_text_client.exe": "f151602090a8700fd48115cd4db1169a9f2cfe8e856f454bc21bf1509ddff85b", + "termforge_shell.exe": "433905e75f4f12699e59adc8c8775756f9098125aa68595d2dec6290c334dfeb", + "transport_completion_test.exe": "ba1c84b60d68d366c9d9e15e0b11e96b84e7066376c805b6ee0122c7defedc67" + }, + "cmake_cache_sha256": "27b22b5286dfc2aba8325201d143f7102400812bf27667300025232fc6394500", + "tests": [ + { + "name": "transport-completion", + "returncode": 0, + "started_utc": "2026-10-26T16:35:57.572260+00:00", + "elapsed_seconds": 0.141528, + "log_sha256": "c804b0583bcb4501edf84ab54dfe03ddcf8aeb00f7e6ffddf65d25cf6e031cb0" + }, + { + "name": "workspace", + "returncode": 0, + "started_utc": "2026-10-26T16:35:57.713788+00:00", + "elapsed_seconds": 10.657289, + "log_sha256": "eae576610ed3c3de80350f33f0d094c51b612339639b0fcf26108f3ba3eb80c2" + } + ], + "cases": [ + { + "case": "workspace-v2-clamped-0", + "sample": { + "mode": "ui-workspace-restore", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-restore", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 0, + 60, + 2560, + 799 + ], + "client": [ + 2544, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 2544, + 238 + ], + "visible": 1, + "grid": [ + 210, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 1272, + 321 + ], + "visible": 1, + "grid": [ + 69, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 1272, + 93, + 1272, + 321 + ], + "visible": 1, + "grid": [ + 139, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "e1b9fdfa4c0be1cf1901dfc4678c2952f5be23b95ca1d2a434c92b17f5069ce0", + "sample.json": "78f0976c47b06a5007bd588febd69f08bf2e3c48ac4476d548896ff6a4af319f", + "workspace.layout": "11f3b16527405dd1119da84184d416509f384d791371fca82644f257d306e87e" + } + }, + { + "case": "workspace-v2-clamped-1", + "sample": { + "mode": "ui-workspace-restore", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-restore", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 0, + 60, + 2560, + 799 + ], + "client": [ + 2544, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 2544, + 238 + ], + "visible": 1, + "grid": [ + 210, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 1272, + 321 + ], + "visible": 1, + "grid": [ + 69, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 1272, + 93, + 1272, + 321 + ], + "visible": 1, + "grid": [ + 139, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "e1b9fdfa4c0be1cf1901dfc4678c2952f5be23b95ca1d2a434c92b17f5069ce0", + "sample.json": "2d802b2f19ac76b98bae2a8455fc094be1abb857543062072d52bdac7d27b74e", + "workspace.layout": "657ae2809533be0c5138485fed93babf98a75061aea17e0f18a4dc74f5913cae" + } + }, + { + "case": "workspace-v2-restore-0", + "sample": { + "mode": "ui-workspace-restore", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-restore", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 90, + 60, + 1216, + 799 + ], + "client": [ + 1200, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 1200, + 238 + ], + "visible": 1, + "grid": [ + 98, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 32, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 600, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 64, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "5cc777f65d580599f8bda2d3e7e3d246b63aba19dba55a27e65d358709402638", + "sample.json": "fa0f1df0488dc2f6c7f650ade82dd1ebe12d40a14ae7b12419dc9f569928daab", + "workspace.layout": "59e62adef0b758158c65b56919350064f7d2c344906d2c848bab80e6f32739de" + } + }, + { + "case": "workspace-v2-restore-1", + "sample": { + "mode": "ui-workspace-restore", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-restore", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 90, + 60, + 1216, + 799 + ], + "client": [ + 1200, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 1200, + 238 + ], + "visible": 1, + "grid": [ + 98, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 32, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 600, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 64, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "5cc777f65d580599f8bda2d3e7e3d246b63aba19dba55a27e65d358709402638", + "sample.json": "95b7d480f900492b5ef1cb2d75ee939eb9bb2c4aa047be9dd213fb49371525fd", + "workspace.layout": "37b9ad8d5a982460f484042018baed17340c713c7e5d361376a1ebd3c31fb21d" + } + }, + { + "case": "workspace-v2-save-0", + "sample": { + "mode": "ui-workspace-save", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-save", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 90, + 60, + 1216, + 799 + ], + "client": [ + 1200, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 1200, + 238 + ], + "visible": 1, + "grid": [ + 98, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 32, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 600, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 64, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "5cc777f65d580599f8bda2d3e7e3d246b63aba19dba55a27e65d358709402638", + "sample.json": "cc79cd9828da737c892cf94466baf7fceda5844c42fee17c140dec83a6ef5275", + "workspace.layout": "59e62adef0b758158c65b56919350064f7d2c344906d2c848bab80e6f32739de" + } + }, + { + "case": "workspace-v2-save-1", + "sample": { + "mode": "ui-workspace-save", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-save", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 90, + 60, + 1216, + 799 + ], + "client": [ + 1200, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 1200, + 238 + ], + "visible": 1, + "grid": [ + 98, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 32, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 600, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 64, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "5cc777f65d580599f8bda2d3e7e3d246b63aba19dba55a27e65d358709402638", + "sample.json": "1316f55f86c6af9c425b9b5d996d0d9fad6b16f3745fda847ca9b798bbe0bb1d", + "workspace.layout": "37b9ad8d5a982460f484042018baed17340c713c7e5d361376a1ebd3c31fb21d" + } + } + ] + }, + { + "build_config": "candidate-system-r72", + "candidate": true, + "backend": "system", + "binaries_sha256": { + "shell_clipboard_reader.exe": "71faaa8009762d7bfe0186cc499422762f532581288eb57fde949d7774872e98", + "shell_ime_client.exe": "c671b3bcc051f2f4397467fa52029a964a721613841913e7c2e334355d4e5236", + "shell_input_client.exe": "a550513b18161e05bae681d4c9ddf79cb1a86d42bafb3a7518057a44105b99c2", + "shell_query_client.exe": "58e704e2090c4b9b0550622c84cb4ffafc6f6f42df1cdfe363a0e99e6e7db486", + "shell_selection_text_client.exe": "37862fcc2f1db49118d10c3e03dac1be851bf255cbebe2f0556f89062342bc18", + "shell_size_client.exe": "427afbd778881f5e921a1782d473ad190a1340fb691b95adce9e138a516317b5", + "shell_test_launcher.exe": "cb96d03fed43d7e63e820e0ced6cf0f241762b0cc4dbec094c458041f38f1e55", + "shell_text_client.exe": "b81cba00bdaed52c50783d6fa0a1528301e3df40d2c6c4e060aa01a9c2f81ce3", + "termforge_shell.exe": "6c756b03e445dfec9a1fd4fd8b5d628f031076d546defd4e3f3eb0c546e1cb4d", + "transport_completion_test.exe": "1945189627c71d0bedd1f8875f6bb80da6065246c687fa8651efa205346d336b" + }, + "cmake_cache_sha256": "65d497ede16ae771b8f5bad312df0d7dc98e70c095622bfbec009f455c6decdb", + "tests": [ + { + "name": "transport-completion", + "returncode": 0, + "started_utc": "2026-10-26T16:36:08.380170+00:00", + "elapsed_seconds": 0.144565, + "log_sha256": "173c950ca1f323012ddd687b96628b2bd747bbf63e28a971fef46fa28b0e2e66" + }, + { + "name": "workspace", + "returncode": 0, + "started_utc": "2026-10-26T16:36:08.524735+00:00", + "elapsed_seconds": 10.226751, + "log_sha256": "276fc4b4f011a8e5eae6e3b748d01a83b0f907771d7463fb7677c23470b846f4" + } + ], + "cases": [ + { + "case": "workspace-v2-clamped-0", + "sample": { + "mode": "ui-workspace-restore", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-restore", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 0, + 60, + 2560, + 799 + ], + "client": [ + 2544, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 2544, + 238 + ], + "visible": 1, + "grid": [ + 210, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 1272, + 321 + ], + "visible": 1, + "grid": [ + 69, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 1272, + 93, + 1272, + 321 + ], + "visible": 1, + "grid": [ + 139, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "e1b9fdfa4c0be1cf1901dfc4678c2952f5be23b95ca1d2a434c92b17f5069ce0", + "sample.json": "4120d7f440c8effc70b25ba522cbc3b44a64999a4e1875f467478fe96b03245a", + "workspace.layout": "2b84a08b2af758e35a196ad71dc3356a5c15836c9ff301845e5a0fcc80fc767a" + } + }, + { + "case": "workspace-v2-clamped-1", + "sample": { + "mode": "ui-workspace-restore", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-restore", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 0, + 60, + 2560, + 799 + ], + "client": [ + 2544, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 2544, + 238 + ], + "visible": 1, + "grid": [ + 210, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 1272, + 321 + ], + "visible": 1, + "grid": [ + 69, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 1272, + 93, + 1272, + 321 + ], + "visible": 1, + "grid": [ + 139, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "e1b9fdfa4c0be1cf1901dfc4678c2952f5be23b95ca1d2a434c92b17f5069ce0", + "sample.json": "0b54da0e331d564807006ede5bc5141d6ebb3dcbc78fc703ec9afb84e9e835f8", + "workspace.layout": "b396797597866e3036655c455b67ea7f3525fb73bc83dcd4a6124740f6a6a00d" + } + }, + { + "case": "workspace-v2-restore-0", + "sample": { + "mode": "ui-workspace-restore", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-restore", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 90, + 60, + 1216, + 799 + ], + "client": [ + 1200, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 1200, + 238 + ], + "visible": 1, + "grid": [ + 98, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 32, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 600, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 64, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "5cc777f65d580599f8bda2d3e7e3d246b63aba19dba55a27e65d358709402638", + "sample.json": "bb7dcffd003f6836c81ebe2c1a76add10137e75c17de550d4c79f02cf09ac770", + "workspace.layout": "fad7156f6463e92356c52c64121b793c2989eada6377228f502b314dcc1cd822" + } + }, + { + "case": "workspace-v2-restore-1", + "sample": { + "mode": "ui-workspace-restore", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-restore", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 90, + 60, + 1216, + 799 + ], + "client": [ + 1200, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 1200, + 238 + ], + "visible": 1, + "grid": [ + 98, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 32, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 600, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 64, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "5cc777f65d580599f8bda2d3e7e3d246b63aba19dba55a27e65d358709402638", + "sample.json": "bb37ee8b05088b72c466f87a716434b2ad1eee5e83f5597a8c438de7c729415b", + "workspace.layout": "dff8885eeadffc72e58dc84b031fe4d6a3179edeffce82853c5f480e12c6d055" + } + }, + { + "case": "workspace-v2-save-0", + "sample": { + "mode": "ui-workspace-save", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-save", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 90, + 60, + 1216, + 799 + ], + "client": [ + 1200, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 1200, + 238 + ], + "visible": 1, + "grid": [ + 98, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 32, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 600, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 64, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "5cc777f65d580599f8bda2d3e7e3d246b63aba19dba55a27e65d358709402638", + "sample.json": "ed0cfb5a2ddcbffb67515f70aceca1817e78da50eee4979166aeae7e07bed292", + "workspace.layout": "fad7156f6463e92356c52c64121b793c2989eada6377228f502b314dcc1cd822" + } + }, + { + "case": "workspace-v2-save-1", + "sample": { + "mode": "ui-workspace-save", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-save", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 90, + 60, + 1216, + 799 + ], + "client": [ + 1200, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 1200, + 238 + ], + "visible": 1, + "grid": [ + 98, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 32, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 600, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 64, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "5cc777f65d580599f8bda2d3e7e3d246b63aba19dba55a27e65d358709402638", + "sample.json": "ab509cefe2e4a9dde03dc8d9e12b0d3060026a8b8392db9a847a004321544d1b", + "workspace.layout": "dff8885eeadffc72e58dc84b031fe4d6a3179edeffce82853c5f480e12c6d055" + } + } + ] + }, + { + "build_config": "candidate", + "candidate": true, + "backend": "reviewed-side-by-side-1.25.260930003", + "binaries_sha256": { + "alpha_normal_smoke.exe": "36ac6dbe36c942b9850d6d9f5b3538fbfe2fc251f38ac026ea12c343b0e9abc8", + "candidate_core_test.exe": "6e9df1abbe2271690a1480a1be9042e219a5947779b6a4b03f529a742fbd10ce", + "candidate_reply_close_test.exe": "40603aa54fbad42d55a5a7ea24b7aecebefdacd8f3c802e3ac89c52ef3a4b74d", + "candidate_resize_replay.exe": "3d91227adb37bba4defcef8f803f0f3c2916001223300b99d18e0e2ab695de94", + "candidate_title_test.exe": "90db63148f6aafaf79d65d5b40e61bbdc9f54486a6be36e26523699e0b46035d", + "clipboard_policy_test.exe": "809815e9838cad5b3443c98620d23a4e6c7186b5304b524d0990988eb47a6da9", + "conpty_backend_probe.exe": "3c54962c20f04e688a0413bb822713e908582af20b0be8bcfd188fb436907ef8", + "conpty_ctrl_c_probe.exe": "ac5ae5ccdb77c82512a8a6d769fafbe74693bf6fc535ce4d22bf60b0c8df2bfa", + "conpty_margin_probe.exe": "5404b009d0e005cd940d8a65afeb0fb9caf9c9ce2a18f651a88146343c41c252", + "core_measure.exe": "df9df797b16239d2fb71707f8a042a1ed956ad30c407d884527cdf18d600e785", + "find_bar_test.exe": "030733a2396ca9b31bbd029c610f1b838ab52394338c08410274501747dd9982", + "history_test.exe": "035507366facfd6de00e0c2762775c8d3816f605da3e55d498995a74a3f447d2", + "ime_context_test.exe": "be0d71ca72aee3dbd1565c9ad2314f81f0cd0c5511ec54cb623bbd39eede9974", + "ime_input_expectation_test.exe": "a5ea4532a06b893d955299793b4859cb9ccf221fab64e51949e1af2ee6ba5d0b", + "keyboard_test.exe": "18a35c2b2ab8a6316e0c938d8c36d0b82bcc131989d683205888b08bf4d33e19", + "layout_state_test.exe": "307519003536ce16d7d6fc552b3a25e9de25356d50a80c2de0c01168a10784a9", + "paste_test.exe": "47e9a5ce33335370b6fb199b0ff11adc19e2ab55bf64657061a13c1c020a24b0", + "presentation_probe.exe": "b7043cace46f65b22c7295df2fc06b214f060c2fd6fe18ac5ff27e13d943e096", + "raw_console_receiver_test.exe": "9c05cdb80d87ff264b3275387eb01dabc8f57640adbc7ee3910b0fc44f378224", + "real_app_capture.exe": "011efaad94da74ce182e30a3ff8abb0c1e1f58235bc77fa8c347ec3d9dcb822e", + "receive.exe": "ef316e11ddcc764ddac57426fc37b8b69f599ddb28a0ffedf5c2e85a3004a9d4", + "recovery_probe.exe": "c84e7b1b1792773b993f2758d5cafa59b80495451c01d194c3afd6cc653f0344", + "renderer_metrics_test.exe": "4cebad62798ff0f8eac42641a1119c7b9725728dd5fd31befcdf7e1ecb686cfd", + "replay.exe": "674f96ebbf00497576951a6780a5cf5195e3b80cc4acdb7f296fa8910a6203b7", + "resize_test.exe": "f566c56a1fa34e2a8ff480900ac5ae5059e2ac6872554208d5dbf0e24673dde0", + "runtime_observation_test.exe": "dfd04533d35d78401da75ad7d0965d923d7b7e95ad304775367b6088acc341ed", + "scenario_tick_gate_test.exe": "06cd4e813f1034d1a8c7fea592979f49c3ef89117c97fc6636cdae5012017ce8", + "search_test.exe": "0f29f3ea3147cc45eb2e500e683f3bcc1db04f820470b2ff473fffcb31cdb4f4", + "selection_test.exe": "104576787b62c2c4bb2428e0786c7742e63a23ceb766df293c88a5ccc6a1b836", + "sender_fence_pipe_test.exe": "6a94ac84631434a81ae1098932813d7ceeb81f2bb33a2e4b2456834bd8972e92", + "sender_fence_test.exe": "ab4af3b0f148f6e6377e70c98936c6c34d0be0d3cee4b17b3f8554c39b2d3e52", + "sent_message_wait_test.exe": "bc3cff14f60e3f1b17b3fd6255ff1c5b507f506d897d528802efa9666be74dcf", + "shell_clipboard_reader.exe": "bacc4bff465ca644fae61f9d26184a7adc22df9793470feca647af1d823fece8", + "shell_ime_client.exe": "d787ebf702cf5895e1ad0d71f895a51b3dead34d02953b5e7deff086c2bf8eb7", + "shell_input_client.exe": "850c82fb8bb58de8170bf096cc6dd4f8ba34d9ba11dbd2addc41508cddc31d8c", + "shell_launch_test.exe": "ace9b63148848bf7b405edb4b8818ca23d52699ac2ce8ca03a90c5bc1e32622d", + "shell_paste_probe.exe": "1370c6381b990270dfd816f53758e5aa57c63deba050fa7cf0dbd0d5cbf4f315", + "shell_query_client.exe": "71419f7918bc58f4ee47d14f8e6de35aa1828ac838a73ed607797fc120f56886", + "shell_queue_probe.exe": "7f34d6ff66b3e00d56798e22a480fb20a5afa083cff65a9d689dcbca44dbe12d", + "shell_selection_text_client.exe": "dbad81a22ecce4508eada90ae445f9a75abfe1effd2571776ef31b32144c8ac4", + "shell_size_client.exe": "5394d9863df9a589012f0d9a31f7e4167e7bc4ea23e93f9711c0a67c540651f6", + "shell_test_launcher.exe": "dbde4249dc08c7df80120398ffd252aa5e4958e4ef640cc161ea4c2d28b9f291", + "shell_text_client.exe": "9485b03424e421a4018596cc9487e7feeb33c26d30820985aa45843fdcf315d9", + "termforge_conpty_probe.exe": "7dd59bc8d0309ad5b131cde1c3ed98819139b6bb70f505347c98aabe8396f618", + "termforge_ime_probe.exe": "788a186b5fafb6d155eeab74f78d4f7cb992a599937b3aa48c5efcf49affe993", + "termforge_shell.exe": "650ea74ba3ad39b91ab4773c72af3ee70e59bc9d63e76249ea8fa6745a367081", + "terminal_trace.exe": "40e869753f4c00515ee0990f3732eac04f965fe2c3070f1bf772717476269f3d", + "text_action_policy_test.exe": "535b469c9aa0da7e77670578f75d04956d98e9f912d5c9a6c310ab796ab58d4f", + "transport_completion_test.exe": "9f63a2f8a08ddcd459d422f69bfbeae9ceaee9527edc0198c7e4fbd07bff7dc4", + "workflow_fixture_test.exe": "a6624a6b1536c6f445f92e05ff72a5318ece47d455500f6adc467d94f9f04222", + "workspace_state_test.exe": "ac3fbc8a56d33efda33cc9040c731c183e0a8666e23fce48d368c1868797714d" + }, + "cmake_cache_sha256": "d652393042e1195908894a592dda1ebf0c8e1e8ccd3e05ecacec2760b41bc2ff", + "tests": [ + { + "name": "transport-completion", + "returncode": 0, + "started_utc": "2026-10-26T16:36:18.796351+00:00", + "elapsed_seconds": 0.134451, + "log_sha256": "2bcde71179fda986d1dab2d5b84737f9d5a4c965da407e6f2f07b4c60faa49cd" + }, + { + "name": "workspace", + "returncode": 0, + "started_utc": "2026-10-26T16:36:18.932314+00:00", + "elapsed_seconds": 10.383378, + "log_sha256": "9004ff6178e1bec317c7c8de5c70c0a7318ef5a298aae3e48a161e85b05a2b34" + } + ], + "cases": [ + { + "case": "workspace-v2-clamped-0", + "sample": { + "mode": "ui-workspace-restore", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-restore", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 0, + 60, + 2560, + 799 + ], + "client": [ + 2544, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 2544, + 238 + ], + "visible": 1, + "grid": [ + 210, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 1272, + 321 + ], + "visible": 1, + "grid": [ + 69, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 1272, + 93, + 1272, + 321 + ], + "visible": 1, + "grid": [ + 139, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "e1b9fdfa4c0be1cf1901dfc4678c2952f5be23b95ca1d2a434c92b17f5069ce0", + "sample.json": "4f5443ddb183618cbe787be35499c53a9c5dc71383e1242f44b98b565ecb2794", + "workspace.layout": "85ad2c772bbe4ae1a16ac48dcb4d6d52dfaed5a50f60cbf818a6538d94fa32af" + } + }, + { + "case": "workspace-v2-clamped-1", + "sample": { + "mode": "ui-workspace-restore", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-restore", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 0, + 60, + 2560, + 799 + ], + "client": [ + 2544, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 2544, + 238 + ], + "visible": 1, + "grid": [ + 210, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 1272, + 321 + ], + "visible": 1, + "grid": [ + 69, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 1272, + 93, + 1272, + 321 + ], + "visible": 1, + "grid": [ + 139, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "e1b9fdfa4c0be1cf1901dfc4678c2952f5be23b95ca1d2a434c92b17f5069ce0", + "sample.json": "88bd123a353389461c7e3f69a05baac16695a3b1a43c70dfbf4d8818f996561d", + "workspace.layout": "eaa185a9288a25bab6e5d3518fb87182b9959b3b591a1840a0699cb999c62fff" + } + }, + { + "case": "workspace-v2-restore-0", + "sample": { + "mode": "ui-workspace-restore", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-restore", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 90, + 60, + 1216, + 799 + ], + "client": [ + 1200, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 1200, + 238 + ], + "visible": 1, + "grid": [ + 98, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 32, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 600, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 64, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "5cc777f65d580599f8bda2d3e7e3d246b63aba19dba55a27e65d358709402638", + "sample.json": "61e5af44331e37e52fc4f46e10f56e8ba11f2c1316d3a123ce79d3dbda073d2e", + "workspace.layout": "c5cc46a5395e488830e636e2cae90b038fe5eaac0cb3244395754c24830757fe" + } + }, + { + "case": "workspace-v2-restore-1", + "sample": { + "mode": "ui-workspace-restore", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-restore", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 90, + 60, + 1216, + 799 + ], + "client": [ + 1200, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 1200, + 238 + ], + "visible": 1, + "grid": [ + 98, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 32, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 600, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 64, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "5cc777f65d580599f8bda2d3e7e3d246b63aba19dba55a27e65d358709402638", + "sample.json": "23088bdaea3a39e571c3407357847fc1559b62839b5a5fd13bad69bc8f0bf9d5", + "workspace.layout": "ce7ccde5387ee25fc6ef9e2485f794d737406942d0cdbd72970c33938bf0cd8d" + } + }, + { + "case": "workspace-v2-save-0", + "sample": { + "mode": "ui-workspace-save", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-save", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 90, + 60, + 1216, + 799 + ], + "client": [ + 1200, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 1200, + 238 + ], + "visible": 1, + "grid": [ + 98, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 32, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 600, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 64, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "5cc777f65d580599f8bda2d3e7e3d246b63aba19dba55a27e65d358709402638", + "sample.json": "118e7fca8cedbd507e67a915eb98425603dc48d12edd50cfeee8119221788d46", + "workspace.layout": "c5cc46a5395e488830e636e2cae90b038fe5eaac0cb3244395754c24830757fe" + } + }, + { + "case": "workspace-v2-save-1", + "sample": { + "mode": "ui-workspace-save", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "job_after_cleanup": { + "active": 0, + "total": 14, + "terminated": 0 + }, + "desktop_gone": true, + "result": { + "valid": true, + "mode": "ui-workspace-save", + "sessions_closed": 3, + "active_children_after": 0, + "settings_save_failed": false, + "presented_done": null + }, + "data_only": true + }, + "geometry": { + "schema": "termforge-workspace-restore-v1", + "dpi": 96, + "window": [ + 90, + 60, + 1216, + 799 + ], + "client": [ + 1200, + 760 + ], + "ratios": [ + 0.24, + 0.21, + 0.57, + 0.38, + 0.69 + ], + "sides": 0, + "focus_mode": 0, + "focused": "bottom", + "bottom_active": 0, + "main_active": 0, + "split_index": 1, + "split_active": 1, + "split_right_focus": 1, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "title": "底部 cmd", + "zoom": 100, + "rect": [ + 0, + 465, + 1200, + 238 + ], + "visible": 1, + "grid": [ + 98, + 9 + ], + "cell": [ + 12, + 26 + ] + }, + { + "group": "main", + "kind": "cmd", + "title": "主 cmd", + "zoom": 150, + "rect": [ + 0, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 32, + 8 + ], + "cell": [ + 18, + 39 + ] + }, + { + "group": "main", + "kind": "powershell", + "title": "研究 PowerShell", + "zoom": 75, + "rect": [ + 600, + 93, + 600, + 321 + ], + "visible": 1, + "grid": [ + 64, + 16 + ], + "cell": [ + 9, + 20 + ] + } + ] + }, + "raw_sha256": { + "restore-geometry.json": "5cc777f65d580599f8bda2d3e7e3d246b63aba19dba55a27e65d358709402638", + "sample.json": "33bb1634252634013da65fd77333a253ad2277350fdbac4ccbad07cc9e1596fa", + "workspace.layout": "ce7ccde5387ee25fc6ef9e2485f794d737406942d0cdbd72970c33938bf0cd8d" + } + } + ] + } + ], + "completed_utc": "2026-10-26T16:36:29.317251+00:00", + "local_identity_sha256": "4584b1e3190d354115907cf46ad9344d9d45a6048da4fc6babe6f61f9407aeaa", + "schema": "termforge-workspace-empty-cr-v1", + "scope": "Test completion oracle correction; no product persistence or timeout change", + "compiler": "MSVC 19.42.34433 x64 Release; Windows SDK 10.0.26100", + "alacritty": "0.26.1-dev+termforge.4", + "candidate_library_sha256": "aa41a9ea5749f3933a0db8bfa41dda68812c68727d47d394fc3edd859973a260", + "preserved_contract": { + "observation_cap_bytes": 8192, + "native_deadline_seconds": 30, + "outer_deadline_seconds": 40, + "rounds_per_configuration": 2, + "processes_per_round": 3, + "independent_exact_output_and_fresh_prompt": true, + "unicode_directories_and_power_shell_version": true, + "layout_titles_zoom_split_window_and_child_grid": true, + "owned_job_and_noninput_desktop": true, + "presented_done": null + }, + "causal_observation": { + "original_and_repeated_failure_bytes_identical": true, + "failure_json_sha256": "71b9a860f70cbee2e8b52501bb0877ee8e218c8962f580321df40e215f4d6f4a", + "powershell_observed_bytes": 122, + "powershell_observed_sha256": "4c1d91f70cb82f745b1fa3eab3bca03048ac419aab0fe8c1e510cc2bc63041b5", + "original_run": 40313671258, + "original_job": 121540798389, + "repeated_run": 40355938302, + "repeated_job": 121688289054, + "repeated_source_head": "848cb0e621a291f732cc0e046d0b2fab6a77d962", + "repeated_checkout": "f72c239bb1bfa6d978c72646d88cbccf434f3fd5", + "repeated_artifact": 12665678304, + "repeated_archive_sha256": "c0b2aad326ee14b3e3c89aba06c12366bdf7f45af3a2233709729427919c4ab2", + "original_status": "failed", + "original_exit_code": 2, + "original_stage": 2, + "native_child_exit": null, + "reader_eof": null, + "native_forced_cleanup": null, + "no_cr_diagnostic_control_passed_old_helper": true, + "unchanged_captured_bytes_passed_old_helper": false, + "unchanged_captured_bytes_passed_corrected_helper": true, + "cr_removal": "diagnostic control only; accepted input and transport bytes remain unchanged" + }, + "causal_runs": [ + { + "phase": "red", + "passed": false, + "runtime_seconds": 0.02, + "files_sha256": { + "ctest.log": "d9d8617d9bbbebd631e88c800872fee19634c2673523b2f7348d7df4b3525a09", + "transport_completion.hpp": "47f85232785ccade107123342019a81f013991e33cacffad7616ce8334fe69ab", + "transport_completion_test.cpp": "c9bb02bd739f916e78530590ee0f3b29bc952c58e445d31bc047ee69d301dcee", + "transport_completion_test.exe": "62b08594caa62170468316ad65e6038954e9d2d40bddb0ce42db1342732cea9d" + } + }, + { + "phase": "green", + "passed": true, + "runtime_seconds": 0.01, + "files_sha256": { + "ctest.log": "8d61cc19d56f28141ec46fdb9d22845036f434cbe5f49301bdc14d426c36c46f", + "transport_completion.hpp": "535ff47bfbdd1038dca22820b467d1ee0d1839a29be38cd750e4de49439b602f", + "transport_completion_test.cpp": "c9bb02bd739f916e78530590ee0f3b29bc952c58e445d31bc047ee69d301dcee", + "transport_completion_test.exe": "9f63a2f8a08ddcd459d422f69bfbeae9ceaee9527edc0198c7e4fbd07bff7dc4" + } + } + ], + "full_candidate_official": { + "source_commit": "4daf99250a2740e1943635b6d46293d633427163", + "platform": "Windows-11-10.0.26200-SP0", + "started_utc": "2026-10-26T16:37:05.024869+00:00", + "backend": "reviewed-side-by-side-1.25.260930003", + "candidate": true, + "binaries_sha256": { + "alpha_normal_smoke.exe": "36ac6dbe36c942b9850d6d9f5b3538fbfe2fc251f38ac026ea12c343b0e9abc8", + "candidate_core_test.exe": "6e9df1abbe2271690a1480a1be9042e219a5947779b6a4b03f529a742fbd10ce", + "candidate_reply_close_test.exe": "40603aa54fbad42d55a5a7ea24b7aecebefdacd8f3c802e3ac89c52ef3a4b74d", + "candidate_resize_replay.exe": "3d91227adb37bba4defcef8f803f0f3c2916001223300b99d18e0e2ab695de94", + "candidate_title_test.exe": "90db63148f6aafaf79d65d5b40e61bbdc9f54486a6be36e26523699e0b46035d", + "clipboard_policy_test.exe": "809815e9838cad5b3443c98620d23a4e6c7186b5304b524d0990988eb47a6da9", + "conpty_backend_probe.exe": "3c54962c20f04e688a0413bb822713e908582af20b0be8bcfd188fb436907ef8", + "conpty_ctrl_c_probe.exe": "ac5ae5ccdb77c82512a8a6d769fafbe74693bf6fc535ce4d22bf60b0c8df2bfa", + "conpty_margin_probe.exe": "5404b009d0e005cd940d8a65afeb0fb9caf9c9ce2a18f651a88146343c41c252", + "core_measure.exe": "df9df797b16239d2fb71707f8a042a1ed956ad30c407d884527cdf18d600e785", + "find_bar_test.exe": "030733a2396ca9b31bbd029c610f1b838ab52394338c08410274501747dd9982", + "history_test.exe": "035507366facfd6de00e0c2762775c8d3816f605da3e55d498995a74a3f447d2", + "ime_context_test.exe": "be0d71ca72aee3dbd1565c9ad2314f81f0cd0c5511ec54cb623bbd39eede9974", + "ime_input_expectation_test.exe": "a5ea4532a06b893d955299793b4859cb9ccf221fab64e51949e1af2ee6ba5d0b", + "keyboard_test.exe": "18a35c2b2ab8a6316e0c938d8c36d0b82bcc131989d683205888b08bf4d33e19", + "layout_state_test.exe": "307519003536ce16d7d6fc552b3a25e9de25356d50a80c2de0c01168a10784a9", + "paste_test.exe": "47e9a5ce33335370b6fb199b0ff11adc19e2ab55bf64657061a13c1c020a24b0", + "presentation_probe.exe": "b7043cace46f65b22c7295df2fc06b214f060c2fd6fe18ac5ff27e13d943e096", + "raw_console_receiver_test.exe": "9c05cdb80d87ff264b3275387eb01dabc8f57640adbc7ee3910b0fc44f378224", + "real_app_capture.exe": "011efaad94da74ce182e30a3ff8abb0c1e1f58235bc77fa8c347ec3d9dcb822e", + "receive.exe": "ef316e11ddcc764ddac57426fc37b8b69f599ddb28a0ffedf5c2e85a3004a9d4", + "recovery_probe.exe": "c84e7b1b1792773b993f2758d5cafa59b80495451c01d194c3afd6cc653f0344", + "renderer_metrics_test.exe": "4cebad62798ff0f8eac42641a1119c7b9725728dd5fd31befcdf7e1ecb686cfd", + "replay.exe": "674f96ebbf00497576951a6780a5cf5195e3b80cc4acdb7f296fa8910a6203b7", + "resize_test.exe": "f566c56a1fa34e2a8ff480900ac5ae5059e2ac6872554208d5dbf0e24673dde0", + "runtime_observation_test.exe": "dfd04533d35d78401da75ad7d0965d923d7b7e95ad304775367b6088acc341ed", + "scenario_tick_gate_test.exe": "06cd4e813f1034d1a8c7fea592979f49c3ef89117c97fc6636cdae5012017ce8", + "search_test.exe": "0f29f3ea3147cc45eb2e500e683f3bcc1db04f820470b2ff473fffcb31cdb4f4", + "selection_test.exe": "104576787b62c2c4bb2428e0786c7742e63a23ceb766df293c88a5ccc6a1b836", + "sender_fence_pipe_test.exe": "6a94ac84631434a81ae1098932813d7ceeb81f2bb33a2e4b2456834bd8972e92", + "sender_fence_test.exe": "ab4af3b0f148f6e6377e70c98936c6c34d0be0d3cee4b17b3f8554c39b2d3e52", + "sent_message_wait_test.exe": "bc3cff14f60e3f1b17b3fd6255ff1c5b507f506d897d528802efa9666be74dcf", + "shell_clipboard_reader.exe": "bacc4bff465ca644fae61f9d26184a7adc22df9793470feca647af1d823fece8", + "shell_ime_client.exe": "d787ebf702cf5895e1ad0d71f895a51b3dead34d02953b5e7deff086c2bf8eb7", + "shell_input_client.exe": "850c82fb8bb58de8170bf096cc6dd4f8ba34d9ba11dbd2addc41508cddc31d8c", + "shell_launch_test.exe": "ace9b63148848bf7b405edb4b8818ca23d52699ac2ce8ca03a90c5bc1e32622d", + "shell_paste_probe.exe": "1370c6381b990270dfd816f53758e5aa57c63deba050fa7cf0dbd0d5cbf4f315", + "shell_query_client.exe": "71419f7918bc58f4ee47d14f8e6de35aa1828ac838a73ed607797fc120f56886", + "shell_queue_probe.exe": "7f34d6ff66b3e00d56798e22a480fb20a5afa083cff65a9d689dcbca44dbe12d", + "shell_selection_text_client.exe": "dbad81a22ecce4508eada90ae445f9a75abfe1effd2571776ef31b32144c8ac4", + "shell_size_client.exe": "5394d9863df9a589012f0d9a31f7e4167e7bc4ea23e93f9711c0a67c540651f6", + "shell_test_launcher.exe": "dbde4249dc08c7df80120398ffd252aa5e4958e4ef640cc161ea4c2d28b9f291", + "shell_text_client.exe": "9485b03424e421a4018596cc9487e7feeb33c26d30820985aa45843fdcf315d9", + "termforge_conpty_probe.exe": "7dd59bc8d0309ad5b131cde1c3ed98819139b6bb70f505347c98aabe8396f618", + "termforge_ime_probe.exe": "788a186b5fafb6d155eeab74f78d4f7cb992a599937b3aa48c5efcf49affe993", + "termforge_shell.exe": "650ea74ba3ad39b91ab4773c72af3ee70e59bc9d63e76249ea8fa6745a367081", + "terminal_trace.exe": "40e869753f4c00515ee0990f3732eac04f965fe2c3070f1bf772717476269f3d", + "text_action_policy_test.exe": "535b469c9aa0da7e77670578f75d04956d98e9f912d5c9a6c310ab796ab58d4f", + "transport_completion_test.exe": "9f63a2f8a08ddcd459d422f69bfbeae9ceaee9527edc0198c7e4fbd07bff7dc4", + "workflow_fixture_test.exe": "a6624a6b1536c6f445f92e05ff72a5318ece47d455500f6adc467d94f9f04222", + "workspace_state_test.exe": "ac3fbc8a56d33efda33cc9040c731c183e0a8666e23fce48d368c1868797714d" + }, + "vim_sha256": "2e0fda63a89171adfc46c9417d5105cc39ed778cc69a890b83a4eabc34e7646a", + "vim_version": [ + "VIM - Vi IMproved 9.1 (2024 Jan 02, compiled Jul 09 2024 07:07:42)", + "Included patches: 1-532", + "Compiled by " + ], + "cmake_cache_sha256": "d652393042e1195908894a592dda1ebf0c8e1e8ccd3e05ecacec2760b41bc2ff", + "returncode": 8, + "completed_utc": "2026-10-26T16:44:34.597552+00:00", + "log_sha256": "2e0a4fd1afa9234461cb2598a6d52063115edcce3510017644d026125c7d37a7", + "last_test_sha256": "4da7c00a9422b9911c25cf4b6aba315e7e723e758132f61e285e5ca61bc0cf11", + "tests": [ + { + "name": "shell_launch", + "status": "run", + "seconds": 0.0119766, + "failed": false, + "skipped": false + }, + { + "name": "candidate_title", + "status": "run", + "seconds": 0.679326, + "failed": false, + "skipped": false + }, + { + "name": "candidate_reply_close", + "status": "run", + "seconds": 0.331894, + "failed": false, + "skipped": false + }, + { + "name": "runtime_observation", + "status": "run", + "seconds": 1.42167, + "failed": false, + "skipped": false + }, + { + "name": "ime_input_expectation", + "status": "run", + "seconds": 0.0101662, + "failed": false, + "skipped": false + }, + { + "name": "sent_message_wait", + "status": "run", + "seconds": 0.272669, + "failed": false, + "skipped": false + }, + { + "name": "find_bar", + "status": "run", + "seconds": 0.372583, + "failed": false, + "skipped": false + }, + { + "name": "renderer_metrics", + "status": "run", + "seconds": 0.89513, + "failed": false, + "skipped": false + }, + { + "name": "ime_context", + "status": "run", + "seconds": 0.333653, + "failed": false, + "skipped": false + }, + { + "name": "layout_state", + "status": "run", + "seconds": 0.123253, + "failed": false, + "skipped": false + }, + { + "name": "workspace_state", + "status": "run", + "seconds": 0.31827, + "failed": false, + "skipped": false + }, + { + "name": "transport_completion", + "status": "run", + "seconds": 0.0096818, + "failed": false, + "skipped": false + }, + { + "name": "scenario_tick_gate", + "status": "run", + "seconds": 0.0094621, + "failed": false, + "skipped": false + }, + { + "name": "candidate_core", + "status": "run", + "seconds": 1.12604, + "failed": false, + "skipped": false + }, + { + "name": "resize_core", + "status": "run", + "seconds": 0.0141228, + "failed": false, + "skipped": false + }, + { + "name": "history_core", + "status": "run", + "seconds": 0.0160041, + "failed": false, + "skipped": false + }, + { + "name": "selection_core", + "status": "run", + "seconds": 0.0736148, + "failed": false, + "skipped": false + }, + { + "name": "search_core", + "status": "run", + "seconds": 0.104065, + "failed": false, + "skipped": false + }, + { + "name": "paste", + "status": "run", + "seconds": 0.0100795, + "failed": false, + "skipped": false + }, + { + "name": "sender_fence", + "status": "run", + "seconds": 1.01974, + "failed": false, + "skipped": false + }, + { + "name": "sender_fence_pipe", + "status": "run", + "seconds": 0.377036, + "failed": false, + "skipped": false + }, + { + "name": "clipboard_policy", + "status": "run", + "seconds": 0.0080975, + "failed": false, + "skipped": false + }, + { + "name": "keyboard", + "status": "run", + "seconds": 0.0083753, + "failed": false, + "skipped": false + }, + { + "name": "text_action_policy", + "status": "run", + "seconds": 0.0075863, + "failed": false, + "skipped": false + }, + { + "name": "candidate_resize_replay", + "status": "run", + "seconds": 0.456575, + "failed": false, + "skipped": false + }, + { + "name": "candidate_shell_tui", + "status": "fail", + "seconds": 30.2581, + "failed": true, + "skipped": false + }, + { + "name": "core", + "status": "run", + "seconds": 5.70024, + "failed": false, + "skipped": false + }, + { + "name": "recovery", + "status": "run", + "seconds": 8.93909, + "failed": false, + "skipped": false + }, + { + "name": "measure", + "status": "run", + "seconds": 3.92379, + "failed": false, + "skipped": false + }, + { + "name": "binary_paths", + "status": "run", + "seconds": 0.190192, + "failed": false, + "skipped": false + }, + { + "name": "conpty_margin_contract", + "status": "run", + "seconds": 0.292098, + "failed": false, + "skipped": false + }, + { + "name": "benchmark_tools", + "status": "run", + "seconds": 5.29884, + "failed": false, + "skipped": false + }, + { + "name": "differential", + "status": "run", + "seconds": 2.9914, + "failed": false, + "skipped": false + }, + { + "name": "ime_inventory", + "status": "run", + "seconds": 48.3912, + "failed": false, + "skipped": false + }, + { + "name": "text_actions", + "status": "run", + "seconds": 5.26478, + "failed": false, + "skipped": false + }, + { + "name": "workflow_fixture", + "status": "run", + "seconds": 22.4382, + "failed": false, + "skipped": false + }, + { + "name": "workflow_sessions", + "status": "run", + "seconds": 73.5678, + "failed": false, + "skipped": false + }, + { + "name": "workflow_resources", + "status": "run", + "seconds": 0.473082, + "failed": false, + "skipped": false + }, + { + "name": "workflow_resource_phases", + "status": "run", + "seconds": 4.20935, + "failed": false, + "skipped": false + }, + { + "name": "workflow_system_content", + "status": "run", + "seconds": 0.139848, + "failed": false, + "skipped": false + }, + { + "name": "workflow_system_runner", + "status": "run", + "seconds": 0.59471, + "failed": false, + "skipped": false + }, + { + "name": "copy_error_frame", + "status": "run", + "seconds": 0.122081, + "failed": false, + "skipped": false + }, + { + "name": "paste_fence_evidence", + "status": "run", + "seconds": 0.101017, + "failed": false, + "skipped": false + }, + { + "name": "resize_trace", + "status": "run", + "seconds": 0.115742, + "failed": false, + "skipped": false + }, + { + "name": "research_guards", + "status": "run", + "seconds": 4.75846, + "failed": false, + "skipped": false + }, + { + "name": "receive", + "status": "run", + "seconds": 0.195176, + "failed": false, + "skipped": false + }, + { + "name": "conpty", + "status": "run", + "seconds": 23.2193, + "failed": false, + "skipped": false + }, + { + "name": "conpty_ctrl_c", + "status": "run", + "seconds": 4.26167, + "failed": false, + "skipped": false + }, + { + "name": "conpty_margin", + "status": "run", + "seconds": 0.348728, + "failed": false, + "skipped": false + }, + { + "name": "conpty_backend", + "status": "run", + "seconds": 1.13411, + "failed": false, + "skipped": false + }, + { + "name": "presentation", + "status": "run", + "seconds": 4.02108, + "failed": false, + "skipped": false + }, + { + "name": "shell", + "status": "fail", + "seconds": 174.653, + "failed": true, + "skipped": false + }, + { + "name": "selection_bytes", + "status": "run", + "seconds": 3.80834, + "failed": false, + "skipped": false + }, + { + "name": "selection_context", + "status": "run", + "seconds": 3.77349, + "failed": false, + "skipped": false + }, + { + "name": "raw_console_receiver", + "status": "run", + "seconds": 0.364619, + "failed": false, + "skipped": false + }, + { + "name": "paste_system", + "status": "run", + "seconds": 7.74685, + "failed": false, + "skipped": false + } + ], + "internal_skips": [ + { + "test": "setUpClass (test_candidate_compare.CandidateComparisonTests)", + "reason": "optional Alacritty binary is not built; core-candidate workflow runs these checks" + }, + { + "test": "setUpClass (test_capability_matrix.CapabilityMatrixTests)", + "reason": "optional Alacritty binary is not built; core-candidate workflow runs these checks" + }, + { + "test": "setUpClass (test_fuzz_matrix.FuzzMatrixTests)", + "reason": "optional Alacritty binary is not built; core-candidate workflow runs these checks" + }, + { + "test": "setUpClass (test_real_app_matrix.RealApplicationMatrixTests)", + "reason": "optional real application capture dependencies are not built" + }, + { + "test": "test_actual_system_copy_paste_and_shortcuts_twice (experiments.shell.test_actions.ActionWindowTest.test_actual_system_copy_paste_and_shortcuts_twice)", + "reason": "actual system Copy/Paste requires a disposable hosted VM" + }, + { + "test": "test_suspended_target_is_reclaimed_if_job_assignment_fails (test_ui_r1.EvidenceBoundaryTests.test_suspended_target_is_reclaimed_if_job_assignment_fails)", + "reason": "opt-in pinned reference failure injection" + }, + { + "test": "test_mouse_selection_to_real_system_clipboard (experiments.shell.test_shell.ShellTests.test_mouse_selection_to_real_system_clipboard)", + "reason": "system copy UI gate requires a disposable hosted VM" + }, + { + "test": "test_system_clipboard_product_read_errors_cancel_and_close (experiments.shell.test_shell.ShellTests.test_system_clipboard_product_read_errors_cancel_and_close)", + "reason": "product clipboard read gate requires a disposable hosted VM" + }, + { + "test": "test_system_clipboard_shortcut_to_window_confirmation_and_receiver (experiments.shell.test_shell.ShellTests.test_system_clipboard_shortcut_to_window_confirmation_and_receiver)", + "reason": "real system clipboard UI gate requires a disposable hosted VM" + }, + { + "test": "test_real_system_copy_shortcuts_for_styled_fixture_twice (experiments.shell.test_selection_bytes.SelectionBytesWindowTest.test_real_system_copy_shortcuts_for_styled_fixture_twice)", + "reason": "system CF requires a disposable hosted VM" + }, + { + "test": "test_real_system_copy_and_edit_context_twice (experiments.shell.test_selection_context.SelectionContextWindowTest.test_real_system_copy_and_edit_context_twice)", + "reason": "real CF/EDIT Copy requires a disposable hosted VM" + }, + { + "test": "test_real_system_clipboard (experiments.shell.test_paste.PasteTests.test_real_system_clipboard)", + "reason": "Private window station OpenClipboard denied (5); system API gate remains unverified." + } + ], + "failed_methods": [ + "test_real_fullscreen_editor_and_cursor_reply", + "test_close_while_resizing_continuous_output", + "test_main_and_bottom_sessions_focus_geometry_and_cleanup", + "test_resize_child_dimensions_and_interrupt", + "test_workspace_sessions_resize_and_cleanup" + ], + "observed_failures": [ + { + "case": "candidate-tui-0", + "sample": { + "mode": "candidate-tui", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "job_after": { + "active": 0, + "total": 7, + "terminated": 0 + }, + "desktop_gone": true + }, + "stage": 59, + "raw_sample_sha256": "556fb95b611df35afd43c36b79c5ecd471b4fc524addcff9568b1178972bb265" + }, + { + "case": "main-2", + "sample": { + "mode": "ui-main", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "job_after": { + "active": 0, + "total": 16, + "terminated": 0 + }, + "desktop_gone": true + }, + "stage": 7, + "raw_sample_sha256": "3958d53e4a69819bd3aa9db04c2fe09d26b78aba22d76db904102b257ea59340" + }, + { + "case": "resize-0", + "sample": { + "mode": "resize", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "job_after": { + "active": 0, + "total": 8, + "terminated": 0 + }, + "desktop_gone": true + }, + "stage": 43, + "raw_sample_sha256": "eab4ad23688907b4cfe66756203e088ccfcfbc251d271e2497f64ee9375ac043" + }, + { + "case": "resize-close-0", + "sample": { + "mode": "resize-close", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "job_after": { + "active": 0, + "total": 8, + "terminated": 0 + }, + "desktop_gone": true + }, + "stage": 43, + "raw_sample_sha256": "24ce02899ca5110cc20fabc847725d28ea73da6866f9b2a387e7620760829700" + }, + { + "case": "ui-2", + "sample": { + "mode": "ui", + "status": "failed", + "timed_out": false, + "exit_code": 2, + "job_after": { + "active": 0, + "total": 12, + "terminated": 0 + }, + "desktop_gone": true + }, + "stage": 52, + "raw_sample_sha256": "fe0985a95d8c70063e214bebec6476afdb651413f554ade5a199d46d79428d18" + } + ], + "persistence_method_passed": true, + "all_tests_completed": true, + "total_seconds": 449.38, + "limitations": [ + "Candidate/official full regression retains actual failures and internal skips.", + "Standalone persistence passes do not replace the failing aggregate shell suite.", + "No actual presented endpoint, long-run stability or WindTerm performance equivalence." + ] + }, + "ci": { + "source_head": "4daf99250a2740e1943635b6d46293d633427163", + "checkout": "c5d397999fbc00a83dd4055d975d8c12f855af5e", + "workflows": [ + { + "run": 40360427005, + "name": "Build and synthetic regression", + "conclusion": "success", + "windows_job": 121704057921, + "windows_tests": 51, + "windows_failed": 0, + "windows_seconds": 273.64 + }, + { + "run": 40360426973, + "name": "External core candidate", + "conclusion": "success", + "windows_job": 121704056881 + } + ], + "normalized_local_job_logs_sha256": { + "121704057921": "939a15bf59996dd31ce5a90cdefb240af0450060087d5b8e2b712b651bfb9310", + "121704056881": "26b187a507ac46184e0dec9da964e08fb82ffdab0a725c58a5979dd7e37acd7e" + }, + "limitation": "CI source is the experimental stack; a separate main-line patch requires its own merge-checkout validation." + } +} diff --git a/experiments/shell/sender_fence.hpp b/experiments/shell/sender_fence.hpp index f17603ba..56e092f0 100644 --- a/experiments/shell/sender_fence.hpp +++ b/experiments/shell/sender_fence.hpp @@ -21,6 +21,8 @@ class SenderFence { uint64_t generation = 0; State state = State::armed; AbortReason abort_reason = AbortReason::none; + State wait_exit_state = State::armed; + AbortReason wait_exit_reason = AbortReason::none; bool attached = false, ever_held = false, wait_finished = false; size_t owned_bytes = 0; tf_shell::PasteProgress held_progress; @@ -141,6 +143,8 @@ class SenderFence { const bool woken = changed_.wait_until(lock, deadline_, [&] { return snapshot_.state != State::held; }); if (!woken) snapshot_.state = State::timed_out; snapshot_.wait_finished = true; + snapshot_.wait_exit_state = snapshot_.state; + snapshot_.wait_exit_reason = snapshot_.abort_reason; if (snapshot_.state != State::released && snapshot_.state != State::cancelled) rejected_wait(snapshot_.state); } }; diff --git a/experiments/shell/sender_fence_pipe.hpp b/experiments/shell/sender_fence_pipe.hpp new file mode 100644 index 00000000..aca1566d --- /dev/null +++ b/experiments/shell/sender_fence_pipe.hpp @@ -0,0 +1,83 @@ +// Bounded test-only control pipe. Neither commands nor replies carry paste bytes. +#pragma once +#include "../conpty/session.hpp" +#include "sender_fence.hpp" +#include +#include + +namespace tf_shell_test { +class SenderFencePipe { + std::shared_ptr fence_; + tf_conpty::Handle request_read_,request_write_,reply_read_,reply_write_; + uint64_t sent_=0,dispatched_=0,received_=0; + bool eof_=false; + static constexpr uint64_t command_limit=8; + static void check(bool value,const char* why) { if(!value) throw std::runtime_error(why); } + static void create(tf_conpty::Handle& reader,tf_conpty::Handle& writer,DWORD required) { + HANDLE read=nullptr,write=nullptr; + // On failure CreatePipe's output handles are unspecified. Adopt only + // successful handles, then verify the actual inbound server buffer. + check(CreatePipe(&read,&write,nullptr,4096)!=FALSE,"fence control pipe creation failed"); + reader.reset(read);writer.reset(write); + DWORD flags=0,capacity=0; + check(GetNamedPipeInfo(reader.get(),&flags,nullptr,&capacity,nullptr) && + (flags&PIPE_SERVER_END) && capacity>=required,"fence control pipe capacity cannot hold its command budget"); + } + template static void write(HANDLE pipe,const std::array& message) { + DWORD size=0; + check(WriteFile(pipe,message.data(),sizeof(message),&size,nullptr) && size==sizeof(message),"fence control write failed"); + } + template static std::array read(HANDLE pipe) { + std::array message{};DWORD size=0; + check(ReadFile(pipe,message.data(),sizeof(message),&size,nullptr) && size==sizeof(message),"fence control frame truncated"); + return message; + } +public: + struct Ack { uint64_t generation;bool accepted; }; + explicit SenderFencePipe(std::shared_ptr fence):fence_(std::move(fence)) { + check(fence_!=nullptr,"fence control requires a transfer binding"); + create(request_read_,request_write_,static_cast(command_limit*2*sizeof(uint64_t))); + create(reply_read_,reply_write_,static_cast(command_limit*3*sizeof(uint64_t))); + } + ~SenderFencePipe() { + const auto state=fence_->snapshot(fence_->ticket()).state; + if(state==SenderFence::State::armed || state==SenderFence::State::held) + fence_->abort(fence_->ticket(),SenderFence::AbortReason::stop); + } + void send_go(uint64_t generation) { + check(request_write_.get() && sent_{++sent_,generation}); + } + void disconnect() { request_write_.reset(); } + bool eof_observed() const { return eof_; } + // The owned UI/test message pump dispatches only complete, already buffered + // frames. Eight commands and replies fit in each pipe without blocking. + void dispatch() { + if(eof_) return; + DWORD available=0; + if(!PeekNamedPipe(request_read_.get(),nullptr,0,nullptr,&available,nullptr)) { + check(GetLastError()==ERROR_BROKEN_PIPE,"fence control peek failed"); + char byte=0;DWORD count=0; + check(!ReadFile(request_read_.get(),&byte,1,&count,nullptr) && GetLastError()==ERROR_BROKEN_PIPE && !count, + "fence control EOF was not confirmed by ReadFile"); + eof_=true;fence_->abort(fence_->ticket(),SenderFence::AbortReason::control_eof);return; + } + if(!available) return; + check(available>=2*sizeof(uint64_t),"fence control partial request"); + const auto request=read<2>(request_read_.get()); + check(request[0]==++dispatched_ && dispatched_<=command_limit,"fence control request sequence invalid"); + auto ticket=fence_->ticket();ticket.generation=request[1]; + const bool accepted=fence_->release(ticket); + write(reply_write_.get(),std::array{request[0],request[1],accepted?1ull:0ull}); + } + std::optional acknowledgement() { + DWORD available=0; + check(PeekNamedPipe(reply_read_.get(),nullptr,0,nullptr,&available,nullptr)!=FALSE,"fence control reply peek failed"); + if(!available) return std::nullopt; + check(available>=3*sizeof(uint64_t),"fence control partial reply"); + const auto reply=read<3>(reply_read_.get()); + check(reply[0]==++received_ && received_<=sent_ && reply[2]<=1,"fence control reply sequence invalid"); + return Ack{reply[1],reply[2]!=0}; + } +}; +} // namespace tf_shell_test diff --git a/experiments/shell/sender_fence_pipe_test.cpp b/experiments/shell/sender_fence_pipe_test.cpp new file mode 100644 index 00000000..64580c0c --- /dev/null +++ b/experiments/shell/sender_fence_pipe_test.cpp @@ -0,0 +1,77 @@ +// The control protocol uses real pipe reads/EOF; payload writes are unit-level here. +#include "sender_fence_pipe.hpp" +#include +#include + +using tf_shell_test::SenderFence; +using tf_shell_test::SenderFencePipe; +static void check(bool value,const char* why) { if(!value) throw std::runtime_error(why); } +struct HeldTransfer { + std::shared_ptr transfer=std::make_shared( + 1,tf_shell::prepare_paste(std::u16string(1000000,u'汉')),true); + std::shared_ptr fence; + SenderFencePipe pipe; + std::future writer; + explicit HeldTransfer(std::chrono::milliseconds lifetime=std::chrono::seconds(2)) + :fence(std::make_shared(transfer,7,SenderFence::Clock::now()+lifetime)),pipe(fence) { + check(fence->attach(transfer) && transfer->claim_writer(),"admit test writer"); + } + void hold() { + while(transfer->progress().sent<16384) { + auto chunk=transfer->take_chunk();transfer->acknowledge_written(chunk.size());transfer->finish_chunk(); + } + const auto chunk=transfer->take_chunk(); + writer=std::async(std::launch::async,[this,count=chunk.size()] { + try {fence->before_send(transfer,count);return true;} catch(const std::runtime_error&) {return false;} + }); + const auto limit=SenderFence::Clock::now()+std::chrono::seconds(1); + while(fence->snapshot(fence->ticket()).state==SenderFence::State::armed && SenderFence::Clock::now()snapshot(fence->ticket()).state==SenderFence::State::held,"writer reached real CV hold"); + } + bool go(uint64_t generation) { + pipe.send_go(generation);pipe.dispatch();const auto ack=pipe.acknowledgement(); + check(ack.has_value() && ack->generation==generation,"pipe GO reply identity");return ack->accepted; + } +}; +int main() { + try { + { + HeldTransfer value; + check(!value.go(7),"GO before held must fail");value.hold(); + check(!value.go(8),"wrong generation must fail"); + check(value.go(7),"matching held GO must succeed"); + check(!value.go(7),"duplicate GO must fail");check(value.writer.get(),"GO wakes writer"); + value.fence->abort(value.fence->ticket(),SenderFence::AbortReason::stop); + const auto state=value.fence->snapshot(value.fence->ticket()); + check(state.state==SenderFence::State::aborted && state.wait_exit_state==SenderFence::State::released, + "later stop cannot rewrite the actual wait exit"); + } + { + HeldTransfer value; + for(unsigned i=0;i<8;++i) value.pipe.send_go(7); + bool rejected=false;try {value.pipe.send_go(7);} catch(const std::runtime_error&) {rejected=true;} + check(rejected,"ninth queued command exceeds bounded budget"); + for(unsigned i=0;i<8;++i) value.pipe.dispatch(); + for(unsigned i=0;i<8;++i) { + const auto ack=value.pipe.acknowledgement(); + check(ack && ack->generation==7 && !ack->accepted,"saturated pipe replies preserve order and rejection"); + } + check(!value.pipe.acknowledgement(),"no extra reply"); + } + { + HeldTransfer value;value.hold();value.pipe.disconnect();value.pipe.dispatch(); + check(value.pipe.eof_observed(),"actual pipe EOF observed"); + check(!value.writer.get(),"EOF fails held writer"); + const auto state=value.fence->snapshot(value.fence->ticket()); + check(state.state==SenderFence::State::aborted && state.abort_reason==SenderFence::AbortReason::control_eof, + "EOF reason retained"); + } + { + HeldTransfer value(std::chrono::milliseconds(300));value.hold(); + check(!value.writer.get(),"missing GO fails held writer"); + check(value.fence->snapshot(value.fence->ticket()).state==SenderFence::State::timed_out,"absolute deadline retained"); + } + std::cout<<"real pipe GO/ack, generation, early/repeated GO, EOF and missing GO passed\n";return 0; + } catch(const std::exception& error) {std::cerr< pending_paste_; std::unique_ptr paste_prompt_; std::shared_ptr paste_transfer_; + std::optional> next_paste_fence_; + std::shared_ptr observed_paste_fence_; std::wstring input_status_, command_prefix_, terminal_title_; int wheel_remainder_ = 0; termforge::TextSelection selection_; @@ -273,7 +275,13 @@ class ShellWindow { void start_prepared_paste(tf_shell::PreparedPaste value) { const auto observed_source=runtime_enabled_?std::optional(value.source_units):std::nullopt; auto transfer=std::make_shared(identity_,std::move(value),true); - if(!input_.begin_paste(transfer)) { input_status(L"Input busy; paste not started"); return; } + std::shared_ptr fence; + if(next_paste_fence_) { + fence=std::make_shared(transfer,next_paste_fence_->first,next_paste_fence_->second); + next_paste_fence_.reset(); + } + if(!input_.begin_paste(transfer,fence)) { input_status(L"Input busy; paste not started"); return; } + observed_paste_fence_=std::move(fence); if(runtime_enabled_) runtime_paste_source_units_=observed_source; viewport_.bottom(parser_.screen); publish(); paste_transfer_=std::move(transfer); input_status(L"Pasting — Escape cancels"); @@ -758,7 +766,10 @@ class ShellWindow { for (const auto& event : parser_.take_events()) { switch (event.kind) { case 1: - if (!event.bytes.empty()) require(input_.send(event.bytes), "terminal reply rejected by input queue"); + // Closing stops input before the remaining output is drained. + // Queries in that output no longer have a live input peer. + if (!closing_ && !event.bytes.empty()) + require(input_.send(event.bytes), "terminal reply rejected by input queue"); break; case 2: terminal_title_ = utf16(event.bytes); break; case 3: terminal_title_.clear(); break; @@ -1119,6 +1130,13 @@ class ShellWindow { return clipboard_read_?std::optional(clipboard_read_->progress()):std::nullopt; } std::shared_ptr observed_paste() const { return paste_transfer_; } + // Owned test scenarios bind before admission, including asynchronous clipboard reads. + void arm_next_paste_fence(uint64_t generation,Clock::time_point deadline) { + require(!closing_ && !paste_busy() && !next_paste_fence_ && generation && deadline>Clock::now(), + "paste fence must be armed before a new request"); + next_paste_fence_=std::make_pair(generation,deadline); observed_paste_fence_.reset(); + } + std::shared_ptr observed_paste_fence() const { return observed_paste_fence_; } bool composing() const { return ime_.composing(); } void begin_ime_observation(unsigned generation) { ime_observation_.begin(generation); update_ime_caret(); @@ -1195,8 +1213,9 @@ class ShellWindow { if(had_prompt) input_status(L"Paste cancelled before sending"); if(clipboard_read_) clipboard_read_->cancel(); paste_prompt_.reset(); pending_paste_.reset(); + next_paste_fence_.reset(); if(had_prompt) restore_input_focus(); - if(paste_transfer_) paste_transfer_->cancel(); + input_.cancel_paste(); } void synthetic_paste(std::u16string text) { require(!closing_ && !paste_busy(),"synthetic paste cannot replace a running operation"); @@ -1342,7 +1361,7 @@ static int application(int argc, char** argv) { require(argc == 1 || (argc == 4 && (std::string(argv[1]) == "--test" || std::string(argv[1]) == "--test-owned")), "termforge_shell [--test mode frame.bmp]"); if (argc == 4) { mode = argv[2]; image = argv[3]; - require(mode == "candidate-tui" || mode == "candidate-alt" || mode == "candidate-scroll" || mode == "candidate-margins" || mode == "candidate-unicode" || mode == "candidate-input-escape" || mode == "normal" || mode == "close" || mode == "watchdog" || mode == "resize" || mode == "resize-close" || mode == "ui" || mode == "ui-edit" || mode == "ui-restore" || mode == "ui-workspace-save" || mode == "ui-workspace-restore" || mode == "ui-workspace-v1" || mode == "ui-workspace-empty-create" || mode == "ui-workspace-empty" || mode == "ui-workspace-corrupt" || mode == "ui-workspace-future" || mode == "ui-workspace-write-failure" || mode == "ui-workspace-close-failure" || mode == "ui-main" || mode == "ui-dpi" || mode == "ui-monitors" || mode == "ui-shells" || mode == "ui-launch-failure" || mode == "ui-split" || mode == "ui-history" || mode == "ui-keyboard" || mode == "ui-paste" || mode == "ui-paste-system" || mode == "ui-clipboard-system" || mode == "ui-selection" || mode == "ui-copy-system" || mode == "ui-find" || mode == "ui-find-stream" || mode == "ui-selection-bytes" || mode == "ui-copy-bytes-system" || mode == "ui-selection-context" || mode == "ui-copy-context-system" || mode == "ui-actions" || mode == "ui-actions-system" || mode == "ui-workflow-sessions", "invalid shell test mode"); + require(mode == "candidate-tui" || mode == "candidate-alt" || mode == "candidate-scroll" || mode == "candidate-margins" || mode == "candidate-unicode" || mode == "candidate-input-escape" || mode == "normal" || mode == "close" || mode == "watchdog" || mode == "resize" || mode == "resize-close" || mode == "ui" || mode == "ui-edit" || mode == "ui-restore" || mode == "ui-workspace-save" || mode == "ui-workspace-restore" || mode == "ui-workspace-v1" || mode == "ui-workspace-empty-create" || mode == "ui-workspace-empty" || mode == "ui-workspace-corrupt" || mode == "ui-workspace-future" || mode == "ui-workspace-write-failure" || mode == "ui-workspace-close-failure" || mode == "ui-main" || mode == "ui-dpi" || mode == "ui-monitors" || mode == "ui-shells" || mode == "ui-launch-failure" || mode == "ui-split" || mode == "ui-history" || mode == "ui-keyboard" || mode == "ui-paste" || mode == "ui-paste-system" || mode == "ui-paste-control-eof" || mode == "ui-paste-control-timeout" || mode == "ui-clipboard-system" || mode == "ui-selection" || mode == "ui-copy-system" || mode == "ui-find" || mode == "ui-find-stream" || mode == "ui-selection-bytes" || mode == "ui-copy-bytes-system" || mode == "ui-selection-context" || mode == "ui-copy-context-system" || mode == "ui-actions" || mode == "ui-actions-system" || mode == "ui-workflow-sessions", "invalid shell test mode"); if(mode=="ui-clipboard-system" || mode=="ui-copy-system" || mode=="ui-copy-bytes-system" || mode=="ui-copy-context-system" || mode=="ui-actions-system") tf_shell_test::require_hosted_clipboard(); if(std::string(argv[1])=="--test") { std::string gate; std::getline(std::cin, gate); require(gate == "GO", "missing owned-job gate"); @@ -1355,7 +1374,7 @@ static int application(int argc, char** argv) { wchar_t desktop_name[256]{}; DWORD desktop_size=0; require(GetUserObjectInformationW(GetThreadDesktop(GetCurrentThreadId()),UOI_NAME,desktop_name,sizeof(desktop_name),&desktop_size)!=FALSE && std::wstring_view(desktop_name).find(L"TermForgeShell-")==0,"shell test requires the runner-owned desktop"); - if (mode == "ui" || mode == "ui-edit" || mode == "ui-restore" || mode == "ui-workspace-save" || mode == "ui-workspace-restore" || mode == "ui-workspace-v1" || mode == "ui-workspace-empty-create" || mode == "ui-workspace-empty" || mode == "ui-workspace-corrupt" || mode == "ui-workspace-future" || mode == "ui-workspace-write-failure" || mode == "ui-workspace-close-failure" || mode == "ui-main" || mode == "ui-dpi" || mode == "ui-monitors" || mode == "ui-shells" || mode == "ui-launch-failure" || mode == "ui-split" || mode == "ui-history" || mode == "ui-keyboard" || mode == "ui-paste" || mode == "ui-paste-system" || mode == "ui-clipboard-system" || mode == "ui-selection" || mode == "ui-copy-system" || mode == "ui-find" || mode == "ui-find-stream" || mode == "ui-selection-bytes" || mode == "ui-copy-bytes-system" || mode == "ui-selection-context" || mode == "ui-copy-context-system" || mode == "ui-actions" || mode == "ui-actions-system" || mode == "ui-workflow-sessions") { + if (mode == "ui" || mode == "ui-edit" || mode == "ui-restore" || mode == "ui-workspace-save" || mode == "ui-workspace-restore" || mode == "ui-workspace-v1" || mode == "ui-workspace-empty-create" || mode == "ui-workspace-empty" || mode == "ui-workspace-corrupt" || mode == "ui-workspace-future" || mode == "ui-workspace-write-failure" || mode == "ui-workspace-close-failure" || mode == "ui-main" || mode == "ui-dpi" || mode == "ui-monitors" || mode == "ui-shells" || mode == "ui-launch-failure" || mode == "ui-split" || mode == "ui-history" || mode == "ui-keyboard" || mode == "ui-paste" || mode == "ui-paste-system" || mode == "ui-paste-control-eof" || mode == "ui-paste-control-timeout" || mode == "ui-clipboard-system" || mode == "ui-selection" || mode == "ui-copy-system" || mode == "ui-find" || mode == "ui-find-stream" || mode == "ui-selection-bytes" || mode == "ui-copy-bytes-system" || mode == "ui-selection-context" || mode == "ui-copy-context-system" || mode == "ui-actions" || mode == "ui-actions-system" || mode == "ui-workflow-sessions") { Workspace window(image,mode); return window.run(); } Result r; diff --git a/experiments/shell/test_paste_checks.py b/experiments/shell/test_paste_checks.py new file mode 100644 index 00000000..0c53e053 --- /dev/null +++ b/experiments/shell/test_paste_checks.py @@ -0,0 +1,27 @@ +"""Reject falsely accepted UI paste evidence without launching a desktop.""" +import copy +import unittest +from experiments.shell.paste_checks import validate_paste_fence + + +class PasteFenceEvidenceTests(unittest.TestCase): + def test_requires_held_partial_progress_and_real_cancel_or_stop(self): + row = dict(name="cancel", before_sent=17391, before_accepted=18414, + fence=dict(generation=1, held=True, owned_bytes=1023, wait_finished=True, + other_responsive=True, exit="cancelled", final_state="cancelled", wrong_generation_rejected=True)) + validate_paste_fence(row, 1) + for key, value in (("generation", 2), ("held", False), ("owned_bytes", 1024), + ("wait_finished", False), ("exit", "released"), ("final_state", "stopped"), + ("other_responsive", False), ("wrong_generation_rejected", False)): + broken = copy.deepcopy(row) + broken["fence"][key] = value + with self.subTest(key=key), self.assertRaises(ValueError): + validate_paste_fence(broken, 1) + with self.assertRaises(ValueError): + validate_paste_fence({k: v for k, v in row.items() if k != "fence"}, 1) + close = copy.deepcopy(row) + close["name"] = "close" + close["fence"]["final_state"] = "stopped" + for actual_wake in ("cancelled", "stopped"): + close["fence"]["exit"] = actual_wake + validate_paste_fence(close, 1) diff --git a/experiments/shell/test_shell.py b/experiments/shell/test_shell.py index e4d01513..66512ac1 100644 --- a/experiments/shell/test_shell.py +++ b/experiments/shell/test_shell.py @@ -373,6 +373,33 @@ def test_paste_confirmation_and_hidden_target_preserve_visible_focus(self): "tab_enter_accept": True}) self.assertEqual((out / "input.bin").read_bytes(), b"DEFAULT_ENTER_ACCEPT\r" + b"P\xe6\xb1\x89\r" * 5000) + def test_paste_control_eof_remains_failure_after_cleanup(self): + failure = self._rejected_paste_control("eof", "EOF") + self.assertTrue(failure["eof_observed"]) + self.assertFalse(failure["timed_out"]) + + def test_paste_missing_go_remains_failure_after_cleanup(self): + failure = self._rejected_paste_control("timeout", "missing GO") + self.assertFalse(failure["eof_observed"]) + self.assertTrue(failure["timed_out"]) + + def _rejected_paste_control(self, mode, reason): + out = self.out / ("paste-control-" + mode) + with self.assertRaisesRegex(ValueError, "paste control " + reason + " rejected"): + execute(self.exe, "ui-paste-control-" + mode, out) + sample = json.loads((out / "sample.json").read_text()) + self.assertEqual(sample["exit_code"], 2) + self.assertFalse(sample["timed_out"]) + self.assertEqual(sample["job_after"]["active"], 0) + self.assertTrue(sample["desktop_gone"]) + failure = json.loads((out / "paste-control-failure.json").read_text()) + self.assertTrue(failure["wait_finished"]) + self.assertEqual(failure["state"], "failed") + self.assertEqual(failure["in_flight"], 0) + self.assertLessEqual(failure["retained_capacity"], 32) + self.assertEqual((out / "cancel.bin").read_bytes(), b"\xe6\xb1\x89" * 256) + return failure + @unittest.skipUnless(os.environ.get("GITHUB_ACTIONS") == "true" and os.environ.get("RUNNER_ENVIRONMENT") == "github-hosted", "real system clipboard UI gate requires a disposable hosted VM") diff --git a/experiments/shell/test_workflow_sessions.py b/experiments/shell/test_workflow_sessions.py index b1122c1a..de6cef3b 100644 --- a/experiments/shell/test_workflow_sessions.py +++ b/experiments/shell/test_workflow_sessions.py @@ -21,6 +21,7 @@ 'experiments/shell/workflow_controller.hpp', 'experiments/shell/workflow_fixture.hpp', 'experiments/shell/runtime_observation.hpp', 'experiments/shell/operation_observation.hpp', 'experiments/shell/queues.hpp', 'experiments/shell/resize.hpp', + 'experiments/shell/sender_fence.hpp', 'experiments/shell/sender_fence_pipe.hpp', 'experiments/shell/paste_checks.py', 'experiments/shell/queue_probe.cpp', 'experiments/shell/raw_console_receiver.hpp', 'experiments/shell/text_client.cpp', 'experiments/shell/input_client.cpp', 'experiments/shell/test_launcher.cpp', 'experiments/shell/paste.hpp', 'experiments/shell/paste_scenario_state.hpp', @@ -171,9 +172,12 @@ def test_paste_exact_prefix_rejects_late_eot_wrong_target_and_unreleased_capacit out=Path(temporary) (out/'cancel.bin').write_bytes('汉'.encode()*6000) (out/'close.bin').write_bytes('汉'.encode()*256) - cases=[dict(name=name,target=3,total=3000000,before_sent=16384,before_accepted=18000, + cases=[dict(name=name,target=3,total=3000000,before_sent=16977,before_accepted=18000, accepted=18000,sent=18000,in_flight=0,retained_capacity=0,checkpoint_bytes=768, - received=18000 if name=='cancel' else 768,flood_observed=True,state='cancelled') for name in ('cancel','close')] + received=18000 if name=='cancel' else 768,flood_observed=True,state='cancelled', + fence=dict(generation=i,held=True,owned_bytes=1023,wait_finished=True,other_responsive=True, + exit='cancelled',final_state='cancelled' if name=='cancel' else 'stopped',wrong_generation_rejected=True)) + for i,name in enumerate(('cancel','close'),1)] data={'paste_cases':cases};_validate_paste_cases(out,data,3) for key,value in [('target',4),('in_flight',1),('retained_capacity',3000000),('checkpoint_bytes',0),('sent',17999)]: broken=copy.deepcopy(data);broken['paste_cases'][0][key]=value diff --git a/experiments/shell/transport_completion.hpp b/experiments/shell/transport_completion.hpp index cc56ce3a..e038584c 100644 --- a/experiments/shell/transport_completion.hpp +++ b/experiments/shell/transport_completion.hpp @@ -13,17 +13,19 @@ namespace tf_shell_test { // A command-editor BS before output invalidates that record until a real LF; // it cannot erase an echo prefix or splice a marker. Unknown ESC forms, // malformed/incomplete envelopes and other controls remain unproven. +// A bare CR is harmless only in an empty, unedited record after an observed +// boundary and before marker output. It cannot discard text or end a record. // ShellWindow retains its independent 8192-byte observation cap. inline bool transport_output_completed(std::string_view raw, std::string_view marker) { if(marker.empty()) return false; for(const unsigned char byte:marker) if(byte<0x20 || byte==0x7f) return false; const auto prompt=[](std::string_view line) { return line=="TF>" || line=="TF> "; }; std::string line; - bool output_seen=false,prompt_seen=false,literal_line=true; + bool output_seen=false,prompt_seen=false,literal_line=true,record_boundary=false; const auto end_line=[&] { if(literal_line && line==marker) output_seen=true; else if(literal_line && output_seen && prompt(line)) prompt_seen=true; - line.clear(); literal_line=true; + line.clear(); literal_line=true; record_boundary=true; }; for(std::size_t i=0;i(raw[i++]); @@ -73,9 +75,11 @@ inline bool transport_output_completed(std::string_view raw, std::string_view ma if(!terminated) return false; } else return false; } else if(byte=='\r') { - // CR alone may overwrite an echo; only a following LF proves the - // literal output line has completed. - if(i==raw.size() || raw[i]!='\n') return false; + // Only CRLF completes literal text. An empty redraw before the + // marker leaves the record empty and still needs fresh output. + if(i==raw.size() || raw[i]!='\n') { + if(!record_boundary || !line.empty() || !literal_line || output_seen) return false; + } } else if(byte=='\n') { end_line(); } else if(byte=='\b') { diff --git a/experiments/shell/transport_completion_test.cpp b/experiments/shell/transport_completion_test.cpp index 1707f4d4..70a412af 100644 --- a/experiments/shell/transport_completion_test.cpp +++ b/experiments/shell/transport_completion_test.cpp @@ -31,6 +31,19 @@ int main() { line+"TF>",marker),"command redraw BS before a fresh literal output line does not poison later records"); check(transport_output_completed("echo old\b\r\n"+line+"TF_PS_MAJOR=5\r\nTF> ",marker), "fresh output remains independent of a prior edited command record"); + const std::string save="\x1b[?25l\x1b[1;3H> \x1b[93mWrite-Output \x1b[36m" + "'TF_SAVE_PS' \x1b[m\r\n" + "\x1b[63X\x1b[63C\r\x1b[?25hTF_SAVE_PS\r\nTF> "; + auto no_empty_cr=save; + no_empty_cr.erase(no_empty_cr.find("\r\x1b[?25h"),1); + check(transport_output_completed(no_empty_cr,"TF_SAVE_PS"), + "removing only the empty-record CR isolates the captured rejection"); + check(transport_output_completed(save,"TF_SAVE_PS"), + "captured PowerShell empty-record CR must not hide independent output and prompt"); + check(transport_output_completed("echo old\r\n\r"+line+"TF>",marker), + "an empty record CR preserves a later exact output and prompt"); + check(transport_output_completed("echo old\b\r\n\x1b[63X\x1b[63C\r\r"+line+"TF> ",marker), + "LF restores an independent record before repeated empty redraw CRs"); check(!transport_output_completed("TF>echo "+line+"TF>",marker),"command echo is not independent output"); check(!transport_output_completed(title+"\a\r\nTF>",marker),"OSC BEL payload is not output"); @@ -69,6 +82,22 @@ int main() { check(!transport_output_completed(line+"\bTF>",marker),"BS after a marker cannot manufacture prompt"); check(!transport_output_completed(line+"TF>\b",marker),"trailing prompt BS remains unknown"); check(!transport_output_completed("\t\r\n"+line+"TF>",marker),"other bare controls remain rejected"); + check(!transport_output_completed("\r"+line+"TF>",marker), + "initial bare CR lacks an observed record boundary"); + check(!transport_output_completed("echo\r\nTF>echo \r"+line+"TF>",marker), + "CR cannot erase a nonempty echo prefix"); + check(!transport_output_completed("echo\r\n\b\r"+line+"TF>",marker), + "CR cannot reset an empty BS-tainted record"); + check(!transport_output_completed("echo\r\nTF_CMD_DIR=\rrestore-cmd \xe7\x9b\xae\xe5\xbd\x95\r\nTF>",marker), + "CR-separated marker fragments do not form literal output"); + check(!transport_output_completed("echo\r\n\rTF>echo "+line+"TF>",marker), + "empty-record CR does not admit a later command echo"); + check(!transport_output_completed("echo\r\n\rTF_CMD_DIR=restore-cmd \xe7\x9b\xfe\xe5\xbd\x95\r\nTF>",marker), + "empty-record CR does not repair corrupted UTF-8 marker bytes"); + check(!transport_output_completed("echo\r\n\r"+line+"\x1b[31",marker), + "empty-record CR does not admit an incomplete trailing envelope"); + check(!transport_output_completed(line+"\rTF>",marker), + "bare CR after matched output does not manufacture a prompt"); std::cout<<"transport completion checks passed\n"; } catch(const std::exception& error) { std::cerr<= row['before_accepted'] and row['in_flight'] == 0 and row['retained_capacity'] <= 32 and row['checkpoint_bytes'] == 768 and row.get('flood_observed') is True and row.get('state') in ('cancelled', 'failed'), 'paste did not remain bounded/partial/terminal') + validate_paste_fence(row, generation) raw = _bounded(out / (row['name']+'.bin'), 3000000) _require(0 < len(raw) == row['received'] <= row['sent'] and raw == expected[:len(raw)] and len(raw)%3 == 0, 'gated receiver differs from exact complete Unicode prefix') diff --git a/experiments/shell/workspace.hpp b/experiments/shell/workspace.hpp index f2411d64..7d4fa607 100644 --- a/experiments/shell/workspace.hpp +++ b/experiments/shell/workspace.hpp @@ -618,7 +618,7 @@ class Workspace { else if(self->test_mode_=="ui-split") self->scenario_split(); else if(self->test_mode_=="ui-history") self->scenario_history(); else if(self->test_mode_=="ui-keyboard") self->scenario_keyboard(); - else if(self->test_mode_=="ui-paste" || self->test_mode_=="ui-paste-system") self->scenario_paste(); + else if(self->test_mode_.rfind("ui-paste",0)==0) self->scenario_paste(); else if(self->test_mode_=="ui-clipboard-system") self->scenario_clipboard(); else if(self->test_mode_=="ui-selection" || self->test_mode_=="ui-copy-system") self->scenario_selection(); else if(self->test_mode_=="ui-selection-bytes" || self->test_mode_=="ui-copy-bytes-system") self->scenario_selection_bytes(); @@ -779,7 +779,7 @@ class Workspace { require(stage_==100 && collected_==1,"workspace keyboard scenario incomplete"); std::cout<<"{\"valid\":true,\"mode\":\"ui-keyboard\",\"sessions_closed\":1,\"active_children_after\":0,\"presented_done\":null}\n"; } - if(!image_.empty() && (test_mode_=="ui-paste" || test_mode_=="ui-paste-system" || test_mode_=="ui-clipboard-system")) { + if(!image_.empty() && (test_mode_.rfind("ui-paste",0)==0 || test_mode_=="ui-clipboard-system")) { require(stage_==100 && collected_==2,"workspace paste scenario incomplete"); std::cout<<"{\"valid\":true,\"mode\":\""<begin_output_observation(); first->command(evidence.receiver_command(directory/"cancel.bin")); stage_=13; } else if(stage_==13 && evidence.receiver->ready()) { + const auto deadline=started_+std::chrono::milliseconds(35000); + first->arm_next_paste_fence(1,test_mode_=="ui-paste-control-timeout"? + (std::min)(deadline,Clock::now()+std::chrono::milliseconds(2000)):deadline); first->synthetic_paste(std::u16string(evidence.large_units,u'\u6c49')); evidence.transfer=first->observed_paste(); + evidence.fence=first->observed_paste_fence(); require(evidence.transfer!=nullptr,"cancel test could not observe its window transfer"); - active()->command(L"\recho TF_CANCEL_RESPONSIVE\r"); stage_=14; - } else if(stage_==14 && evidence.transfer->progress().sent>=16384 && active()->live_contains_line("TF_CANCEL_RESPONSIVE")) { + stage_=14; + } else if(stage_==14 && evidence.held()) { + active()->command(L"\recho TF_CANCEL_RESPONSIVE\r"); stage_=114; + } else if(stage_==114 && active()->live_contains_line("TF_CANCEL_RESPONSIVE")) { + evidence.pending.other_responsive=true; require(!tf_shell::paste_terminal(evidence.transfer->progress().state),"cancel transfer completed before receiver gate"); evidence.receiver->allow_read(); stage_=15; - } else if(stage_==15 && evidence.receiver->checkpoint()) { + } else if(stage_==15 && evidence.receiver->checkpoint() && first->observed_output().find("TF_PASTE_FLOOD")!=std::string::npos) { evidence.before_action(directory/"cancel.bin",first->observed_output().find("TF_PASTE_FLOOD")!=std::string::npos); + if(test_mode_=="ui-paste-control-eof") { + evidence.control->disconnect();evidence.control->dispatch();stage_=40;return; + } + if(test_mode_=="ui-paste-control-timeout") {stage_=41;return;} select(0); evidence.key(first->handle(),VK_ESCAPE); stage_=16; } else if(stage_==16 && (evidence.transfer->progress().state==tf_shell::PasteState::cancelling || evidence.transfer->progress().state==tf_shell::PasteState::cancelled)) { @@ -97,14 +108,19 @@ inline void Workspace::scenario_paste() { evidence.start_receiver(directory/"close.bin","close"); evidence.target=first->handle(); first->begin_output_observation(); first->command(evidence.receiver_command(directory/"close.bin")); stage_=19; } else if(stage_==19 && evidence.receiver->ready()) { + first->arm_next_paste_fence(2,started_+std::chrono::milliseconds(35000)); first->synthetic_paste(std::u16string(evidence.large_units,u'\u6c49')); evidence.transfer=first->observed_paste(); + evidence.fence=first->observed_paste_fence(); require(evidence.transfer!=nullptr,"close test could not observe its window transfer"); - select(1); active()->command(L"echo TF_CLOSE_RESPONSIVE\r"); stage_=20; - } else if(stage_==20 && evidence.transfer->progress().sent>=16384 && active()->live_contains_line("TF_CLOSE_RESPONSIVE")) { + select(1);stage_=20; + } else if(stage_==20 && evidence.held()) { + active()->command(L"echo TF_CLOSE_RESPONSIVE\r");stage_=120; + } else if(stage_==120 && active()->live_contains_line("TF_CLOSE_RESPONSIVE")) { + evidence.pending.other_responsive=true; require(!tf_shell::paste_terminal(evidence.transfer->progress().state),"close transfer completed before receiver gate"); evidence.receiver->allow_read(); stage_=21; - } else if(stage_==21 && evidence.receiver->checkpoint()) { + } else if(stage_==21 && evidence.receiver->checkpoint() && first->observed_output().find("TF_PASTE_FLOOD")!=std::string::npos) { evidence.before_action(directory/"close.bin",first->observed_output().find("TF_PASTE_FLOOD")!=std::string::npos); select(0); SendMessageW(window_,WM_COMMAND,2,0); stage_=30; } else if(stage_==30 && collected_==1 && !IsWindow(evidence.target)) { @@ -113,5 +129,15 @@ inline void Workspace::scenario_paste() { Bitmap frame(physical_width_,physical_height_); paint(frame.dc); const RECT rect=physical(terminal_); active()->render_to(frame.dc,rect.left,rect.top); frame.save(image_); stage_=100; close_all(); + } else if(stage_==40 && evidence.transfer->progress().state==tf_shell::PasteState::failed) { + require(evidence.control->eof_observed() && evidence.fence->snapshot(evidence.fence->ticket()).abort_reason== + tf_shell_test::SenderFence::AbortReason::control_eof,"control failure requires actual pipe EOF"); + evidence.save_control_failure(directory/"paste-control-failure.json"); + throw std::runtime_error("paste control EOF rejected"); + } else if(stage_==41 && evidence.transfer->progress().state==tf_shell::PasteState::failed) { + require(evidence.fence->snapshot(evidence.fence->ticket()).state==tf_shell_test::SenderFence::State::timed_out, + "missing GO requires actual fence deadline expiry"); + evidence.save_control_failure(directory/"paste-control-failure.json"); + throw std::runtime_error("paste control missing GO rejected"); } } diff --git a/experiments/shell/workspace_workflow_scenario.hpp b/experiments/shell/workspace_workflow_scenario.hpp index 36470e9a..029ef4bc 100644 --- a/experiments/shell/workspace_workflow_scenario.hpp +++ b/experiments/shell/workspace_workflow_scenario.hpp @@ -148,7 +148,8 @@ inline void Workspace::workflow_write() { <<",\"before_sent\":"<begin_output_observation();target->fixture_command(paste_scenario_.receiver_command(path));stage_=21; } else if(stage_==21 && paste_scenario_.receiver->ready()) { auto* target=workflow_shell("C"); + target->arm_next_paste_fence(1,started_+std::chrono::milliseconds(70000)); if(workflow_system_) { tf_shell_test::require_hosted_ime_input();workflow_select("C");tf_shell::write_unicode_clipboard(window_,std::u16string(paste_scenario_.large_units,u'汉')); workflow_system_shortcut("C",false);workflow_system_point("large-cancel-requested");stage_=221; } else { target->synthetic_paste(std::u16string(paste_scenario_.large_units,u'汉')); paste_scenario_.transfer=target->observed_paste();require(paste_scenario_.transfer!=nullptr,"workflow cancel transfer missing");stage_=22; + paste_scenario_.fence=target->observed_paste_fence(); } } else if(stage_==221) { paste_scenario_.transfer=workflow_shell("C")->observed_paste();if(!paste_scenario_.transfer) return; + paste_scenario_.fence=workflow_shell("C")->observed_paste_fence(); workflow_system_point("large-cancel-ready");stage_=22; - } else if(stage_==22 && paste_scenario_.transfer->progress().sent>=16384) { + } else if(stage_==22 && paste_scenario_.held()) { + paste_scenario_.other_consumed=workflow_shell("F1")->runtime_snapshot().consumed_bytes;stage_=222; + } else if(stage_==222 && workflow_shell("F1")->runtime_snapshot().consumed_bytes>paste_scenario_.other_consumed) { + paste_scenario_.pending.other_responsive=true; require(!tf_shell::paste_terminal(paste_scenario_.transfer->progress().state),"workflow cancel transfer completed before gate"); paste_scenario_.receiver->allow_read();stage_=23; - } else if(stage_==23 && paste_scenario_.receiver->checkpoint()) { + } else if(stage_==23 && paste_scenario_.receiver->checkpoint() && workflow_shell("C")->observed_output().find("TF_PASTE_FLOOD")!=std::string::npos) { paste_scenario_.before_action(std::filesystem::path(image_).parent_path()/"cancel.bin",workflow_shell("C")->observed_output().find("TF_PASTE_FLOOD")!=std::string::npos); backgrounds_running();workflow_checkpoint("paste-cancel-pending",0,{"B","C"}); workflow_select("C");paste_scenario_.key(workflow_shell("C")->handle(),VK_ESCAPE);stage_=24; @@ -353,19 +360,25 @@ inline void Workspace::scenario_workflow() { auto* target=workflow_shell("C");paste_scenario_.target=target->handle();target->begin_output_observation();target->fixture_command(paste_scenario_.receiver_command(std::filesystem::path(image_).parent_path()/"close.bin"));stage_=41; } else if(stage_==41 && paste_scenario_.receiver->ready()) { auto* target=workflow_shell("C"); + target->arm_next_paste_fence(2,started_+std::chrono::milliseconds(70000)); if(workflow_system_) { tf_shell_test::require_hosted_ime_input();workflow_select("C");tf_shell::write_unicode_clipboard(window_,std::u16string(paste_scenario_.large_units,u'汉')); workflow_system_shortcut("C",false);workflow_system_point("large-close-requested");stage_=241; } else { target->synthetic_paste(std::u16string(paste_scenario_.large_units,u'汉')); paste_scenario_.transfer=target->observed_paste();require(paste_scenario_.transfer!=nullptr,"workflow close transfer missing");stage_=42; + paste_scenario_.fence=target->observed_paste_fence(); } } else if(stage_==241) { paste_scenario_.transfer=workflow_shell("C")->observed_paste();if(!paste_scenario_.transfer) return; + paste_scenario_.fence=workflow_shell("C")->observed_paste_fence(); workflow_system_point("large-close-ready");stage_=42; - } else if(stage_==42 && paste_scenario_.transfer->progress().sent>=16384) { + } else if(stage_==42 && paste_scenario_.held()) { + paste_scenario_.other_consumed=workflow_shell("F4")->runtime_snapshot().consumed_bytes;stage_=242; + } else if(stage_==242 && workflow_shell("F4")->runtime_snapshot().consumed_bytes>paste_scenario_.other_consumed) { + paste_scenario_.pending.other_responsive=true; require(!tf_shell::paste_terminal(paste_scenario_.transfer->progress().state),"workflow close transfer completed before gate");paste_scenario_.receiver->allow_read();stage_=43; - } else if(stage_==43 && paste_scenario_.receiver->checkpoint()) { + } else if(stage_==43 && paste_scenario_.receiver->checkpoint() && workflow_shell("C")->observed_output().find("TF_PASTE_FLOOD")!=std::string::npos) { paste_scenario_.before_action(std::filesystem::path(image_).parent_path()/"close.bin",workflow_shell("C")->observed_output().find("TF_PASTE_FLOOD")!=std::string::npos); workflow_checkpoint("paste-close-pending",5,{"B","C"});workflow_select("C");workflow_shell("C")->close();stage_=44; } else if(stage_==44) { diff --git a/research/windterm-2.7.0-platform-scope.md b/research/windterm-2.7.0-platform-scope.md index f48cdedd..c406af56 100644 --- a/research/windterm-2.7.0-platform-scope.md +++ b/research/windterm-2.7.0-platform-scope.md @@ -1,5 +1,7 @@ # WindTerm 2.7.0 平台、架构与发行边界 +> 当前产品优先级由 [ADR-0006](../docs/adr/0006-confirmed-project-direction.md)确认:首个完整版本验收 Windows 11 x64,最终覆盖 Windows/Linux/macOS;其他系统版本、最低版本和架构后续单独明确。下表的官方声明及历史调查不改写,也不自动成为 TermForge 首个版本的支持承诺。平台可行性可提前调查,Linux/macOS 完整客户端验收在 R5 的 Windows 出口之后完成。 + 调查日期:2026-10-24。本文是 [#23](https://github.com/JavaZeroo/termforge/issues/23) 的单一范围台账,供 R2 核心路线(#26)和产品架构(#34)判断候选支持缺口;不进行核心构建、功能实现或平台验收。全量目标仍归 [#19](https://github.com/JavaZeroo/termforge/issues/19),TermForge 当前能力与限制以 [FEATURES](../docs/FEATURES.md) 为准。 ## 固定对象与证据口径 @@ -17,7 +19,7 @@ 3. 实际发行形式为 Windows/Linux **portable ZIP**、macOS **portable DMG**;macOS 安装说明是打开 DMG 并把 `WindTerm.app` 拖入 `Applications`。release 另给 Scoop/Chocolatey 命令,但它们不是本次四个官方上传资产,具体包身份和系统边界未知。 4. release 的 Linux 命令提到 `.tar.gz`,实际资产却是 `.zip`;Checksums 提到 macOS ZIP,实际资产却是 DMG。两处均保留为官方材料内部差异,不虚构缺失资产,也不把 macOS ZIP 校验值用于 DMG。 5. 固定 commit 没有完整 WindTerm 应用的顶层构建/打包工程;公开 `src/libssh` 的 CMake/CPack 配置属于第三方库,不能据此重建四个应用资产或推导其平台支持。 -6. TermForge 当前只有有限 Windows x64 客户端原型;Linux 核心 CI 不是 Linux GUI 客户端,Windows 11/Server 2025 验证也没有覆盖官方 Windows 7/8/10 或 x86 范围。R2 只使用本台账做选型输入,真实跨平台客户端和完整矩阵主要归 R4/R5。 +6. TermForge 当前只有有限 Windows x64 客户端原型;Linux 核心 CI 不是 Linux GUI 客户端,Windows 11/Server 2025 验证也没有覆盖官方 Windows 7/8/10 或 x86 范围。R2 使用本台账做候选差距输入;平台调查可并行,真实跨平台整合及完整验收归 R5 的后续全平台出口。 ## 目标台账 @@ -25,10 +27,10 @@ | ID | 官方声明与发行物事实 | 最低系统边界及未知 | 来源类型与精确来源 | TermForge 当前实现/验证 | 负责路标与后续验收 | |---|---|---|---|---|---| -| PLAT-001 | Windows **x86_32**,portable ZIP;资产 `WindTerm_2.7.0_Windows_Portable_x86_32.zip` | 官方列出 Windows 7/8/10/11,故声明最低为 **Windows 7**;Service Pack、edition、Windows 8.1、Server 及各版本实测均未知 | 官方声明:[release Usage](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0);发行物:[资产元数据](https://api.github.com/repos/kingToolbox/WindTerm/releases/assets/236583144) | **未实现、未验证**;现有 Windows 客户端仅 x64 | R2 仅供 #26/#34 判断 32 位支持缺口;真实客户端/打包归 R4/R5;R5 在 #19 全矩阵验收 | -| PLAT-002 | Windows **x86_64**,portable ZIP;资产 `WindTerm_2.7.0_Windows_Portable_x86_64.zip` | 官方列出 Windows 7/8/10/11,故声明最低为 **Windows 7**;Service Pack、edition、Windows 8.1、Server 及各版本实测均未知 | 官方声明:[release Usage](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0);发行物:[资产元数据](https://api.github.com/repos/kingToolbox/WindTerm/releases/assets/236582459) | **部分**:有限 x64 原型与免安装 ZIP;只已验 Windows 11 本机和 Server 2025 CI。Windows 7/8/10 未验收,Server 2022 已知资源门槛未过 | R2 #26/#34 选型输入及 Windows 开发版;旧系统/更广矩阵归 R4/R5;R5 在 #19 验收,不把当前验证范围写成官方范围 | -| PLAT-003 | macOS **x86_64**,portable DMG;资产 `WindTerm_2.7.0_Mac_Portable_x86_64.dmg`;说明为拖入 `Applications` | 官方列出 macOS 10.13/10.14/10.15/11/12,故声明最低为 **10.13**;具体 build、10.12 及更早、13 及更新版本和实机验证未知 | 官方声明/安装说明:[release Usage](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0);发行物:[资产元数据](https://api.github.com/repos/kingToolbox/WindTerm/releases/assets/236583377) | **未实现、未验证** macOS 客户端 | R2 仅供 #26/#34 判断旧 macOS、窗口/输入/字体和打包缺口;真实客户端归 R4/R5;R5 在 #19 验收 | -| PLAT-004 | Linux **x86_64**,portable ZIP;资产 `WindTerm_2.7.0_Linux_Portable_x86_64.zip` | 官方声明 **Generic Linux(glibc 2.17+)**;具体发行版、内核、glibc 之外运行库、X11/Wayland/桌面环境和实机验证未知 | 官方声明:[release Usage](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0);发行物:[资产元数据](https://api.github.com/repos/kingToolbox/WindTerm/releases/assets/236583277) | **未实现、未验证** Linux 客户端;只有 Linux 核心 CI,不证明 GUI/PTY 整合或可分发产品 | R2 仅供 #26/#34 判断 libc/显示/输入/打包缺口;真实客户端归 R4/R5;R5 在 #19 验收 | +| PLAT-001 | Windows **x86_32**,portable ZIP;资产 `WindTerm_2.7.0_Windows_Portable_x86_32.zip` | 官方列出 Windows 7/8/10/11,故声明最低为 **Windows 7**;Service Pack、edition、Windows 8.1、Server 及各版本实测均未知 | 官方声明:[release Usage](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0);发行物:[资产元数据](https://api.github.com/repos/kingToolbox/WindTerm/releases/assets/236583144) | **未实现、未验证**;现有 Windows 客户端仅 x64 | #23/#26 的历史调查供 #34 判断 32 位支持缺口;x86 产品范围待用户明确,不阻塞 Windows 11 x64 首个完整出口 | +| PLAT-002 | Windows **x86_64**,portable ZIP;资产 `WindTerm_2.7.0_Windows_Portable_x86_64.zip` | 官方列出 Windows 7/8/10/11,故声明最低为 **Windows 7**;Service Pack、edition、Windows 8.1、Server 及各版本实测均未知 | 官方声明:[release Usage](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0);发行物:[资产元数据](https://api.github.com/repos/kingToolbox/WindTerm/releases/assets/236582459) | **部分**:有限 x64 原型与免安装 ZIP;只已验 Windows 11 本机和 Server 2025 CI。Windows 7/8/10 未验收,Server 2022 已知资源门槛未过 | R2 #34 选型输入及 Windows 开发版;R5 先验 Windows 11 x64,旧系统/更广矩阵由用户后续明确,不把当前验证范围写成官方范围 | +| PLAT-003 | macOS **x86_64**,portable DMG;资产 `WindTerm_2.7.0_Mac_Portable_x86_64.dmg`;说明为拖入 `Applications` | 官方列出 macOS 10.13/10.14/10.15/11/12,故声明最低为 **10.13**;具体 build、10.12 及更早、13 及更新版本和实机验证未知 | 官方声明/安装说明:[release Usage](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0);发行物:[资产元数据](https://api.github.com/repos/kingToolbox/WindTerm/releases/assets/236583377) | **未实现、未验证** macOS 客户端 | R2 仅供 #26/#34 判断旧 macOS、窗口/输入/字体和打包缺口;可行性调查可并行;真实客户端整合及验收归 R5 Windows 出口之后,系统/架构由用户明确 | +| PLAT-004 | Linux **x86_64**,portable ZIP;资产 `WindTerm_2.7.0_Linux_Portable_x86_64.zip` | 官方声明 **Generic Linux(glibc 2.17+)**;具体发行版、内核、glibc 之外运行库、X11/Wayland/桌面环境和实机验证未知 | 官方声明:[release Usage](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0);发行物:[资产元数据](https://api.github.com/repos/kingToolbox/WindTerm/releases/assets/236583277) | **未实现、未验证** Linux 客户端;只有 Linux 核心 CI,不证明 GUI/PTY 整合或可分发产品 | R2 仅供 #26/#34 判断 libc/显示/输入/打包缺口;可行性调查可并行;真实客户端整合及验收归 R5 Windows 出口之后,系统/架构由用户明确 | 固定 README 的 [Download](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/README.md#download) 与 [GUI](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/README.md#gui) 还明确声明 Linux/macOS/Windows binary 和三平台 GUI。这一声明由上面四行落实为 2.7.0 的具体资产;README 本身没有给出额外架构或更低系统版本。 @@ -37,7 +39,7 @@ | ID | 目标事实或组件边界 | 来源类型与精确来源 | 当前实现/验证状态 | 负责路标与后续验收 | |---|---|---|---|---| | PLAT-005 | release 提供 `scoop bucket add extras` + `scoop install windterm` 和 `choco install windterm`。这是官方说明中的安装入口,不是额外的官方 release 资产;包版本、架构、维护者、依赖和最低系统均未知 | 官方声明:[release Installation](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0);四个实际上传资产:[release API](https://api.github.com/repos/kingToolbox/WindTerm/releases/tags/2.7.0) | TermForge 未提供或验证这两个入口;未把第三方包管理器元数据当作官方目标事实 | R2/R3 决定 Windows 分发时另行核验;R5 验收所声明的安装/升级路径,不能以命令存在代替测试 | -| PLAT-006 | 固定公开 `ConPty.cpp` 定义 `CONPTY_MINIMAL_WINDOWS_VERSION 18309`,`isAvailable()` 检查该 build,失败文字为 Windows 10 below 1809;因此 **ConPTY 路径静态下限为 Windows 10 1809 / build 18309**。这不是整个 WindTerm 的最低系统,也不是 2.7.0 二进制运行观察;release 另称该版本暂以 WinPTY 替代 ConPTY | 源码静态事实:[`src/Pty/ConPty.cpp`](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/src/Pty/ConPty.cpp);官方 release 的 `[SHELL]` 说明:[2.7.0 release](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0) | 当前原型使用 ConPTY,已验 Windows 11/Server 2025;Windows 10 未验收,Server 2022 资源门槛未过。没有验证官方 2.7.0 的 WinPTY/ConPTY 选择行为 | R2 #26 必须比较 ConPTY/WinPTY 或其他后端的支持和行为缺口,#34 明确 PTY 抽象;R4/R5 验证系统矩阵,不能把 1809 错写成全应用下限 | +| PLAT-006 | 固定公开 `ConPty.cpp` 定义 `CONPTY_MINIMAL_WINDOWS_VERSION 18309`,`isAvailable()` 检查该 build,失败文字为 Windows 10 below 1809;因此 **ConPTY 路径静态下限为 Windows 10 1809 / build 18309**。这不是整个 WindTerm 的最低系统,也不是 2.7.0 二进制运行观察;release 另称该版本暂以 WinPTY 替代 ConPTY | 源码静态事实:[`src/Pty/ConPty.cpp`](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/src/Pty/ConPty.cpp);官方 release 的 `[SHELL]` 说明:[2.7.0 release](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0) | 当前原型使用 ConPTY,已验 Windows 11/Server 2025;Windows 10 未验收,Server 2022 资源门槛未过。没有验证官方 2.7.0 的 WinPTY/ConPTY 选择行为 | 保留 #26 的历史比较,#34 根据原版观察提交后端与 PTY 边界方案供用户确认;R5 按确认的系统矩阵验收,不能把 1809 错写成全应用下限 | | PLAT-007 | 固定公开树只有 `README.md`、`benchmark/`、`images/`、`src/`,没有完整应用入口或顶层构建/安装/打包工程。`src/libssh/CMakeLists.txt` 是 `project(libssh VERSION 0.9.5 LANGUAGES C)`,其 CPack 可生成 libssh ZIP/NSIS;这些不是 WindTerm 应用发行配置 | 源码树事实:[固定 commit 根目录](https://github.com/kingToolbox/WindTerm/tree/a8336c1d1e3dd1a981500dae8ef9eb42066de95f)、[递归 tree API](https://api.github.com/repos/kingToolbox/WindTerm/git/trees/a8336c1d1e3dd1a981500dae8ef9eb42066de95f?recursive=1)(本次返回 `truncated=false`);构建配置:[`src/libssh/CMakeLists.txt`](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/src/libssh/CMakeLists.txt)、[`src/libssh/CPackConfig.cmake`](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/src/libssh/CPackConfig.cmake)、[`src/libssh/INSTALL`](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/src/libssh/INSTALL) | 未从官方公开源码构建官方应用;TermForge 的独立可复建目标不受此阻塞,但不能宣称恢复官方构建或复用其完整源码 | R2 #26/#34 把依赖许可、平台构建和维护成本分开;R5 对 TermForge 自己声明的平台完成可复建发布和矩阵验收 | ### 未确认架构与版本范围 @@ -46,10 +48,10 @@ | ID | 未知范围 | 来源类型与精确来源 | 当前实现/验证状态 | 负责路标与后续验收 | |---|---|---|---|---| -| PLAT-008 | Windows ARM/ARM64,以及 x86/x64 之外架构;原生运行、仿真运行和最低系统均未知 | 官方声明与发行物事实:[固定 README](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/README.md) 只声明 Windows;[release API](https://api.github.com/repos/kingToolbox/WindTerm/releases/tags/2.7.0) 四资产无 Windows ARM | 未实现、未验证 | R2 #26/#34 记录候选硬限制,不作支持承诺;是否扩展须单独裁决,若纳入则 R4/R5 真实客户端验收 | +| PLAT-008 | Windows ARM/ARM64,以及 x86/x64 之外架构;原生运行、仿真运行和最低系统均未知 | 官方声明与发行物事实:[固定 README](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/README.md) 只声明 Windows;[release API](https://api.github.com/repos/kingToolbox/WindTerm/releases/tags/2.7.0) 四资产无 Windows ARM | 未实现、未验证 | R2 #26/#34 记录候选硬限制,不作支持承诺;是否纳入由用户明确,若纳入则在 R5 后续平台出口完成真实客户端验收 | | PLAT-009 | macOS ARM64/Apple Silicon,包括原生与 Rosetta 路径;macOS x86 之外架构 | 官方声明与发行物事实:[release](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0) 只有 x86_64 和 10.13–12 声明;[macOS 资产元数据](https://api.github.com/repos/kingToolbox/WindTerm/releases/assets/236583377) 只有 x86_64 DMG,没有 ARM 资产或兼容声明 | 未实现、未验证 | 同 PLAT-008;不能从 x86_64 DMG 推断 ARM 原生或仿真通过 | | PLAT-010 | Linux ARM64/AArch32、x86_32 及其他架构 | 官方声明与发行物事实:[release](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0) 只有 x86_64 和 glibc 2.17+ 声明;[Linux 资产元数据](https://api.github.com/repos/kingToolbox/WindTerm/releases/assets/236583277) 没有其他架构或系统边界 | 未实现、未验证;核心 CI 不覆盖这些客户端 | 同 PLAT-008;核心可编译不等于产品平台支持 | -| PLAT-011 | 未列出的旧/新系统、Windows Server/edition/Service Pack、Linux 发行版与显示栈 | 官方声明:[release](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0) 只给 PLAT-001–004 的枚举或 glibc 条件;未提及项均保持未知,不自行填具体版本 | 仅 FEATURES 所列有限 Windows 环境和 Linux 核心 CI 有验证 | R2 选型必须列环境缺口;R4/R5 只对实际声明支持的环境做真实验收,范围变化另留证据 | +| PLAT-011 | 未列出的旧/新系统、Windows Server/edition/Service Pack、Linux 发行版与显示栈 | 官方声明:[release](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0) 只给 PLAT-001–004 的枚举或 glibc 条件;未提及项均保持未知,不自行填具体版本 | 仅 FEATURES 所列有限 Windows 环境和 Linux 核心 CI 有验证 | R2 选型必须列环境缺口;R5 对用户确认范围做真实验收,其他系统/架构保持待明确,范围变化另留确认依据 | ## 2.7.0 官方资产快照 diff --git a/tools/prepare_hosted_shells.py b/tools/prepare_hosted_shells.py index 65a29e16..c2f497d2 100644 --- a/tools/prepare_hosted_shells.py +++ b/tools/prepare_hosted_shells.py @@ -29,6 +29,11 @@ ROOTFS_SHA256 = "8251e27ffff381a4af5f41dcb94d867de3e0d9774a9241908ab34555d99315ea" MAX_DOWNLOAD = 400 * 1024 * 1024 STEPS = {"preflight": 45, "download": 120, "import": 90, "check-before": 25, "check-after": 25} +COMMANDS_BY_STEP = {"preflight": ("cmd", "ps51", "ps7", "list"), "download": (), + "import": ("list-before-import", "import"), + "check-before": ("list", "startup"), "check-after": ("list", "startup")} +COMMAND_DEADLINES = {"cmd": 10, "ps51": 10, "ps7": 10, "list": 10, + "list-before-import": 10, "import": 80, "startup": 20} class PreparationError(Exception): @@ -165,16 +170,95 @@ def copy_verified_rootfs(response, destination, *, expected_hash=ROOTFS_SHA256, raise PreparationError("download_hash") +def validate_command_events(value, step): + """Whitelist an ordered prefix; an invoking tail is not a completed command.""" + if (step not in COMMANDS_BY_STEP or not isinstance(value, dict) or set(value) != {"events"} or + not isinstance(value["events"], list) or len(value["events"]) > 8): + raise ValueError("command event schema") + expected = COMMANDS_BY_STEP[step] + index, waiting, timed_out = 0, False, False + for event in value["events"]: + if (not isinstance(event, dict) or + set(event) != {"command", "phase", "deadline_ms", "elapsed_ms", "returncode"} or + index >= len(expected) or timed_out or event["command"] != expected[index]): + raise ValueError("command event identity") + phase = event["phase"] + if (phase not in (("returned", "timed_out") if waiting else ("invoking",)) or + type(event["deadline_ms"]) is not int or + event["deadline_ms"] != 1000 * COMMAND_DEADLINES[expected[index]] or + type(event["elapsed_ms"]) is not int or not 0 <= event["elapsed_ms"] <= STEPS[step] * 1000 + 10000): + raise ValueError("command event boundary") + if phase == "returned": + if type(event["returncode"]) is not int or not -(2**31) <= event["returncode"] < 2**32: + raise ValueError("command exit code") + waiting = False + index += 1 + else: + if event["returncode"] is not None or (phase == "invoking" and event["elapsed_ms"] != 0): + raise ValueError("unobserved command exit") + waiting = phase == "invoking" + timed_out = phase == "timed_out" + return value["events"] + + +def read_command_events(directory, step): + with (directory / "command-events.json").open("rb") as stream: + raw = stream.read(8193) + if len(raw) > 8192: + raise ValueError("command event size") + return validate_command_events(json.loads(raw), step) + + +def write_command_event(directory, name, phase, elapsed_ms, returncode=None): + try: + path = directory / "command-events.json" + events = read_command_events(directory, directory.name) if path.exists() else [] + event = {"command": name, "phase": phase, "deadline_ms": 1000 * COMMAND_DEADLINES[name], + "elapsed_ms": elapsed_ms, "returncode": returncode} + value = {"events": events + [event]} + validate_command_events(value, directory.name) + write_json(path, value) + except (OSError, ValueError, KeyError, RecursionError): + raise PreparationError("command_observation") from None + + +def public_command_observation(directory, step): + # Read only after the worker is reaped. Partial traces survive its deadline; + # raw arguments/output and malformed data never enter the public report. + if step not in COMMANDS_BY_STEP: + return {"status": "invalid", "events": []} + if not COMMANDS_BY_STEP[step]: + return {"status": "not_applicable", "events": []} + try: + return {"status": "observed", "events": read_command_events(directory, step)} + except FileNotFoundError: + return {"status": "not_observed", "events": []} + except (OSError, ValueError, RecursionError): + return {"status": "invalid", "events": []} + + def run_command(command, directory, name, *, cwd, timeout=10, data=None): # This function only runs inside a GO-gated worker already in the parent's # exact Job. No shell string construction or global-process cleanup. + if name not in COMMANDS_BY_STEP.get(directory.name, ()) or timeout != COMMAND_DEADLINES[name]: + raise PreparationError("command_observation") + def observe(phase, elapsed_ms, returncode=None): + try: + write_command_event(directory, name, phase, elapsed_ms, returncode) + except PreparationError: + # A missing/partial trace cannot replace the actual command result. + pass stdout_path, stderr_path = directory / (name + ".stdout"), directory / (name + ".stderr") with stdout_path.open("xb") as stdout, stderr_path.open("xb") as stderr: + observe("invoking", 0) + started = time.monotonic_ns() try: result = subprocess.run(command, input=data, stdout=stdout, stderr=stderr, cwd=cwd, timeout=timeout, creationflags=subprocess.CREATE_NO_WINDOW) except subprocess.TimeoutExpired: + observe("timed_out", (time.monotonic_ns() - started) // 1000000) raise PreparationError("command_deadline") from None + observe("returned", (time.monotonic_ns() - started) // 1000000, result.returncode) if stdout_path.stat().st_size > 65536: raise PreparationError("command_report_size") return result.returncode, stdout_path.read_bytes() @@ -268,7 +352,8 @@ def worker(step, out): FAILURES = {"hosted_guard", "workspace_guard", "windows_admin_guard", "worker_job_guard", "cwd_identity", "wsl_report_size", "wsl_list_encoding", "wsl_list_identity", "marker_encoding", "shell_startup", "download_deadline", "download_size", "download_hash", "command_deadline", "command_report_size", "worker_directory_guard", - "wsl_list", "download_response", "ubuntu_already_present", "wsl_import", "ubuntu_missing", "worker_step"} + "wsl_list", "download_response", "ubuntu_already_present", "wsl_import", "ubuntu_missing", "worker_step", + "command_observation"} def run_step(step, out): @@ -375,6 +460,8 @@ def terminate_job(): job.close() (private / "worker.stdout").write_bytes(stdout) (private / "worker.stderr").write_bytes(stderr) + record["command_observation"] = (public_command_observation(private, step) if cleanup["root_reaped"] else + {"status": "worker_not_reaped", "events": []}) return record diff --git a/tools/test_prepare_hosted_shells.py b/tools/test_prepare_hosted_shells.py index b7110612..9036d951 100644 --- a/tools/test_prepare_hosted_shells.py +++ b/tools/test_prepare_hosted_shells.py @@ -166,5 +166,155 @@ def test_local_runtime_is_rejected_before_any_subprocess_or_download(self): urlopen.assert_not_called() +class CommandBoundaryTests(unittest.TestCase): + @staticmethod + def event(command="cmd", phase="invoking", elapsed=0, code=None): + return {"command": command, "phase": phase, "deadline_ms": 10000, + "elapsed_ms": elapsed, "returncode": code} + + def test_timeout_keeps_actual_command_boundary_without_private_arguments(self): + secret = "synthetic-private-command-path" + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) / "preflight" + directory.mkdir() + failure = prep.subprocess.TimeoutExpired([secret], 10, output=b"synthetic-private-output") + with mock.patch.object(prep.subprocess, "run", side_effect=failure) as command, \ + mock.patch.object(prep.time, "monotonic_ns", side_effect=[0, 10000000000]), \ + mock.patch.object(prep.subprocess, "CREATE_NO_WINDOW", 0, create=True): + with self.assertRaisesRegex(prep.PreparationError, "^command_deadline$"): + prep.run_command([secret], directory, "cmd", cwd=directory) + command.assert_called_once() + self.assertEqual(command.call_args.kwargs["timeout"], 10) + data = json.loads((directory / "command-events.json").read_text(encoding="utf-8")) + self.assertEqual(data, {"events": [ + {"command": "cmd", "phase": "invoking", "deadline_ms": 10000, "elapsed_ms": 0, "returncode": None}, + {"command": "cmd", "phase": "timed_out", "deadline_ms": 10000, "elapsed_ms": 10000, "returncode": None}]}) + self.assertNotIn(secret, json.dumps(data)) + self.assertNotIn("synthetic-private-output", json.dumps(data)) + + def test_returned_command_and_partial_invocation_are_reported_independently(self): + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) / "preflight" + directory.mkdir() + def returned(command, **kwargs): + kwargs["stdout"].write(b"private-native-output") + return prep.subprocess.CompletedProcess(command, 7) + with mock.patch.object(prep.subprocess, "run", side_effect=returned), \ + mock.patch.object(prep.time, "monotonic_ns", side_effect=[0, 123000000]), \ + mock.patch.object(prep.subprocess, "CREATE_NO_WINDOW", 0, create=True): + self.assertEqual(prep.run_command(["private-command"], directory, "cmd", cwd=directory), + (7, b"private-native-output")) + self.assertEqual(prep.public_command_observation(directory, "preflight"), + {"status": "observed", "events": [self.event(), self.event(phase="returned", elapsed=123, code=7)]}) + prep.write_json(directory / "command-events.json", {"events": [self.event()]}) + self.assertEqual(prep.public_command_observation(directory, "preflight"), + {"status": "observed", "events": [self.event()]}) + + def test_unknown_or_unordered_data_is_rejected_without_echoing_it(self): + good = self.event() + values = [ + {"events": [{**good, "command": "private-path"}]}, + {"events": [{**good, "argv": ["private-token"]}]}, + {"events": [self.event(phase="returned", code=0)]}, + {"events": [good, self.event(phase="returned", code=True)]}, + {"events": [good, self.event(phase="timed_out", code=91)]}, + {"events": [good, self.event(phase="returned", code=0), self.event(command="ps7")]}, + {"events": [{**good, "deadline_ms": 11000}]}, + {"events": [good, self.event(phase="returned", elapsed=-1, code=0)]}, + {"events": [good] * 9}, + ] + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) / "preflight" + directory.mkdir() + for value in values: + with self.subTest(value=value): + prep.write_json(directory / "command-events.json", value) + self.assertEqual(prep.public_command_observation(directory, "preflight"), + {"status": "invalid", "events": []}) + (directory / "command-events.json").write_bytes(b"private-token" * 1000) + self.assertEqual(prep.public_command_observation(directory, "preflight"), + {"status": "invalid", "events": []}) + (directory / "command-events.json").write_bytes(b"[" * 2000 + b"]" * 2000) + self.assertEqual(prep.public_command_observation(directory, "preflight"), + {"status": "invalid", "events": []}) + + def test_trace_write_failure_cannot_replace_observed_timeout(self): + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) / "preflight" + directory.mkdir() + with mock.patch.object(prep.subprocess, "run", side_effect=prep.subprocess.TimeoutExpired(["synthetic"], 10)), \ + mock.patch.object(prep, "write_command_event", side_effect=[None, prep.PreparationError("command_observation")]), \ + mock.patch.object(prep.subprocess, "CREATE_NO_WINDOW", 0, create=True): + with self.assertRaisesRegex(prep.PreparationError, "^command_deadline$"): + prep.run_command(["synthetic"], directory, "cmd", cwd=directory) + + def test_trace_write_failure_preserves_command_and_marker_result(self): + marker = "TERMFORGE_HOSTED_CMD_READY" + output = (marker + "\r\n").encode("ascii") + write_json = prep.write_json + for phase in ("invoking", "returned"): + for code in (0, 7): + with self.subTest(phase=phase, code=code), tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) / "preflight" + directory.mkdir() + def failed_write(path, value): + if value["events"][-1]["phase"] == phase: + raise OSError("synthetic-private-storage-error") + write_json(path, value) + def returned(command, **kwargs): + kwargs["stdout"].write(output) + return prep.subprocess.CompletedProcess(command, code) + with mock.patch.object(prep, "write_json", side_effect=failed_write), \ + mock.patch.object(prep.subprocess, "run", side_effect=returned) as command, \ + mock.patch.object(prep.subprocess, "CREATE_NO_WINDOW", 0, create=True): + result_code, result_output = prep.run_command(["synthetic"], directory, "cmd", cwd=directory) + command.assert_called_once() + self.assertEqual((result_code, result_output), (code, output)) + if code: + with self.assertRaisesRegex(prep.PreparationError, "^shell_startup$"): + prep.validate_marker(result_output, result_code, marker) + else: + prep.validate_marker(result_output, result_code, marker) + observation = prep.public_command_observation(directory, "preflight") + self.assertEqual(observation, {"status": "not_observed", "events": []} if phase == "invoking" else + {"status": "observed", "events": [self.event()]}) + + def test_parent_retains_command_or_worker_deadline_after_cleanup(self): + for worker_timeout in (False, True): + with self.subTest(worker_timeout=worker_timeout), tempfile.TemporaryDirectory() as temporary: + out = Path(temporary) + events = [self.event(), self.event(phase="returned", elapsed=2, code=0), self.event(command="ps51")] + if not worker_timeout: + events.append(self.event(command="ps51", phase="timed_out", elapsed=10000)) + process = mock.Mock(pid=123, returncode=None) + process.poll.side_effect = lambda: process.returncode + calls = [] + def communicate(*args, **kwargs): + calls.append(kwargs["timeout"]) + prep.write_json(out / "_private/preflight/command-events.json", {"events": events}) + if worker_timeout and len(calls) == 1: + raise prep.subprocess.TimeoutExpired(["synthetic-worker"], 45) + process.returncode = 99 if worker_timeout else 1 + return (b"" if worker_timeout else b'{"failure_code":"command_deadline"}', b"private-worker-error") + process.communicate.side_effect = communicate + job = mock.Mock() + job.drain.return_value = {"active": 0, "total": 2, "terminated": 0} + audit = mock.Mock(stable=True, members=[], private=[]) + audit.process_ids.return_value = [] + with mock.patch.object(prep, "OwnedJob", return_value=job), \ + mock.patch.object(prep, "JobMembers", return_value=audit), \ + mock.patch.object(prep.subprocess, "Popen", return_value=process), \ + mock.patch.object(prep.subprocess, "CREATE_NO_WINDOW", 0, create=True): + result = prep.run_step("preflight", out) + self.assertEqual(result["status"], "failed") + self.assertEqual(result["failure_code"], "worker_deadline" if worker_timeout else "command_deadline") + self.assertEqual(result["command_observation"], {"status": "observed", "events": events}) + self.assertTrue(result["cleanup"]["root_reaped"]) + self.assertEqual(result["cleanup"]["root_natural_exit"], not worker_timeout) + self.assertEqual(result["cleanup"]["job_termination_requests"], int(worker_timeout)) + self.assertEqual(calls, [45, 5] if worker_timeout else [45]) + self.assertNotIn("private-worker-error", json.dumps(result)) + + if __name__ == "__main__": unittest.main()