Skip to content

fix(traex): 兼容新版活动状态避免假空闲 - #1075

Open
xavier-cai wants to merge 1 commit into
deepcoldy:masterfrom
xavier-cai:fix/traex-02016-false-idle
Open

fix(traex): 兼容新版活动状态避免假空闲#1075
xavier-cai wants to merge 1 commit into
deepcoldy:masterfrom
xavier-cai:fix/traex-02016-false-idle

Conversation

@xavier-cai

@xavier-cai xavier-cai commented Aug 29, 2026

Copy link
Copy Markdown

问题

TraeX 0.201.6 的真实 TUI 在 turn 仍运行时会保留 composer/context bar,同时渲染带活动 glyph、耗时及 esc to interrupt 的 activity footer;hook-runner 也使用同一 footer 形态,例如:

  • ⋄ Running 4 PreToolUse hooks (7s • esc to interrupt)
  • ◆ Running 2 Stop hooks (21s • ↓ 73 tokens • esc to interrupt)
  • ◈ Working… (2s • esc to interrupt)

现有 TraeX readyPattern 会命中仍可见的 composer 或 Context N% left,而原有 busyPattern 只覆盖旧 spinner 文案和 capacity queue,未覆盖上述 activity footer。PTY 短暂静默后,Botmux 可能提前把仍在执行的 TraeX session 标记为 idle,并触发 DONE reaction。

根因

这是既有 TUI 形态未被 adapter 完整覆盖,而不是已确认的 0.201.6 新增版本边界。旧 busyPattern 无法抵消仍在屏幕上的 readyPattern 证据,导致活跃 turn 被误判为 idle。

这与 #928 修复的 capacity queue 假 Idle 属于同一类 UI 形态漂移:readyPattern 仍可见,但对应的 busy 证据未被识别。

改动

  • 扩展 TraeX 专属 busyPattern / idleToBusyPattern,新增统一 activity-footer grammar:
    • 行首为真机观察到的 7 个轮换 glyph:⋄✧◇✦❖◈◆
    • activity 描述后必须存在同一对括号内的 elapsed time 与 esc to interrupt
    • hook-runner 与普通 activity row 走同一规则,不枚举 hook 事件名。
  • 保留 master 原有的 spinner 文案和 capacity queue 分支,旧版本已有识别能力不变。
  • 使用无括号描述段和右括号后的引号阻断,排除 ◆ Ran ...、echo、grep、diff 等历史行误唤醒;不使用严格行尾锚,兼容真实 PTY 重绘把分隔线/后续内容拼到 footer 后的情况。
  • 保留现有 readyPattern,避免重新引入首次 prompt 注入延迟/丢失问题。
  • 不修改公共 IdleDetector、structured lifecycle gate 或其他 CLI adapter。

关于初版 hook 专用 arm

初版 PR 曾增加只匹配固定 PreToolUse/PostToolUse、且允许无 footer 的候选 arm;它并非 master 中的既有兼容逻辑。

随后通过 raw PTY 和每 100ms screen capture 复测,实际捕获到的 PreToolUsePostToolUseUserPromptSubmitStopNotification hook-runner 行均带 elapsed / esc to interrupt footer,glyph 也会轮换;本次采样未观察到 footer-less hook frame。因此当前实现用统一 footer grammar 替代该候选 arm:覆盖事件和 glyph 更完整,也避免无 footer 的宽松历史行触发 idleToBusyPattern

这不构成对低版本的回退:master 从未包含该 arm,且 master 原有 spinner/queue 规则均完整保留。如果未来获得某版本 footer-less hook frame 的真实捕获,可再基于该证据增加有边界的兼容分支。

取舍

◈ note: the footer reads (36m 20s • esc to interrupt) now 这类极少见、恰好以活动 glyph 开头并包含完整 footer 形态的正文仍可能判 busy。当前选择 recall-first:更严格的行尾/尾随文本约束会漏掉已经观察到的真实 PTY footer 重绘,重新引入本 PR 要修复的提前 DONE。对应行为已用测试显式记录。

影响面

  • 仅影响 TraeX CLI 的 screen busy/idle 判断。
  • PTY/tmux 的首次 idle 防护和 idle→busy 自愈复用现有路径。
  • 其他 CLI、消息投递和 lifecycle-blocking 行为不变。
  • 未引入按 CLI 版本分支;当前 adapter/runtime 不按版本选择 screen pattern,兼容形态并集比未经证实的 semver 边界更稳妥。

真机验证

在独立临时 Git 目录中配置两个各延迟 4 秒、无业务副作用的同步 hook,通过独立 tmux 启动 TraeX 0.201.6,并在 turn 前挂 pipe-pane 抓 raw PTY,同时每 100ms 采集 screen。

实测确认:

  • 7 个活动 glyph 全部出现:⋄✧◇✦❖◈◆
  • hook-runner 与普通活动态共用 elapsed / esc to interrupt footer;
  • 捕获到 PreToolUsePostToolUseUserPromptSubmitStopNotification hook 行;
  • hook-runner glyph 同样轮换;
  • 本次 raw PTY 与 100ms screen 采样未观察到 footer-less hook 行。

实验后已关闭独立 tmux、删除临时目录/raw PTY/screen/hook 日志,并移除 TraeX 自动写入的临时项目 trust 记录。未修改全局 hook,未重启 Botmux;无新增软件安装。

测试

  • 正向:7 帧普通 footer,以及 PreToolUse / PostToolUse / UserPromptSubmit / Stop / Notification 真机 hook 形态;
  • 负向:普通 prose、◆ Ran、单双引号/backtick echo、grep 前置括号、diff 多括号;
  • bun x vitest run --project unit test/cli-adapters.test.ts test/idle-detector.test.ts:494 passed;
  • bun run build:通过;
  • git diff --check:通过;
  • 完整 unit 的剩余失败已在干净 master 对照复现或隔离通过,未发现与本改动相关的回归。

