Skip to content

[R2][回归] 定位 candidate-scroll 末帧生成后的原20秒退出超时 #97

Closed as not planned

Description

@JavaZeroo

真实失败与范围

PR #96 的 push candidate run 40449286764,Windows job 122007956956,HEAD 5c073e49ecef6722f997cdc9a7d3b8dd97e594ee,candidate_shell_tui 中 test_real_scroll_region 失败。该配置为 candidate termforge.5 + system ConPTY/flags0;该提交只改上游隔离构建与证据,没有修改此产品路径。不能因此断定环境抖动或根因。

6个该组测试中另5个通过,本组总计24.853s;失败调用是原 execute(...,"candidate-scroll",timeout=20)。同HEAD PR candidate成功不能覆盖该push失败;没有重跑取绿。

已核对的证据

artifact 12709588341 ZIP SHA d6d0a533acdef574b60de38dc65ce052b3dce1137ef1679ecc5eedb9259cb942 已下载核对;原件保存于私有 local/r56-conpty-build/ci-fifth-candidate-failure.zip。

  • sample.json SHA 458e49164de09028d86f1f42a9e8476e92e8ebd284378f7d0afe1ac08c2d0027:timed_out=true;期限时Job active5/total7/terminated0;外层返回99,stdout/stderr为空;清理后Job active0/total7/terminated0,desktop_gone=true。清理结果不能冒充自然退出。
  • before/region/final 原生capture、源与实际运输、candidate末帧均已产生。末帧80×24,cursor[5,0],alternate=false,frame_matches_full=true;可见前五行是TOP_FIXED/REGION_C/REGION_D/BOTTOM_FIXED/TF_SCROLL_DONE,与本场景预期一致。
  • 原107B源 SHA f27448bdc7f3811e68ae5e6f9779d14409d179df55761b7b1355997d69afaff1;实际运输737B SHA e8553b2ba03e4e51a7189104f340cf5a9c11f8ce55256419568f9f144c51d144。
  • 最后已完成阶段、阻塞线程/进程、child自然退出及关窗/回收的具体边界未知,不能仅凭画面正确宣布通过。它不是#56的175B主/备用margin输入,也没有证明与[R1回归] 持续 cmd 输出时 ETX 未中断导致 normal/resize 超时 #36/#44同因。

有界后续与验收

  • 固定原源码/二进制、107B源、20秒总期限与已有清理预算;核对末帧之后的child退出、reader EOF、会话关闭、UI退出和launcher等待边界。
  • 执行前列明可证伪假设、最小阶段观测、样本数与停止条件;优先离线检查现有capture及代码,再进行限定自有Job/desktop复现。不改变测试成功预期、不延长期限、不无限刷绿。
  • 若取得同类失败,给出同合同的因果red→green及必要反例;若未复现,保留原失败和未知。
  • 最终同源candidate/system回归和自然退出/Job/desktop回收通过;单独末帧正确不替代生命周期。

父项 #38,关联 #27/#28;本项是独立可靠性诊断,不以其他配置或上游adapter412/412来结项。

离线复核与有界接续

用既有项目虚拟环境执行 validate_scroll_output 对上述原 artifact 复算,原生 before/region/final、107B源、737B运输的独立 pyte 0.8.2 重放、候选末帧/默认属性/光标/有限历史均通过。该函数不检查进程自然退出;因此这只是确定已有输出证据的边界,不能把原20秒超时改为通过。首次全局 Python 缺少 pyte 元数据,随后使用已有项目环境复核,无安装或源码修改。

已按上述预定批次固定失败 HEAD 5c073e49ecef6722f997cdc9a7d3b8dd97e594ee,在独立工作树/全新candidate/system构建执行原 CandidateTuiTests.test_real_scroll_region:一个单测内原两次场景均通过,合计1.204秒,每次原20秒外部期限和原清理预算不变。两次均exit37、cleanup_complete、内外Job active0、desktop已删除;没有追加循环。

本机为Windows11 build26200/MSVC19.42,CI为Windows Server环境,结果分列。本机未复现原CI超时,不构成因果修复,原阻塞阶段仍unknown。源码/二进制/运行日志与两份完整sample保存在独立工作树的 local/r97/;脱敏清单SHA dffb1487c9597b563d91da7d2cf1c2b994db4f48625cb446662c168debed167f。本次没有修改实现或断言。

后续须在能区分末帧完成之后各退出阶段的最小观测下获得同类失败:child返回、cmd接收退出输入、Session wait/reader EOF、窗口退出与launcher等待分别留证。先固定探针/样本/停止条件,保持原期限;目前只记录待定位阶段,不宣布任一环节为原因。

