fix(kimi): 修复多行输入未提交 - #788
Conversation
There was a problem hiding this comment.
Pull request overview
该 PR 修复 Kimi Code TUI 在收到 Botmux 首条包含多行内容的 routing prompt 时,因换行走按键事件路径而导致“未作为一条消息提交”的问题;通过统一输入提交流程,确保多行内容以一次性提交的方式进入 Kimi,从而避免用户侧“消息发出后无回应 / 仍显示 No session yet”的现象。
Changes:
- Kimi 适配器的输入写入统一改为显式 bracketed paste(
\x1b[200~...\x1b[201~)后再单独发送 Enter,并在文本写入/Enter 明确失败或异常时返回{ submitted: false }。 - 补充 Kimi 在单行、多行、Unicode、以及发送失败(Enter 被拒绝 / 传输抛异常)场景下的单元测试覆盖。
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| test/write-input.test.ts | 将 Kimi 纳入 writeInput 行为矩阵,并新增对 bracketed paste + Enter 及失败返回的测试用例。 |
| src/adapters/cli/kimi.ts | Kimi writeInput 改为显式 bracketed paste + Enter,增加失败/异常路径的 { submitted: false } 返回。 |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
deepcoldy
left a comment
There was a problem hiding this comment.
感谢修复。括号粘贴 + 独立 Enter 的主修复方向是正确的,与 coco / claude / codex / oh-my-pi 处理多行 TUI 输入的既有做法一致。但新加的「提交结果判定」语义有一个 blocking 问题,合并前需要修正。
🔴 Blocking:把「歧义的传输失败」当成「干净的未提交」,会导致重复提交
背景:当前 master(合入 #597 后)的 PtyHandle 契约已明确——sendText / sendSpecialKeys 返回 false 只表示无法确认,字节仍可能已经落地;它不是「零字节送达」的证据。
本 PR 在 writeInput 里把两个 === false 分支和外层 catch 都归类成 { submitted: false }。对首条消息而言(#597 会给它带 queuedActivationToken),worker 收到 submitted:false 后会调用 requeueUnsubmittedQueuedActivation,把同一条消息重新入队,下一次 flush 再跑一遍 writeInput。于是有三条真实的重复路径:
sendText已经把 bracketed paste 写进输入框、但因确认超时返回 false → 本轮不发 Enter;重试再粘一次 → 输入框里出现两份正文,一起提交。- Enter 已经落地、但返回 false → 第一轮其实已经开始执行;重试 → 同一个用户 turn 执行两次。
- 写调用先产生副作用再抛异常(例如
sendText已写入后抛确认超时)→ 外层catch同样归成{submitted:false}→ 同样触发上面的双发。
这是本 PR 新引入的回归:改动前的 adapter 全程返回 void(assume-issued 语义),从不返回 submitted:false,因此从不触发 requeue、也就不会重复提交。
对照 oh-my-pi:它虽然也返回 {submitted:false},但额外有 composerDirty 状态 + 失败后用 Ctrl+C 清输入框、并在未清理干净前阻止继续追加新正文的恢复层。本 PR 没有这一层,所以这里的 {submitted:false} 重试并不安全。
🟡 次要:raw PTY 分支的 write() 返回值未检查(两分支语义不对称)
else 分支里两次 pty.write(...) 都没有检查返回值;#597 之后 write() 在进程已退出时也会返回 false,此时 adapter 仍然返回 {submitted:true}——等于零字节送达却报成功。这一点与其它 raw adapter(pi / gemini / hermes)现状一致、属既有共性问题,本 PR 不强制在此处修;但既然 tmux 分支加了失败检测,建议 raw 分支也一并检查,避免两条分支语义分叉。
建议修法(按推荐顺序)
- 最小收敛(推荐):保留括号粘贴 + 独立 Enter,撤回新增的两个
=== false判定和外层catch → {submitted:false},整体恢复成旧 adapter 的 assume-issued(全程void)语义。注意:只改其一不够——只回退=== false、保留catch,side-effect-then-throw 仍会双发(两者是彼此独立的触发向量)。 - 若要保留传输失败检测:先确认 Kimi 的 Ctrl+C 清输入框 / 运行中取消语义,再做 oh-my-pi 式的 dirty-composer 状态机。
- 通用正确解:在 worker / backend 层引入显式的 ambiguous disposition(禁止把歧义结果当 clean non-submit 自动重试)——但这是公共层改造,会横向影响所有 CLI / 后端,不建议塞进这个两文件 PR,应另开 PR 并做横向测试。
验收用例(重要)
只补「sendText 返回 false → 期望 {submitted:false}」的测试不足以验收,反而会把上面的错误语义固化。请至少覆盖以下断言「不会重复提交」的用例:
sendText产生副作用后返回 false → 不得把同一正文粘贴两次;- Enter 产生副作用后返回 false → 不得重复执行同一 turn;
sendText产生副作用后抛异常 → 同上(覆盖 catch 路径);- raw PTY
write()返回 false → 不得返回submitted:true。
其它
- 建议先 rebase 到最新 master 再改:当前分支落后 master 3 个 commit,其中 #597 正是引入「首条消息带
queuedActivationToken→ 失败即 requeue」的那次改动,也是上面 blocking 问题成立的前提。
15e01ca to
e02349c
Compare
为 PR deepcoldy#788 保存完全脱敏的最终修复前后真实 Kimi TUI 对照截图,不进入产品代码分支。 Co-authored-by: TRAE CLI <noreply@bytedance.com>
|
@deepcoldy @alexander2618 已按 blocking review 完成整改并更新到 Blocking 逐项关闭
实测结果
修复前: 修复后: 全量单测为 13,973 passed / 10 skipped / 4 failed:2 条 Codex runner timeout 单跑通过;剩余 2 条已在最新 |
将 Kimi 的 tmux 输入改为 paste-buffer bracketed paste,避免多行 routing prompt 被 TUI 吞掉提交,并规避大 prompt 经 send-keys argv 传输的长度上限。每个 backend 首次写入前额外等待 250ms,让冷启动 composer 完成初始化;后续输入不增加延迟。保持 assume-issued 返回语义:传输返回 false 或副作用后抛错均不标记 clean non-submit,防止 queued activation 自动重放导致重复执行。保留 raw PTY 回退,并补齐 64KB 大输入、首次写入与歧义传输回归测试。 Co-authored-by: TRAE CLI <noreply@bytedance.com>
e02349c to
6d7693e
Compare
|
@deepcoldy @alexander2618 补充一次 live daemon 验收后的闭环,head 已更新为
验证更新:
blocking review 的 exactly-once 收敛保持不变:仍返回 assume-issued |
|
@deepcoldy @alexander2618 最终 live daemon + 飞书真实首条验收已通过:
至此 bracketed paste、exactly-once assume-issued、冷启动首写时序三项均完成真实端到端闭环,请重新 review / 合并。 |
|
同学可以帮填下这个 https://bytedance.larkoffice.com/wiki/WJ1nwWbtxi89erkNGNbcgkt9nUe |
deepcoldy
left a comment
There was a problem hiding this comment.
复审通过:作者 force-push 6d7693e38 已按最小方案①修好 blocking——writeInput 全程恢复 assume-issued(返 undefined,无 {submitted:false}/{submitted:true}),worker 不再把歧义传输结果当 clean non-submit 自动重放,double-send 回归封闭。
已在最新 master(47ead6e)合并树上独立复核:build 通过;write-input/cli-adapters 聚焦测试全绿;变异验证(将 catch{return;} 回退为 {submitted:false} 时对应测试立即变红)确认新测试能拦住原双发向量。
herdr 后端 pasteText 为无 marker 的 literal write,该后端多行仍可能被拆——但与旧版本行为一致、非本 PR 回归,production 默认 tmux 主路径已修复并有 E2E 闭环;建议后续把描述里的后端表述收窄为明确列举 tmux/Zellij/ZMX 并注明 herdr 限制(纯文案,不阻断)。
双人复核一致 APPROVE。
|
🚀 Released in v3.12.1 |


问题
Kimi Code TUI 收到 Botmux 首条消息时,旧适配器通过普通
sendText写入完整 routing prompt。该 prompt 包含多行 XML、技能上下文和用户消息;在生产规模下,Kimi 会把这段快速输入视为仍未结束的 paste burst,随后发送的 Enter 被吸收为换行,内容留在输入框中。冷启动时还存在一个更窄的窗口:TUI 第一次显示 composer 后立即写入,首条消息仍可能被初始化过程丢弃。用户侧表现为消息发出后没有回应、仍显示No session yet。修复
pasteText能力的后端统一走 backend bracketed-paste 传输,再单独发送 Enter。load-bufferstdin +paste-buffer -p,避免把完整 prompt 放进send-keysargv,同时支持大输入。\x1b[200~...\x1b[201~回退。writeInput在 paste 前等待 250ms,让冷启动 composer 完成初始化;同一 backend 的后续输入不增加该等待。false或副作用后抛错都不转换为 clean non-submit,避免 queued activation 自动重放造成正文重复或同一 turn 执行两次。影响面
src/adapters/cli/kimi.ts与对应输入测试。验证
pnpm buildpnpm vitest run --project unit test/write-input.test.ts test/tmux-backend-input.test.ts test/tmux-pipe-backend.test.tsundefined;undefined;write()返回 false:仍保持 assume-issued,不触发自动重放;pasteText,sendText调用为 0;0.34.0+ tmux2.8真实 TUI:sendTextCalls=0;paste-buffer正常响应;pnpm testtmux-backend-envshell wrapper 与 adopted Pi tmux fixture,已在最新origin/master独立 worktree、同机同命令稳定复现,与本 PR 两个改动文件无关。实证截图
修复前:首轮多行 prompt 留在输入框,仍为
No session yet修复后:同一脱敏 prompt 一次提交,模型返回结果