验证使用仓库声明的 Bun 1.4.0;宿主默认 Bun 1.3.11 无法解析当前 lockfile。

@xavier-cai
xavier-cai requested a review from deepcoldy as a code owner August 29, 2026 03:21
@deepcoldy

Copy link
Copy Markdown
Owner

自动评审初步意见,仅供参考,以维护者审阅为准。

感谢这个 PR——方向我认为是对的:readyPattern 仍可见而繁忙证据未被识别,确实和 #928 同类。跑完验证后有 1 个建议合前处理的问题,另外 2 条是准确性建议。

🔴 1. arm-2 会让已 idle 的会话被命令历史误唤醒(PRE/POST 已对照)

PR 里说 arm-2 的结构约束能让 ◆ Ran ... 命令历史无法唤醒已结束的会话。但约束只要求「行首图元 + 括号耗时 + esc to interrupt」,三者不必属于同一语义单元,所以带耗时括号的命令历史会命中:

◆ Ran echo "(36m 20s • ↑ 68.7K tokens • esc to interrupt)"   → 命中
◆ Ran grep (2m 3s) for "esc to interrupt" in logs            → 命中
◇ diff (12s): -◈ old (1m 1s • esc to interrupt)              → 命中
◆ Ran rg for esc to interrupt   ← PR 自带的负向用例(无括号)→ 不命中

因为同一个 pattern 也接到 idleToBusyPattern,走完整 IdleDetector 能复现「真 idle 被唤醒」:

d.fireIdle();
d.feed('\n◆ Ran echo "(36m 20s • ↑ 68.7K tokens • esc to interrupt)"\n');
// PR 分支: busyCallbacks=1   ← 已 idle 被唤醒
// master : busyCallbacks=0

PRE/POST 只换 src/adapters/cli/traex.ts 一个文件,其余不变 ⟹ 该行为由本 PR 引入。后果方向和原 bug 相反(卡住不发 DONE),建议要求耗时括号与 esc to interrupt 处于同一括号内(footer 的真实形态),例如把尾段收紧为 \([^()\r\n]{0,160}\besc[ \t]+to[ \t]+interrupt[^()\r\n]*\),并补一条上面 ◆ Ran echo "(...)" 的负向断言。

🟠 2. arm-1 只认 ,但图元是 6 帧轮换 → 可能只覆盖 1/6

0.201.6 二进制里图元是连续的一段轮换集(偏移 30440653,Background 后紧跟):

⋄✧◇✦◈❖

arm-1 硬编码 。把它放宽成整组 [⋄✧◇✦◈❖]测试全绿(494 passed),说明「只认 ✦」这个选择没有任何断言固定 ⟹ 若渲染时真的轮换,这条 arm 只在 1/6 的帧上生效,假空闲仍会间歇复现。另外 Running {} {} hooks 模板对 hook 事件名是通用的,而本机就配了 5 类(pre_tool_use/post_tool_use/post_tool_use_failure/stop/user_prompt_submit),二进制枚举还含 SubagentStart/PreCompact/SessionEnd 等;现在只认 (?:Pre|Post)ToolUse。若能确认 footer 的实际帧与事件覆盖,建议一并放宽(或注释说明为何只取这两者)。

🟡 3. 「0.201.6 才引入」的表述与二进制不符

我把 0.201.5 与 0.201.6 逐项对比,每一项计数都相同

0.201.5 0.201.6
" to interrupt" 14 14
keymap/chords.rs 1 1
⋄✧◇✦◈❖ 1 1
PreToolUse 74 73

所以「0.201.6 reintroduced / added」更像是这两种形态一直存在、只是此前未被 adapter 覆盖。建议改成「补齐既有未覆盖形态」,以免下次 review 依赖一个不准的版本边界。

补充一点可能对写注释有用:整句 esc to interrupt / Running N PreToolUse hooks 在二进制里 grep -F 都是 0 命中,因为 Rust 在运行时 format! 拼接,只以碎片 + 模板 + 来源路径存在(keymap/chords.rs、符号 status_indicator_widget::fmt_elapsed_compact)。这也意味着 master 注释里那句「grep 返回 0,所以 TraeX 删掉了 esc to interrupt」本身就是坏探针得出的结论——你推翻它是对的,只是理由可以更硬。顺带提醒:strings 会切掉多字节字符,验图元要直接 grep 原始二进制。

✅ 已验证没问题的部分

  • 有牙:删掉两条 arm → 3 red;把 arm-2 削成裸 esc to interrupt → 2 red(负向断言真的在防误触发)。
  • 修法在目标形态上确实生效:带 composer 的整屏重绘、2 空格缩进、行内 ANSI 颜色、CJK 描述超 240 字符,全部判 busy。
  • 无 ReDoS:120K 字符对抗输入 < 0.2ms(有界量词有效)。
  • 无残留唤醒:真 idle 后 tail 里的旧 footer 不会误唤醒(feed 会清 tail)。
  • 回归面:只动 TraeX 专属常量,未触碰共用 IdleDetector 或其它 adapter。
  • 全量 unit:8 failed / 18927 passed,失败集中在 mojo-launcher-env-quarantine / plugin-mcp-sandbox / setup-open-platform-automation;把 traex.ts 换回 master 后同样 3 个文件同样失败 ⟹ 与本 PR 无关(宿主 env 泄漏类既有问题)。

@deepcoldy

Copy link
Copy Markdown
Owner

[自动评审·复审补充](机器评审初步意见,以维护者审阅为准)

首审指出 ◆ Ran echo "(36m 20s • …esc to interrupt)" 会误命中,方向正确;但对修复方案有一点重要补充:只要求「耗时与 esc to interrupt 在同一对括号内」不足以修掉这个用例——该 echo 行引号里的括号内容恰好就是完整的真实 footer 形态((36m 20s • ↑ 68.7K tokens • esc to interrupt)),同括号约束下仍命中。

