Skip to content

fix(traex): 提交确认改用 history.jsonl 修 type-ahead 误报 - #748

Merged
deepcoldy merged 4 commits into
masterfrom
wt/botmux-claude-1-t
Aug 5, 2026
Merged

fix(traex): 提交确认改用 history.jsonl 修 type-ahead 误报#748
deepcoldy merged 4 commits into
masterfrom
wt/botmux-claude-1-t

Conversation

@deepcoldy

Copy link
Copy Markdown
Owner

问题

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.tswriteInput 弃用 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()

影响面

  • 跨 CLI:未触碰 codex.tscodex-transcript 的共用导出(splitCodexEventsByCutoff 等),Codex/CoCo 等其它 Codex 家族适配器不受影响。
  • 跨后端writeInputPtyHandle 抽象,PTY/tmux 后端一致;cliPid 缺失时回退 accept-first。
  • 会话类型:仅影响 traex 适配器的提交确认路径。

验证

  • 真机复现根因:用 traecli 0.200.19 跑 PTY 复现——忙碌中追问 +1.0shistory.jsonl,rollout 到 +20s 死线仍无,旧路径必误报。
  • 对照 Codex:同款脚本跑真 codex——追问 +1.0shistory.jsonl,全程 20s 内确认,证实选对文件即免疫。
  • 修复后 adapter 级 PTY 实证(直接驱动编译后的 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,隔离复跑全绿)。

@deepcoldy

Copy link
Copy Markdown
Owner Author

复审结论:有 1 个 blocking,当前共享 TRAE_HOME 的多 pane 所有权保护实际没有接入生产调用链。

Blocking:cliPid 未注入 + TRAE 无 worker 侧 backstop,会接受并持久化 sibling 的 history SID

新代码在 traex.ts 中只从 pty.cliPid 构造 acceptSid。但 fresh managed worker 的同步/异步 PID wiring 目前都只覆盖 claudeDataDir || cfg.cliId === 'grok'worker.ts 的 spawn 后 PID wiring),traex 不在其中;adapterInputHandle() 对非 ZMX 又原样返回 backend。因此普通 PTY/tmux/zellij TRAE 会话里 pty.cliPid 通常是 undefined,过滤器直接退化成 accept-first。现有 adapter 测试构造的 PTY 也都没有 cliPid,所以正好只覆盖了这个退化路径。

这不只是“少一道过滤”:注释所说的 worker attach backstop 对 TRAE 不存在。codexBridgeNotifyCliSessionId() 只有 Codex 分支调用 codexHistorySidOwnedByCurrentPid();TRAE 的 rotate 分支与 generic initial attach 都直接信任 history 返回的 SID。并且 flushPending() 会先 persistCliSessionId(result.cliSessionId),再通知 bridge。于是同一 TRAE_HOME 下 sibling pane 的同文本 history 行先到时,会把 foreign SID 持久化并把 bridge 重挂到 sibling rollout;后续可能串回复,重启还可能 resume 错会话。

findTraexRolloutSetByPid() 返回 undefined 时也有同一问题:adapter 当前 fail-open 接受首条,而 TRAE worker 没有二次校验。返回 empty Set 时则会一直拒绝;如果拿到 wrapper/sandbox supervisor PID 而非 leaf,就会退化为本 PR 原本要修的 20s 误报。

PID 语义核对结果:

  • plain PtyBackend 直接 spawn CLI,getChildPid() 是 leaf;
  • plain TmuxBackend 的 shell 脚本最终 exec /usr/bin/env ... <cli>,稳定后 pane PID 与 CLI leaf 同 PID;
  • adopt discovery 传入的是已解析的 CLI PID;
  • 但这些 PID 当前都没有给 fresh TRAE 的 PtyHandle.cliPid;wrapper/外层 sandbox 场景还需要显式做 descendant leaf 解析,现有 startWrapperRealPidResolve() 又被 claudeDataDir 限制,不能替 TRAE 收敛。

建议至少补齐两层:① fresh sync/async 后端把经 leaf 解析的 TRAE PID 暴露给 adapter;② TRAE 的 SID 持久化、initial attach、rotation attach 都做 pid-fd ownership gate(或在不能证明 ownership 时只确认 submitted、不返回/持久化 SID),不要依赖 accept-first。测试需要覆盖真实 production wiring,而不只是给纯函数手工传 acceptSid:至少包括 foreign-first/owned-later、枚举 unavailable、wrapper PID、initial attach 与 rotation attach。

其余重点检查结论:

  • 删除 SQLite 写前 fail-closed 本身合理:history.jsonl 首次提交才创建,缺文件不能视为不可提交;worker 对未确认写入走 ambiguous/deferred recheck,不会自动重放正文。
  • 半写行处理正确:无末尾换行的 tail 不解析,后续完整 poll 可见;baseline 落在旧半行中间时旧行只会解析失败,不会制造匹配。
  • CRLF/裸 CR 归一是对 decoded text 做精确比较,覆盖充分。
  • 文件头仍写着 “There is no global history.jsonl”,与本 PR 实现相反,建议顺手更新(非 blocking)。

