Skip to content

[R2][调查] 核对 resize 期间 ConPTY 重绘与终端视口/缓冲的对齐 #194

Closed as not planned

Description

@JavaZeroo

背景

#156 关闭时把选区失效归因到"resize 触发的 ConPTY 重绘改写内容"。本项把这条线索单独抽出,核对重绘与终端视口/缓冲是否对齐,因为它可能影响真实产品(不只是测试)。

现有证据

复现作业 run 40916697774(windows-2025-actions-repeat,含 shell 套件):

  • 失败轮 text_actions 的诊断:row-text-changed row=78, saved_bytes=29, current_bytes=29, saved_columns=111, current_columns=111 —— 等长同列,是内容移位而不是裁剪;
  • 该轮 output.bin 里,一轮几何序列(144→192→96→zoom75→restored)出现约 10 次"回 home 重打尾部 6 行(075–080)"的重绘,并带 ESC[7;1H 定位与若干额外 ESC[K CR LF;
  • 进度记录显示缓冲在这期间从 82 行增长到 85 行(top 75→78,始终在底部):
geometry-96   grid [111,7]  top 75  maximum 75  selected ok
geometry-zoom75 grid [149,10] top 75 maximum 75  selected ok
geometry-failure grid [111,7] top 78 maximum 78  selected invalidated

即重绘把 3 个空行当作新输出写入,缓冲区增长、行索引整体位移,选区指向的行内容随之改变。

待回答的问题

  1. ConPTY 的 resize 重绘是否假定"终端缓冲与 conhost 缓冲完全一致、视口在底部"?我们的历史/滚动模型是否与此假设不符?
  2. 重绘里的额外空行(ESC[K CR LF)应该被当作内容吗?真正的 WindTerm/Windows Terminal 会如何处理同一段字节?
  3. 若确属假设不符,产品侧应如何吸收这类重绘(例如识别重绘前缀、或在 resize 后做一次受控重同步)?这属于重要行为,需用户确认。

可用的复现手段

  • dispatch-only 作业 windows-2025-actions-repeat(已扩展到 shell);
  • 每次场景保留完整 output.bin 与 selection-discard.jsonl(含 t_ms/字节/列数);
  • 现场字节流可用 Parser(..., ParseMode::recovery) 逐字节回放。

边界

关联 #38 #156 #123

Activity

  1. JavaZeroo commented on Oct 29, 2026

    @JavaZeroo
    OwnerAuthor

    新证据:移位量可以精确算出来,且与"重绘带尾部空行"吻合

    失败轮(run 40916697774 / run-dtb8lw0m/system-1)的观测:

    量 值
    几何变化前 grid 111×7,top=75(可见 075–081)
    重绘内容 从 home 重打 075–080(6 行)+ 紧随 3 个 ESC[K CR LF
    缓冲行数 82 → 85(+3)
    视口 top 75 → 78(仍在底部)
    丢弃行 index 78 由 TF_FIND_078 变为 TF_FIND_075(正好上移 3)

    算术完全对得上:7 行屏幕 + 6 行内容 + 3 个换行 = 9 行输出,超出 7 行 2 行 → 滚动,且末尾空行又追加,最终净增 3 行;历史前进 3 行,于是 index 78 的内容变成原来 index 75 的 TF_FIND_075。

    由此得到的假设

    重绘很可能是按旧几何(7 行)生成的:ConPTY 在 resize 生效前发出"重打可见区 + 补空行"的序列,我们的屏幕仍按旧尺寸接收,于是这些补空行被当作滚动输出,缓冲区增长、内容整体位移;随后新尺寸生效,位移被固化。

    这解释了为什么:

    • 通过轮次里重绘落点不同(新尺寸先生效)就不位移;
    • 位移量恰好等于补空行数(本轮 3);
    • 选区按行 id 校验时会整段失效(第 78 行不再是原来那行)。

    下一步(仍不改产品)

    1. 在本地用 Parser(7,111) 回放"重绘 + 3 空行",确认能得到同样的 +3 行与 index 78 位移(纯核心验证);
    2. 若核心行为符合 VT 语义(大概率),则问题在时序:resize 与重绘的先后;下一步需要在 shell 侧记录"收到重绘字节时的屏幕尺寸"与"WM_SIZE 处理时刻",确认是不是旧尺寸吃下了补空行;
    3. 据此再决定是否需要在 resize 前后对这类重绘做特殊处理(重要行为,需用户确认)。
  2. JavaZeroo commented on Oct 29, 2026

    @JavaZeroo
    OwnerAuthor

    机制闭合:重绘是按旧几何生成的,被我们按新几何应用

    本地核心实验(Parser(7,111,recovery),81 行后喂"重绘 075–080 + 3 空行")只得到 +3 行、索引 78 不变——说明单纯重放不足以产生位移。把失败帧的视口数据代进去就吻合了:

    量 值
    重绘内容起点 从 home 写 075–080(即 ConPTY 认为首行是 075)
    应用时的实际视口 top=78(7 行屏,首行是 078)
    结果 行 0–5 被写成 075–080,原本 078–083 被覆盖 → 文档索引 78(= 行 0)由 078 变 075

    也就是:ConPTY 的重绘是按 resize 前(10 行、top=75)的几何发出的;我们的屏幕已经切到 resize 后(7 行、top=78)才消费这批字节,于是整体错位 3 行,正好是 10−7。

    这同时解释了:

    • 位移量 = 高度差(本轮 3 = 10−7);
    • 为什么只有部分轮次失败:取决于重绘字节与尺寸切换的先后;
    • 为什么诊断是"等长同列":只是整行内容被换成相邻行,宽度不变。

    结论与建议

    这不是选区逻辑、也不是裁剪策略问题,而是 resize 期间"新尺寸"与"旧尺寸重绘字节"的次序问题。可选方向(都属重要行为,需用户确认):

    1. 延迟应用新网格:resize 后先排空已到达的输出(或等到"重绘批次"消费完)再切换尺寸;
    2. 按尺寸世代标记输出:为 ConPTY 输出附带当前 PTY 尺寸,重绘批次按旧尺寸消费;
    3. 识别并吸收重绘批次:识别 ESC[?25l ESC[H 起的重绘序列,在尺寸切换瞬间丢弃/重放。

    第 3 条风险最高(启发式),第 1 条最保守。需要你决定方向后再动产品代码;本轮仍只做调查。

  3. JavaZeroo commented on Oct 29, 2026

    @JavaZeroo
    OwnerAuthor

    新增样本(PR #195 的 PR 回归,只改诊断代码):run 40918632787、job 123605523470;

    • text_actions 8.77s 失败:test_actual_local_actions_twice_without_clipboard(repeat=1)→ actions geometry lost terminal selection;
    • 注意这是 local 变体(非 system 剪贴板路径),说明该形状不依赖系统剪贴板,是场景共有的几何/重绘问题。

    与本项已有结论一致:resize 期间 ConPTY 尾部重绘按旧几何生成、被按新几何消费,错位量 = 高度差,导致选区行内容等长互换而失效。

    合并 PR #195(输出消费轨迹)后,下一次命中即可用 output-trace.jsonl 直接确认"重绘块消费时 rows 已是新值",把机制从推断升级为确证。

  4. added 5 commits that reference this issue on Oct 29, 2026
  5. JavaZeroo commented on Oct 30, 2026

    @JavaZeroo
    OwnerAuthor

    新样本(system 变体,带完整轨迹)

    run 40979768991、text-actions/.../system-1:actions geometry lost terminal selection。

    阶段时间线

    788921 ready           788953 terminal-empty     788984 empty-copy-rejected
    789078 context-all     789234 context-find
    789281 geometry-144    789359 geometry-192    789421 geometry-96
    789453 geometry-zoom75 789468 geometry-failure
    

    选区丢弃

    {"schema":"termforge-selection-discard-v1","reason":"row-text-changed","row":78,
     "saved_bytes":29,"current_bytes":29,"saved_columns":111,"current_columns":111,"t_ms":789468}

    丢弃发生在 geometry-zoom75 之后 15 ms、geometry-96 之后 47 ms。

    期间的输出块

    终端 1 在这一段收到的重绘批次 columns/rows 全部是 112×7(t=789265/789343/789406…),终端 2 为 74×17;也就是说消费时刻的网格在这几次几何切换中没有随阶段变化。

    与既有结论的一致性

    • 等长换行(29/29 字节、111/111 列)+ 紧随缩放阶段 → 与"重绘按旧几何生成、按新几何消费"的机制一致;
    • 本样本进一步说明:消费时刻网格在几个几何阶段之间保持不变(112×7),而阶段本身在切换(144→192→96→zoom75),说明几何切换与重绘消费之间存在时间差窗口。

    仍未决定

    修法方向仍需确认(先排空 / 尺寸世代 / 识别吸收),本文档 conpty-resize-repaint-timing.md 已列出三个方向的取舍。

  6. added 2 commits that reference this issue on Oct 30, 2026
  7. JavaZeroo commented on Oct 30, 2026

    @JavaZeroo
    OwnerAuthor

    本地 repeat14 的核对结果:错配已可从字节+消费网格证明,缺的只是 resize 时刻

    样本:local/repeat14/text-actions/run-kkdmmpu8/system-1(sample.json: status=failed、exit_code=2、stderr="actions geometry lost terminal selection")。同一 repeat14 的 12 个样本中唯一失败(system 6 个中 1 个;另 3 个 run 的 system-0/system-1 与 local-0/local-1 全部 passed)。

    源身份:identity.json 的 experiments/shell/shell.cpp = 6aebf4088ef0b42342e9ad2368a8938c8b857cb134f7c0207bd06033eb70efce,与提交 5ab207c^ 的 CRLF 检出形式逐字节相同(5ab207c "diag(shell): record geometry switches in the output trace" 提交时间 2026-10-30 09:43 +0800)。即该样本早于 termforge-resize-trace-v1,本来就不会有 resize 记录。

    1. 精确阶段序列(actions-progress.jsonl,t_ms 为 GetTickCount64 开机毫秒)

    phase t_ms 网格(bottom) top/max 选区 备注
    ready → geometry-96 788921–789421 111×7(144/192 DPI 时 112×7) 75/75 ok 104 行内容稳定,Select All 2099B
    geometry-zoom75 789453 149×10 75/75 ok 唯一 10 行几何
    geometry-failure 789468 111×7 78/78 invalidated top/max +3,缩放已回到 100%

    2. 重绘批次与消费网格(output-trace.jsonl 的 columns/rows 是喂入前记录的消费网格,shell.cpp:861-869)

    output.bin 是两终端按写入顺序拼接,16 段字节数合计 9202 = 文件长度,可逐段定位。bottom 终端共 5 批:

    t_ms 消费网格 字节 段形状 SHA-256(段)
    788828 111×7 57 空屏 6 行 + ESC[2;4H 195d02780cf6…
    789265 112×7 210 7 行(6 内容 + 1 仅 EL) 2f9e22d1b637…
    789343 112×7 210 同上 2f9e22d1b637…
    789406 111×7 210 同上 2f9e22d1b637…
    789468 111×7 441 231B(10 行: 6 内容 + 4 仅 EL, 末行以 ESC[7;1H 收束) + 210B(7 行) 89a7c1fca166…

    441B 一批里含两条重绘(2 个 ESC[?25l ESC[H):前段 10 行形状 = 为 149×10 生成,后段 7 行形状 = 为 111×7 生成;两段在同一读里被以 111×7 消费。

    3. 关键差分:同一 1071B 传输,消费网格不同 → 结果不同

    五段 210/210/210/231/210 = 1071B,SHA-256 c1d7cb18d5da399a838c997e0abe9375fd4db037e9f8f609ac02e1a58878cb84,与 docs/research/resize-repaint-witness-plan.md:20 固定的原失败传输完全相同。

    样本 231B 段消费网格 结果
    run-kkdmmpu8/system-1 111×7 failed(选区失效)
    run-kkdmmpu8/system-0 149×10 passed
    run-8wqcm75p/system-1 149×10 passed
    run-lcxpdh8g/system-1 149×10 passed

    这是"按旧几何生成、被新几何消费"的直接证据,不再只是推测:同一字节串只在消费网格为 111×7 时出错。

    4. 丢弃记录与行位移

    selection-discard.jsonl:{"reason":"row-text-changed","row":78,"saved_bytes":29,"current_bytes":29,"saved_columns":111,"current_columns":111,"t_ms":789468} —— 与 progress 的 geometry-failure 同刻、同行号;top/max 75→78 的 +3 恰等于 10 行 − 7 行。

    5. 从这些产物能证到哪一步

    结论 是否可证 依据
    441B 批被以 111×7 消费 ✅ 可证 trace 字段(喂入前记录)+ 9202B 逐段对齐
    其中 231B 段是 10 行几何的重绘 ✅ 可证 行数形状(10 行、末行 ESC[7;1H)+ 通过样本同字节在 149×10 消费
    依此出现 +3 行、行 78 内容改变、选区失效 ✅ 可证(现象级) progress top/max 78/78 + discard row=78
    两次 resize 的精确时刻与先后(149×10 何时应用、何时回退) ❌ 不可证 无 resize 记录;progress 只有 5 个检查点,t_ms 无墙钟
    3 个空行全部来自重绘、而非本项目滚动策略 ❌ 不可证 需回放(witness A/B/C/D)或按字节重放
    该批次在 resize 请求之前生成(方向 1 的前提) ❌ 不可证 需 resize 时刻与消费时刻同钟对齐

    结论:"错配"可由本产物证明,"错配的时序成因"不能。后者正是 termforge-resize-trace-v1 的用途(shell.cpp:228-234 记录 old/new 网格与 t_ms,与 output-trace 同一 GetTickCount64 时钟):本 run 的 output-trace.jsonl 692 行仅有 output/heartbeat/input 三类 schema、0 条 resize 记录,且源身份已证明早于该提交,故不能也不需要在旧样本上要求它。

    6. 三个方向各自还缺什么证据

    方向 能判定它的那一次观测 现有材料 缺什么
    1 切换网格前先消费完已到达输出 resize 记录(old→new+t_ms)与 repaint 消费 t_ms 同钟对齐;若 repaint 消费时刻 > 新网格应用时刻,说明批次是切换后才到,仅"排空已到达"不够 有消费时刻、无应用时刻 需一次带 resize trace 的 repeat 运行,逐条判定 5 批的前后关系
    2 按尺寸世代标记输出 证明"批次形状可唯一反推生成网格":本样本只靠行数可辨(10 vs 7);144/192 DPI 与 96 DPI 都是 7 行(112×7 与 111×7 的 210B 段字节相同),列信息不可辨 只有行维度的形状证据 需要同 rows 不同 columns 的受控样本,或证明 ConPTY 无法提供世代;另需 Session 读取路径状态模型评审(架构级)
    3 识别并吸收重绘批次 正常 TUI 同样以 ESC[?25l ESC[H 重绘的反例语料与误伤率;丢弃后可见内容仍正确的呈现校验 仅本项目 fixture 需要真实程序的原始字节语料 + 呈现校验,否则启发式不能上线

    三者共同的必要前置:用 docs/research/resize-repaint-witness-plan.md:26-37 的四个切点(A/B/C/D)确认该交错是否足以独立产生同形坏状态——目前该 witness 未运行,本评论不作因果裁定。

    边界

    • 只读 local/repeat14(gitignored 本地产物)与仓库文档,未改任何产品/测试代码、未重放 transport、未运行 witness、未 dispatch 任何作业。
    • t_ms 是 GetTickCount64 相对开机毫秒,不能换算墙钟;本评论不给出任何绝对时间。
    • 只有 1 个失败样本(12 中 1),不能据此给失败率;同形错配在本地/CI 之外是否普遍未知。
    • "行 78 内容被换"只按 discard 与 progress 记录陈述,未逐字节核对行身份(需回放)。
    • 不改变 [R2][回归] 定位默认/system的动作几何恢复后选区失效 #156 已关闭的机制结论;方向选择仍需用户确认后才实施。
  8. JavaZeroo commented on Oct 30, 2026

    @JavaZeroo
    OwnerAuthor

    独立复核:441B/111×7 这一段属实

    我按子代理给出的路径重新读了 local/repeat14/text-actions/run-kkdmmpu8/system-1/output-trace.jsonl,逐块确认(terminal 1,共 16 块):

    t=788828  bytes=57   cols=111 rows=7  repaint=true
    t=789265  bytes=210  cols=112 rows=7  repaint=true
    t=789343  bytes=210  cols=112 rows=7  repaint=true
    t=789406  bytes=210  cols=111 rows=7  repaint=true
    t=789468  bytes=441  cols=111 rows=7  repaint=true   <- 含 231B 十行形状 + 210B 七行形状
    t=789500  bytes=16   cols=111 rows=7  repaint=false
    

    同一轮的兄弟样本(local-0/local-1/system-0)里,对应的 231B 十行重绘被消费在 149×10;只有失败样本把它消费在 111×7。

    这使 #194 的机制从"推断"变成可从字节与记录的消费网格直接证明:一段按 10 行生成的尾部重绘,被按 7 行的网格消费。

    仍缺的只是精确的 resize 时刻(该样本早于 termforge-resize-trace-v1,轨迹里 0 条 resize 记录),因此"+3 行的来源"(top 75→78)还需下一次带 resize 记录的命中。

    修法方向仍等确认(先排空 / 尺寸世代 / 识别吸收)。

  9. 6 remaining items

  10. added 2 commits that reference this issue on Oct 31, 2026
  11. JavaZeroo commented on Oct 31, 2026

    @JavaZeroo
    OwnerAuthor

    又一条纯文档 PR 的 CI 命中了同一症状

    PR #273(只改 docs/research/r2-performance-budget.md 与 docs/milestones/R2-local-terminal.md)的 Windows 回归失败:

    ValueError: shell probe failed: actions geometry lost terminal selection
    FAILED (errors=1, skipped=1)
    47 - text_actions (Failed)
    

    要点:

    • 该 PR 不含任何代码改动,因此这是环境/时序侧复现,而不是该 PR 引入的回归;
    • 症状与本项一致:几何变化后选区被判丢(text_actions 用例);
    • 同提交的第二个 Windows 作业通过,说明是概率性时序问题。

    与已有结论的关系

    本项此前的证据是"同一轮四个样本中,失败样本把 231B 十行重绘消费在 111×7,通过样本消费在 149×10"。本条补充的是:在纯文档 PR 上也能复现同一句错误,进一步说明它与业务改动无关。

    边界

    • 未下载该作业的 artifact,未取得这一样本的 selection-discard.jsonl 具体字段;
    • 不据此改变本项结论或关闭。
  12. JavaZeroo commented on Oct 31, 2026

    @JavaZeroo
    OwnerAuthor

    新的命中:ui-actions 的几何丢失,带上了每标签页计数器

    run 41192442670,text_actions 用例 8.17 s:

    workspace-failure mode=ui-actions what=actions geometry lost terminal selection
      tab bottom:0 input_high_water=1 input_rejected=0 output_bytes=4267 read_error=0 consumed=4267 stage=0 closing=false child_signaled=false exit_code=259
      tab main:0   input_high_water=1 input_rejected=0 output_bytes=4971 read_error=0 consumed=4971 stage=0 closing=false child_signaled=false exit_code=259
    

    判读

    读数 含义
    input_rejected=0 输入没有被拒绝
    input_high_water=1(两个会话) 队列几乎空
    output_bytes == consumed(4267/4267、4971/4971) 读到的字节已全部消费,读取侧无堆积
    read_error=0 读取无错误
    child_signaled=false、exit_code=259 子进程仍活着

    这条样本把本项与"读取/输入侧的停滞"彻底分开:几何丢失发生时,两个会话的输出都已全部消费、输入无积压、子进程存活。也就是说,问题出在几何变化与已记录选区之间的判定,而不是数据还没到。

    与既有证据的关系

    此前本项的证据是:失败样本把 231B 十行重绘消费在 111×7,而三个通过样本消费在 149×10(字节形状 + 消费网格)。本样本补充的是同一时刻的队列与读取状态,两者不冲突:重绘确实到了、也确实被消费了,只是消费时的网格与重绘的形状不匹配,于是选区被判失效。

    边界

    • 本样本仍未带 selection-discard.jsonl(该诊断需 TERMFORGE_SELECTION_DIAG 才写出,本次作业未设置);
    • stage=0 属 Workspace 场景已知局限;
    • 不据此关闭本项:修法方向仍需你确认(先排空 / 尺寸世代 / 识别吸收)。
  13. JavaZeroo commented on Oct 31, 2026

    @JavaZeroo
    OwnerAuthor

    找到了丢弃记录:全部指向「行文本变短」,不是尺寸变化

    此前我一直以为失败样本里没有 selection-discard.jsonl。实际是有的——run.py:470 默认就把它写到每个样本目录(setdefault("TERMFORGE_SELECTION_DIAG", …)),只是我只 grep 了 stdout/stderr。

    下载 run 41192442670 的 artifact 后统计(local/a194/actions-repeat-evidence):

    套件 记录数 原因分布
    text-actions 12 row-text-changed 12
    shell 70 anchor-missing-or-shrunk 40、row-text-changed 30
    selection-context 20 row-text-changed 20

    记录字段:reason、row、saved_bytes、current_bytes、saved_columns、current_columns、t_ms。

    典型记录:

    {"schema":"termforge-selection-discard-v1","reason":"row-text-changed",
     "row":0,"saved_bytes":23,"current_bytes":17,"saved_columns":54,"current_columns":54,"t_ms":586093}

    判读

    • saved_columns == current_columns(54/54)→ 列数没变,不是 resize 造成的;
    • saved_bytes 23 → current_bytes 17 → 选中的那一行内容变短了 6 字节;
    • 原因码 row-text-changed 说明判定依据是"行文本与保存时不同";
    • 另有 40 条 anchor-missing-or-shrunk(锚点行消失或变短),同属"行内容在重绘过程中短暂不完整"这一类。

    结合此前样本(输出全部消费、输入无积压、子进程存活),这组数据把成因收窄为:几何变化触发重绘时,选中的行在某一时刻只被写入了部分内容(23→17 字节),判定逻辑据此认为选中文本已失效。

    对三个候选方向的意义

    方向 这组数据支持度
    先排空(等重绘完成再判定) 直接支持:如果等到整行写完(17→23 字节)再比对,就不会误判
    尺寸世代(按世代区分) 不直接相关:列数没变(54/54),世代变化未必能区分这种"同一尺寸下的部分重绘"
    识别吸收(把已知重绘吸收掉) 也支持:需要识别出这是重绘中间态

    边界

    • 统计来自单个 artifact(run 41192442670),不是全部历史样本;
    • 这些记录来自 text-actions/shell/selection-context 套件;失败的那个 ui-actions 样本目录本身没有记录,说明它的丢弃走的是另一条路径(场景自身的断言),需进一步核对;
    • 仍需要你确认采用哪个方向,我不自行改判定。
  14. JavaZeroo commented on Oct 31, 2026

    @JavaZeroo
    OwnerAuthor

    按样本分组的精确结果(更正我上一条的字节方向)

    上一条我引用的是 selection-context 目录里的 23→17(变短)。把 shell 套件的 70 条记录按样本分组后,实际分布是:

    样本目录 记录数 原因 (saved_bytes, current_bytes)
    copy-system-0 10 anchor 5 + row-text 5 见下
    copy-system-1 10 anchor 5 + row-text 5 见下
    find-0 / find-1 各 5 anchor 5 (0, 0)
    selection-0 / selection-1 各 10 anchor 5 + row-text 5 见下

    全量字节差值分布:

    (saved_bytes, current_bytes) 记录数 对应原因
    (0, 0) 40 anchor-missing-or-shrunk
    (11, 29) 30 row-text-changed

    因此正确的描述是

    • row-text-changed(30 条):保存时该行 11 字节,判定时 29 字节 —— 行变长了,即重绘把更多内容写了进去;
    • anchor-missing-or-shrunk(40 条):行内容与列数都是 0 —— 锚点行在那一刻不存在(被清空或在另一屏);
    • 列数:row-text-changed 组是 54 → 54(未变),anchor 组是 0 → 0。

    这对三个方向的意义(在更正后重新评估)

    方向 支持度 理由
    先排空 支持 两种原因都表现为"判定发生在重绘中间态":要么整行还没写完(11→29),要么锚点行还在切换中(0)
    尺寸世代 不支持 row-text-changed 组列数完全没变(54→54),世代变化识别不了这种部分重绘
    识别吸收 支持 需要认出"这一行仍在被写入"这一中间态

    边界

    • 统计来自单个 artifact 的 shell 套件(run 41192442670);
    • selection-context 套件另有 20 条 row-text-changed,字节方向与 shell 组不同(23→17),说明变长与变短都会触发,即判定只比较"内容相等",不区分方向;
    • 仍等你确认方向,我不自行改判定。
  15. JavaZeroo commented on Oct 31, 2026

    @JavaZeroo
    OwnerAuthor

    已合入 main 的新诊断(可用于判定修法方向)

    观测 位置 能回答什么
    丢弃记录带 saved_text / current_text(各 48 字节,JSON 转义) selection-discard.jsonl(#288) 部分重绘(两者前缀相同)还是内容替换(不同)
    失败信息自动附判读 rejected/drained/child run.py(#287) 该次失败是否属于“读完即消费完 + 子进程存活”这一形态
    场景自己的 stage workspace.hpp(#284) 卡在哪个阶段
    closed / closer_joined / frame_at_timeout shell.cpp、run.py(#285/#283) 收尾走到哪一步

    对三个方向的意义

    决策的关键是:几何变化导致的行文本变化,是“写了一半”还是“真的换了内容”。

    若下次记录显示 则说明 支持的方向
    current_text 是 saved_text 的前缀(如 ONETWO → ONETWO+新增) 重绘中间态 先排空
    两者完全不同(如 ONETWO → XXXXXX) 内容被替换 识别吸收
    (0,0) 且 anchor 原因 行在切换(备用屏/清屏) 先排空 + 世代

    边界

    • 这只是把判定所需的数据补齐,不代表已经选定方向;
    • 方向仍需要你确认,因为它属于“重要行为差异”。
  16. JavaZeroo commented on Oct 31, 2026

    @JavaZeroo
    OwnerAuthor

    新字段已生效,本机跑出两条真实记录

    本机执行 run.execute(termforge_shell, "ui-selection")(通过的场景),selection-discard.jsonl 现在带上了文本:

    {"reason":"anchor-missing-or-shrunk","row":0,"saved_bytes":0,"current_bytes":0,"saved_columns":0,"current_columns":0,"saved_text":"","current_text":""}
    {"reason":"row-text-changed","row":0,"saved_bytes":11,"current_bytes":29,"saved_columns":74,"current_columns":74,
     "saved_text":"OTHER  text","current_text":"TF_FIND_002 needle 中文 e\u0301"}

    判读

    观察 含义
    saved_text="OTHER text" → current_text="TF_FIND_002 needle 中文 e◌́" 完全不同的内容,不是前缀,也不是半行
    saved_columns == current_columns(74/74) 列数没变
    场景本身通过 这条丢弃是预期行为(行内容确实被新内容替换)

    对修法方向的影响(重要)

    此前我推断"三种原因都指向判定发生在重绘中间态",因此倾向"先排空"。这条实测数据说明:至少在本机这条路径上,row-text-changed 是内容真的被替换,先排空不会、也不应该阻止它。

    所以现在有三种可能:

    可能 对应的正确做法
    CI 失败样本与本机一样是"内容替换" 丢弃是对的;问题在场景/断言把一次正常的替换当成失败 → 修用例而非修判定
    CI 失败样本是前缀(写了一半) 先排空/识别吸收
    anchor 类(0/0)占主导 处理"行在切换中"(世代或排空)

    结论

    我需要一次带新字段的 CI 失败样本才能给出确定方向;本机数据已经推翻了"一定是中间态"的假设。

  17. JavaZeroo commented on Oct 31, 2026

    @JavaZeroo
    OwnerAuthor

    用 #288 的文本字段做了统计:40 条 row-text-changed 里 0 条是部分重绘

    数据来源:run 41200844601 的 actions-repeat-evidence(80 条带 saved_text/current_text 的记录)。

    原因 记录数
    row-text-changed 40
    anchor-missing-or-shrunk 40

    其中 row-text-changed 的判定:current_text 以 saved_text 开头的记录数 = 0。

    典型记录:

    {"reason":"row-text-changed","row":0,"saved_bytes":23,"current_bytes":17,
     "saved_columns":54,"current_columns":54,
     "saved_text":"TF_STREAM_TICK_00000001","current_text":"TF_STREAM_STOPPED"}

    即:行内容被真的替换了(TICK_00000001 → STOPPED),不是"写了一半"。这类丢弃是正确行为。

    对本项的影响

    我之前倾向的"先排空"方向,在这批数据里不会、也不应该阻止这些丢弃——因为没有任何一条是中间态。

    因此需要重新收敛问题:

    观察 结论
    actions 场景在 DPI 变化后要求 Find 匹配与选区存活 这是场景的验收要求
    若底层行文本确实变了 丢弃是对的,问题在"几何变化是否应该改变行文本"
    若底层行文本没变 那丢弃就是误判,需要看那时的 saved_text/current_text

    要区分这两者,需要一次 ui-actions 几何阶段的失败样本(本批 80 条都来自 stream/selection 场景,不是几何阶段)。

    边界

    • 统计来自单一 artifact;
    • 不含 ui-actions 几何阶段(stage 80-84)的记录,因此不能直接外推到本项的失败点。
  18. JavaZeroo commented on Oct 31, 2026

    @JavaZeroo
    OwnerAuthor

    本机复现 #194 的可行性:不可行

    我按上一轮的计划试了三条路,结论都是否定的。

    1. 本机能跑 ui-actions,但跑不到几何阶段

    test_actions.ActionWindowTest.test_actual_local_actions_twice_without_clipboard 不要求 hosted,本机可执行:

    python -m unittest experiments.shell.test_actions  →  Ran 10 tests, OK (skipped=1)
    

    但把它单独跑 10 次:

    runs 10 fails 0
    

    一次完整执行里,ui-actions 产出的 snapshots 是 0 条、geometry-* 阶段 0 个(本机无受控 DPI 切换能力,那几段直接不产生记录)。

    环境 结果
    本机(96 DPI,无受控 DPI 消息) 0 个几何阶段快照,10/10 通过
    CI hosted(144/192/96 真实切换) 有几何阶段,偶发 actions geometry lost terminal selection

    2. 因此本机既拿不到样本,也无法做修前/修后对照

    要验证任何针对本项的改动,都需要在有真实 DPI 切换能力的环境里跑,而我这里不具备。

    3. 也不该继续盲目加采样轮次

    上一轮 3 次定向采样全绿(序列 18 绿 / 1 红),命中率约 1/9。继续派发只是把成本线性堆高。

    结论

    本项需要「一次 ui-actions 几何阶段失败样本」才能定论;该样本只能在 hosted 或真实异 DPI 环境产生。这不是我能在本机补齐的数据。

    可行路径 需要什么
    继续 CI 采样 时间/配额,命中率约 1/9
    真实异 DPI 宿主 环境,属 #31 的同类前置
    放弃本项在本轮闭合 保持开放,标注"等环境/样本"

    边界

    • 本机跑出的 1 条丢弃记录不含几何阶段,不能用于本项判定;
    • 结论只针对“能否本机复现”,不代表已排除任何修法。
  19. JavaZeroo commented on Nov 1, 2026

    @JavaZeroo
    OwnerAuthor

    按用户决定:本项关闭(不可复现,不再追)

    决定

    用户明确:「无法复现 就不管了。可以先 close」。因此本项按“不可复现”关闭,不是根因修复。

    本会话试过的复现路径

    尝试 结果
    本机跑 ui-actions 10/10 通过;产出 0 个几何阶段快照(本机无受控 DPI 切换),无法复现
    定向 CI 采样(only=windows-2025-actions-repeat) 21 次运行仅 1 次命中(约 1/9)
    改 oracle 让 CUP 成为记录边界 被既有合同测试否决(CUP cannot discard an echo prefix…),已回退

    已落地的诊断(若再次命中可据此定位)

    观测 位置
    场景名 + 场景自己的 stage workspace.hpp 失败行
    每标签页 input_high_water/input_rejected/output_bytes/consumed/closed/closer_joined shell.cpp 的 diagnostic_line()
    丢弃记录带 saved_text/current_text selection-discard.jsonl
    失败信息自动附判读(rejected/drained/child) run.py

    关闭时的已知事实(供以后参考)

    • 40 条带文本的 row-text-changed 中,没有一条是部分重绘(current_text 不以 saved_text 开头);
    • 40 条 anchor-missing-or-shrunk 的字节与列数均为 0(行在切换中);
    • 失败断言在 workspace_actions_scenario.hpp:202-204,要求 DPI/zoom 变化后 Find 匹配与选区存活。

    重开条件

    若再次命中该形态,凭上述诊断字段可直接判读“行文本是否真的变了”,无需重做本轮的探索。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions