fix(reaction): 修复 Grok card-off 假 DONE / 卡 GoGoGo - #633
Conversation
reliableTurnTerminal CLI 跳过 post-submit busy-absent 假 idle;DONE 仅 working→idle 翻转;argv 冷启动首 prompt 先报 working 以免反应卡住。
d204557 to
2296f32
Compare
shouldArmSpawnArgvInitialPromptBusy 仅 Grok 类(argv+SessionStart+可靠 terminal)置位,避免 Riff/Pi 假 busy;flushPending 提交时立即上报 working 以覆盖 2s 采样漏掉的短 turn。
deepcoldy
left a comment
There was a problem hiding this comment.
首审(Claude)— 1 处 P2 回归(UX,非 correctness),其余无阻塞
先用白话讲清这个 PR 在做什么,再说发现的问题。未经申晗确认不合码,findings 交 @codex 复审。
白话:这 PR 三处改动的逻辑
卡关背景:card-off(关掉流式卡片)的会话,靠给用户消息打 reaction 表示进度 —— 收到消息打 ✋(GoGoGo),干完活翻成 ✅(DONE)。Grok 出了两个毛病,这 PR 一起治:
-
假 DONE:Grok 提交后,它的 busy 标记(
Waiting for response/Ctrl+c:cancel)常常滞后几秒才出现。老逻辑在 submit 后跑一个「busy 标记不在场 = 空闲」的探针(scheduleBusyPatternIdleProbe),于是提交后几秒就误判空闲、过早翻 DONE。改法:reliableTurnTerminal的 CLI(Grok/Codex/Claude/TRAE)改由 transcript 的assistant_final决定 turn 结束,不再跑这个 post-submit / queued 探针(worker.ts两处加reliableTurnTerminal !== true门)。 -
假 DONE 的另一半:把 ✋→✅ 的翻转从「任何 settle-to-idle 边」收紧成「只有
working|analyzing → idle|limited才翻」(worker-pool.ts加prevStatus === 'working' || prevStatus === 'analyzing'门),避免冷启动 settle 出来的假 idle 误翻。 -
卡 GoGoGo:叠加改动 2 后,argv 冷启动首个 prompt 从没进过 working,第一次 idle 就被门挡住,永远停在 GoGoGo。改法:给 Grok 类(argv 塞首 prompt +
injectsReadyHook+reliableTurnTerminal,当前只有 Grok 命中)冷启动首次 ready 报 working(非 idle),并在flushPending提交瞬间立即 publish 一次 working,覆盖 2s 采样漏掉的短 turn。
方向我认同,Grok 那条链路是对的。
🟠 P2:card-off + {gemini, pi, mtr, opencode} 冷启动首轮,✋ 永不翻 ✅
这个 PR 治好了 Grok 的「卡 GoGoGo」,却在同一处给另外 4 个 argv CLI 引入了同款「卡 GoGoGo」。
改动 2 的门是对所有 card-off 会话生效的,但补偿它的 working 上报只覆盖了两类:
- 改动 3 的 arm —— 只匹配 Grok;
flushPending的即时 working —— 只覆盖排队投递的 prompt。
而 gemini / pi / mtr / opencode 这 4 个 CLI:passesInitialPromptViaArgs=true 但 injectsReadyHook=false(不被 arm),冷启动首 prompt 是塞进 argv 的(不走 flushPending,拿不到即时 working),且冷启动期 screen 采样被 awaitingFirstPrompt 早退(worker.ts:6093)静默 —— 所以 daemon 看到的第一个 screen_update 就是 idle(markPromptReady 里的 publishScreenStatus('idle')),此刻 prevStatus(ds.lastScreenStatus)还是 undefined。
于是门 prevStatus === 'working' || 'analyzing' = false → finishTurnReactions(全仓唯一调用点 worker-pool.ts:3138)不被调用 → 首条 ✋ 滞留。
差分 probe 实证(用 __testOnly_setupWorkerHandlers 驱动真实 handler,喂 prevStatus=undefined → idle,只 mock Lark 调用):
- master:
DONE added=true, removed=true(✋ 正确翻 ✅) - 本 PR:
DONE added=false, removed=false, pendingLeft=[om_a](✋ 滞留)
→ 确认是本 PR 引入的回归,非既存。
可达性:disableStreamingCard 是 per-bot 配置,与 CLI 选择正交 —— 任意 gemini/pi/mtr/opencode bot 都能 card-off;且常规长度首 prompt 走 argv(resolveInitialPromptDelivery 返 argvPrompt)就是默认冷启动路径,非边角。新建话题的三条 spawn 路径(pinned / no-projects / repo-pick)都在 accept 时先 noteTurnReceived 打 ✋、之后才 fork argv prompt,均可达。
严重度:UX-only(reaction 不翻,不涉 correctness/数据);finishTurnReactions 翻整个 pending 列表,所以 turn 2 的 working→idle 会补翻 turn 1 的滞留 → 自愈;永久滞留仅限 one-shot 会话(没有 turn 2)。但首轮(最显眼的第一问)+ one-shot 不愈,且正是 PR 想修的症状被换个 CLI 集合复现。
修法方向(不 prescribe 精确 patch,交作者/@codex 收敛):argv-baked 首 prompt 的 working 上报应覆盖所有 card-off 相关 argv CLI,而非仅 Grok arm。这些 quiescence-argv CLI 的首个 idle 是真 turn end(不是 Grok 那种假 idle),需要在 prompt 开始执行时(spawn 时 preparedInitialPrompt 非空 / ready 后)先 publish 一次 working,让真 idle 到来时 prevStatus=working —— 镜像 flushPending 对排队 prompt 的处理即可。注意别去放宽门的 allow-list({working, analyzing} 对照 ScreenStatus 枚举已完备,放宽会误翻真假 idle)。
✅ 无阻塞 / 正确的部分
- probe-skip 安全:Codex/Claude/TRAE 三个
reliableTurnTerminalCLI 本就没有busyPattern,scheduleBusyPatternIdleProbe对它们一直是 no-op(worker.ts:6394早退)—— 所以 probe-skip 实际只影响 Grok,而 Grok 的 idle 由 transcriptassistant_final → fireIdle(grok-transcript.ts:377→worker.ts:3830)驱动,不依赖这个探针。安全。 - Grok arm 不会永久卡住:arm 后停在 working、
isPromptReady=false,随后turn_completed → assistant_final → fireIdle → markPromptReady(flag 已清)→publishScreenStatus('idle')产生真 idle 边;外加deferFirstPromptTimeoutUntilReady+ first-prompt-timeout 兜底。 - merge 干净:对当前 master
fd455bcf(#624 改了worker.ts+52/-3)trial-merge 零冲突,PR 5 处 worker.ts 改动与 #624 的改动都完整保留,另 6 个文件与 PR head blob 逐字节一致。 - build 绿;PR 三个测试文件 46/46,扩展套件(turn-reactions / worker-pipe / initial-prompt / session-lifecycle)121/121 全绿。
次要:新测试全是 source-level string pin
turn-reactions.test.ts / worker-pipe-initial-screen-order.test.ts 的新用例都是 readFileSync(源码).toContain('...') 形态 —— 锁的是改动的代码形状,从不驱动真实冷启动 status 序列过 handler,所以这个 P2 对现有测试完全不可见。建议随修法补一条 behavioral 回归:驱动真 handler 喂 prevStatus=undefined → idle,断言 card-off 首轮 ✋ 被翻。
|
To use Codex here, create a Codex account and connect to github. |
deepcoldy
left a comment
There was a problem hiding this comment.
Codex 独立复审:确认首审 P2,当前 head b17a7b35 仍受影响
我独立沿 worker → daemon 的真实状态链复核,并用 __testOnly_setupWorkerHandlers 做了 master/PR 差分。结论与 Claude 首审对齐:card-off + Gemini/Pi/MTR/OpenCode 的常规 argv 冷启动首轮会残留 GoGoGo。
时序证据
- 这 4 个 adapter 都是
passesInitialPromptViaArgs=true,常规长度首 prompt 不进入pendingMessages,因此不走flushPending()新增的publishScreenStatus('working')。 - 新增的
shouldArmSpawnArgvInitialPromptBusy()又要求injectsReadyHook && reliableTurnTerminal;目前只有 Grok 命中,4 个 quiescence argv adapter 明确返回 false。 - 冷启动 sampler 在
awaitingFirstPrompt为 true 时直接 return(worker.ts:6093)。 - 真正完成/检测到 prompt 时,
markPromptReady()在同一个同步调用里先isPromptReady=true(5243),再清awaitingFirstPrompt(5266-5268),最后因未 arm 而直接publishScreenStatus('idle')(5320)。因此 sampler 不存在可观察到awaitingFirstPrompt=false && isPromptReady=false的窗口,daemon 在正常短首轮看不到 working。 - daemon 的首个有效
screen_update因而是prevStatus=undefined → idle;新门只允许working|analyzing → idle|limited调finishTurnReactions(worker-pool.ts:3135-3139),首轮 pending reaction 不会翻 DONE。
差分 probe
同一个 behavioral probe:card-off ds、lastScreenStatus=undefined、预置 pendingAckReactions=[om_a],驱动真实 handler 收到 idle:
- PR head:DONE 未添加、旧 reaction 未移除、
pendingAckReactions=[om_a]; - 临时恢复 master 的无条件 idle settle 逻辑:DONE 添加、旧 reaction 移除、pending 清空。
两边 probe 都通过各自预期,确认这是门收紧后、working 补偿覆盖不完整造成的差分,不是 handler mock 假象。
影响与建议
严重度维持 P2 / UX-only:后续 turn 的正常 working→idle 会批量补翻,永久残留主要是 one-shot 会话;但首轮是默认可达路径。修复应在 argv-baked 首 prompt 开始执行时为全部相关 adapter 建立 working 边,同时保留当前 {working, analyzing} 收口,不建议放宽 daemon 门。
验证:
pnpm vitest run test/turn-reactions.test.ts test/initial-prompt-arg-limit.test.ts test/worker-pipe-initial-screen-order.test.ts:46/46 通过pnpm vitest run test/session-lifecycle-hooks.test.ts:13/13 通过pnpm build:通过- 临时 probe 已删除,未改 PR 代码;未执行合并
…g→idle quiescence argv 首 ready 为真结束:先 publish working 再 idle,避免 card-off 门闩下 undefined→idle 永远不 DONE;Grok SessionStart arm 不变。 补 handler 行为回归。
改了什么
reliableTurnTerminalCLI(Grok/Codex 等)跳过 post-submit / queued 的 busy-absent idle probe,避免提交后几秒误判空闲、过早 DONE。working|analyzing → idle|limited时 ✋→✅,避免冷启动 settle 的假 idle。为什么
Grok 配了
busyPattern,提交后 UI busy 标记常滞后,post-submit probe 会假 idle 并提前 DONE;Codex 无busyPattern,不走该路径。叠加上一轮「只允许 working→idle 才 DONE」后,argv 冷启动又会出现「永远 GoGoGo」(从未进入 working)。影响面
busyPattern且 card-off 的会话(尤其 Grok);Codex 无 busyPattern,行为基本不变。worker.ts、worker-pool.ts、adapters/cli/grok.ts(注释)。测试验证