From a24b230bfcfca3dc6f4da13e76903f91d310f39f Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:11:13 +0800 Subject: [PATCH 1/4] diagnose(conpty): repeat the original lifecycle sample on the CI host refs #145 Adds a bounded repeat runner that keeps one independent owned-job result per iteration and preserves the probe's native failure diagnostics. Dispatched only; it does not change the lifecycle oracle, the 5s wait or any product code. --- .github/workflows/conpty-compat.yml | 39 +++++++++++- experiments/conpty/repeat_run.py | 98 +++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 experiments/conpty/repeat_run.py diff --git a/.github/workflows/conpty-compat.yml b/.github/workflows/conpty-compat.yml index 06ae433a..ff1148bc 100644 --- a/.github/workflows/conpty-compat.yml +++ b/.github/workflows/conpty-compat.yml @@ -74,4 +74,41 @@ jobs: name: conpty-resize-reply-r145 retention-days: 14 if-no-files-found: warn - path: build/r145-* \ No newline at end of file + path: build/r145-* + + windows-2025-original-probe-repeat: + # #145 second slice: repeat the *original* lifecycle sample on the hosted + # image that produced the stale reply. Each iteration keeps its own result; + # a red sample carries the probe's native failure diagnostics. + runs-on: windows-2025 + timeout-minutes: 25 + env: + PYTHONUTF8: '1' + PYTHONPATH: ${{ github.workspace }} + TERMFORGE_TEST_ARTIFACTS: ${{ github.workspace }}/build/test-artifacts + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: '3.12' + - name: Configure + run: cmake -S . -B build + - name: Build lifecycle probe + run: cmake --build build --config Release --target termforge_conpty_probe --parallel 2 + - name: Repeat the recorded lifecycle sample + shell: pwsh + run: | + $binary = Get-ChildItem build -Recurse -Filter termforge_conpty_probe.exe | Select-Object -First 1 + if (-not $binary) { throw "lifecycle probe binary missing" } + python -m experiments.conpty.repeat_run --binary $binary.FullName --iterations 40 --repeats 3 --mode normal --out build/r145-repeat + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Preserve repeat evidence + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: conpty-probe-repeat-r145 + retention-days: 14 + if-no-files-found: warn + path: | + build/r145-repeat + build/test-artifacts/conpty diff --git a/experiments/conpty/repeat_run.py b/experiments/conpty/repeat_run.py new file mode 100644 index 00000000..3d0d00e9 --- /dev/null +++ b/experiments/conpty/repeat_run.py @@ -0,0 +1,98 @@ +"""Bounded repeat of the recorded lifecycle sample (issue #145). + +Every iteration runs one gated probe host inside its own owned job and keeps the +independent result. A failing iteration retains the native failure record +instead of being retried to green. +""" +from __future__ import annotations + +import argparse +import importlib.util +import json +import platform +import statistics +import time +from datetime import datetime, timezone +from pathlib import Path + + +def load_run(): + path = Path(__file__).resolve().with_name("run.py") + spec = importlib.util.spec_from_file_location("termforge_conpty_run", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def summarize(records, iterations, repeats, mode): + failures = [row for row in records if row["status"] != "passed"] + elapsed = [row["elapsed_s"] for row in records] + return { + "iterations": iterations, + "sessions_per_iteration": repeats + 1, + "sessions_total": iterations * (repeats + 1), + "mode": mode, + "failures": len(failures), + "elapsed_s": {"min": min(elapsed), "median": statistics.median(elapsed), "max": max(elapsed)} if elapsed else {}, + "failure_samples": failures[:5], + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--binary", type=Path, required=True) + parser.add_argument("--iterations", type=int, default=40) + parser.add_argument("--repeats", type=int, default=3) + parser.add_argument("--mode", choices=("normal", "large", "cancel", "close"), default="normal") + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + if not 1 <= args.iterations <= 200: + parser.error("iterations must be 1..200") + if not 1 <= args.repeats <= 20: + parser.error("repeats must be 1..20") + if not args.binary.is_file(): + parser.error("probe binary missing") + args.out.mkdir(parents=True, exist_ok=False) + run = load_run() + summary = { + "schema": "termforge-conpty-probe-repeat-v1", + "issue": 145, + "status": "running", + "started_utc": datetime.now(timezone.utc).isoformat(), + "platform": platform.platform(), + "binary": str(args.binary.resolve()), + "performance_claim": False, + } + records = [] + for index in range(1, args.iterations + 1): + begin = time.monotonic() + try: + result = run.execute(args.binary, args.mode, args.repeats, timeout=120) + record = {"iteration": index, "status": result["status"], + "elapsed_s": round(time.monotonic() - begin, 3)} + except BaseException as error: + detail = getattr(error, "conpty_result", {}) + record = {"iteration": index, "status": "failed", + "elapsed_s": round(time.monotonic() - begin, 3), + "error": str(error), + # Keep the native diagnostics that the lifecycle probe + # already collects for a red sample. + "failure": {key: detail.get(key) for key in + ("returncode", "timed_out", "job_after", "startup", + "stdout", "stderr", "error") if key in detail}} + records.append(record) + summary.update(summarize(records, args.iterations, args.repeats, args.mode)) + summary["completed_iterations"] = index + (args.out / "repeat.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + print(f"iteration {index}: {record['status']} ({record['elapsed_s']}s)", flush=True) + summary["status"] = "passed" if not summary["failures"] else "failed" + summary["finished_utc"] = datetime.now(timezone.utc).isoformat() + (args.out / "repeat.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + # A red sample is the evidence we are looking for, not a job failure. + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 69361785172af66cfff8c53711212d236004113c Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:14:49 +0800 Subject: [PATCH 2/4] diagnose(conpty): sample the probe after the regression suite refs #145 The recorded failures happened inside the full ctest job. This job runs the same suite first, records its exit without gating, then samples the original lifecycle probe twenty times to test job-level contamination. --- .github/workflows/conpty-compat.yml | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/.github/workflows/conpty-compat.yml b/.github/workflows/conpty-compat.yml index ff1148bc..386a254a 100644 --- a/.github/workflows/conpty-compat.yml +++ b/.github/workflows/conpty-compat.yml @@ -112,3 +112,46 @@ jobs: path: | build/r145-repeat build/test-artifacts/conpty + + windows-2025-probe-after-suite: + # #145: the recorded failure happened inside build.yml's full ctest job, not + # in an isolated probe job. This job runs the same suite first and only then + # samples the original probe, to test job-level contamination as the trigger. + runs-on: windows-2025 + timeout-minutes: 40 + env: + PYTHONUTF8: '1' + PYTHONPATH: ${{ github.workspace }} + TERMFORGE_TEST_ARTIFACTS: ${{ github.workspace }}/build/test-artifacts + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: '3.12' + - name: Configure and build the default tree + run: | + cmake -S . -B build + cmake --build build --config Release --parallel 2 + - name: Run the regression suite without the conpty test + shell: pwsh + run: | + ctest --test-dir build -C Release -E "^conpty$" --output-on-failure + # Suite failures are recorded, not used to gate the sampling stage. + Write-Output "suite_exit=$LASTEXITCODE" + - name: Sample the original probe after the suite + shell: pwsh + run: | + $binary = Get-ChildItem build -Recurse -Filter termforge_conpty_probe.exe | Select-Object -First 1 + if (-not $binary) { throw "lifecycle probe binary missing" } + python -m experiments.conpty.repeat_run --binary $binary.FullName --iterations 20 --repeats 3 --mode normal --out build/r145-after-suite + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Preserve post-suite evidence + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: conpty-probe-after-suite-r145 + retention-days: 14 + if-no-files-found: warn + path: | + build/r145-after-suite + build/test-artifacts From f1c48d988862d31f728a61ce2a67978886e8c2ad Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:23:40 +0800 Subject: [PATCH 3/4] diagnose(conpty): sample after the console-heavy ConPTY tests refs #145 Replaces the whole-suite variant, which failed on fixtures this dispatch job does not prepare. Runs conpty_ctrl_c and conpty_margin first (exit recorded, not gated), then samples the original probe twenty times. --- .github/workflows/conpty-compat.yml | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/conpty-compat.yml b/.github/workflows/conpty-compat.yml index 386a254a..be4a9795 100644 --- a/.github/workflows/conpty-compat.yml +++ b/.github/workflows/conpty-compat.yml @@ -113,12 +113,12 @@ jobs: build/r145-repeat build/test-artifacts/conpty - windows-2025-probe-after-suite: - # #145: the recorded failure happened inside build.yml's full ctest job, not - # in an isolated probe job. This job runs the same suite first and only then - # samples the original probe, to test job-level contamination as the trigger. + windows-2025-probe-after-conpty-tests: + # #145: the recorded failure happened inside build.yml's long ctest job, not + # in an isolated probe job. This job runs the console-heavy ConPTY tests + # first (their exit is recorded, not gated), then samples the original probe. runs-on: windows-2025 - timeout-minutes: 40 + timeout-minutes: 25 env: PYTHONUTF8: '1' PYTHONPATH: ${{ github.workspace }} @@ -128,30 +128,30 @@ jobs: - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.12' - - name: Configure and build the default tree + - name: Build the console-heavy fixtures run: | cmake -S . -B build - cmake --build build --config Release --parallel 2 - - name: Run the regression suite without the conpty test + cmake --build build --config Release --target termforge_conpty_probe conpty_ctrl_c_probe conpty_margin_probe --parallel 2 + - name: Run the console-heavy tests first shell: pwsh run: | - ctest --test-dir build -C Release -E "^conpty$" --output-on-failure - # Suite failures are recorded, not used to gate the sampling stage. - Write-Output "suite_exit=$LASTEXITCODE" - - name: Sample the original probe after the suite + ctest --test-dir build -C Release -R "^(conpty_ctrl_c|conpty_margin)$" --output-on-failure + Write-Output "predecessor_exit=$LASTEXITCODE" + exit 0 + - name: Sample the original probe after the predecessors shell: pwsh run: | $binary = Get-ChildItem build -Recurse -Filter termforge_conpty_probe.exe | Select-Object -First 1 if (-not $binary) { throw "lifecycle probe binary missing" } - python -m experiments.conpty.repeat_run --binary $binary.FullName --iterations 20 --repeats 3 --mode normal --out build/r145-after-suite + python -m experiments.conpty.repeat_run --binary $binary.FullName --iterations 20 --repeats 3 --mode normal --out build/r145-after-conpty if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - name: Preserve post-suite evidence + - name: Preserve post-predecessor evidence if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: conpty-probe-after-suite-r145 + name: conpty-probe-after-tests-r145 retention-days: 14 if-no-files-found: warn path: | - build/r145-after-suite + build/r145-after-conpty build/test-artifacts From 7bb99403481bcbf1cd0f47c0872a2da6ded7b5d6 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:28:38 +0800 Subject: [PATCH 4/4] diagnose(conpty): repeat the original probe and sample after console tests refs #145 160 isolated sessions and 80 post-test sessions keep one owned-job result each. No stale resize reply reproduced; one post-test red is the session thread baseline exceeding the 1s settle window, not the recorded #145 symptom. --- docs/EVIDENCE.md | 1 + .../2026-10-29-probe-repeat-r145/README.md | 44 ++++++++++++++ .../validation.json | 57 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 experiments/conpty/results/2026-10-29-probe-repeat-r145/README.md create mode 100644 experiments/conpty/results/2026-10-29-probe-repeat-r145/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 26edb7d7..e36b6486 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -70,6 +70,7 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | 行存储封存与结构单一发布(#169 第二片) | [固定结果](../experiments/core/results/2026-10-29-sealed-row-storage/README.md)、[验证身份](../experiments/core/results/2026-10-29-sealed-row-storage/validation.json) | `Screen::lines/history` 私有,只读口 + 23 文件/363 处迁移;Find overlay 改文档 rank;默认 Release 构建 exit 0、核心 8/8、扩展快测 20/20、candidate 源 /W4 编译 0。无新 VT/重排行为;运行中跨重排 Search、Qt 与候选运行由宿主作业复核。 | | 默认核心缩小与重绘交错(#156) | [固定四-case机制诊断](../experiments/shell/results/2026-10-29-resize-repaint-witness/README.md)、[脱敏验证](../experiments/shell/results/2026-10-29-resize-repaint-witness/validation.json) | A/C保留2499B,B/D在第五段重绘首次失效且行分配路径不同;只见证机制,原CI切点/根因未知,未修生产。witness结束后Job仍active1,发起一次终止后归零;成员身份/逐句柄退出未知,不称全树自然退出。 | | 系统 ConPTY resize 后的客户端尺寸回复(#145 未关闭) | [有界重复采样](../experiments/conpty/results/2026-10-29-resize-reply-r145/README.md)、[固定验证身份](../experiments/conpty/results/2026-10-29-resize-reply-r145/validation.json) | 新增 tag 化探针:hosted 2000 次交替 resize + 500 次全新会话、本机 200 + 100,均无陈旧回复,buffer/window 无分歧,resize VT 全部先于回复,回复中位 8.7–16.1ms。只否定“resize 后立即查询存在必然竞态”;原两条 CI 失败、原始探针/5 秒等待与 #145/#38 状态不变。 | +| 原生命周期探针重复与控制台测试后采样(#145 未关闭) | [固定结果](../experiments/conpty/results/2026-10-29-probe-repeat-r145/README.md)、[验证身份](../experiments/conpty/results/2026-10-29-probe-repeat-r145/validation.json) | 隔离重复 40×4=160 会话 0 红;先跑 conpty_ctrl_c/conpty_margin 后采样 20×4=80 会话,1 红但 resize 回复正确(resized=1、120x40、exit 37),原因是会话后线程基线 4→6 超过 1s 结算窗口。旧尺寸回复仍未复现;该线程稳态红归 #38 族,不并入 #145 根因。 | | 持续 cmd 输出与原始 Ctrl-C 回归(#36 未关闭) | [受监督入口](../experiments/conpty/README.md#持续-cmd-输出与-ctrlc-独立回归)、[三次 fresh 公开记录及本地字节复核](../experiments/conpty/results/2026-10-26-ctrl-c-retest/validation.json) | 原百万次输出、单次 ETX、五秒期限和退出 37 保留;三次 fresh 原始字节/Job0 本地重算。后续成功未解释历史失败,未改产品 Ctrl-C 路径。 | | 持续输出中断的原失败记录 | [#36](https://github.com/JavaZeroo/termforge/issues/36)、[ConPTY 输入机制研究](research/conpty-ctrl-c-input.md)、[原 R1 历史证据](../experiments/shell/results/2026-10-23-r1-foundations/validation.json) | 原 Windows 11 回归中的 normal/resize 超时与 ETW/插桩观察;调试扰动及管道读完成缺口保留。不是当前全套结果,后续未复现不能抹去此失败。 | | controller B 正常结束后 cmd 退出未验收(#44) | [原失败脱敏记录](../experiments/shell/results/2026-10-26-controller-exit/validation.json)、[新退出诊断及有界重放](../experiments/shell/results/2026-10-26-controller-exit-observation/validation.json)、[关闭合同](https://github.com/JavaZeroo/termforge/issues/44) | 原 child 收完 32B、退出 0/join/Job0;cmd 只留下 close 后 0xC000013A,close 前未知。新观察分列输入、wait、pre-close 与清理;多轮未复现不构成历史修复,继续阻塞 #38。 | diff --git a/experiments/conpty/results/2026-10-29-probe-repeat-r145/README.md b/experiments/conpty/results/2026-10-29-probe-repeat-r145/README.md new file mode 100644 index 00000000..2b13b6d1 --- /dev/null +++ b/experiments/conpty/results/2026-10-29-probe-repeat-r145/README.md @@ -0,0 +1,44 @@ +# 原生命周期探针重复与“测试后采样”(#145) + +本片回答一个具体问题:**把 [#145](../..#145) 记录的同一条样本原样重复,或在其它 ConPTY 测试之后采样,能否复现 resize 后仍回复旧尺寸?** + +结论:**都没有复现 resize 旧尺寸**;但在“先跑控制台密集测试、再采样”的条件下,复现了另一类红色——探针的会话后**线程基线**在 1 秒结算窗口内未回到基线(4→6),`steady_resources=false`,该样本的 resize 回复本身是正确的(`resized=1`、`TFSIZE:120x40`、child exit 37)。 + +## 方法 + +新增 `experiments/conpty/repeat_run.py`:每次迭代复用既有 `run.execute()`(自有 Job、GO 门、原 5 秒等待、原断言),保留每次独立结果;红色迭代保留探针原始失败字段,不重跑取绿。两个 dispatch-only 作业: + +| 作业 | 前置 | 采样 | +|---|---|---| +| `windows-2025-original-probe-repeat` | 无(隔离) | 40 次 ×(1 startup + 3 normal) | +| `windows-2025-probe-after-conpty-tests` | 先跑 `conpty_ctrl_c`(5.31s)与 `conpty_margin`(0.31s),两者通过;其退出只记录不设门 | 20 次 ×(1 startup + 3 normal) | + +## 结果 + +| 条件 | run/job | 样本 | resize 旧尺寸 | 其它红色 | +|---|---|---|---|---| +| isolated(hosted windows-2025) | [40847785625](https://github.com/JavaZeroo/termforge/actions/runs/40847785625) | 160 | 0 | 0 | +| 控制台密集测试之后 | [40849464556](https://github.com/JavaZeroo/termforge/actions/runs/40849464556) | 80 | 0 | 1(iteration 10, repeat 1) | +| 对照:克隆探针(#171) | 40841303342 / 40841934880 | 2800 | 0 | 0 | + +- 隔离重复:每迭代 1.312 / 1.344 / 1.390 s(min/median/max),40/40 完成。 +- 测试后采样:20/20 完成;唯一红色样本为 `valid=false`,原因 `steady_resources=false`:`threads_before=4`、`threads_after=6`、`resource_settle_ms=1011.02`;同一样本 `resized=1`、`output_bytes=608`、`exit_code=37`、`read_error=109`,最终 VT 以 `TFSIZE:120x40:END` 结束。同迭代的 startup 样本因 `startup=true` 豁免基线比较(`handles 45→51` 属已知首次初始化)。 +- 未捕获任何 `resized=0` 或 80×24 旧回复。 + +## 解释与边界 + +- `#145` 的旧尺寸回复在这三种条件下都没有复现;因此“原样本 × 原宿主 × 隔离或轻度前置”仍不足以触发,根因继续未知。 +- 复现的红是**另一类机构**:会话结束后控制台宿主线程比基线多 2 个且超过 1 秒结算窗口。它属于既有 `steady_resources` 夹具合同的边界(首次初始化只对 startup 豁免),与 resize 回复无关;记录为 #38 族的可靠性观察,不并入 #145 根因。 +- 本片没有改变原探针、其 5 秒等待、断言或任何产品代码;两个作业都是 `workflow_dispatch`,不进入常规 CI 门槛。 +- 分支上曾有“先跑完整 ctest 再采样”的作业,因该 dispatch 作业缺少 build.yml 的夹具准备而在 `benchmark_tools`/`differential`/`workflow_*`/`shell` 失败,未进入采样阶段;该失败保留在 run 40848143873,作为“作业环境不等价”的反例,不用于 #145 推断。 + +## 复现 + +```powershell +cmake -S . -B build +cmake --build build --config Release --target termforge_conpty_probe --parallel 2 +python -m experiments.conpty.repeat_run --binary build/bin/Release/termforge_conpty_probe.exe ` + --iterations 40 --repeats 3 --mode normal --out build/r145-repeat +``` + +作业入口见 [conpty-compat.yml](../../../../.github/workflows/conpty-compat.yml) 的两个 `windows-2025-*` 作业。 diff --git a/experiments/conpty/results/2026-10-29-probe-repeat-r145/validation.json b/experiments/conpty/results/2026-10-29-probe-repeat-r145/validation.json new file mode 100644 index 00000000..294d9997 --- /dev/null +++ b/experiments/conpty/results/2026-10-29-probe-repeat-r145/validation.json @@ -0,0 +1,57 @@ +{ + "schema": "termforge-conpty-probe-repeat-r145-v1", + "issue": 145, + "commit": "f1c48d9", + "runs": { + "isolated": { + "run": 40847785625, + "job": "windows-2025-original-probe-repeat", + "iterations": 40, + "sessions": 160, + "failures": 0, + "elapsed_s": { + "min": 1.312, + "median": 1.344, + "max": 1.39 + } + }, + "after_console_tests": { + "run": 40849464556, + "job": "windows-2025-probe-after-conpty-tests", + "predecessors": [ + "conpty_ctrl_c (5.31s, passed)", + "conpty_margin (0.31s, passed)" + ], + "iterations": 20, + "sessions": 80, + "failures": 1 + }, + "clone_control": { + "runs": [ + 40841303342, + 40841934880 + ], + "samples": 2800, + "stale": 0 + } + }, + "revealed_red": { + "iteration": 10, + "sample": "normal repeat 1", + "valid": false, + "reason": "steady_resources=false", + "threads_before": 4, + "threads_after": 6, + "resource_settle_ms": 1011.02, + "resized": 1, + "output_bytes": 608, + "exit_code": 37, + "read_error": 109, + "note": "resize reply was correct (TFSIZE:120x40); the red is the post-session thread baseline, not a stale dimension" + }, + "preserved_failures": [ + "run 40848143873: whole-suite predecessor variant failed on fixtures the dispatch job does not prepare (benchmark_tools, differential, workflow_sessions, workflow_resources, workflow_resource_phases, shell); sampling stage never ran. Environment not equivalent to build.yml." + ], + "conclusion": "#145 的旧尺寸回复在隔离重复、控制台测试后采样和克隆探针放大三种条件下均未复现;复现的是另一类线程稳态红色。", + "performance_claim": false +}