Skip to content

fix(adopt): 兼容 Codex Desktop 新版会话记录 - #881

Open
Hyphen-Wang wants to merge 2 commits into
deepcoldy:masterfrom
Hyphen-Wang:fix/codex-response-item-resume-discovery
Open

fix(adopt): 兼容 Codex Desktop 新版会话记录#881
Hyphen-Wang wants to merge 2 commits into
deepcoldy:masterfrom
Hyphen-Wang:fix/codex-response-item-resume-discovery

Conversation

@Hyphen-Wang

@Hyphen-Wang Hyphen-Wang commented Aug 14, 2026

Copy link
Copy Markdown

背景

Fixes #878.

Codex Desktop / CLI 0.147 的部分 rollout 不再写入 event_msg/user_message,真实用户输入只存在于 response_item/message(role=user)。现有磁盘恢复扫描器因此无法生成标题,导致 /adopt 找不到 Codex 自身仍可恢复的 session。

改动

  • 保留并继续优先旧版 event_msg/user_message 标题路径。
  • response_iteminput_text 作为文件结束时的 fallback。
  • 过滤 AGENTS、environment、permissions、skills、collaboration、apps、plugins 等 Codex 合成上下文。
  • 在新版记录结构下继续识别并排除 botmux 自身注入的 session。
  • 为 Codex Desktop、TRAE 共用解析路径、混合格式优先级和 botmux envelope 增加回归测试。

影响范围

仅修改 Codex/TRAE 磁盘 resumable session discovery;未改动 live backend、Claude-family、Antigravity、worker 或 IM 路径。现有 exclude、limit 与旧格式行为保持不变。

验证

  • pnpm build:通过
  • pnpm exec vitest run --project unit test/resumable-session-discovery.test.ts:31/31 通过
  • pnpm workflow-core:test:通过
  • 使用脱敏前真实 rollout 只读验证:目标 session 可发现并生成标题;已知 botmux-origin session 仍被隐藏
  • 本机完整 pnpm test:15,195 通过;31 项为无关环境依赖失败(系统 Git 2.20 不支持测试使用的 git init -b、缺少 bwrap,以及 host/runtime 相关用例),相关 discovery 套件无失败

@Hyphen-Wang
Hyphen-Wang requested a review from deepcoldy as a code owner August 14, 2026 13:42

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

首次 Review — REQUEST CHANGES(1 处真实回归,已在本机真实数据上复现)

结论先行

改动的核心意图正确且有价值:Codex Desktop/CLI 0.147 起部分 rollout 只把真实用户输入写进 response_item/message(role=user) 而不再写 event_msg/user_message,旧扫描器取不到标题就把这些其实可 resume 的会话丢弃(#878)。本 PR 加的 response_item fallback 确实能把它们捞回来 —— 我在本机 755 条真实 codex rollout 上跑编译后的代码验证:相比 master 多恢复出 10 条此前被漏掉的真实外部会话(PONG/hi 等)。方向没问题。

但同一条新路径引入了一处回归,必须先修。


🔴 P0 阻断:新 response_item fallback 把 botmux 自己的会话泄漏进 /adopt

现象(真实数据复现): 在本机 755 条真实 rollout 上,对比 master 与本 PR head 的 discoverRolloutSessions() 输出:

master 本 PR
发现会话数 707 755
相比 master 新浮出 48
↳ 其中真实外部会话(修复生效) 10
↳ 其中 botmux-origin 会话(泄漏) 38 🔴

这 38 条 master 上全部被正确隐藏,本 PR 让它们浮进了 picker,标题就是原始的 <botmux_routing> 你运行在飞书(Lark)话题群中… 整块。这直接违反 /adopt 的既定不变量 —— 代码注释白纸黑字写着:botmux 自己的会话对 picker 隐藏,picker 只用来导入真正外部的会话(它们已可通过话题卡/会话关闭卡 resume,重复导入既冗余又困惑)。

根因:复用的 isBotmuxInjected guard 对真实新版 codex envelope 匹配不到。 真实 envelope(单个 input_text 里)的结构是:

<botmux_routing>…</botmux_routing>
<botmux_builtin_skills>…</botmux_builtin_skills>   ← 中间插了这一块
<identity>…</identity>
<session_id>…</session_id>
<user_message>…</user_message>          ← part 到这里结束,没有尾部 <sender open_id="ou_…"> footer

于是本该命中的三条正则全部落空:

  1. <botmux_routing> 那条要求 </botmux_routing> 之后(最多隔一个 <identity>)紧接 <session_id> —— 被中间的 <botmux_builtin_skills> 打断;
  2. </user_message>\s*<(?:sender|session_id|…) 邻接模式 —— 该 part 到 </user_message> 就结束,后面没有 footer;
  3. <sender … open_id="ou_…"> —— 同理,该 part 无 footer。

我把这个真实 part 单独喂给 isBotmuxInjectedPrompt(),返回 false(完整 envelope 就在这一个 part 里,guard 拿到了完整输入仍漏 —— 所以要修的是 guard 本身,不是 response_item 的分片迭代逻辑)。

为什么 PR 的测试绿着却漏了: PR 新增的 "drops botmux-origin rollouts … response_item" 用例,用的是简化版 envelope(没有 <botmux_builtin_skills>、routing 后直接 <session_id>),恰好命中 guard → 测试过。真实 envelope 命中不了。属于"测试夹具比真实数据干净,给了假绿"。

修复方向(已用真实数据验证可行):<botmux_routing> 结构模式容忍 </botmux_routing><session_id>/<user_message> 之间插入 <botmux_builtin_skills>/<identity> 等块,即"以 <botmux_routing> 开头 → 出现 </botmux_routing> → 其后出现 <user_message>…</user_message>"。我实测这种起始锚定模式在本机 821/821 条真实泄漏 part 上全部命中,且对两个"仅讨论 botmux XML 的外部 prompt"反例均不误伤。附带好处:master 上经 event_msg 路径泄漏的 152 条同类(同一 guard 不完整根因、非本 PR 引入)也会一并修好。

请同时把回归测试的 envelope 换成真实结构(<botmux_builtin_skills> 插在中间、part 结尾即 </user_message> 无 footer),否则改完 guard 测试仍可能假绿。


🟡 P2 非阻断(质量项):合成上下文跳过列表不全

真实数据里还有两类会作为标题浮出,不在 ROLLOUT_SYNTHETIC_USER_PATTERNS 内:

  • <codex_internal_context source="goal">…(1 条) —— 疑似 botmux v3 goal-mode 续跑注入,可能也应归为 botmux-origin 排除;
  • <turn_aborted>…(2 条) —— Codex 的中断续跑合成上下文,当标题无意义。

建议顺手补进跳过/排除列表(非阻断)。


验证记录

  • pnpm build:通过
  • pnpm exec vitest run --project unit test/resumable-session-discovery.test.ts:31/31 通过(即 PR 自带用例全绿 —— 但如上,绿不代表覆盖真实 envelope)
  • 编译产物对 ~/.codex/sessions755 条真实 rolloutdiscoverRolloutSessions(),并与 master 版(esbuild 转译同文件)逐会话 diff:坐实 38 条 botmux-origin 泄漏为本 PR 新引入
  • 把真实泄漏 part 单独喂 isBotmuxInjectedPrompt() 复现 guard 落空;对候选修复模式做 821 条命中 + 2 条外部讨论反例不误伤的验证

@deepcoldy

Copy link
Copy Markdown
Owner

复核补充 — 收敛后的作者动作清单(REQUEST CHANGES 维持)

经独立数据复核确认阻断项判定与修复方向,补充如下,供作者一次收口。

+48 条的精确构成(本机真实库,base 707 → head 755)

类别 数量 说明
botmux-origin 泄漏(回归) 38 master 全隐藏,本 PR 新浮出
真正有意义的外部会话(修复生效) 7 应恢复 ✅
合成上下文伪标题 3 <codex_internal_context source="goal"> ×1 + <turn_aborted> ×2

(此前 review 表格把后两类合并计为「外部会话 10」,精确拆分后为 7 真实 + 3 合成伪标题 —— 修好下述第 3 点后这 3 条即从 picker 消失。)

建议一次收口的三项

  1. 放宽 <botmux_routing> 结构 guard。 关键是保留三点:^ 起始锚定、要求完整 </botmux_routing>、并要求其后按顺序出现完整 <user_message>…</user_message>不要在中间维护「固定插入块白名单」(如枚举 <botmux_builtin_skills>/<identity>)—— 否则以后再插入新的稳定上下文块会再次复发。松锚定对本机真实库 826 个完整 routing envelope part 全部命中,语言无关(结构 tag 固定,routing 内中英文不参与判定),Antigravity 仍走原路径匹配、Claude/Grok 走既有 ^<user_message> 分支不受影响。唯一理论误伤:外部用户手工提交一份从 <botmux_routing> 开头且含完整 <user_message> 的完整仿真 envelope —— 这在持久化层与真实注入不可区分,符合既有「完整结构优先判 botmux」策略,可接受。

  2. 回归夹具换成真实形态。 把新增的 response_item 排除用例改为:<botmux_routing>…</botmux_routing> 后插入 <botmux_builtin_skills> / <identity>,并让该 input_text 恰好结束于 </user_message>、无 <sender>/footer。否则 guard 改完测试仍可能假绿。

  3. 合成上下文跳过列表补齐 + 补测。^<codex_internal_context\b^<turn_aborted\b 加入 ROLLOUT_SYNTHETIC_USER_PATTERNS,并覆盖「整条会话只有合成上下文 → 不进入 picker」的用例。它们是本次 fallback 新浮出的无意义标题,建议本 PR 一并收口。

改完建议再对真实 rollout 库跑一遍 discoverRolloutSessions() 与 master 逐会话 diff,确认 routing 泄漏归零、真实外部会话仍在。

Copy link
Copy Markdown
Author

已按 review 修复并推送(commit 1bbf2976):

  • <botmux_routing> guard 放宽为:从 prompt 开头匹配完整 routing 块,并要求其后出现完整 <user_message>…</user_message>;不再对白名单中的中间上下文块做结构假设。
  • response_item 回归夹具替换为真实 envelope:包含 <botmux_builtin_skills><identity>,并恰好结束于 </user_message>,无 footer。
  • ^<codex_internal_context\b^<turn_aborted\b 加入合成用户项过滤,并补了各自回归测试。

验证:

  • test/resumable-session-discovery.test.ts:33/33 通过。
  • test/daemon-rename-route.test.ts 联合运行:112/112 通过。
  • pnpm buildpnpm workflow-core:testgit diff --check 均通过。
  • 当前本机保留的 23 条 rollout 上做了 master/修复版只读对比:8 → 15,新增 7 条均为有效外部会话;以 <botmux_routing><codex_internal_context><turn_aborted> 开头的坏标题为 0。

烦请复审。

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审 (v2 · commit 1bbf297) — 阻断已解除,APPROVE(仍待授权合码)

作者按清单三项全部收口,我在本机真实数据上逐项复验通过。

逐项核对

1. 放宽 <botmux_routing> guard —— ✅ 正确实现
旧的严格模式(要求 </botmux_routing> 后紧接 <session_id>)已删除,替换为松结构锚定:^<botmux_routing>[\s\S]*?</botmux_routing>[\s\S]*?<user_message>[\s\S]*?</user_message>。旧严格模式是新模式的子集,删除不丢覆盖;注释也明确写了"匹配顺序边界而非维护白名单,避免新块插入时失效"。

2. 回归夹具换真实形态 —— ✅
既有 "drops botmux-origin … response_item" 用例的 envelope 已改为真实结构:<botmux_routing>…</botmux_routing> 后插入 <botmux_builtin_skills> + <identity>,并结束于 </user_message> 无 footer —— 正是 v1 假绿的那个形态,现在能真正命中。

3. 合成跳过列表 + 补测 —— ✅
^<codex_internal_context\b^<turn_aborted\b 已加入 ROLLOUT_SYNTHETIC_USER_PATTERNS,并新增两条"整条会话只有合成上下文 → 不进 picker"的用例。

真实数据复验(本机 755 条 codex rollout)

v1(旧 head) v2(本 commit)
发现会话数 755 562
botmux-origin 泄漏 / 合成伪标题 190 🔴 0 ✅
相比 master 新浮出的真实外部会话 7 7 ✅(保留)

v2 相比 master(707)少 152 条,全部是此前经 event_msg 路径泄漏的 botmux-origin 会话 —— 松模式把这批非本 PR 引入的 pre-existing 泄漏也一并修好了,它们本就不该出现在 picker,属正确收益。数字自洽:707 − 152 + 7 = 562。

额外健壮性检查

  • ReDoS: 新模式含三个惰性量词,对 8 组针对性构造的 200KiB 对抗输入(未闭合标签、大量近似标签逼回溯、双边界交替片段等)逐一计时,全部 <1ms —— 惰性 [\s\S]*? + 互异字面分隔符,线性无灾难回溯,安全。
  • 误伤: "仅在正文讨论 <botmux_routing>/<user_message>"、代码块展示 envelope、tag 出现在中段(非 ^ 起始)等 —— 均正确 KEEP(不误 drop)。唯一理论误伤(prompt 字面以 <botmux_routing> 开头且含完整 <user_message>)在持久化层与真实注入不可区分,符合既有"完整结构优先判 botmux"策略,可接受。
  • 共享 guard 消费方: isBotmuxInjectedPrompt 亦被 grok-transcript.ts 复用;grok + adopt + session-adopt 等 8 个测试文件共 127/127 通过,无跨消费方回归。

验证记录

  • pnpm build:通过
  • pnpm exec vitest run --project unit test/resumable-session-discovery.test.ts:33/33(新增 2 条 SYNTH 用例)
  • guard 全部消费方套件:127/127
  • 编译产物对 ~/.codex/sessions 755 条真实 rollout 复跑 + 与 master 逐会话 diff:泄漏归零、真实外部会话保留
  • 新模式 ReDoS 8 输入计时 + 误伤反例电池:全部符合预期

阻断项已彻底解除,方向与实现均正确。建议合并 —— 但按流程仍需授权后再合码。

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审(v2 · commit 1bbf297)— REQUEST CHANGES:松正则存在可复现的超线性回溯

功能修复本身已验证正确:真实 rollout 结果为 562 条、7 条目标外部会话保留、152 条既有 botmux 泄漏移除,新增 synthetic 过滤和 127 个相关测试也都通过。误伤边界判断我认可。

但当前新增模式:

/^<botmux_routing>[\s\S]*?<\/botmux_routing>[\s\S]*?<user_message>[\s\S]*?<\/user_message>/

并非线性。只要输入以 routing 开头,随后重复候选边界、但始终缺少最终 </user_message>,三个惰性量词会组合回溯:

const input = '<botmux_routing>'
  + '</botmux_routing><user_message>X'.repeat(n);
isBotmuxInjectedPrompt(input);

在本 PR 导出的真实 guard 上实测:

n 输入大小 耗时
160 5,136 B 11.1 ms
320 10,256 B 88.0 ms
640 20,496 B 697.4 ms

输入翻倍约 8 倍耗时,呈立方级增长。这里不是不可达的纯理论输入:discovery 明确按完整 JSONL 行读取,仓库也专门支持单条大于 200KiB 的 prompt;一次 /adopt 会在 daemon 内同步执行该 guard,因此一个病态外部 rollout 足以长时间阻塞 daemon。此前“200KiB 全部 <1ms”的输入电池没有覆盖“重复精确 closing-routing + opening-user、缺 closing-user”这一组合。

建议不要继续用多段 wildcard regex,改为有序 startsWith + indexOf

  1. 必须以 <botmux_routing> 开头;
  2. 从其后查找 </botmux_routing>
  3. 从 closing routing 之后查找 <user_message>
  4. 从 opening user 之后查找 </user_message>

这个实现保持相同的结构语义且线性。我用该 helper 做了两组验证:

  • 对当前真实库 14,237 个 event/response user text 与现正则逐一比对:两者均命中 1,518 个,0 个不一致
  • 对上述 204,816 B 病态输入:helper 返回 false,耗时约 0.25ms

请替换这一个松正则,并增加该精确对抗形态的回归覆盖。其余 v2 改动无新增阻断。

验证记录:pnpm build 通过;8 个 guard/adopt 相关测试文件 127/127 通过;真实 rollout base 707 → head 562(新增 7、正确移除 152、伪标题 0)。未合码。

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审更正 (v2 · commit 1bbf297) — 撤回上一条 APPROVE,改 REQUEST CHANGES

复核指出的正则性能阻断成立,我已独立复现坐实。我上一条 APPROVE 的 ReDoS 判断有漏 —— 此处更正。

🔴 P0 阻断:新增松正则存在灾难性回溯(可达 DoS)

新模式 ^<botmux_routing>[\s\S]*?</botmux_routing>[\s\S]*?<user_message>[\s\S]*?</user_message> 含两个相邻惰性量词,当中间分隔符成对完整重复但最终 </user_message> 缺失时组合回溯。我用真实 guard 复现:

输入 <botmux_routing> + </botmux_routing><user_message>X×n 长度 耗时
n=800 ~25KB 1.5s
n=1600 ~51KB 10.8s
n=3200 ~100KB 88s

输入翻倍耗时约 ×8(组合回溯)。可达性坐实:forEachJsonLine完整行(无字节上限,注释明确"200KiB 单条记录也须整条解析"),guard 在 discoverRolloutSessions 里逐行调用 —— 磁盘上一条病态外部 rollout 就能让一次 /adopt 卡死几十秒(daemon 侧同步阻塞)。

我上一条为何漏: 我的对抗电池用的是近似片段(</botmux_routin 这类不闭合近似),没构造成对完整的中间分隔符重复——而后者才是让两个惰性量词乘积回溯的真正触发形态。教训记下。

修复:改成有序 startsWith + indexOf 线性扫描(不用三段 wildcard)

依次定位 routing 起点 → routing 终点 → user 起点 → user 终点,任一缺失即 false。已独立验证等价 + 免疫:

/** Structural check for a botmux routing envelope, done with ordered index
 *  scans instead of lazy `.*?` quantifiers — three chained lazy wildcards
 *  backtrack combinatorially on repeated complete delimiters with a missing
 *  final close (a disk rollout can be 200KiB+ and is scanned whole), so the
 *  regex form is a reachable DoS via /adopt. This is O(n). */
function matchesRoutingEnvelope(t: string): boolean {
  if (!t.startsWith('<botmux_routing>')) return false;
  const rc = t.indexOf('</botmux_routing>', '<botmux_routing>'.length);
  if (rc < 0) return false;
  const uo = t.indexOf('<user_message>', rc + '</botmux_routing>'.length);
  if (uo < 0) return false;
  return t.indexOf('</user_message>', uo + '<user_message>'.length) >= 0;
}

把这个 helper 从 BOTMUX_INJECTION_PATTERNS 里拆出来,在 isBotmuxInjectedPrompt 里作为一条 || matchesRoutingEnvelope(text) 分支(其余正则保留)。

独立验证(与复核数据一致):

  • 免疫: 同一 ~100KB 病态输入 0.099ms、~200KB 0.18ms(正则 88s → helper 亚毫秒);
  • 等价: 本机 15,199 条真实 user text 上,正则与 helper 各命中 1,518,差异 0;
  • 误伤: 现有反例(讨论 XML / 中段 tag / 无 user close)结果不变,真实 envelope(含 interposed skills / legacy shape)仍命中。

请作者补齐

  1. 用上面的线性 matchesRoutingEnvelope 替换新增的松正则分支。
  2. 补一条对抗回归:'<botmux_routing>' + '</botmux_routing><user_message>X'.repeat(n)(成对完整分隔符 + 缺尾闭合),断言在合理时限内返回 false —— 锁死这个形态不再退化。

功能结果与误伤边界我在 v2 上已确认无误(707→562、+7 真实外部、0 泄漏、0 合成伪标题),唯此性能项需收口。换成线性扫描 + 补回归后,再做一次最终复核即可。当前不应合码。

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.

fix(adopt): Codex 0.147 Desktop 会话未被磁盘恢复扫描器发现

2 participants