实际验证:

  • pnpm build:通过。
  • pnpm exec vitest run --project unit test/traex-transcript.test.ts test/traex-adapter-submit.test.ts test/codex-adapter-history-ownership.test.ts test/codex-worker-bridge-wiring.test.ts:4 files / 26 tests 全绿。
  • git diff --check origin/master...074f93b:通过。

测试全绿说明 history delta 解析本身成立,但目前没有覆盖上述 worker→backend→adapter PID wiring 与 TRAE attach 所有权链路。

@deepcoldy

Copy link
Copy Markdown
Owner Author

补充严重度与反例:结论调整为 P2 blocking(低概率,但会造成跨会话错误绑定,合并前应补齐),不是普通 managed 消息路径上的 P1 active bug。

“每条 botmux prompt 都带唯一 <session_id>”并非全局不变量,至少有两条真实 traex.writeInput 路径没有它:

  1. adopt/bridge 输入buildBridgeInputContent() 明确保证不注入 <session_id>buildReforkCliInput()ds.adoptedFrom 返回这段 raw text;worker 的 structured adopt path 又会对 TRAE 调 cliAdapter.writeInput(),并在拿到 SID 后直接 persist + attach。两 pane 同时发“继续”这类文本是可构造的。adopt 正常情况下有 adoptCliPid,所以 /proc 可读时 adapter gate 能挡住;但 findTraexRolloutSetByPid() 返回 undefined 时当前会 accept-first,且 worker 没有 backstop,正好会暴露该反例。
  2. TUI 文本答复handleTuiTextInput() 把按钮/输入答案原文交给 adapter,不含 session UUID。这里虽不持久化返回 SID,主要风险是误确认提交,但也证明“全文天然唯一”不能作为所有权保证。

因此建议修复范围保留生产接线 + defense-in-depth,而不是只改注释:

  • fresh managed TRAE 接入经过 leaf 收敛的 cliPid,让普通 PTY/tmux/zellij 的 adapter filter 真正生效;plain PTY 是直启 leaf,plain tmux 的 shell 最终 exec /usr/bin/env ... <cli> 后 pane PID 即 leaf。wrapper/外层 supervisor 需单独处理,不能无条件把 launcher 当 leaf。
  • owned === undefined 时可继续把精确 history 文本当作“submitted 已确认”,但不要把该行 SID 当作已验证 SID 返回/持久化;否则会重新引入 20s 误报或 foreign SID 污染二选一。
  • TRAE 的 SID persist、initial attach、rotation attach 增加 pid-fd membership gate,至少达到 Codex attach gate 的防御强度;并覆盖 adopt raw-input 反例。

建议测试用 raw 继续(无 <session_id>)覆盖 foreign-first/owned-later、fd 枚举 unavailable,以及 worker initial/rotation attach。当前 blocking 维持,但分级按 P2。

@deepcoldy

Copy link
Copy Markdown
Owner Author

20b8ee671a9985bca0a663e35f46a007ede43677 上补充两处 blocking 验证结论:

P1:Linux bwrap 下接入的 cliPid 不是 TRAE leaf,fresh bridge 无法首轮 attach

PtyBackend.getChildPid() 返回 node-pty 直接 spawn 的进程;tmux 返回 pane_pid。文件沙盒和 credential-only isolation 都把实际命令改写为 bwrap,因此两者拿到的是 outer bwrap,而不是持有 rollout fd 的 TRAE leaf。

真 bwrap probe 的 host 进程树为:

bwrap (backend cliPid, owned rollout set = [])
└─ bwrap (owned rollout set = [])
   └─ tail/traex leaf (owned rollout set = [SID_1])

因此当前 findTraexRolloutSetByPid(backend.cliPid) 永远不能证明所有权,adapter 只返回 {submitted:true}、不返回 SID。对 fresh TRAE 这不只是 resume-id 降级:bridge 此时没有 rollout path/pending SID;timer 的 pid fallback 只使用 fresh 场景下为空的 codexAdoptPendingPid,而 maybeFollowTraexSessionRotationViaPid 又要求已经有 rollout path,所以无法自愈。TRAE 的 reliableTurnTerminal=true 会禁用 screen-idle terminal,最终 task_complete 无法被消费,durable turn 可能一直不开闸。

修复需同时覆盖 full bwrap 与 credentialOnlyBwrap,并覆盖 sync/async(zellij) 两个 PID 接线站点。leaf 可能在首次查询时尚未 fork,建议复用已有 bounded descendant resolver/stale-backend guard,而非一次性查询。

P2:现有 foreign-first 用例没有覆盖 owned-later,当前实现会漏掉有效 SID