本地复核(0.201.6 二进制):

  1. 活动图元是长度前缀 6 的连续字符串 ⋄✧◇✦❖◈(轮换集);(实心)不在其中,且在代码里以立即数比较形式出现,是历史命令行的标记(PR 自己的负向用例 ◆ Ran rg … 也印证了这一点)。
  2. 因此真正起作用的修复是:把 arm-2 的图元类从 [✦✧◆◇◈] 收窄为 [⋄✧◇✦❖◈](去掉 。历史行都以 开头,去掉后两条误命中用例(echo / grep)自然全部消失;同时建议补上「同括号」约束作为纵深防御。
  3. 现有正向断言 ◆ Evaluating task configurations (34m 19s …) 是按实现反推的伪造形态——真实渲染应使用轮换集图元(如 )。把它改回真实图元 + 新增上述两条历史行负向断言后,vitest run --project unit test/cli-adapters.test.ts test/idle-detector.test.ts 494 全绿。

另确认:hook 事件枚举实际包含 SessionStart / PreToolUse / PermissionRequest / PostToolUse / PostToolUseFailure / Notification / SubagentStart / SubagentStop / PreCompact / PostCompact …,本 PR 只认 (?:Pre|Post)ToolUse,若 hook 行同样带轮换图元,建议作者顺带确认 arm-1 的覆盖策略( 单字符 vs 整组轮换)。

改动面很小(一个字符类 + 一个括号约束 + 两条断言),不影响 PR 的其余设计与验证。建议作者按此微调后合入。

@deepcoldy

Copy link
Copy Markdown
Owner

自动评审·首审对复审的回应(机器评审初步意见,以维护者审阅为准)

复审那条修正我复核后认同:我原先建议的「同括号」约束确实修不掉 ◆ Ran echo "(...)" —— 那行引号里的括号内容恰好就是完整的真实 footer,同括号约束照样命中。我的修法方向不足,这一点更正是对的。

是历史命令行标记这一点,我从另一个角度独立证实了:字面量 "◆ " 在二进制里有 13 处(含 ◆ Submitting feedback… 这类成品行),而轮换集图元的 "◈ " / "✧ " / "◇ " 全部是 0 处——因为轮换图元由运行时拼接、只以那段长度 6 的连续串存在。实心 被当作立即数直接比较,形态上确实和轮换集不同类。

⚠️ 但「去掉 」是必要不充分:两条对抗用例仍能唤醒已 idle 的会话

我把四种组合跑成矩阵(同一组对抗用例):

变体 误命中 真命中
A 原样(PR 现状) 4/4 3/3
B 只去 2/4 2/3
C 只加同括号 3/4 3/3
D 去 + 同括号 2/4 2/3

把 D(即复审建议的完整补丁)打进去后,两条幸存者走完整 IdleDetector 仍真的唤醒已 idle 会话

◇ diff (12s): -◈ old (1m 1s • esc to interrupt)          → REVIVED
◈ note: the footer reads (36m 20s • esc to interrupt) now → REVIVED

它们要求轮换图元出现在行首的非指示器内容里(diff/引用/正文),比 ◆ Ran 少见,但 --no-alt-screen 下正文与历史同屏,并非不可能。若要收干净,判据应该锚定「图元 + 紧随其后的活动描述 + 同括号 footer」这一整体,而不是只排除某个图元。

📌 一处容易漏掉的强制联动

「去掉 」会让 PR 自带的正向断言 ◆ Evaluating task configurations (34m 19s …) 变红——所以这不是可选项,必须同时把该断言改成真实轮换图元(复审补丁里确实一起改了,这里显式点出,避免只改正则导致测试红)。我按 D + 断言联动实跑复现:494 passed

✅ 顺带排除一条我本来想报的(避免误伤作者)

--no-alt-screen 下 scrollback 里旧的 indicator 行被重绘时会唤醒 idle 会话——但我在 master 上用 spinner arm 测同形态一样会唤醒busy=1),所以这是既有的架构特性(viewport 探针 + 历史同屏),与本 PR 无关,不应计入本次改动。

@deepcoldy

Copy link
Copy Markdown
Owner

[自动评审·复审收敛](机器评审初步意见,以维护者审阅为准)

接前两条评论:首审的 🔴(历史行误命中)与复审的修正(「同括号」不足、需去 )均已确认,且首审指出的两条「幸存者」用例(◇ diff (12s): -◈ old (1m 1s • …)◈ note: … (36m 20s • …) now)我方实证复现——确实会唤醒已 idle 会话。收敛后的最小合前要求如下,供作者参考:

arm-2 建议改为锚定整体语义单元(行首轮换图元 + 无括号活动描述 + 同括号 footer + 行尾收束):

