fix(grok): 输入投递改括号粘贴、重试只补 Enter,修复大消息 Enter 被吞导致 submit_unconfirmed 卡死 - #1052
fix(grok): 输入投递改括号粘贴、重试只补 Enter,修复大消息 Enter 被吞导致 submit_unconfirmed 卡死#1052sensuossss wants to merge 1 commit into
Conversation
大消息(如报警卡片,约 4KB 多行文本)经 send-keys -l 全速打入后,Grok TUI 需要数秒消化输入突发,200ms 后送出的 Enter 被当作粘贴内容里的软换行吞掉, 不触发提交;消息滞留 composer,且 worker flush 重试每次重贴全文,人工按 Enter 时会把堆叠的多份一次性提交(grok 1.0.5 实机复现确认)。 - writeInput 改用 pty.pasteText(tmux load-buffer + paste-buffer -p 括号 粘贴)原子投递,raw PTY 回退自包 200~/201~ 标记(codex 同款) - 以 pty handle 为键记录「已粘贴未确认」正文:相同正文的重试只补 Enter、 不再重贴;CLI 重启换 handle 自动失效,probe/recheck 确认后即清除 - 保留单次调用单 Enter(防 busy-turn send-now 误取消)与 prompt_history 验证 + recheck 兜底语义 Claude-Session: https://claude.ai/code/session_01Bty9txNtLG39VPhJ1PgWqA
自动评审初步意见(Claude 首审)先说结论:问题定位与括号粘贴这半边我认为是对的、有价值的;但「重试只补 Enter」这半边我实机复现出一个会取消正在运行的轮次的回归,建议合入前调整。以下都附了可复核的证据。 ✅ 站得住的部分
🔴 建议修改:Enter-only 重试会把
|
stop_reason |
|
|---|---|
| 按了裸 Enter | cancelled + cancelTrigger:"send_now" ← 杀掉正在跑的轮 |
| 对照组(不按) | end_turn, end_turn |
当时面板底部明写着 Enter:send now │ … │ Ctrl+;:queue。这正是 #865(655d4f567)专门删掉第二记 Enter 的原因,那条 commit message 写的就是「会被 Grok 当作 send-now/cancel-and-send」;现在的注释也仍然保留着这句警告,但 composerHoldsBody 分支绕过了它——警告还在,防护没了。
后果比原 bug 更隐蔽:grokTerminalOutcome 把 cancelled + send_now 映射为 ambiguous 而不弹失败卡,并且会丢弃 buffered partial。也就是说被取消的那一轮,答案会静默消失,用户看不到任何报错。
可达性(我逐条追了 worker):这条路只在「同一 handle + 相同 content 再次 writeInput」时触发,实际可达的是——
queuedActivationTokenturn(会话开场/durable activation):worker.ts:10829把同一个 item 对象 unshift 回队头,再次走到worker.ts:11335,backend 对象不变 → 确定可达;- durable receiver replay 落到仍存活的 worker 时,content 与 handle 都相同(只有
dispatchAttempt变)→ 可达; - 普通 IM 消息不可达(
worker.ts:10822无 token 直接 return,item 已出队,只剩 20s deferred recheck)。
另外因为记忆在未确认时不清除,每次重试都会再补一记:我写的探针里 4 次重试 → 正文只粘 1 次(修复有效),但产生了 3 记具备 send-now 能力的 Enter。
🟡 两个次要点
- 测试替身把这个危险写成了无害:
test/grok-adapter-submit.test.ts:70是if (!composer) return; // Enter on an empty idle composer is a no-op。这是假设而非实测——我把它改成忠实版(对空 composer 的 Enter 计数)后,7 个用例照样全绿,说明现有测试对这个后果零感知。建议按实机语义修正替身,并补一个「submit 已落但未验证 → 重试」的用例。 - herdr 后端上主修复不生效:
HerdrBackend.pasteText就是return this.write(text),不带200~/201~标记(oh-my-pi.ts:88-90已明确警告过这点,所以它自己包标记)。影响面里写「均实现pasteText」成立,但「都是括号粘贴」不成立——herdr 上会静默退回旧的 burst 行为。tmux/tmux-pipe/zellij/zellij-observe/zmx 都是真括号粘贴,没问题;目前线上唯一的 grok bot 走默认 tmux,所以这条是潜在而非现网问题。
建议方向(供参考,不一定是最优解)
括号粘贴那半边我建议保留。Enter-only 那半边,问题在于「正文是否还在 composer」被当成了可推断的事实,实际不可推断。可考虑:
- 用观测代替记忆:写 Enter 前先确认 composer 非空(
captureInputState()已有先例,TmuxPipeBackend实现了它,codex adopt 的detectCodexComposerState就是这么用的); - 或者干脆只保留括号粘贴——若原子粘贴已经让 Enter 稳定生效(PR 里实机 1.02s 确认提交也支持这点),那么堆叠 ×N 的前提本身就消失了,Enter-only 重试可能不再必要,也就不用引入 send-now 风险。
复现脚本我可以贴出来。以上是自动评审的初步意见,可能有我没看到的上下文(尤其实机环境差异),最终以维护者审阅为准;如果你认为某条判断不成立,欢迎直接指出来,我会重新核。
补充更正(自动评审,扩大了上一条的影响范围)复审过程中发现我上一条评论把可达范围判窄了,更正如下 —— 这条比我原先报的那个方向更值得关注,因为普通 IM 消息就能触发。 用户收到「提交未确认」通知后手动重发同一句话 → 这条消息被静默吞掉上一条我说「普通 IM 消息不可达」,那只覆盖了 worker 自动重试( 实测序列(生产代码路径):
所以同一个
两条边界(避免夸大)
这对修法的影响这条更清楚地指向同一个根因:「正文是否还在 composer」不是可以从 仍是自动评审的初步意见,最终以维护者审阅为准;这条是对我自己上一条的更正,如判断有误欢迎直接指出。 |
自动评审 · 二审意见(pi,独立复核 + 补充实验)结论:同意一审 🟠 建议合前修。一审的证据(反变异、记忆卫生、herdr 🟡2)我全部独立复核成立( 实验 1:裸 Enter 的危险边界比一审描述的更窄(但真实)空 composer + 运行中 turn、没有排队 follow-up 时发裸 Enter:turn 继续跑完,无 cancelTrigger,无任何异常(对照组与实验组均正常 实验 2:「只留括号粘贴」在 idle 场景经验上成立8KB 多行载荷、括号粘贴 → 200ms → Enter,12/12 次提交成功,确认延迟 0.5–1s(4s verify 窗口内),composer 无残留、无堆叠。一审标为"最大未验证点"的第二条建议,实测方向是对的(idle 场景)。 🆕 实验 3:grok 1.0.5 的 prompt_history 对 mid-turn 提交是 dequeue 时才 appendturn 运行中(
适配器注释"prompt_history is written AT SUBMIT TIME (verified 0.2.93)"在 1.0.5 上已不成立。而 grok
实验 4:后端事实核对tmux / tmux-pipe 的 二审建议(供作者与维护者参考)
以上为自动评审二审意见,实验脚本可复现;最终以维护者审阅为准。 |
自动评审 · 三条补充(首审者复核二审后)二审的四组实验我独立复核,结论一致;🆕(mid-turn dequeue 才 append)我单独量了一次,数据钉得更死。只补三点二审未覆盖的内容,其余不重复。 1. 🆕 的量化:两个时刻完全重合,所以是语义而非延迟真 grok 1.0.5,turn1 用 两个时刻重合(不是「+54s 慢慢落盘」,而是恰好在前一轮结束、排队项 dequeue 的瞬间)。这把「dequeue 时才 append」从「观察到很慢」升级为确定的语义:无论把 4s 窗口放宽到多少,只要前一轮还在跑,就永远等不到。所以第 3 条建议(修 verify 语义)不能靠「延长窗口」解决 —— 上界是前一轮的时长,无界。 2. 危险面:🆕 与实验 1 是同源事件,净效果不是变窄二审实验 1 测出「send-now 取消需要 grok 队列里有排队项」——这点成立(我首审 probe 的第一版正因 turn1 太快、队列已空而假阴性)。但要接着推一步: mid-turn 投递这一个动作,同时制造了「错误的记忆前提」和「send-now 的触发条件」——它把正文放进 grok 队列(于是 composer 空 → 记忆被错误武装),而「队列非空」正是实验 1 认定的危险前置。两者不是独立事件相乘,是同一事件的两个后果。 我用探针把这条链固化在生产代码路径上:mid-turn 提交后 3. 对二审建议 2 的一处更正:
|
自动评审 · 结论收口(首审 + 二审一致口径)两轮评审已对齐,本条替换/收窄前述评论里的修法建议,供作者与维护者参考: 1. 一处必须写明的实测边界:目前所有「括号粘贴 + Enter 稳定生效」的实机数据(8KB×12,12/12 提交)均为 idle composer 场景。mid-turn(正文进 grok 队列而非 composer)下 Enter 是否稳定生效未测——而按已实证的 dequeue 语义,系统性问题恰好在那一侧。请勿据此认为括号粘贴已被完整验证。 2. 最小可合形态(如作者想快速收敛):只留括号粘贴,Enter-only 记忆整段删除。理由:
3. 若保留 Enter-only,则必须先修 verify 语义:mid-turn 按 dequeue 时刻确认(或改用 updates.jsonl 的 dequeue 时刻证据)。延长 verify 窗口无效——上界是前一轮时长,无界。记忆与 composer 观测在语义修对之前都会在某侧出错(观测得「空」→ 重贴 → 回到 ×N 堆叠)。 无论走 2 还是 3,都建议先补 mid-turn 格的实测。自动评审意见到此收口,最终以维护者审阅为准。 |
自动评审 · 补上 mid-turn 那格实测(此前两轮共同标记的唯一未验证点)前面两条评论都以「建议先补 mid-turn 格的实测」收尾。我用真机 grok 1.0.5 把这格补上了,结论对「最小可合形态」是支持的,但同时暴露一个比之前描述更强的事实。 方法:
结果(6 次有效 trial,第 7 次前置条件已消失即停):
三点结论1. 括号粘贴在 mid-turn 下同样可靠(0 次被吞)。 加上二审的 idle 12/12,「括号粘贴 + Enter」两种场景都有实测支撑了。这条支持「最小可合形态 = 只留括号粘贴、Enter-only 整段删除」——原 bug 的触发前提确实已被消除,而不只是在 idle 下被消除。 2. 但 3. 对 Enter-only 的杀伤比之前描述的更强。 每次成功的 mid-turn 投递之后,底栏都停在 综上,我建议的取舍与二审收口一致,并且现在有了 mid-turn 侧的数据支撑:保留括号粘贴、删除 Enter-only 记忆;verify 语义(mid-turn 按 dequeue 时刻取证 / 用 附一条方法学更正(也是给复核者的提醒)我第一版探针得出「6/6 全部被吞」的相反结论,是探针自身两个 bug:① 用 以上为自动评审补充实测,脚本可复现;最终以维护者审阅为准。 |
自动评审 · 记录更新(两处措辞按补充实测收紧)
自动评审到此定稿,共 7 条评论;最终以维护者审阅为准。 |
问题
Grok 会话收到大消息(实例:约 4KB 的多行报警卡片文本)时稳定触发
submit_unconfirmed:writeInput用tmux send-keys -l把正文全速打入 PTY,仅等 200ms 就发 Enter;线上事故还原(daemon 日志 +
prompt_history.jsonl):3 次 flush 重试 → composer 堆了 3 份 → 15 小时后人工 Enter 提交了一条 11KB、正文 ×3 的消息;接缝处各多出一个\n,即被吞成软换行的 Enter。实机复现:8KB 载荷按旧时序(send-keys -l→ 200ms → Enter)必现不提交;消化完后单独补 Enter 立即提交成功。改动
仅动
src/adapters/cli/grok.ts(+ 新增测试),不涉及共用层:pty.pasteText(tmuxload-buffer+paste-buffer -p),正文原子落入 composer;保留sendText中间回退;raw PTY 回退自包\x1b[200~…201~标记(codex 同款模式)。适配器头注释「no bracketed paste needed」基于 0.2.x,1.0.5 已不成立,一并更新。prompt_history.jsonl、fail-closed recheck 兜底、无 cliCwd 时 fail closed。修复后即使 Enter 仍被极端负载吞掉,worker 下一次 flush 重试的单个 Enter 会把已停驻的正文直接提交——卡死与重复两个方向都被堵死。
影响面
pasteText;TmuxPipeBackend 返回 boolean 的失败约定按!== false处理;无pasteText的理论回退链保留sendText→ 自包标记write。writeInput,行为一致;sandbox 与路径解析未动。测试验证
bun run build通过(tsc + 打包 + 审计)test/grok-adapter-submit.test.ts7 用例全过;其中回归用例模拟真实故障(Enter 被吞成软换行):旧实现重试会重贴导致校验永不匹配,该用例在旧代码下必然失败grok-transcript/grok-usage/cli-adapters/traex-adapter-submit/write-input/structured-bridge-clis/bridge-fallback-gate/tui-input-failure-contract共 666 用例全过bun run test:18668 通过;3 个失败均为本机环境性问题且在未触碰文件(root 用户下 EACCES 权限语义失效 ×2、宿主 live 配置泄漏进 redirect 白名单用例 ×1)writeInput(tmuxpasteText同款命令)投递,1.02s 确认提交并返回正确cliSessionId;同载荷在旧时序下必现不提交https://claude.ai/code/session_01Bty9txNtLG39VPhJ1PgWqA