当前测试只在 delta 中写了一条 foreign SID_2,没有再写当前进程实际 owned 的 SID_1。实际同一 delta 顺序写入 SID_2SID_1 时,unfiltered traexHistoryMatchDelta 在第一条 foreign line 就返回,confirmResult 立即产生 {submitted:true},后续 owned line 不再扫描。

最小实测结果:

{"pasted":"继续","result":{"submitted":true}}

期望应保留提交确认与 SID 归属解耦,同时在可枚举 PID 时继续扫描/轮询 owned exact match;预算内若出现 SID_1 则返回它,只有最终仍仅见 foreign/不可证明时才退化为 submitted:true 且不带 SID。请补真正的 foreign-first → owned-later 回归。

本地验证:

pnpm build
# passed

pnpm exec vitest run test/traex-adapter-submit.test.ts test/traex-transcript.test.ts test/traex-worker-bridge-wiring.test.ts
# 3 files passed, 36 tests passed

@deepcoldy

Copy link
Copy Markdown
Owner Author

53cf4a5ef690a3e52406518b37e1bbc6ca8928b5 的 P1 bwrap leaf 收敛已验证通过,但仍有一处 P2 blocking:foreign-first 的异步 owned-later 时序没有被当前实现/测试覆盖。

P1 验证通过

使用真实 traex 0.200.19bwrap --unshare-pid PTY 中冷启,实测:

{"outerComm":"bwrap","resolvedComm":"traex"}

findLaunchedCliPid(outerPid, 'traex') 能从 outer bwrap 收敛到真实 leaf;full sandbox / credential-only 门、sync/async 两个接线点及 bounded retry/stale-backend guard 未发现问题。

剩余 P2:第一条 foreign line 会在 owned line 尚未落盘时提前结束轮询

当前 resolveConfirmed()src/adapters/cli/traex.ts:292)每一轮先扫描 owned;没有命中时立即扫描 any-text 并返回 {submitted:true}。新增测试在同一个 Enter 回调中同步 append foreign 与 owned 两行,因此第一次扫描时两行已经同时存在,不能覆盖真实的异步竞态。

生产时间尺度最小复现:

  1. Enter 后 foreign SID_2 立即落盘;
  2. owned SID_1 在 400ms 后落盘;
  3. adapter 在 201ms 即返回。

实际结果:

{"elapsedMs":201,"result":{"submitted":true}}

后续 owned line 不再被扫描,SID 仍会丢失。建议维护 sawAnyText:每轮优先扫描 owned;仅见 any-text 时记录提交证据但继续轮询,最终预算耗尽仍无 owned 时才退化为 {submitted:true} 且不带 SID。既然 any-text 已被视为提交证据,等待 owned 期间也无需重复发送 Enter。若要让 fd enumeration unavailable 快速退化,需要保留 findTraexRolloutSetByPidundefined/Set 三态,而不是通过 boolean helper 抹平。

请将测试改成 timer 延迟 append owned line,确保它晚于第一次 probe,再断言返回 SID_1

本地验证:

pnpm build
# passed

4 个定向文件:58 tests passed

全量 unit:12647 passed;2 个既有 hook-timeout flaky
隔离复跑:31 passed

当前 PR CI 与 CodeQL 均为 green。

@deepcoldy

Copy link
Copy Markdown
Owner Author

验证结论:fff7b42b2941498059a93215ada60c9708c4384a 未发现新的 blocking,建议合并。

重点确认:

  • foreign-first 异步时序已闭环:fd 枚举的 undefined / Set 三态保留;枚举可用时仅见 foreign line 会继续轮询 owned line,预算结束才退化为无 SID 的提交确认。
  • sawAnyText 后不再重复 Enter,避免等待 owned SID 期间产生重复提交。
  • 独立按真实时间复现:foreign 立即写入、owned 延迟 400ms,结果约 1002ms 返回 owned SID_1,且 Enter 仅发送 1 次。
  • bwrap leaf 收敛、worker initial/rotation/poller ownership gate、半写行与 CRLF 路径均保持通过;改动仍局限于 TRAE 相关路径。

验证结果:

pnpm build
# passed

定向:4 files / 59 tests passed
# 异步 foreign→owned 用例约 1157ms,通过

全量 unit:12648 passed / 20 skipped
# 2 个既有 hook-timeout flaky;隔离复跑 31 passed

git diff --check
# passed

GitHub CI + CodeQL
# all 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(隔离复跑全绿)。
@deepcoldy
deepcoldy force-pushed the wt/botmux-claude-1-t branch from fff7b42 to 81b32c7 Compare August 5, 2026 13:51
@deepcoldy
deepcoldy merged commit 1f17eb4 into master Aug 5, 2026
6 checks passed
@deepcoldy
deepcoldy deleted the wt/botmux-claude-1-t branch August 5, 2026 14:03
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🚀 Released in v3.9.2

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.

1 participant