fix(codex-app): 修复假忙/假闲 + Riff 关停/PM2 恢复#537
Conversation
5ef85e5 to
a980cf8
Compare
…overy (PR 1-5) Fixes three user-visible failures in the Codex App path: - False idle: task still running but card/Dashboard show idle - False busy: task finished but UI stays busy - Wrong turn ownership: message routed to incorrect task turn Root cause: three state sources (TUI screen, worker memory, runner notifications) could override each other. Fix makes runner signed state the single owner of idle/ready; display layer only projects it. Also includes Riff shutdown (prepare/commit/abort fence) and PM2 recovery (exact start, descriptor guard, supervisor shutdown protocol) as separate concerns with independent test gates. Tests: 29/29 + 32/32 false-busy/false-idle cases pass; full unit regression shows 0 new failures. tsc --noEmit passes. Doc: https://bytedance.larkoffice.com/docx/TZUydIskCoJZT8xfMXzc2WI5ntc
a980cf8 to
72783ba
Compare
deepcoldy
left a comment
There was a problem hiding this comment.
PR #537 独立复审结论(head 72783ba)
结论:REQUEST CHANGES
审查期间 head 从 a980cf8 被推进到 72783ba(rebase 到最新 master cb55621,现已 MERGEABLE)。我对两 head 的 PR-diff 做了归一化 interdiff 人工核对:这轮 rebase 是纯 master 语义适配、无夹带(read-isolation capabilities marker 合成、deferred-topic-send × revalidateIsolatedOriginBeforeEffect 编织、types 双方字段联合、logicalContent 合并守卫等均正确),全部 findings 平移适用。
工程质量总体罕见地严谨(世代化 spawn、签名控制通道、三段式 Riff fence、处处 fail-closed),核心互斥/签名/账本机制未找到可攻破路径。问题集中在集成缝与「拆掉超时逃逸却没补有界性兜底」。
Blocker(必须修复才能合并)
B1. Lark 事件层:新硬死锁 + 整 bot 冻结族
- seedRoutingGate × capMs=0 ingress lane 死锁(event-dispatcher.ts:2624 / 2947 / 2964):话题种子 S 与其 root 回复 R 乱序到达(重启积压重推/6h re-push/WS 重连)时,R 持有 chat 级 lane(capMs=0 无逃逸)await S 的 gate,S 排在 R 之后永不执行 → 整群永久卡死。gate 的 ready 只能由 S 的 handler resolve,无超时兜底(已核 2126-2145)。放大项:claimMessageOnce 在入队前持久化 → 卡死期间所有消息被 claim,重启后 dedupe 拒重放 → 跨重启永久丢消息。本 PR 新引入(基点两者并发执行无此死锁)。修法:gate 等待加界(Promise.race + forwardFollowupWaitMs 口径)或检测 gate 完成者排在本任务之后时跳过等待。
- 挂死 admission × 60s 周期 sweeper mutation = 整 bot 冻结(bot-turn-mutation-gate.ts:152-154 × daemon.ts:18236-18239 × idle-worker-sweeper.ts:96-102):任一 admitted handler 永久挂起(Lark SDK Client 未配超时,网络静默断连即可)→ sweeper 的 runDetachedBotTurnMutation 置 mutating=true 后无界等 drain → 该 bot 全部新消息/卡片/scheduler 排队至重启。基点行为仅该 anchor 延迟 5s。修法:sweeper 改用已有的 tryWithBotTurnMutation 超时放弃本轮;给 Lark Client 配请求超时。
- 同族:canonical FIFO capMs=0 拆掉了 bd2bddd 修真实生产事故(「上一条没触发到你」)时加的 5s 兜底,且只改了人类路径——bot 发送方路径(2213/2344)仍是 5s 逃逸,serializer 注释点名的原始场景(bot dispatch 的 /repo prime>5s + kickoff)原样保留;同时 self-/close、bot @mention、stale-seed-flush、paired-forward 四条路径在 lane 内 await 完整 handler → bot 编排群(多 bot 协作正是核心场景)整群串行数秒。修法:四处统一「同步入队即释放 lane」模式 + cap 语义统一 + anchor-serializer 过期注释更新。
B2. PM2 restart/升级边界在生产规模必然翻车(CLI 侧与 Riff 侧两个独立审查代理同时命中)
- verify 预算 10s 覆盖不了 30+ bot 冷启(cli.ts:263 / 3340-3358;能力位是 daemon boot 最后一步 commit,排在 restoreActiveSessions 等所有耗时步骤之后)→ 超时进 rollback:daemon 已 attest 则优雅关停并 delete 刚拉起的整个 fleet;未 attest 则 rollback 预检 throw 报「rollback failed」(fleet 实际还在启动)。两种结局都错。修法:预算按 bot 数伸缩(≥60s)/能力位发布提前到 SIGTERM handler 装好即发/verify 超时降级为警告不 teardown。
- 首次升级边界三路全堵(pm2-shutdown-capability.ts:44-52 + pm2-start-transaction.ts:84-104):线上旧 daemon 无能力位、旧 PM2 行无 stop_exit_codes:[42] → 部署本 PR 后
botmux restart/stop/start全部拒绝;错误信息指向的「operator-approved one-time manual bootstrap」在 CLI 里不存在(已全文 grep)。修法:提供显式一次性 bootstrap 命令/flag + 部署 runbook,错误信息给出确切命令。 - fleet-shutdown 补偿预算过紧(fleet-shutdown.ts:208-213):projection cap 1.2s,每次冷启 pm2 jlist 子进程在 30+ daemon 同退的 CPU 风暴下常态超过 → 「fleet state unverified, no compensation attempted」:旧 fleet 已灭、新 fleet 未起、不补偿。修法:cap 下限 ≥5s + 超时重试。
B3. doc-comment 分流回归:波及全部非 codex-app CLI(cli.ts:7232 / 7360 / 7577)
exactOriginDispatch 唯一来源是 codexAppDispatchLedger(仅 codex-app 写入),doc-comment 分流硬性要求 deliverySink === 'doc_comment' → Claude/Gemini 等 20+ CLI 的 /watch-comment、/subscribe-lark-doc 会话里 botmux send 不再分流,落进 sendMessage(appId, 'doc:xxx', …) → 飞书 400 直接失败(基点对所有 CLI 均分流,已对照确认)。修法:无 ledger 时回落 docCommentTargets[originTurnId] 分流(沿用新的 originTurnId 权威与 payload 校验)。
Major(强烈建议随本 PR 修)
- M1 codex-app-control.ts:1188-1190 — v2 兼容分支恒真式:查的是 record 自身 generation(校验已保证 undefined)而非目录 identity 的 generation → 混版本窗口 v2 记录匹配任意同 dev+ino 的 v3 目录,generation-marker 防护对 v2 静默失效(假阻塞 10s→spawn 失败,代理已实测复现)。一行修:
identity.generation === undefined。 - M2 codex-app-control.ts:1007-1010 — 双 generation marker 抛普通 Error 穿透 retry → lease 目录永久毒化无自愈(SIGKILL 落在 mkdir 与 marker 写入的 1s 窗口即可制造,代理实测复现),违背自身 SIGKILL 可恢复设计。修法:多 marker 状态可回收化。
- M3 codex-app-runner.ts:703-769, 1119 — identity conflict 只发 diagnostic 不 settle
turn.done(已核)→ runner FIFO 永久卡死 = 永久假忙,恰是本 PR 要消灭的形态。修法:冲突时发带说明的 error-final 让 FIFO settle。 - M4 worker.ts:7695 之后 — spawn generation 守卫只盖了
prepareCodexAppControlGeneration,紧随的prepareCliPluginGenerationAndGatewayawait 后与 backend.spawn 前无重查 → restart 竞态双 spawn(基点竞态残留,PR 自建的 CliSpawnSupersededError 机制覆盖不全)。补一行 generation 重查。 - M5 worker.ts:10684-10707 — codex RPC hybrid FRESH 下 queued activation token 无 ACK 发射点(prompt 被 turn/start 预发、不进 pendingMessages)→ admission gate 恒真,后续消息全部进 tail 永不投递(消息黑洞)。RPC hybrid 默认 OFF 故未列 blocker;修法:非入队消费路径补发
queued_activation_submitted,或铸 token 前断言 ACK 可达。 - M6 command-handler.ts:1643-1682 × daemon.ts:16273 — a980cf8 启用 pendingRepoCommitInFlight 后,文字 /repo 提交窗口(sessionReply+deleteMessage 0.2-2s)内的同 anchor 消息进 pendingFollowUps,而 pty 会话此后无任何消费路径 → 静默吞消息 + 残留 buffer 可能陈旧重放。修法:finally 清 flag 时 drain 新增 followUps。
- M7 worker.ts:8823 × managed-origin-capability.ts:521 — macOS 读隔离下
hasMatchingManagedOriginCapability无 channelId 形参、relay 恒 null → ready-gate 预检恒 false,静默降级 readyPattern + 每次 spawn 噪音日志(明确回归;daemon 生产在 Linux 故非 blocker)。 - M8 dashboard-ipc-server.ts:3049-3090 — 读隔离开关 teardown 探测对含 closed 在内的行做
kill(pid,0),crash 残留 pid 被复用后恒「活」→ 409 永久卡死开关且无自愈;另 legacy 行触发同步 tmux/herdr/zellij 探测阻塞事件循环。修法:只探 active 行 / pid 加出生身份校验 / close 统一清 pid。 - M9 dashboard-ipc-server.ts:2653 等 4 处 — protected-ownership 守卫把
queued===true(待办池)和pendingRepoSetup也算保护态且对所有 CLI 生效:任意 bot 挂一张待办卡即无法 dashboard 切 agent,错误码却叫codex_app_dispatch_pending。修法:非 codex-app 变更收窄守卫或独立错误码+列出阻塞会话。
Minor(可 follow-up,摘要)
- codex-app:turn/started 预接受缓冲按 method 而非 turnId 去重(Goal continuation 先到会顶掉真 started);
.retired-*残留无清扫;steer 拒绝×completed 晚到窗口;控制队列 2048 触顶自杀(长断连下杀死可 warm-reattach 的 CLI)。 - /repo:flag 置位→return 间抛出则永久泄漏(置位挪最后);command-handler.ts:1814 悬空 deleteMessage 无 catch(daemon 无 unhandledRejection handler,可达即崩);卡片路径不置 flag 的双入口不对称。
- 关停链:
explicit_close_in_progress双语义错误串(backend 级可达时 worker 永久持 stale requestId);任一 Riff 会话有排队输入即拒整机 shutdown(一个会话可无限阻塞 fleet 重启);SIGTERM 1s 抢锁失败即放弃优雅停机(高负载下恰好丢掉三段 drain);成功 shutdown 路径卡在 gate 的消息静默消失(~14s 窗口);restart-report 45s 等待窗与 verify 预算无共享常量;混版本 intent 消费;supervisor client 整体超时把已 ACK daemon 一并判失败。 - CLI:pm2-jlist 尾部噪音判 malformed;descriptor-guard fresh 分支缺出生身份复核(PID 复用 90s 误判窗);picker stdin async 重入;send-dispatch hookOrigin 单独缺席静默丢;sleepSyncMs SAB 兜底 no-op。
- worker:crash-loop respawn 被 superseded 时静默吞触发消息;ACK 后 promote 失败×worker 退出卡死 initialStartPending;transferSession 终段绕 per-key 锁;initPromptMaterialized 未挡 flushPending;混 build 窗口硬失败面(release note 注明)。
- store:deferredIntent 新鲜度锚容器 at(围栏期 update 报告可静默丢);persistActiveRiffLineageExact 宽松 reader 把 IO 失败误类为 retain_fence;queuedActivationTail 无上界;closeSession 附件簿记先删后清理;daemon 启动×离线 CLI 写残余竞态;macOS 旧 pane 升级后 fail-closed 需重建(PR 描述注明);sandbox-store staged API 注释失实。
- 其它:v3 session-relay-client.ts:103 端口优先级翻转(spawn-time env 压过在线 discovery,daemon 重启端口漂移后 v3 mutation 打死端口;应
claim.ipcPort ?? discovered ?? envPort);restore 屏障连 supervisor shutdown 路由一起挡(restore 卡死时救场通道不可达);gate fail-open 无强制/遥测。
验证记录
- 两个 head 均
pnpm build绿。 - 全量测试(BOTMUX_SCHEDULE_TIMEZONE=America/Los_Angeles):老 head 648/649 文件(唯一败 v3-distillation×6 = 本机既有基线,PR 未触及);新 head 665/667,失败 = v3-distillation×6 + session-discovery.smoke×2(纯 master cb55621 复现,既有基线)+ workflow-v3-ephemeral-pool×1(flake:单跑绿、master 侧新测试)。无 PR 归因的新增失败。
- 7 个模块并行对抗审查(lease/worker/daemon 并发/Riff+PM2/store/CLI 侧/adapters)+ 全部高严重级 finding 由我逐一二次核实(代码直读/基点对照/调用链追踪);两个代理对 B2 独立撞车互证。
- 正面清单(未发现问题):lease v3 抢锁原子性与 TOCTOU 防护、Ed25519 signed state 不可伪造性、dispatch ledger 有界性与 exactly-once、store 白名单/own-write/无锁 RMW 三大历史坑逐项未踩、managed-origin attestation 全面 fail-closed 且权限只收窄、session-mutation-guard 覆盖面、anchor FIFO 入队同步性与 GC 身份守卫、riff 三段 fence crash 点逐一可恢复、PM2 事务文件锁与出生身份世代绑定、共用 adapter 契约全 optional 无跨 CLI 误分支。
修复进度:已启动(head
|
|
修复进展(阻塞项阶段):
当前验证:
|
修复进展(复审 blocker + major 已完成,待推送)针对 CHANGES_REQUESTED @
验证
下一步:整理 commit、确认作者远端 head 未漂移后安全推送,再请求 Claude 基于新 head 独立复审。 |
修复已推送作者分支在本轮修复期间从
组合 head 验证:
请基于 |
deepcoldy
left a comment
There was a problem hiding this comment.
PR #537 delta 复审(head 141cd63,基于已审 72783ba + 修复 delta)
结论:APPROVE
上一轮 REQUEST CHANGES 的 3 组 blocker + 9 major 已逐项验证修复到位;作者中途新增的 129686e(幂等 replay settlement)与修复 commit 无交叠区、组合无回归。
逐项验证
B1 Lark 事件层
- 死锁:
waitForSeedRoutingGate以 forwardFollowupWaitMs 为界,超时警告继续(event-dispatcher.ts:2126)——乱序种子/回复不再互等 ✓ - bot 路径(self-/close 2229、bot @mention 2360)改「同步入队 canonical FIFO(capMs=0) + 立即释放 lane」,与人类路径统一,5s 逃逸不一致消除、整群串行消除 ✓
- 外层 ingress lane 恢复默认 5s cap(2972 移除
,0)——lane 级永久卡死类风险消除 ✓ - sweeper 改
tryWithBotTurnMutation(1s 有界,超时跳过本轮)(idle-worker-sweeper.ts)✓ - Lark SDK 全局 15s HTTP 超时(bot-registry + client 探测客户端双路径)封掉已知无界 await 源头 ✓
- stale seed 分支改异步双入队(先 seed 后当前消息,同步 append 保序)✓
- 残余观察(P3 不阻塞):canonical FIFO 仍为无界严格 FIFO——若 handler 在 SDK 超时之外的路径挂死,单 anchor 仍会滞留(claim 已持久化);滞留面已从整群收敛到单 anchor 且已知源头有界。stale seed 分支在 sessionsReady 未决的启动窗口有极窄同 anchor 重排窗(需 mention-mode 变更×启动×burst 三重合)。
B2 PM2 链
- verify 预算
max(60s, 2s×进程数),restart/start/start-bot 三处应用(cli.ts:267-276)✓ botmux restart|stop --bootstrap-shutdown-protocol --yes真实实现(cli.ts:3170 bootstrapDeleteAllBotmuxProcesses):逐行 name+pm_id+PID+出生身份 CAS、每次 delete 前重读复核、世代变化即拒、legacy PM2 home 同护、拒与 --include-pm2 组合;两处错误文案与 help 均指向确切命令 ✓- fleet 补偿预算生产分档(reserve 10-25s / projection ≥5s / exact-start ≥10s)+ projection 双尝试重试 + FLEET_DAEMON_EXIT_WAIT 35→60s ✓
B3 doc-comment
isOriginDocCommentTurn = ledger 判定 || (!exactOriginDispatch && cliId!=='codex-app' && docTarget)(cli.ts:7551-7556):非 codex-app CLI 恢复分流且 payload-shape 校验同覆盖;codex-app 保持 ledger 独占 fail-closed(settled 后不回落可变态)✓
M1-M9
- M1
identity.generation === undefined一行修(codex-app-control.ts:1314)✓ - M2 多 generation marker 可恢复:observe 返回全 marker 及逐 marker 双 lstat 验证;acquire 循环先证死全部 actor(live/fresh→retry)→grace→
retireAmbiguousPosixDirectoryGenerations逐 marker inode CAS rename-out(随机 retired 后缀防覆盖)→rmdir;失败走既有 retry 预算,有界收敛 ✓ - M3
reportIdentityConflict现发显式 error-final(稳定 client turn id、不复用 native 文本)、清 nativeActiveTurnId、settleturn.done——FIFO 不再永久卡死 ✓ - M4 spawn generation 重查补两处:plugin-gateway await 后(worker.ts:7987)+ control bootstrap 准备前(9061)✓
- M5 RPC hybrid FRESH
accepted分支补发queued_activation_submitted(worker.ts:541-551,daemon 侧 worker-pool.ts:4142 按 sessionId+token 校验)✓;残余:ambiguousoutcome 仍无 ACK——罕见降级态,tail 持久化、重启重放,恢复面为 notify/web terminal,fail-closed 非静默黑洞,可接受 - M6 daemon.ts:16370 从缓冲条件移除
pendingRepoCommitInFlight:提交窗口普通轮直达已 fork worker,不再进无消费路径的 source buffer;flag 收窄为二次 repo 选择互斥(比我建议的 drain 方案更简洁且正确)✓ - M7
hasMatchingManagedOriginCapability增 channelId 形参,worker 传readIsolationOriginChannelId(worker.ts:9351)✓ - M8 双端修:
closeSession原子清 pid(含失败回滚恢复 priorPid)+ toggle 移除 pid 探测与 legacy 三 CLI 同步 fan-out(顺带消除事件循环阻塞)✓ - M9
protectedSessionMutationReasons8 种 reason 枚举;仅当全部 reason 为 codex dispatch 才报codex_app_dispatch_pending,否则session_mutation_pending+blockingSessions(sessionId/cliId/reasons) 列表 ✓ - 附带:上轮 minor「turn/started 缓冲按 method 去重」也已修为 method+turnId(codex-app-runner.ts:809-811)✓
作者 129686e 组合检查
CodexAppControlRecordApplicationGate:per-(generation,seq) 单飞行 application,release 置于 replay-window commit 之后/拒绝路径——关闭「resolution→replay-commit」间隙的双重应用竞态;accepted(start/chunk)早退不进 gate 与不承诺 ACK 的语义一致projectCodexAppControlReadinessStatus:签名 accepting-input 状态观测前 idle 一律投影为 working——与「runner signed state 唯一 owner」方向一致- 与 141cd63 的 worker/control 改动无交叠行,组合路径(engageCodexRpc ACK、spawn 重查、readiness 投影)逐一核过无交互回归
测试验证
pnpm build绿。- 全量(BOTMUX_SCHEDULE_TIMEZONE=America/Los_Angeles):665/667 文件、10624 passed / 1 skipped;仅剩 v3-distillation×6 + session-discovery.smoke×2 两个已知宿主环境基线(后者上轮已在纯 master 复现)。无 PR 归因新增失败。
未阻塞的遗留(follow-up 即可)
上轮 ~25 条 minor 未在本轮处理(.retired-* 残留清扫、explicit_close_in_progress 双语义错误串、queued 输入拒整机 shutdown、restart-report 45s 窗口、v3 relay 端口优先级、restore 屏障挡 shutdown 路由、queuedActivationTail 上界、transferSession 绕锁、picker stdin 重入等),均为低概率/可观察项,建议挂 follow-up issue 逐步消化。
问题
Codex App 路径下有三类用户可见故障:
同一批排查还发现,Riff 关停可能留下半关闭状态,PM2 恢复可能把旧实例误认为新实例已就绪。
根因
旧实现允许 CLI 画面、worker 内存、runner 通知和进程管理器分别推断当前状态,且这些来源可以互相覆盖。输入链路也缺少统一的持久化、提交与 ACK 边界,异常时可能发生重复提交、丢失、插队或错误归属。
修复
变基与验证
本 PR 已重新变基到
origin/master@cb556211,head 为72783ba8。在独立、干净的 PR1 worktree 中:
git diff --check和tsc --noEmit通过;未执行 daemon 重启、真实飞书 live E2E、Riff 远端任务和 PM2 生产式恢复演练;这些项目阻塞发布验证,不阻塞代码审核。
后续 PR
普通 Codex / RPC 生命周期修复以本 PR 为前置:PR #571。请先审核本 PR,再处理第二个 PR。
详细说明:https://bytedance.larkoffice.com/docx/TZUydIskCoJZT8xfMXzc2WI5ntc
本次未执行合并。