(?:^|[\n\r])[ \t]*[⋄✧◇✦❖◈][ \t]+[^()\r\n]{0,200}\([^()\r\n]*\d+(?:h|m|s)(?:[ \t]+\d+(?:m|s))?[^()\r\n]*\besc[ \t]+to[ \t]+interrupt\b[^()\r\n]*\)[ \t]*(?:\x1b\[[0-9;]*m)?[ \t]*(?=[\r\n]|$)

相对原版的四点变化及其针对性:

  1. 图元类收窄为真实轮换集 [⋄✧◇✦❖◈](去 )—— 是历史命令行标记(二进制立即数比较 + "◆ " 字面量 13 处,轮换图元 0 处),修掉 ◆ Ran … 两类误命中;
  2. 图元与 ( 之间为无括号描述段 [^()\r\n]{0,200}——描述段里出现任何括号即不命中(修掉 ◇ diff (12s): -◈ old (1m 1s • …),第一个 ( 被强制对齐 (12s),其内无 esc to interrupt,正则无法跳到第二个括号);
  3. footer 括号必须收在行尾(容许尾随空白/SGR reset;IdleDetector 匹配前已 stripAnsi)——修掉 ◈ note: … (36m 20s • …) now 这类 footer 后还跟正文的行;
  4. 耗时与 esc to interrupt 强制同一对括号(纵深防御)。

验证:18 条对抗矩阵(含上述全部误命中形态 + 真 footer + 缩进/长 CJK/行尾 CRLF/尾随 ANSI 变体)全符合预期;对抗输入 <0.2ms 无 ReDoS;替换后 vitest run --project unit test/cli-adapters.test.ts test/idle-detector.test.ts 494 全绿(同步把伪造正向断言 ◆ Evaluating … 改为真实轮换图元 ,并新增 5 条负向断言——此改动为强制联动,否则测试红)。

遗留请作者确认(不影响上述修复):arm-1 hook 行的 单字符 vs 轮换整组、hook 事件枚举仅覆盖 (?:Pre|Post)ToolUse

@deepcoldy

Copy link
Copy Markdown
Owner

自动评审·首审对收敛版 v3 的独立复核(机器评审初步意见,以维护者审阅为准)

v3 我独立跑了矩阵:误命中侧 6/6 全封死、真 footer 侧 7/7 全保留(0 mismatch),含缩进 / CRLF / 长 CJK / footer 后接 composer 换行。前两轮的 🔴 与两条幸存者确实都被关掉了,收敛方向我认同。

但补两点,供作者决定取舍。

⚠️ 1. v3 用三条新约束换来了 8 类新的假阴性,方向与本 PR 要修的 bug 相同

我把矩阵反向也跑了一遍(「它会不会误拒真实形态」):

◈ Running fix() in app.ts (2m 1s • ↑ 6K tokens • esc to interrupt)  → REJECT   描述段含括号
◈ Reading Bubble (draft) resource (36m 20s • esc to interrupt)      → REJECT   同上
◈ Working (2m 1s • esc to interrupt)        100% context left       → REJECT   footer 非行尾(右对齐状态)
◈ Working (2m 1s • esc to interrupt) │                              → REJECT   footer 后接 TUI 边框
◈ Working (2m 1s • (cached) • esc to interrupt)                     → REJECT   footer 内嵌套括号
◈ <描述 230 字符> (2m 1s • esc to interrupt)                         → REJECT   超 200 上限
◈Working (2m 1s • esc to interrupt)                                 → REJECT   图元后无空格

两类错误后果相反:误命中 = 卡住不发 DONE;误拒 = 提前打 DONE,即本 PR 要修的原 bug。所以「18 条全符合预期」若矩阵里没有误拒侧用例,只覆盖了半张表。其中「描述段禁括号」我认为最值得掂量——活动描述里出现 fix() / (draft) 这类括号并不罕见,而这条约束正是用来封 ◇ diff (12s) 的,收益与代价挂在同一个条件上。

若倾向更稳,可考虑把「同括号 footer」保留,但把描述段从 [^()\r\n]{0,200} 放宽为 [^\r\n]{0,200} 并改用 esc to interrupt 在 footer 括号内做唯一判别(\([^()\r\n]*\besc[ \t]+to[ \t]+interrupt\b[^()\r\n]*\))——◇ diff 用例的第一个括号 (12s) 内没有该短语,第二个括号里有但前面已被行首图元锚死,实际取舍请作者自行验。

📌 2. (?:\x1b\[[0-9;]*m)? 是死代码,建议删掉

该子句用于容忍尾随 SGR reset,但 IdleDetector.feed()src/utils/idle-detector.ts:88 stripAnsi(data) 再 test,strip 后字符串已不含 ESC:

raw  contains ESC: true
post contains ESC: false          ← 子句永不可达
post = "◈ Working (2m 1s • esc to interrupt) "
V3 结果 : true
去掉该子句同一输入: true          ← 逐字相同

留着会让后来的读者以为这里要处理原始 ANSI,而实际契约是「进来的已是 stripped 文本」,反而误导。尾随空白由 [ \t]* 已覆盖。

✅ 顺带排除一条(两版共有,不是 v3 引入

status_indicator_widgetwrapped_details_lines 符号,说明描述会换行;若 footer 落到无图元的续行上,行首图元锚点就失效:

◈ 读取 Bubble resource draft and a very long description that wraps
  (36m 20s • ↑ 68.7K tokens • esc to interrupt)
      → PR 原版 arm-2: IDLE    v3: IDLE

PR 原版同样判不出,所以这是两版共有的局限,不计入本次改动;只是提示:若线上仍偶发假空闲,这里是下一个怀疑点(也是 arm-1 单字符覆盖问题的同类)。

合前最小要求(与复审一致)

修 arm-2 的历史行误唤醒 + 联动◆ Evaluating … 断言改为真实轮换图元(否则测试红)。v3 我实跑确认 494 passed 可复现;上面两点属于取舍与整洁度建议,由作者与维护者定。

@deepcoldy

Copy link
Copy Markdown
Owner

[自动评审·真机目击证据(改变前几轮的部分结论)](机器评审初步意见,以维护者审阅为准)

前几轮(首审🔴 → 同括号修正 → 去◆/v3/v4 讨论)全部建立在静态分析上。本轮我们在已登录的本机用 tmux PTY 真跑起了 TraeX 0.201.6 TUI,抓到了 turn 进行中的原始字节流。三个事实推翻或修正了此前共识:

1. 就在活动轮换集里(推翻「去 ◆」路线)。
真机 footer 逐帧轮换覆盖 7 个图元:⋄✧◇✦❖◈◆,其中 约占 1/4~1/3 的帧(如 ◆ Working… (0s • esc to interrupt))。任何把 从图元类剔除的修法都会漏掉这些帧 → 假空闲间歇回归,恰好复发本 PR 要修的 bug。此前二进制里「◆ 是历史标记、不在 6 帧串中」的推断是静态分析的陷阱——作者把 ◆ 放进类里很可能是目击过真机渲染的(撤回我此前「伪造断言」的指摘)。代价是历史行误命中不能再靠排除图元解决,只能靠结构约束(见下)。

2. PR 现有图元类缺 (PR 自身的真缺口)。
[✦✧◆◇◈] 只有 5/7。按真机频率,约 2/7 的 footer 帧不被识别 → PR 按原样合入,假空闲仍会间歇复现。这是比历史行误命中更直接的本 PR 目标缺陷。

3. 严格的行尾锚在真实 PTY 路径上不可用(撤回 v3 的 $ 锚)。
原始字节流里 footer 的 ) 之后紧跟的是光标定位序列 + 下一行内容(── 分隔线 / └ Tip: …),且 stripAnsi(idle-detector.ts:274-277)不剥 \x1b7/\x1b8(DECSC/DECRC 两字符转义)——strip 后 ) 与后续内容拼接在同一逻辑行。任何 \)$ 类锚都会拒绝真实 footer。

基于真机证据的 arm-2 收敛版(已在本仓库 worktree 验证 494 全绿):

(?:^|[\n\r])[ \t]*[⋄✧◇✦❖◈◆][ \t]*[^()\r\n]{0,240}\([^()\r\n]*\d+(?:h|m|s)(?:[ \t]+\d+(?:m|s))?[^()\r\n]*\besc[ \t]+to[ \t]+interrupt\b[^()\r\n]*\)(?![ \t]*["'\x60])

相对 PR 原版的三点变化及依据:

  • 图元类扩到 7 帧全集(修缺口 2);
  • 图元与 ( 之间为无括号描述段——真机观察到的描述全部无括号(Working…/Thinking longer…/On its way…/Reading 2 files…),此约束使 \( 强制对齐第一个括号,封死 ◆ Ran grep (2m 3s) for …◇ diff (12s): -◈ old (1m 1s • …) 两类历史行误命中;
  • ) 之后禁止紧跟引号——封死 ◆ Ran echo "(36m 20s • …esc to interrupt)"(🔴 原案),而不触碰真机 ) 后的 ──//行尾形态。

已记录的接受残差◈ note: the footer reads (36m 20s • esc to interrupt) now 这类「轮换图元出现在行首正文 + footer 形态括号」的行仍会命中——因为更强的锚(行尾/字词阻断)在剥离后的 PTY 流上会误拒真 footer(事实 3),且误拒的代价(提前 DONE,即原 bug)重于这类少见误唤醒。是否值得用真实遥测再收紧,交作者判断。

验证:23 条对抗矩阵(真机目击形态正向 ×12 + 结构性误命中负向 ×8 + 边界)全符合预期;对抗输入 <0.2ms;替换后 vitest run --project unit test/cli-adapters.test.ts test/idle-detector.test.ts 494 全绿(新增 ⋄/❖/◇ 真机正向断言 + 5 条结构负向断言)。

仍无法本地目击、留作者确认:hook-runner 行(本机未配置 hooks)——arm-1 的 单字符 vs 7 帧轮换、事件枚举仅 (?:Pre|Post)ToolUse 两点沿前述评论。若 hook 行同样走轮换图元,建议同卡收窄。

@deepcoldy

Copy link
Copy Markdown
Owner

自动评审·首审的真机独立复现(机器评审初步意见,以维护者审阅为准)

我独立把 TraeX 0.201.6 真跑起来复现了一遍(tmux PTY + pipe-pane原始字节流,而非 capture-pane 的渲染行),结论与「真机目击」那条一致,并补上几个可直接用的数字。我此前「 是历史标记、不在轮换集」的判断是错的,撤回。

真机实测(stripAnsi 后的真实 PTY 流,14 次 footer 采样)

真 footer 形态 :  ◈ Working… (2s • esc to interrupt)
图元频率       :  ◈×7  ◆×3  ⋄×2  ◇×1  ✦×1
描述段         :  只观察到 "Working…"(无括号)

关键: 同时是历史行标记又是活动轮换帧。 同一次会话里两者都出现了:

◆ Working… (3s • esc to interrupt)     ← 活动帧(必须判 busy)
◆ Ran echo hook-probe-9911             ← 历史行(必须不判 busy)

所以「把 从图元类剔除」这条路(含我上轮认可的方向)根本不成立——必须靠结构约束区分,不能靠排除图元。我之前的静态依据(长度 6 的连续串 ⋄✧◇✦◈❖ 不含 、字面量 "◆ " 13 处而轮换图元 0 处)每一条都属实,但合起来指向了错的结论:轮换图元由运行时拼接,静态串看不出渲染频率,也看不出同一字符的双重语义。

三点可直接落地的量化结论

1. PR 现有图元类漏 ,实测漏检 2/14 ≈ 14% 的真 footer 帧。 这是本 PR 自身的缺口,且方向正是它要修的假空闲(漏检 → 提前 DONE),比历史行误命中更贴目标。建议扩到 7 帧 [⋄✧◇✦❖◈◆]

2. 任何 $ / 行尾锚在真实 PTY 流上会拒绝每一条真 footer。 原始流里 footer 的 ) 之后紧跟分隔线,strip 后拼在同一逻辑行

⋄ Working… (0s • esc to interrupt)──────────────────────────────────────────…
◆ 1◆ Working… (4s • esc to interrupt)───────────────────────────────────────…

三版正则在这条真流上的命中数:PR 原版 = 1,v3($ 锚)= 0,v6 = 2。我上一轮复核 v3 时给的 13 条矩阵是手工构造的单行输入,没有还原「footer 后拼分隔线」这一真实形态,所以没能暴露这个问题——这是我上轮复核的盲区,一并说明。

(补一个细节:本流里 strip 后存活的转义只有 \x1bM\x1b[,未出现 \x1b7;但"锚不住"的结论不依赖 DECSC,靠分隔线拼行就已成立。)

3. v6 在真机形态上全部符合预期。 我用真机抓到的 8 条形态(含 3 种轮换帧正向、◆ Ran 历史行、 子行、以及 echo/diff 两类误命中)跑 v6:0 mismatch

⚠️ 一处与「本机未配置 hooks」不同的观察

本机 ~/.trae/traecli.toml 实际 5 个 hook 条目在册:

hooks.json:pre_tool_use   hooks.json:post_tool_use   hooks.json:post_tool_use_failure
hooks.json:stop           hooks.json:user_prompt_submit

但我带 --dangerously-bypass-hook-trust 真跑并触发工具调用(◆ Ran echo hook-probe-9911 已确认执行)后,hook-runner 行始终没有出现。所以 arm-1 的形态双方都仍未目击——✦ Running N PreToolUse hooks 究竟长什么样、是否走同一轮换集、 单字符是否够用,全都没有实证。鉴于 arm-1 目前只认 且只认 (?:Pre|Post)ToolUse,建议作者要么给出目击证据,要么在注释里注明该 arm 未经真机验证。

合前最小要求(两家一致)

  1. arm-2 换成能同时容纳 7 帧图元、且不含行尾锚的形态(v6 是可用参考);
  2. 联动把 ◆ Evaluating … 断言与新增负向断言一并更新(否则测试红);
  3. arm-1 的图元/事件覆盖请作者确认或标注未验证。

最后一句方法论,也是我自己这轮最贵的教训:静态分析(二进制、正则、对抗矩阵)能证伪一个实现,但不能证实「界面真长什么样」。 我首审那句"两种活动行的真实渲染形态我没能直接目击"是全程唯一没被推翻的结论——它本该更早促使我们去真跑一次。

@deepcoldy

Copy link
Copy Markdown
Owner

[自动评审·收口补充——独立捕获上的命中率数据](机器评审初步意见,以维护者审阅为准)

评审已收敛(见上一条评论),本条只补一组我方独立捕获的量化数据,供维护者校准对修复生效面的预期,不新增议题。

两段独立抓取的原始 PTY 字节流(已登录 0.201.6,真实 turn,共 22 次 footer 渲染),按 IdleDetector 同款 stripAnsi 处理后统计各版本对真实 footer 的命中数:

捕获 真 footer 出现 PR 现状 v3(去◆+$锚) v6(7帧+结构锚)
raw.log(86KB) 7 1 0 1
raw2.log(169KB) 15 3 0 2

结论与上条评论一致且更强化:

  1. v3 类收紧在真实流上命中率归零,确证不可用;
  2. v6 相对 PR 在屏幕快照路径上是严格改进(7/7 图元 vs 5/7,屏幕路径行结构干净、约束全部成立);
  3. 值得维护者留意:在剥离后的原始 PTY 流上,连 PR 现状也只命中少数 footer 重绘——TUI 用光标定位逐段重绘,strip 后多行内容拼接导致行首锚失配。这是所有行锚 pattern(含 master 的 spinner/queue 臂)共有的既有架构特性,非本 PR 引入,不应阻塞本 PR;但若希望 PTY 路径的 busy 探测更可靠,可能需要 IdleDetector 在 strip 时把光标行定位(ESC[<row>;1H / 行清除序列)规范化为换行,属于独立的后续改进。

(另注:我方捕获中 strip 后存活的转义为 \x1b7/\x1b8/\x1bM,与上条评论所述流略有差异——终端环境不同,不影响结论。)

合前清单维持:arm-2 换 v6 形态 + 联动改断言;arm-1 hook 行请作者给出真机证据或标注未验证。评审双方已无分歧,等待维护者。

@deepcoldy

Copy link
Copy Markdown
Owner

自动评审·收口(机器评审初步意见,以维护者审阅为准)

评审双方已无分歧。补最后一组数据:把「生效面主要在屏幕快照路径」这个判断在屏幕路径上直接量化,并核对了它在代码里的依据。

屏幕快照路径实测:PR 80% → v6 100%

worker.ts 的屏幕路径把 captureBackendScreen() 的内容经 busyProbeRegion()(取末 1/3、至少 12 行,worker.ts:12202)喂给 busyPatternworker.ts:12212 / 12235 / 6849)。我按同一形状抽样一次真实 turn(40 次快照,其中 10 次 region 内含真 footer):

命中
PR 现状 [✦✧◆◇◈] 8/10 = 80%
v6 [⋄✧◇✦❖◈◆] 10/10 = 100%

同一批样本里 7 种图元全部出现×3、×2、×2、×1、×1、×1。漏的 2 次正是 / —— 与前面「PR 漏 2/7 帧」的结论独立互证,这次是在生产真正使用的输入形状上测的。

所以对维护者的取舍可以说得很干脆:屏幕路径上 v6 是把 80% 提到 100%,且不引入误命中◆ Ran / echo / diff 三类负向在真机形态上均不命中)。PTY 流路径的行锚大面积失配确属既有架构损耗(master 的 spinner/queue 臂同样受影响),不阻塞本 PR。

关于「strip 时把 ESC[<row>;1H 规范化为换行」那条后续线索:方向合理,但属于公共 IdleDetector 改动,会同时影响全部 20+ 个 CLI 适配器的所有行锚 pattern(本仓 CLAUDE.md 明确要求公共层改动逐一核对受影响的 CLI × 后端 × 会话类型组合)。建议独立开 PR 并配跨 CLI 回归,不要挂在这个 TraeX 专属修复里。

合前清单(双方一致,未变)

  1. arm-2 换成 7 帧图元 + 结构约束、不含行尾锚的形态(v6 可用参考);
  2. 联动更新 ◆ Evaluating … 断言 + 新增负向断言(否则测试红);
  3. arm-1 hook 行:请作者给出真机证据,或在注释里标注未经真机验证 —— 本机 5 条 hook 配置在册、工具调用确认执行,hook-runner 行仍未渲染,双方都没能目击。

补充一句 / 的实测意义:它们不是理论帧,是真机采样里出现过的帧,所以第 1 项不只是"更严谨",而是直接对应 20% 的漏检。

Co-authored-by: TRAE CLI <traecli@bytedance.com>
@xavier-cai
xavier-cai force-pushed the fix/traex-02016-false-idle branch from 2f8ef73 to 4ebaf41 Compare August 29, 2026 22:22
@xavier-cai

Copy link
Copy Markdown
Author

感谢完整复核,已按最新真机证据收敛并更新本 PR。

真机验证

我在独立临时 Git 目录中配置了两个各延迟 4 秒、无业务副作用的同步 hook,通过独立 tmux 启动 TraeX 0.201.6,并在 turn 前挂 pipe-pane 抓 raw PTY,同时每 100ms 采集 screen。

实测确认:

  • 7 个活动图元全部出现:⋄✧◇✦❖◈◆
  • hook-runner 与普通活动态共用 elapsed / esc to interrupt footer;
  • 实际抓到 PreToolUsePostToolUseUserPromptSubmitStopNotification hook 行;
  • hook-runner 图元同样轮换,并非固定

实验后已关闭独立 tmux、删除临时目录/raw PTY/screen/hook 日志,并移除 TraeX 自动写入的临时项目 trust 记录。未修改全局 hook,未重启 Botmux;无新增软件安装。

代码调整

  • 删除独立的 hook event-name arm;hook-runner 由统一 activity-footer pattern 覆盖,避免维护不完整的事件枚举;
  • activity glyph 扩为真机 7 帧全集;
  • 采用无行尾锚的结构约束:行首活动图元 + 无括号活动描述 + 同括号 elapsed/interrupt footer;
  • 右括号后禁止紧跟单双引号/backtick,阻断 ◆ Ran echo "(...)" 历史行;
  • 保留 reviewer 明确记录的 recall-first 残差,避免为封少见正文误命中而漏掉真实 footer。

测试

  • 正向:7 帧普通 footer,以及 PreToolUse / PostToolUse / UserPromptSubmit / Stop / Notification 真机 hook 形态;
  • 负向:普通 prose、◆ Ran、单双引号/backtick echo、grep 前置括号、diff 多括号;
  • bun x vitest run --project unit test/cli-adapters.test.ts test/idle-detector.test.ts:494 passed;
  • bun run build:通过;
  • git diff --check:通过。

更新 commit:4ebaf41a60a5937d827834b28234b0078dcecf45

@deepcoldy

Copy link
Copy Markdown
Owner

自动评审·增量复审(4ebaf41a6)(机器评审初步意见,以维护者审阅为准)

新 commit 把前几轮的评审意见基本吸收了,方向我认同。先说结论:三条合前必修里有两条已修好,剩一条从"必修"降级为"请补一句依据"。

✅ 已解决

  1. 图元集扩到 7 帧 [⋄✧◇✦❖◈◆] —— 我此前实测 PR 旧版漏 /(屏幕路径 8/10 = 80%)。用我先前真机抓到的 9 条正向形态跑新正则:9/9 全中
  2. 未加行尾锚,改用「无括号描述段 + 同括号 footer + ) 后禁引号」。这点很关键——真机 PTY 流里 footer 的 ) 后面会紧跟 ─── 拼进同一逻辑行,任何 $ 锚都会拒绝每一条真 footer。新版避开了这个坑,测试里那条 ' ⋄Working… (0s • esc to interrupt)────' 断言正是覆盖它。
  3. 误命中全封:我先前抓到的 8 条负向形态(◆ Ran echo hookrow-probe 子行、三种引号的 echo、◆ Ran grep (2m 3s)◇ diff (12s)0/8 命中
  4. 断言联动做了,且新增的 per-glyph 循环有牙——我做了两次变异验证:把图元类改回旧的 5 个 → 3 red;删掉 (?![ \t]*["'\])` 引号阻断 → 2 red

⚠️ 唯一还想请作者补一句的:删掉 arm-1 的依据

新版把 hook-runner 专属 arm 整条删了,理由是注释里那句「Hook-runner rows use the same footer shape, so they need no event-name-specific arm」。如果这句成立,删得对——用同一个 footer grammar 覆盖所有 hook 事件,比原来只认 (?:Pre|Post)ToolUse 更完整(顺带解决了我上轮提的事件枚举覆盖问题:SubagentStart/PreCompact/Stop/Notification 等现在都被覆盖)。

但这带来一个可验证的取舍:

形态 新版 旧 arm-1
⋄ Running 4 PreToolUse hooks (7s • esc to interrupt) ✅ BUSY ❌ MISS
✦ Running 2 PreToolUse hooks无 footer ❌ MISS ✅ BUSY

也就是说:hook 行只要有那个 footer,新版更好;一旦存在「hook 行不带 footer」的瞬间(例如 footer 尚未渲染的第一帧),新版就完全看不到它,而这正是旧 arm-1 覆盖的形态、也是 PR 最初描述里写的形态(✦ Running N PreToolUse hooks,无括号耗时)。

我自己没能目击 hook 行来判定:本机 5 条 hook 配置在册,但我用带 --dangerously-bypass-hook-trust 的真跑 + 自建慢 hook(sleep 9)都没能让 hook-runner 行出现(自建 hook 的 fire 标记始终没落地,说明我的探针没生效,不构成对 PR 的反证)。所以想请作者补一句:

  • 那条 ⋄ Running 4 PreToolUse hooks (7s • esc to interrupt)目击方式(截图 / raw 抓包片段即可);
  • 以及是否见过不带 footer 的 hook 行。若见过,建议把旧 arm-1 以「7 帧图元 + 事件名通配」的形式保留一条(例如 [⋄✧◇✦❖◈◆][ \t]+Running[ \t]+(?:\d+[ \t]+)?\w+[ \t]+hooks?\b)作为纵深;若确认 footer 恒在,当前删除是合理简化,注释已写清楚就够了。

PR 描述里现在仍留着最初那句「新增 ✦ Running N PreToolUse hooks」的问题陈述,与实现已不一致(实现改成了统一 footer grammar),建议同步更新描述,避免后来人按描述去找那条 arm。

📌 一条明确记录的残差(我同意作者的取舍)

◈ note: the footer reads (36m 20s • esc to interrupt) now 现在断言为 true(会判 busy)。作者在注释里写明了原因:收紧它会误拒真实 PTY footer 重绘。我认同这个方向——误拒的代价(提前打 DONE,即本 PR 要修的原 bug)重于这类少见误唤醒,而且断言把它显式钉成了「已知接受」而非默默放过,这比留个隐式漏洞好。

🧪 验证

  • 本地 rebase 到最新 origin/mastera421b4021零冲突(未 force-push,未改作者提交;本地 rebase 结果 cee503936 仅用于验证)
  • test/cli-adapters.test.ts + test/idle-detector.test.ts494 passed
  • 全量 unit:19577 passed / 5 failed。失败为 mojo-close-failclosedmojo-launcher-env-quarantineplugin-mcp-sandboxworker-terminal-read-auth.integration —— 无一与 TraeX 相关;这 4 个文件在干净 master 上单独跑同样 4 红(PR 侧单独跑也是同样 4 红),全量里多出的第 5 个是高负载下的时序 flake,与本 PR 无关
  • 变异:图元类回退 → 3 red;删引号阻断 → 2 red(新断言有区分力)
  • 无 ReDoS:240K 对抗输入 4.3ms

除上面那句 arm-1 的依据外,我这边没有阻塞项。

@xavier-cai

Copy link
Copy Markdown
Author

感谢增量复审。关于删除初版 arm-1,我补充如下证据和兼容性边界:

  1. arm-1 不是 master 中的既有规则。 origin/master 只有 Braille spinner 文案与 capacity queue 两类 busy 分支;固定 + (?:Pre|Post)ToolUse、允许无 footer 的 arm 是本 PR 初版新增的候选规则。因此用统一 footer grammar 替换它,不会移除 master 对旧版本已有的支持。原 spinner/queue 分支在当前 commit 中也完整保留。

  2. 目击方式。 我在独立临时 Git 目录中配置两个各 sleep 4 的同步 command hook,通过独立 tmux 启动 TraeX 0.201.6;在提交 turn 前使用 tmux pipe-pane 捕获 raw PTY,并每 100ms 采集 screen。捕获到的 hook-runner 包括:

⋄ Running 4 PreToolUse hooks (... • esc to interrupt)
❖ Running 2 PostToolUse hooks (... • esc to interrupt)
✦/◈ Running 3 UserPromptSubmit hooks (... • esc to interrupt)
◆ Running 2 Stop hooks (... • esc to interrupt)
✦ Running Notification hook (... • esc to interrupt)

实际 hook 行的 glyph 会轮换,事件名也不限于 Pre/PostToolUse,但均与普通 activity row 共用 elapsed / esc to interrupt footer。

  1. footer-less 边界。 本次 raw PTY 与 100ms screen 采样中没有观察到不带 footer 的 hook-runner frame。这里仅陈述本次实测边界,不主张所有历史/未来版本都不可能出现该形态。若未来拿到某版本 footer-less hook frame 的真实捕获,应基于其稳定结构增加有边界的兼容分支;当前保留未经实证、且只有单 glyph/两个事件名的宽松 arm,反而可能让 no-alt-screen 历史行经 idleToBusyPattern 唤醒已完成 session。

因此当前取舍是:保留 master 的全部旧规则,并用一条真机验证的 footer grammar 覆盖所有已观察到的 hook 事件与 7 个 glyph。PR 描述已同步更新,删除了“0.201.6 新增形态”和独立 ✦ Running ... arm 的旧表述,并补充了以上依据。代码与 commit 未再改动。

@deepcoldy

Copy link
Copy Markdown
Owner

[自动评审·最终确认——hook 行已目击,arm-1 删除安全](机器评审初步意见,以维护者审阅为准)

唯一开放项(hook 行是否带 footer)现已通过真机原始字节流目击关闭。在已登录 0.201.6 上配置慢 PreToolUse hook(sleep 15s)触发工具调用,pipe-pane 抓到的原始渲染序列:

\x1b[20;3H R unn \x1b[20;10H  2 PreToolUse hooks (8s • esc to interrupt)

即完整行形态为 ✦ Running 2 PreToolUse hooks (8s • esc to interrupt)——与活动行同一 footer grammar、同一次原子重绘(图元 + 描述 + footer 在同一帧的定位写入中)。这证实:

  1. arm-1 删除是安全的:hook 行恒带 footer,统一 grammar 完整覆盖;旧 arm-1(无 footer 的 ✦ Running N hooks)反而是冗余的误命中风险源。
  2. 描述段无括号Running 2 PreToolUse hooks 不含括号,no-paren-desc 约束对 hook 行无副作用。
  3. 不存在「footer 未渲染的第一帧」:footer 与描述在同一原子渲染周期内写入,无中间态。

最终验证(作者 head 4ebaf41a):

  • 两个相关 suite 494 passed(独立复跑确认)
  • 19 条对抗矩阵(7 帧真机形态 × 含 hook 行 + 3 种引号 echo + grep/diff/task-paren 误命中 + 分隔线拼接 + CRLF)全符合预期
  • 真机 footer 描述样本(Working…/Thinking longer…/Still working…/Reading 2 files…/Running N hooks)全部无括号
  • 无 ReDoS

评审双方零分歧,合前清单全部满足:① arm-2 统一 footer grammar(7 帧 + 结构约束,无行尾锚)② 断言联动有牙 ③ arm-1 已有真机证据。等待维护者审阅。

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants