fix(traex): 提交确认改用 history.jsonl 修 type-ahead 误报 - #748
Conversation
|
复审结论:有 1 个 blocking,当前共享 Blocking:
|
|
补充严重度与反例:结论调整为 P2 blocking(低概率,但会造成跨会话错误绑定,合并前应补齐),不是普通 managed 消息路径上的 P1 active bug。 “每条 botmux prompt 都带唯一
因此建议修复范围保留生产接线 + defense-in-depth,而不是只改注释:
建议测试用 raw |
|
在 P1:Linux bwrap 下接入的
|
|
P1 验证通过使用真实 {"outerComm":"bwrap","resolvedComm":"traex"}
剩余 P2:第一条 foreign line 会在 owned line 尚未落盘时提前结束轮询当前 生产时间尺度最小复现:
实际结果: {"elapsedMs":201,"result":{"submitted":true}}后续 owned line 不再被扫描,SID 仍会丢失。建议维护 请将测试改成 timer 延迟 append owned line,确保它晚于第一次 probe,再断言返回 本地验证: 当前 PR CI 与 CodeQL 均为 green。 |
|
验证结论: 重点确认:
验证结果: PR 当前为 mergeable,worktree clean。 |
TRAE 是 Codex 家族的 type-ahead CLI:忙碌中发的追问会被 TRAE 暂存进队列,
只在上一轮出队时才写入 per-session rollout JSONL。但 traex 适配器的 writeInput
之前轮询 rollout 来确认提交,导致被暂存的追问在上一轮结束前查不到——超过
worker 的 20s 确认死线(SUBMIT_DEFERRED_RECHECK_MS)就误报「没能确认提交,
请去 Web 终端手动按 Enter 或重发」,而实际上 TRAE 已收到并会处理。
根因是选错了验证文件:适配器旧注释称「没有全局 history.jsonl」,但
~/.trae/cli/history.jsonl 真实存在且在提交时刻即写入(格式与 Codex 完全一致:
{session_id, ts, text})。Codex 适配器正是靠这个文件确认提交、绕开同一个坑。
改动(仅限 TRAE,不动共用 Codex 逻辑):
- traex.ts:writeInput 弃用 SQLite rollout 快照,改为轮询 history.jsonl 的
byte-delta(traexHistoryMatchDelta),非换行结尾的半写行不参与匹配。
- history.jsonl 是同一 TRAE_HOME 下所有 pane 共享的全局文件,加 Codex 同款
pid 所有权过滤(findTraexRolloutSetByPid,走 /proc/<pid>/fd):只接受本
pid 持有的 session,防并发 sibling pane 发相同文本抢答返回错误 session_id;
pid 未知时 accept-first,保持单 pane 语义。
- 删除旧「SQLite 索引不可用即写前拒绝」分支:history.jsonl 首次提交才懒创建,
baseByte=0 时首行天然匹配。
- traex-paths.ts:新增 traeHistoryPath()。
影响面:仅 traex 适配器的提交确认路径。跨 CLI——未触碰 codex.ts 或
codex-transcript 的共用导出(splitCodexEventsByCutoff 等),Codex/CoCo 等
其它 Codex 家族适配器不受影响。跨后端——writeInput 经 PtyHandle 抽象,
PTY/tmux 后端一致;cliPid 缺失时回退 accept-first。
验证:
- 用真 traecli 0.200.19 跑 PTY 复现:忙碌中追问 +1.0s 落 history.jsonl,
rollout 到 +20s 死线仍无——旧路径必误报,坐实根因。
- 对照真 codex:同款脚本追问 +1.0s 落 history.jsonl,全程 20s 内确认——
证实选对文件即免疫。
- 修复后 adapter 级 PTY 实证(直接驱动编译后的 writeInput):忙碌中追问
1.0s 返回 {submitted:true} 且 session_id 正确(修前为 {submitted:false})。
- 单测:traex-transcript 新增 7 例(提交时确认/delta/CRLF 归一/半写行安全/
所有权过滤 sibling 与仅 foreign 拒绝/size),traex-adapter-submit 重写 6 例
(旧用例 mock 的是已移除的 SQLite 路径),含忙碌中 type-ahead 追问回归。
两文件隔离与全量 vitest --project unit 均绿;pnpm build 绿。
复审指出上一版依赖 history.jsonl 确认提交后,会话 id 的归属校验不足: history.jsonl 是同一 TRAE_HOME 下所有 pane 共享的全局文件,adopt 模式的 buildBridgeInputContent 不注入唯一 <session_id>(如裸「继续」),因此并发 sibling pane 的同文本提交可能让 writeInput 返回 foreign session id,进而被 persist(污染 resume 目标)或用于 bridge 重绑(回复从错会话 harvest)。原 TRAE rotation attach 分支没有 codex 那套 pid-fd 所有权闸,注释里宣称的 「worker attach gate backstop」对 TRAE 并不成立。 修法(三层,与 codex 对齐;提交确认与会话归属解耦): - 提交确认保持 ownership-independent:writeInput 仍对 history.jsonl 全文匹配 确认 submit(否则 foreign-first 行或未知 pid 会重新触发误报);但只有当本 pid 可证明持有该 rollout(traexSidOwnedByPid / findTraexRolloutSetByPid) 才返回 cliSessionId,否则确认 submit 但不带 id——绝不回传未验证的 id。 - 新增纯谓词 traexHistorySidIsOwned(fail closed:集合为 undefined 或非成员 均返回 false),便于脱离真实 pid 单测。 - worker 侧新增 currentTraexObservedPid(backend.cliPid → 子进程 pid → adopt-pending pid,与 codex 同序)+ traexHistorySidOwnedByCurrentPid 闸, 接入 TRAE 的 rotation attach(refuse history-only re-attach to unowned id); 该闸 pid 解析比 adapter 的 pty.cliPid 更全,是权威判定点。 - 生产接线:fresh-managed TRAE 现在也 set backend.cliPid(与 grok 并列,两处 同步/异步站点),否则普通 PTY/tmux 会话 cliPid 恒空、所有权闸永远无法放行 真实 id。maybeFollowTraexSessionRotationViaPid 天然安全(id 由 findTraexRolloutByPid(pid) 反查得来,本就属该 pid,无需额外闸)。 影响面:仅 TRAE。未改 codex 共用逻辑;worker 新增函数仅在 structuredBridgeIsTraex 分支与 traex spawn 接线处调用。跨后端——adopt(TmuxPipe,pty.cliPid 未设)下 adapter fail closed 不回传 id,权威归属由 worker 闸(用 adoptCliPid/codexAdoptPendingPid) 判定;fresh(pty/tmux) 下 getChildPid 即 leaf。 验证: - adapter 级真机 PTY 复现(reworked):忙碌中 follow-up 仍 1.0s 确认 submitted:true,且 cliPid=leaf 时正确返回 owned cliSessionId。 - 单测:traex-adapter-submit 8 例(含 owned→返回 id、foreign sibling→确认但 扣留 id、dead pid→扣留 id、lazy-create/mid-turn/never-commit 仍确认提交), traex-transcript +4 例覆盖 traexHistorySidIsOwned 四态,traex-worker-bridge-wiring +4 例断言 rotation 所有权闸先于 detach、pid 解析顺序、生产 cliPid 接线。 - 三文件隔离 34 例全绿;全量 vitest --project unit 仅 2 个与本改动无关的既有并发 flaky suite(隔离复跑全绿);pnpm build 绿。
复审在沙盒/凭据隔离组合下发现 P1:file sandbox 与 Linux credential-only 都以 `bwrap --unshare-pid -- traex` 启动,tmux pane leaf / getChildPid() 是 bwrap supervisor,其 /proc/<pid>/fd 不持 rollout,所有权闸永远无法放行会话 id。fresh 沙盒 TRAE 首轮因此拿不到 SID:bridge 无 rollout path 也无 pending sid,1s poller 的 pid fallback 只吃 codexAdoptPendingPid(fresh 为 undefined), maybeFollowTraexSessionRotationViaPid 又要求已有 rollout path——不会自愈。又因 reliableTurnTerminal=true 关掉了 screen-idle,未 attach 就收不到 task_complete, durable turn 一直不开闸,后续输入可能一起卡住(会话失活,非单纯 metadata 降级)。 修复: - 真机 bwrap 实测确认内层 traex leaf 在 host 侧可见(ps -A ppid 链跨 pid ns), 故 resolveTraexOwnershipPid 用 findLaunchedCliPid(pid,'traex') 从 bwrap supervisor BFS 下钻到真 leaf,再 wire backend.cliPid。 - 下钻是 BOUNDED RETRY 而非 one-shot(bwrap 可能尚未 fork traex):新增 startTraexSandboxPidResolve 复用 scheduleWrapperRealCliPid 的定时重试 + stale-backend 守卫;sync 与 zellij-async 两处 wire 站点都 kick。 - 触发条件是 outerBwrapActive = sandboxRequested || credentialOnlyBwrap(凭据 隔离同样产生 outer bwrap,不能只判 sandboxRequested)。credentialOnlyBwrap 需 host 探测、gate-check 时无法从 cfg 重算,故用 lastSpawnOuterBwrapActive 记下 spawn 时结论供 currentTraexObservedPid 的 live fallback 读取。 同时修复复审指出的会话归属漏扫:无过滤 matcher 在第一条匹配行即返回,foreign-first 行会掩盖随后的自有行,导致明明拥有后一条却扣留 id。改为有可枚举 pid 时先跑 owned 扫描(跳过 foreign 行继续找自有行),仅在无 owned 命中时回退 any-text 扫描确认提交 (不带 id)。 按复审范围本 PR 仅修 TRAE:codex fresh-managed 目前不接线 backend.cliPid(走 accept-first),一并改会动到现有 submit-confirm 语义,需单独设计与回归,不属本 active regression。 验证: - 真 bwrap probe:outer bwrap pid 的 owned set=[];bwrap→中间→leaf 中仅 leaf 持 rollout fd 且 host 可见。 - 单测:find-launched-cli-pid +2(bwrap→中间→traex 命中 leaf / leaf 未 fork 返 null 触发重试);traex-adapter-submit +1(foreign-first→owned-later 返回 SID_1,直打漏扫回归);traex-worker-bridge-wiring +3(sandbox leaf 收敛、 outerBwrapActive 门、bounded-retry 两站点 kick)。四文件隔离 58 例全绿。 - pnpm build 绿;全量 vitest --project unit 仅 2 个与本改动无关的既有并发 flaky suite(隔离复跑全绿)。
复审指出上一版的 owned-first 只在“同一次读取里 foreign+owned 两行都已落盘”时
有效:真实生产时序下 foreign 行 Enter 后立即落盘、owned 行数百毫秒后才落盘时,
resolveConfirmed 每轮先查 owned(未命中)随即查 any 命中并返回——首个 foreign
sighting(~200ms)就返回 {submitted:true} 无 id,后续 owned 行永不再扫描。
修法(按复审建议的最小改动):
- 保留 findTraexRolloutSetByPid 的三态,不经 boolean helper 抹平:
· undefined(无 pid / 非 Linux / proc 不可读,枚举不可用)→ 无从证明归属,
any-text 一命中即确认提交(无 owned 行可等,快速退化)。
· Set(可能为空,枚举可用)→ owned 行可能尚未落盘,继续轮询;foreign-first
的 any-text 命中只记 sawAnyText,不结束循环。
- sawAnyText 记住“提交已被证明”:一旦置位,后续不再补 Enter(消息已进 TRAE 日志,
再 Enter 有重复提交风险),只等 owned 行浮现。
- 预算耗尽仍无 owned 行时才 {submitted:true} 无 id;worker recheck 同样先 owned
后 any-text(届时惰性 rollout fd 可能已解析)。
- 删除已无引用的 traexSidOwnedByPid(其两态被上面三态逻辑取代)。
验证:
- 新增 async-timing 回归:Enter 后 foreign 立即落盘、owned 由 timer 于 400ms 后
落盘(真实时间跑,不加 BOTMUX_TIME_SCALE,让 200ms+800ms 轮询覆盖该窗口),
断言最终返回 SID_1。对旧逻辑必红(旧代码 ~200ms 即返回无 id)。
- traex-adapter-submit 10 例、四文件 59 例隔离全绿;pnpm build 绿;全量
vitest --project unit 仅 2 个与本改动无关的既有并发 flaky(隔离复跑全绿)。
fff7b42 to
81b32c7
Compare
|
🚀 Released in v3.9.2 |
问题
TRAE 用户经常遇到
⚠️ 刚才那条消息发给 TRAE 后没能确认提交(重试 Enter 后等了 20s 仍未在会话 JSONL 里看到新记录)。可能卡在输入框里——请去 Web 终端看一下,手动按 Enter 或重发。—— 但实际上 TRAE 已收到并会处理。根因
TRAE 是 Codex 家族的 type-ahead CLI:忙碌中发的追问会被 TRAE 暂存进队列,只在上一轮出队时才写入 per-session rollout JSONL。但
traex适配器的writeInput之前轮询 rollout 来确认提交,导致被暂存的追问在上一轮结束前查不到——超过 worker 的 20s 确认死线(SUBMIT_DEFERRED_RECHECK_MS)就误报。选错了验证文件:适配器旧注释称「没有全局 history.jsonl」,但
~/.trae/cli/history.jsonl真实存在且在提交时刻即写入(格式与 Codex 完全一致:{session_id, ts, text})。Codex 适配器正是靠这个文件确认提交、绕开同一个坑——所以 Codex 免疫,TRAE 中招。截图里报错的 preview 开头是
<session_id>…,正是**追问(follow-up)**而非新话题首条,与该场景完全吻合。「经常而非每次」:worker 的仲裁器(
decideSubmitConfirmationAction)在 20s 内若检测到 PTY 输出/transcript/botmux send活动就会压掉告警——长任务持续刷屏时被掩盖,安静窗口才暴露,因而间歇性。改动(仅限 TRAE,不动共用 Codex 逻辑)
traex.ts:writeInput弃用 SQLite rollout 快照,改为轮询history.jsonl的 byte-delta(traexHistoryMatchDelta),非换行结尾的半写行不参与匹配。history.jsonl是同一TRAE_HOME下所有 pane 共享的全局文件,加 Codex 同款 pid 所有权过滤(findTraexRolloutSetByPid,走/proc/<pid>/fd):只接受本 pid 持有的 session,防并发 sibling pane 发相同文本抢答返回错误session_id;pid 未知时 accept-first,保持单 pane 语义。history.jsonl首次提交才懒创建,baseByte=0时首行天然匹配。traex-paths.ts:新增traeHistoryPath()。影响面
codex.ts或codex-transcript的共用导出(splitCodexEventsByCutoff等),Codex/CoCo 等其它 Codex 家族适配器不受影响。writeInput经PtyHandle抽象,PTY/tmux 后端一致;cliPid缺失时回退 accept-first。验证
history.jsonl,rollout 到 +20s 死线仍无,旧路径必误报。history.jsonl,全程 20s 内确认,证实选对文件即免疫。writeInput):忙碌中追问 1.0s 返回{submitted:true}且session_id正确(修前为{submitted:false, recheck}→ 20s 后误报)。traex-transcript新增 7 例(提交时确认/delta/CRLF 归一/半写行安全/所有权过滤 sibling 与仅 foreign 拒绝/size),traex-adapter-submit重写 6 例(旧用例 mock 的是已移除的 SQLite 路径),含忙碌中 type-ahead 追问回归。pnpm build绿;两测试文件隔离与全量vitest --project unit均绿(另有 2 个与本改动无关的并发 flaky suite,隔离复跑全绿)。