From 5dd3a9d5cbdf4334f1d4d6c53b100bbb4f66ac15 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Fri, 30 Oct 2026 01:09:38 +0800 Subject: [PATCH] diagnose(shell): trace the grid size at output consumption refs #194 TERMFORGE_OUTPUT_TRACE records terminal id, byte offset, chunk size and current grid per consumed chunk, so a resize repaint can be shown to have been generated for the previous geometry while the grid already had the new one. --- experiments/shell/README.md | 2 +- experiments/shell/run.py | 1 + experiments/shell/shell.cpp | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/experiments/shell/README.md b/experiments/shell/README.md index 248b059a..abdb7095 100644 --- a/experiments/shell/README.md +++ b/experiments/shell/README.md @@ -61,7 +61,7 @@ R1 的 [#12–#17 限定合同](../../docs/milestones/R1-local-text.md)和 [#18 ## 选区丢弃诊断 -几何/DPI/缩放变化后若选区被 `TextSelection::reconcile` 丢弃,可用环境变量 `TERMFORGE_SELECTION_DIAG=` 打开一次性诊断:每次丢弃写入一行 `{"schema":"termforge-selection-discard-v1","reason":"...","row":N}`(`row` 是发生不一致的选区行序号,`0` 表示与具体行无关的分支),`reason` 取 `epoch-mismatch`、`anchor-missing-or-shrunk`、`row-identity-replaced`、`row-text-changed`、`endpoint-unavailable` 之一。未设置该变量时不写任何文件、行为不变;另有 `TERMFORGE_CAPTURE_OUTPUT=<路径>` 会把该窗口消费的**全部输出字节**追加写入文件(同样 opt-in、默认不写),供失败时逐字节回放;`experiments/shell/run.py` 的 `execute()` 会自动把该变量指向本次场景产物目录下的 `selection-discard.jsonl`,因此失败时该文件随 `build/test-artifacts/` 一起保留。`selection_core` 里新增的分组只验证观察器能收到分支名。 +几何/DPI/缩放变化后若选区被 `TextSelection::reconcile` 丢弃,可用环境变量 `TERMFORGE_SELECTION_DIAG=` 打开一次性诊断:每次丢弃写入一行 `{"schema":"termforge-selection-discard-v1","reason":"...","row":N}`(`row` 是发生不一致的选区行序号,`0` 表示与具体行无关的分支),`reason` 取 `epoch-mismatch`、`anchor-missing-or-shrunk`、`row-identity-replaced`、`row-text-changed`、`endpoint-unavailable` 之一。未设置该变量时不写任何文件、行为不变;另有 `TERMFORGE_CAPTURE_OUTPUT=<路径>` 会把该窗口消费的**全部输出字节**追加写入文件(同样 opt-in、默认不写),供失败时逐字节回放;`TERMFORGE_OUTPUT_TRACE=<路径>` 则按块记录 `terminal/t_ms/offset/bytes/columns/rows`,用于判断"某批重绘字节被消费时网格已经是新尺寸还是旧尺寸"。`experiments/shell/run.py` 的 `execute()` 会自动把该变量指向本次场景产物目录下的 `selection-discard.jsonl`,因此失败时该文件随 `build/test-artifacts/` 一起保留。`selection_core` 里新增的分组只验证观察器能收到分支名。 ## 所有权与调度 diff --git a/experiments/shell/run.py b/experiments/shell/run.py index e1c2efb7..82d7bc4e 100644 --- a/experiments/shell/run.py +++ b/experiments/shell/run.py @@ -433,6 +433,7 @@ def execute(exe, mode, out, timeout=40, settings_source=None, *, resources=False environment = dict(os.environ) environment.setdefault("TERMFORGE_SELECTION_DIAG", str((out / "selection-discard.jsonl").resolve())) environment.setdefault("TERMFORGE_CAPTURE_OUTPUT", str((out / "output.bin").resolve())) + environment.setdefault("TERMFORGE_OUTPUT_TRACE", str((out / "output-trace.jsonl").resolve())) process = subprocess.Popen([str(exe.with_name("shell_test_launcher.exe").resolve()), desktop.full_name, mode, str((out / "frame.bmp").resolve())], cwd=out, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/experiments/shell/shell.cpp b/experiments/shell/shell.cpp index e455bbee..fbc3e739 100644 --- a/experiments/shell/shell.cpp +++ b/experiments/shell/shell.cpp @@ -106,6 +106,8 @@ class ShellWindow { inline static uint64_t next_identity_ = 0; const uint64_t identity_ = ++next_identity_; std::ofstream capture_output_; + std::ofstream trace_output_; + uint64_t trace_offset_ = 0; std::unique_ptr clipboard_read_; std::optional pending_paste_; std::unique_ptr paste_prompt_; @@ -818,6 +820,19 @@ class ShellWindow { capture_output_.write(bytes.data(), static_cast(bytes.size())); capture_output_.flush(); } + // Opt-in trace of what the grid looked like when each chunk was + // consumed: TERMFORGE_OUTPUT_TRACE=. This is what tells a + // resize-triggered repaint apart from ordinary output in a failure + // artifact; nothing is written unless the variable is set. + if (trace_output_) { + trace_output_ << "{\"schema\":\"termforge-output-trace-v1\",\"terminal\":" << identity_ + << ",\"t_ms\":" << GetTickCount64() + << ",\"offset\":" << trace_offset_ << ",\"bytes\":" << bytes.size() + << ",\"columns\":" << parser_.screen.width() << ",\"rows\":" << parser_.screen.height() + << "}\n"; + trace_output_.flush(); + trace_offset_ += bytes.size(); + } parser_.feed(bytes); batch += bytes.size(); #ifdef TERMFORGE_CANDIDATE_CORE route_terminal_events(); @@ -1004,6 +1019,8 @@ class ShellWindow { // Opt-in full output capture for replayable failure evidence. const std::string capture = env_path("TERMFORGE_CAPTURE_OUTPUT"); if (!capture.empty()) capture_output_.open(capture, std::ios::binary | std::ios::app); + const std::string trace = env_path("TERMFORGE_OUTPUT_TRACE"); + if (!trace.empty()) trace_output_.open(trace, std::ios::app); snapshot_ = std::make_shared(viewport_.snapshot(parser_.screen), 0); } ~ShellWindow() {