Activity

  1. self-assigned this
    on Oct 27, 2026
  2. added
    ready-for-agent范围明确且真实阻塞已解除,可开始实施
    research有明确问题与出口的证据调查
    on Oct 27, 2026
  3. JavaZeroo commented on Oct 30, 2026

    @JavaZeroo
    OwnerAuthor

    失败配置在现树上可达、断言未变;缺的是「末帧之后收尾状态机」的一条快照

    1. 可达性(逐行引用,均可复核)

    环节 位置 结论
    候选核心开关 CMakeLists.txt:4 option(TERMFORGE_BUILD_CANDIDATE_ADAPTER ... OFF);:170-172 if(TERMFORGE_BUILD_CANDIDATE_ADAPTER) termforge_candidate_shell(termforge_shell);:54-63 加 TERMFORGE_CANDIDATE_CORE=1 并链 Rust 静态库 打开该 option 即得 candidate 核心
    后端选择 experiments/conpty/backend.hpp:112-122:未定义 TERMFORGE_CONPTY_SIDEBYSIDE 时 configured() 返回 system();:25、:129-130 creation_flags_ = 0 即 system ConPTY / flags0
    谁定义 SIDEBYSIDE cmake/ConptyBackend.cmake:19;该函数只经 CMakeLists.txt:406(仅 TERMFORGE_BUILD_QT_SHELL 内 include)与 :417 用于 Qt 入口 termforge_shell(:166-172)从未调用 → 该目标永远 system/flags0,没有第二种后端可选
    用例注册与期限 CMakeLists.txt:343-346 candidate_shell_tui 需 option 且 WIN32;test_candidate_tui.py:93-96 execute(...,"candidate-scroll",...,timeout=20) 原 20 s 外部期限原样存在

    结论:现树上 cmake -S . -B <dir> -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=ON + ctest -R candidate_shell_tui 就是 issue 记录的配置,可达。

    2. 与失败 HEAD 5c073e4 相比,断言与路径未变

    • test_real_scroll_region 逐字未变(git diff 5c073e4..HEAD -- experiments/shell/test_candidate_tui.py 只改动 margins 用例);scroll_checks.py、backend.hpp 无改动。
    • run.py 的期限分支(:467-477)未改;normal 退出仍是 exit 37(shell.cpp:561-563、:1478-1485)。
    • 副产品(对下一步有用):run.py:450-453 现在对每个 mode 默认设 TERMFORGE_OUTPUT_TRACE / TERMFORGE_CAPTURE_OUTPUT。原失败 artifact 早于该改动,所以它不可能带 output-trace.jsonl;同配置再红一次会自带读写时间线。

    3. local/repeat* 里没有 candidate-scroll 样本

    • 本工作树 local/repeat2…repeat16 的场景目录全是默认构建的 ui-*(无 candidate-*);local 下 *candidate* 文件为空;文本里 grep candidate-scroll 为空。
    • 机内唯一一份在另一个工作树:D:\Code\termforge-scroll-exit-r97\local\r97\original-test\candidate-shell\run-13cpoqeb\candidate-scroll-0 与 …\candidate-scroll-1(before/region/final 的 console+cells、source.bin 107 B、transport.bin 801/813 B、candidate-scroll.json、sample.json、frame.bmp)。同目录 manifest.json 记 source 5c073e4、configuration candidate termforge.5/system ConPTY/flags0、original_outer_timeout_seconds 20、retry_allowed false;original-test.log 为 Ran 1 test in 1.204s ... OK。
    • 即:这是正文「本机未复现」那批留下的两份 sample,不是新样本;也没有第二条独立命中。

    4. 三条假设与一条可分离观测

    末帧内容的最后写入点在 shell.cpp:557(证据 JSON);之后收尾顺序是:gate 信号(:558-560)→ type(L"exit 37\r")(:562)→ stage_=6 → begin_close()(:401-422 起 closer 线程:input_.stop()、resize_.stop()、session_.cancel() / session_.close() → closed_=true)→ tick 里 if (closing_ && closed_) 且 drained 才 DestroyWindow(:903-909)。

    假设(正文原话) 对应代码位置 判据
    H1「child 返回」未发生 session_.wait_exit(0)(:912)/ candidate 的 terminal event 6(:825)→ 从未 begin_close() child 未 signaled、closing_=false
    H2「Session wait/reader EOF」未观察到 closer 线程卡在 input_.stop() / session_.close()(:413-421),或 drained 永不为真(:905) closing_=true 但 closed_=false 或 drained=false
    H3「窗口退出与 launcher 等待」未返回 DestroyWindow 之后到进程返回(:907-909、:1474-1506) child 已退出、closed_=true、窗口已销毁,但进程 20 s 内未返回

    单一观测:一行「收尾状态机快照」——同一 schema、同一文件,从末帧写完开始每 tick 一行,字段为 stage_、closing_、closed_、drained(当拍输入/输出已取空)、session_.child_signaled / exit_code / read_error。取值组合即可三分:child 未 signaled→H1;signaled 但 closed_ 假→H2;都真而进程不返回→H3。
    两个必须注意的放置约束:① 失败时 stdout/stderr 为空(正文 sample 事实),而 shell 只在成功路径打印 JSON(shell.cpp:1487-1505)、异常路径 return 2(:1507-1510),所以快照必须旁路写出;② 若 UI 线程卡住,tick 内的行也写不出来 → 同一 schema 还要由 closer 线程(:413-421)和 reader EOF 处(session.hpp:96-97)各写一行。
    为什么现有 artifact 必然缺这一观测:run.py:469-473 在期限处 job.terminate(),而 OwnedJob.terminate 是 TerminateJobObject(handle, 99)(experiments/conpty/run.py:83-84)——正文记的「外层返回99」就是我们自己的杀进程码,不是 child 的自然退出码。正文「清理结果不能冒充自然退出」在此有了代码位置。

    5. 关闭判据(原文)

    • 「若未复现,保留原失败和未知。」——本机 1.204 s 内两次场景通过 = 未复现,本条现在适用;
    • 「最终同源candidate/system回归和自然退出/Job/desktop回收通过;单独末帧正确不替代生命周期。」
    • 正文已记:「本机未复现原CI超时,不构成因果修复,原阻塞阶段仍unknown。」
      因此本项仍应开放;下一步是一次限定运行(同 HEAD 或同配置、20 s 不变、不扩样),带上面那一行快照,取到同类 red 即为三分;取不到就继续保留未知,不以「再绿一次」结项。

    边界

  4. JavaZeroo commented on Oct 31, 2026

    @JavaZeroo
    OwnerAuthor

    R1 #97 复核:失败配置在当前树可达、CI 每轮都在跑;单一观测是收尾心跳与 frame.bmp

    1. 可达性(当前树逐行核对)

    环节 位置 结论
    候选开关 CMakeLists.txt:4 option(TERMFORGE_BUILD_CANDIDATE_ADAPTER ... OFF) 打开即得候选核心
    候选编译定义 CMakeLists.txt:54-58 termforge_candidate_shell():加 TERMFORGE_CANDIDATE_CORE=1 并链 Rust 静态库 —
    入口目标 CMakeLists.txt:170 add_executable(termforge_shell ...);:174-175 if(...) termforge_candidate_shell(termforge_shell) 打开 option 后该可执行即候选入口
    后端 experiments/conpty/backend.hpp:112-122 configured():未定义 TERMFORGE_CONPTY_SIDEBYSIDE 时返回 system()(:79-82),creation_flags_ 默认 0(:25) 即 system ConPTY / flags0
    谁会定义 SIDEBYSIDE cmake/ConptyBackend.cmake:19 仅在 termforge_use_bundled_conpty() 内;该函数只被 CMakeLists.txt:432 的 termforge_qt_entry()(:427-447)调用,Qt 目标在 :415 的 if(TERMFORGE_BUILD_QT_SHELL) 内 termforge_shell 永远 system/flags0,无第二种后端
    用例与期限 CMakeLists.txt:348-350(candidate_shell_tui,TIMEOUT 120);experiments/shell/test_candidate_tui.py:95-96 execute(..., "candidate-scroll", ..., timeout=20) 原 20 s 外部期限原样存在
    CI 入口 .github/workflows/core-candidate.yml:78(-DTERMFORGE_BUILD_CANDIDATE_ADAPTER=ON)、:88-90(windows 上 ctest -R candidate_shell_tui) 该配置每轮被构建并执行

    结论:cmake -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=ON + ctest -R candidate_shell_tui 就是 issue 记录的配置,可达且被 CI 覆盖。

    2. 最近 CI 里它确实在跑(但没有复现原超时)

    2026-10-31T08:20Z 两次 core-candidate 失败(d2864711 run 41184212920、cdc00deb run 41184198882)step 17 Build and test candidate shell on Windows 失败:

    • test_real_scroll_region ... ok([R2][回归] 定位 candidate-scroll 末帧生成后的原20秒退出超时 #97 的原失败用例通过),失败的是 test_real_fullscreen_editor_and_cursor_reply:unsupported terminal event kind;
    • 该失败 dump 为 stage=50 bytes=949 input_high_water=1 input_rejected=0 output_bytes=2783 read_error=109(日志原文),说明扩展 dump 已在 CI 生效。

    即:配置可达、用例在跑、原 20 s 退出超时未复现;同时看到同一用例组在 CI 确实会红,不能把"没复现"当成修复。

    3. 原失败 artifact 里的一条新事实:没有 frame.bmp

    原件(issue 正文所指)保留在 D:\Code\termforge-r51-resize\local\r56-conpty-build\ci-fifth-candidate-failure.zip,我直接读了它的目录与样本:

    项 实测
    失败目录 candidate-shell/run-jrd4hlz1/candidate-scroll-0/
    存在 candidate-scroll.json(3673 B)、candidate-scroll.source.bin(107 B)、candidate-scroll.transport.bin(737 B)、before/region/final 的 console+cells
    不存在 frame.bmp(同 zip 内通过样本如 candidate-margins-0/frame.bmp 有 2,396,214 B)
    sample.json(524 B) timed_out=true、exit_code=99、stdout/stderr 空、job_at_timeout active=5/total=7/terminated=0、清理后 active=0、desktop_gone=true

    代码对应:frame.bmp 只在 closing_ && closed_ && drained 分支内写(experiments/shell/shell.cpp:917-923,save 在 :921、DestroyWindow 在 :922)。而 candidate-scroll.json 写在场景末尾(:557),type(L"exit 37\r") 在其后(:562)。

    因此原失败样本确实执行到"末帧证据写完 + exit 37 已排入"之后,且没有进入收尾分支。这直接排除了"窗口已销毁、只是进程/launcher 未返回"这一类(我在前一条评论里列的 H3),把范围压缩为:closing_ 未置位(child 未退出 / wait_exit(0) 一直 false,:926)或 closed_ 未置位(closer 线程卡住)或 drained 一直为假。

    4. 单一后续观测

    原 zip 来自 5c073e4,早于 output/input trace(5dd3a9d/1f0ce20 都不在该 HEAD),所以它没有 output-trace.jsonl;现在同一配置再跑会自动带(experiments/shell/run.py:453 默认设置 TERMFORGE_OUTPUT_TRACE)。

    一次同合同 red 即可三分,只需读该样本 output-trace.jsonl 的最后一条 heartbeat(termforge-output-heartbeat-v1,每 500 ms 一条,字段 closing/complete/pid/child_signaled/exit_code/read_error,shell.cpp:889-909),外加同目录 frame.bmp 是否存在:

    末条 heartbeat 判读
    closing=false 且 child_signaled=false begin_close 未发生:child 未退出,exit 37 未被处理(下一步转到输入侧:现在 input trace 已能证明 exit 37\r 是否写入)
    closing=true 且心跳持续到超时 closer 线程卡住(input_.stop()/session_.close(),shell.cpp:413-421)
    心跳在 closing=true 后停止 UI tick 卡住(heartbeat 只由 tick 线程写)
    出现 frame.bmp 却仍超时 收尾分支已走完,问题在 DestroyWindow 之后(本条已被原 artifact 排除,仅作后续判据)

    不改 20 s 期限、不扩样、不改断言;这一步只是把已有字段读全。

    5. issue 原文句子与可关闭性

    若未复现,保留原失败和未知。

    最终同源candidate/system回归和自然退出/Job/desktop回收通过;单独末帧正确不替代生命周期。

    (正文"有界后续与验收"第 3、4 条)

    当前:配置可达但原超时未复现(本机与 CI 都没有),收尾状态仍未观测,不可关闭;只有当同源 candidate/system 回归 + 自然退出/Job/desktop 回收在同一源码通过,或用上面的心跳取得同类 red 并给出因果 red→green,才谈得上关闭。

    边界

  5. added 2 commits that reference this issue on Oct 31, 2026
  6. JavaZeroo commented on Nov 1, 2026

    @JavaZeroo
    OwnerAuthor

    按「不死磕」原则关闭:candidate-scroll 末帧生成后的 20 秒退出超时

    关闭理由

    用户明确「可以接受不完美、不需要一直死磕一个问题」。本项属于批次内不可复现、诊断已就位的一类,因此按此原则关闭。

    本项已有的证据

    已加入 frame_at_timeout 与标签页生命周期字段:frame.bmp 只在 closing_ && closed_ && drained 三者成立后写出,因此其在期限瞬间的存在性可区分"收尾未完成"与"帧已出但进程未返回"。

    已就位的诊断(再次命中时可立即判读)

    观测 位置
    帧是否写出 sample.json 的 frame_at_timeout
    收尾进度 失败行的 closing/closed/closer_joined
    读取与子进程 output_bytes/consumed/child_signaled/exit_code

    关闭时的真实状态

    本项不是根因修复。原始失败记录仍保留,未被新批次覆盖。

    重开条件

    若同类失败再次出现,凭上述诊断字段可直接判读,恢复本项即可,无需重做本轮探索。

  7. removed
    ready-for-agent范围明确且真实阻塞已解除,可开始实施
    on Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

research有明确问题与出口的证据调查

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions