feat(session): 新增 replyDelivery=transcript 转写回复模式,并桥接 Claude Code statusline 配额 - #1310
feat(session): 新增 replyDelivery=transcript 转写回复模式,并桥接 Claude Code statusline 配额#1310lRoccoon wants to merge 5 commits into
Conversation
|
你好!评审群已自动创建:pr 1310 转写回复模式并桥接statusline配额 但你暂未拉入群中——自动拉群名单里你的 GitHub 账号尚未关联飞书信息。请自行把 GitHub 账号和飞书信息补进名单文档:https://bytedance.larkoffice.com/wiki/WJ1nwWbtxi89erkNGNbcgkt9nUe ,补好后后续复审会自动拉你进群。 这是自动流程,如有疑问请联系维护者。谢谢! |
自动评审意见(Claude 首审 + 复审,五轮收敛后的终版)
先说结论:工程质量很高——三个提交切分干净、fail-closed 判定齐备、注释写的是「为什么」而不只是「做了什么」。有 1 条建议合入前确认(F1),其余为非阻断。 验证基线:在最新 F1(建议合入前确认)transcript 下,一次中途
|
|
感谢这份评审,F1 的三条路径都复现了,已在本 PR 内一并处理;rebase 也做了。 F1 修复抑制闸改成 mode-aware:
裸 sentinel / adopt / isLocal / 测试照要求用 多数用例是 send/transcript 双向断言(同一输入两种模式期望相反),实现若失效必然转红。 rebase已 rebase 到最新 master( rebase 后 |
4067757 to
f89cb56
Compare
复审(f89cb564d F1 修复):F1 已正确修复,补一条窄影响面的新点
基线:本分支当前基于 #1323( F1 修复正确,三条路径都堵住了 ✅
worker 把 新发现 F2(窄影响面,建议顺手修;不影响默认用户)
const finalNormalized = normaliseForFingerprint(finalText ?? '');
if (!finalNormalized) return true; // 注释说「keep the old behaviour」,但并非如此
实测组合路径:failed 回合无 send、transcript → 失败卡被抑制;改成下面一行后恢复: if (!finalNormalized) return markers.length > 0;改后 74 个闸测试全绿,空回合诊断与失败卡在无 send 时送达、有中途 send 时仍抑制(与 send 语义一致),正常答案的去重不受影响。 影响面:默认 transcript 的 claude-code 不受影响——它走 辛苦!修复整体质量很高。 |
per-bot 配置 `replyDelivery: 'send' | 'transcript'`(默认 send,行为与之前逐字节相同)。 transcript 模式下: - 系统提示与 shell hints 改口为「最终 assistant message 由 botmux 自动转发回飞书」, `botmux send` 只用于中途推送 / 附件 / 跨 bot @;哨兵 BOTMUX_NOTHING_TO_SEND 语义保留。 - 不再逐轮注入 `<botmux_reminder>`(hook 模式下 sidecar 随之为空,自动回退 inline)。 - solo 会话(p2p,或仅 owner + 本 bot 的 1v1 普通群,fail-closed)用裸文本 + `[附件]` 行 代替 `<user_message>` 壳、`<sender/>`、`<attachments>`、`<mentions>`;identity 块去掉 多 bot routing_rules。判定复用 getChatMode / getGroupStats 缓存,send 模式零额外 API。 - 转写 fallback(模型不 send 时自动转发最终回复)升为主通道,抑制规则不变;最终回复卡 投递成功后该轮流式卡头部标「已完成」(DaemonSession.completedIdleTurnId,FrozenCard.idleLabel 兼容旧盘 silentIdle)。 - 只允许对有转写采集的 CLI 开启(claude-code + structured-bridge 白名单),bot-config-store 拒绝不支持的 CLI,运行时二次 fail-closed;/botconfig set、dashboard 开关、bots-json 文档。 影响面:改了 session-manager / shared-hints / worker-pool / card-builder 公共层,所有新分支 都以 replyDelivery === 'transcript' 为前置;adopt、v3 workflow(V3 marker 强制 send)、 oncall 多人群、话题群、HTTP no-transport 会话路径不变。 验证:tsc --noEmit 通过;vitest 定向 16 个文件 1176 用例通过(prompt-builder / prompt-hook-injection / cli-adapters / bridge-turn-queue / reply-delivery / bot-config-store / dashboard-ipc / card-builder / recall-frozen-cards / bridge-final-output-retry 等)。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YMAYq1f2eZstsgAu2HKFmN
claude-code 的转写没有上下文窗口字段,卡片只能显示上下文绝对值,且完全没有账号配额 信息。Claude Code 的 statusLine 机制会把 context_window.used_percentage 与 rate_limits.five_hour / seven_day(used_percentage、resets_at)喂给配置的命令,本次把它 接进卡片: - 新子命令 `botmux statusline`:读 stdin JSON → 原子写 <DATA_DIR>/statusline/<sessionId>/latest.json(目录粒度:沙盒对单文件只授 readWrite, tmp+rename 需要目录可写);BOTMUX_STATUSLINE_CHAIN 非空时把原始 stdin 字节转发给用户 自己的 statusline 命令并透传 stdout / 退出码(10s 看门狗按进程组收尾),用户的终端 状态栏不被遮蔽。任何失败都不影响 Claude;BOTMUX_WORKFLOW 白名单放行。 - claude-code buildArgs 的进程级 --settings 恒注入 statusLine(refreshInterval 60s, 冷启动实测 node 0.34s / 二进制 0.24s);spawn 前按 Claude 优先级解析被遮蔽的 statusLine.command 写入 BOTMUX_STATUSLINE_CHAIN(child-env 白名单、tmux pane 转发); 沙盒预创建目录并在 fs-policy 两分支授权。 - services/statusline-snapshot.ts:parse / 原子写 / mtime 缓存读 / 10 min 陈旧丢弃 / 已滚动窗口丢百分比;worker-pool getDaemonSessionUsageSnapshot 对 claude-code 合并 quota,回复卡页脚、流式卡用量行、IPC /usage 三个读取点一处覆盖;normalizeCardUsageSnapshot 迁到 cli/card-usage-normalize.ts 并放行 quota。 - md-card:有 statusline 数据时上下文段渲染纯百分比 `ctx 23%`(不带绝对值、不画进度条、 不渲染 resets_at),追加 `5h N%`、`7d N%`;「建议压缩」阈值对 claude-code 首次生效。 无数据时输出逐字节不变。 影响面:非 claude-code CLI 的 quota 恒为空,走原渲染分支;wrapperCli=aiden 剥掉 --settings 时无数据、卡片省略;disableCliBypass 的会话现在也会带一个只含 statusLine 的 --settings。 验证:tsc --noEmit 通过;vitest 定向 statusline-cli / statusline-snapshot / claude-settings-hook / cli-adapters / md-card / worker-pool-statusline-quota / card-usage-normalize / hook-command / fs-policy / child-env / tmux-backend-env / dashboard-ipc 共 1078 用例通过。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YMAYq1f2eZstsgAu2HKFmN
…mux send 用户反馈:最终回复已由转写通道自动转发,claude-code 在任何情况下都不该再被教 「用 botmux send 回复」。 - 缺省值按 CLI:`defaultReplyDeliveryFor` 让 claude-code 缺省 transcript,其它 CLI 缺省 send;bots.json 显式 `"send"` / `"transcript"` 才覆盖,`unset` 回 CLI 缺省。 bot-config-store 不再把 send 归一为删 key(那是 claude-code 退回旧行为的唯一方式), `/config get` 缺省显示随 CLI;dashboard GET 返回生效值与 `replyDeliveryDefault`。 - transcript 模式的系统提示只保留:自动转发说明、`botmux history` / `botmux bots` 辅助命令、`BOTMUX_NOTHING_TO_SEND` 哨兵(仍是转写 fallback 的抑制规则)、workflow、 防注入、白板;去掉 send 用法、heredoc、@ 三选一、附件参数;identity 在非 solo 下保留 三条归属规则、去掉「协作必须 botmux send --mention」;非注入式 CLI 的 short_routing 同样不再注入。附件 / 跨 bot @ 的能力仍可经 `--plugin-dir` 的 botmux-send skill 按需发现。 - 删除不再引用的 `ai.routing.usage_send_transcript`、`ai.shell.how_to_send_transcript`; 改写 `intro_transcript` / `when_to_send_transcript` 为不含 send 的措辞(zh/en)。 - 文档:bots-json 的 replyDelivery 行与小节按新缺省改写;顺手给 senderTag 说明段补回 丢失的小标题(e0effcda 插入 replyDelivery 小节时把它挤成了上一节的尾巴)。 影响面:显式 `replyDelivery: 'send'` 与非 claude-code CLI 的输出逐字节不变;claude-code 未配置时从本次起进入 transcript(系统提示部分需会话冷启动生效)。 验证:tsc --noEmit 通过;vitest reply-delivery / prompt-builder / cli-adapters / prompt-hook-injection / bot-config-store / dashboard-* / bridge-final-output-retry / cot-message / md-card / session-adopt / skill-injection-mode 共 12 文件 1003 用例通过。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YMAYq1f2eZstsgAu2HKFmN
评审 F1:shouldSuppressBridgeEmit 的抑制规则在 send 与 transcript 两种语义下
后果相反。send 模式里 final 只是兜底,「本轮已 send 过」就抑制它,正是去重;
transcript 模式里 final **就是**投递通道,同一条规则会让模型中途发过一条短
消息之后,整轮的真答案静默消失——而中途 send 在 transcript 下是被设计为合法
的(附件、举手、跨 bot @)。
闸改成 mode-aware(新增 replyDelivery 参数,缺省 'send' = 历史行为逐字节
不变),transcript 下三条路径一并翻转:
- 长度比例闸(2x + 120 字符)→ 换成「只有内容等同才算重复」。marker 只存
归一化长度与截断预览、没有完整正文,因此判据是长度完全相等且预览是 final
的前缀;长度不同一律送达。此前中途 send 7 字就要求 final ≥127 字。
- 「已 send + final 尾部 sentinel」硬抑制(不看长度)→ transcript 下不再适用。
提示词仍教 sentinel,模型「发完附件说明 → 真答案写进 final → 习惯性补
sentinel」必吞;剥掉 sentinel 后交给重复判定即可。
- 无 contentLength 的 marker 走 back-compat 分支直接抑制 → 不再抑制。这不是
存量文件问题而是现役路径:`botmux send --images` 不带正文时
buildBridgeSendMarkerContent 对空内容返回 undefined,--voice 的 marker 手工
拼装同样没有该字段。重复一条消息远比静默吞答案便宜。
裸 sentinel、adopt、isLocal、markTimeMs=undefined 四条方向不变。
测试用 buildBridgeSendMarkerContent 真实构造 marker:手搓 { sentAtMs,
contentLength } 永远是结构化形状,会把第三条测成假阴性。覆盖三种形状
(--images 无正文 / 混合 / 正常正文),并钉住两个方向——prose+sentinel 送达、
内容等同仍抑制(长度相同但正文不同的也必须送达,防止 dedup 被一起改没)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXTEzQtcEcQkJzv1gNnxfa
评审复审 F2:markerSetDuplicatesFinal 里「空 final 一律抑制」的注释写着
「保持旧行为」,实际相反。emitReadyCodexTurns 会为**合成的**失败卡与空回合
诊断重跑这道闸,那时 gateInput.finalText 是空的(可见文案现合成在 content
里),于是 transcript 下即使模型一条消息都没发,失败原因也被压掉;而 send
模式对「空 final + 零 marker」是送达的(markerSetCoversFinal 在 markers 为空
时返回 false)。改为 `return markers.length > 0`,与 send 语义对齐:本轮确实
发出过东西才抑制。
structuredFallbackKind 一并接收 replyDelivery 并透传。不只是口径统一:失败
回合带一段 partial final、且中途发过一条较短 send 时,若 partial 没长过
2x+120 阈值,send 缺省分类会跳过 failed 判定落到 'final',只送 partial 而吞掉
失败原因;透传后 transcript 走它本该走的语义。
影响面窄:默认 transcript 的 claude-code 走 emitReadyTurns,空文本回合直接
continue,不合成这类诊断卡;只有显式配置 replyDelivery=transcript 的结构化
CLI 才触发。
另修 test/dashboard-ipc.test.ts:上一次 rebase 解冲突时把本 PR 的
`describe('PUT /api/bot-reply-delivery')` 整块插进了 master 那个
`describe('streaming card buttons')` 的 it 内部,两个块交错嵌套导致括号不闭合,
整个文件在 CI 上解析失败。改用「master 干净基底 + 整块插入到相邻 describe 之前」
重建,254 用例恢复通过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXTEzQtcEcQkJzv1gNnxfa
f89cb56 to
70d0cdf
Compare
|
F2 已修,同时修了一个你们没提但更要命的问题:CI 上那 4 个红是我上一次 rebase 解冲突时弄坏的。 CI 红的根因(我的失误)
已用「master 干净基底 + 整块插入到相邻 describe 之前」重建,254 用例恢复通过。 另两个红是 CI 资源问题,非代码缺陷: F2 修复照建议改成
补了测试钉住三个方向:空 final + 零 marker 在 transcript 下送达、send 模式同样送达(parity 断言)、有中途 send 时仍抑制。 rebase已 rebase 到最新 master(
感谢这轮复审,尤其是把我那行注释和实际行为的矛盾挑出来。 |
复审(70d0cdf0e F2 修复):两处修法都正确落地 ✅,补一条测试覆盖建议
基线:本分支已 rebase 到 #1334( F2 两处修法均按预期实现 ✅
一个非阻断建议:第 2 处(透参)目前没有单元测试钉住。 我把 rebase 损坏的 dashboard-ipc.test.ts 已正确重建 ✅ 这条很关键——上一版 rebase 曾把 验证: 辛苦,F1/F2 至此都收敛了。 |
动机
当前所有 CLI 的最终回复都要模型自己执行
botmux send。为此每轮 prompt 要注入<botmux_reminder>提醒它别沉默,系统提示里要教它 heredoc 写法、--mention三选一硬门、附件参数,<identity>里还要写「协作必须botmux send --mention」。对私聊、或只有本人和一个 bot 的 1v1 群来说,这些多 bot 归属规则和发送规约全是噪声,模型每轮都要读一遍;用户在输入框里看到的也不是自己发的话,而是一层 XML 信封。而
worker.ts里其实早就有一条完整的转写兜底:模型这一轮没调botmux send时,daemon 会从 CLI 的 transcript 取本轮最后的 assistant 文本,发成最终回复卡(抑制规则见bridge-fallback-gate.ts,投递带重试与 uuid 幂等)。codex-app适配器更是已经在系统提示里明说「最终 assistant 消息由 botmux 自动转发,普通回复不要调用botmux send」。这个 PR 把那条兜底升为一等通道,并顺带把 Claude Code 的 statusline 数据接进卡片用量段。
三个提交
1.
replyDelivery: 'send' | 'transcript'(新配置,缺省send,行为不变)transcript模式下:botmux send只保留给中途推送、附件、跨 bot @;BOTMUX_NOTHING_TO_SEND哨兵语义不变(它仍是转写兜底的抑制规则)。<botmux_reminder>。user_count ≤ 1 && bot_count ≤ 1且本轮发言者就是 owner。这时<user_message>壳、<sender/>、<attachments>、<mentions>全部省掉,改用buildBridgeInputContent的裸文本 +[附件]/[@提及]行(与/adopt桥接会话同一形态,那条路径已在线上跑了很久)。判定 fail-closed:话题群、多人群、成员数取不到、非 owner 发言一律不算 solo。复用getChatMode/getGroupStats的现有缓存,send模式下不产生任何额外 API 调用。DaemonSession.completedIdleTurnId,FrozenCard.idleLabel兼容旧盘的silentIdle)。claude-code加structured-bridge-clis白名单)。bot-config-store在写盘前拒绝不支持的 CLI,effectiveReplyDelivery运行时再兜一层,/botconfig set cli换到不支持的 CLI 后自动回落send并 warn 一次。turn 指纹不受影响:
bridgeMarkPendingTurn用的是 daemon 发来的原文,指纹是归一化后前 30 字符加后缀证明;裸文本反而比现状更有区分度(现状每轮都以<botmux_reminder>开头,前 30 字符完全相同,只能靠 FIFO 顺序区分)。2. 桥接 Claude Code statusline,卡片显示
ctx 23% · 5h 18% · 7d 5%claude-code的 transcript 里没有上下文窗口字段,所以卡片只能显示上下文绝对值(上下文 674.3K),既没有百分比,也没有任何账号配额信息——contextOverCompactThreshold的「建议压缩」提示对 Claude Code 从来没触发过。而 Claude Code 的 statusLine 机制会把
context_window.used_percentage与rate_limits.five_hour / seven_day喂给配置的命令。本提交:botmux statusline子命令:读 stdin JSON,原子写<DATA_DIR>/statusline/<sessionId>/latest.json(用目录而非单文件,因为沙箱对单文件只授 readWrite,tmp+rename 需要父目录可写)。用户自己的 statusline 不会被吃掉:spawn 前按 Claude 的优先级解析出被遮蔽的statusLine.command写进BOTMUX_STATUSLINE_CHAIN,子命令把原始 stdin 字节转发给它并透传 stdout 与退出码,10s 看门狗按进程组收尾。任何失败都不影响 Claude 的状态栏。claude-code的进程级--settings注入statusLine(refreshInterval: 60)。由此 claude-code 会恒传--settings(此前disableCliBypass时不传),test/claude-settings-hook.test.ts与test/cli-adapters.test.ts的相应断言已更新为「传,但只有statusLine键」。getDaemonSessionUsageSnapshot对 claude-code 合并 quota,回复卡页脚、流式卡用量行、GET /api/sessions/:id/usage三个读取点一处覆盖。有数据时上下文段渲染纯百分比ctx 23%,追加5h N%/7d N%;没有数据时输出逐字节不变。不渲染resets_at、不画进度条。沙箱侧:
fs-policy两个分支各授statusline/<sid>目录 readWrite,spawn 前预建目录(bwrap 不能 bind 不存在的源),BOTMUX_STATUSLINE_CHAIN进BOTMUX_INJECTED_ENV_KEYS与SESSION_TURN_MARKER_ENV_KEYS。wrapperCli剥掉--settings时(如 aiden)自然没有数据,卡片省略该段。3. claude-code 缺省切到
transcript,提示里彻底不提botmux send缺省值改为按 CLI 决定:
claude-code缺省transcript,其它 CLI 缺省send;bots.json显式写"send"才让 claude-code 退回旧行为,unset回各 CLI 缺省。transcript分支的提示只保留自动转发说明、botmux history/botmux bots list、哨兵、workflow、防注入与白板;heredoc、--mention三选一、附件参数、<identity>的mention_must全部去掉。附件与跨 bot @ 的用法仍在--plugin-dir注入的botmux-sendskill 里,模型需要时能自己发现。为什么把缺省也翻了:留着
botmux send的教学文案而实际通道已经不是它,模型会两头都做——既在最终消息里作答,又调一次send,用户看到重复内容。要么全套保留,要么全套去掉,中间态最差。是否接受这个缺省由维护者决定,去掉本提交不影响前两个。影响面
session-manager(信封)、shared-hints(系统提示)、worker-pool(init IPC、用量快照、最终卡投递)、card-builder/card-handler(idle 标签)、md-card(用量段)、cli.ts(新子命令)。effectiveReplyDelivery(...) === 'transcript'为前置。显式send与非 claude-code 的 CLI 输出逐字节不变(现有 prompt 测试里的toBe等式守卫已扩展到「缺省 vs 显式 send」两种写法)。/adopt桥接(本就无信封)、v3 workflow(V3_MARKER时强制send)、oncall 多人群与话题群(非 solo)、HTTP /apiOnly无传输会话(noTransport优先级更高)、codex-app(天然转写模式,不在白名单)。<user_message>/<sender>结构,/adopt的自产会话识别少一条指纹(与senderTag: false同类)。验证
新增用例覆盖:
computeSoloSession全分支表驱动、CLI 白名单与 fail-closed 回落、transcript 续轮无 reminder、solo 裸文本信封(附件成[附件]行、自 @ 被剥)、非 solo 保留壳与 sender、noTransport优先、缺省与显式send的字节等式、裸文本 turn 指纹命中、idle + completed标签与冻结卡回读、__testOnly_deliverFinalOutput下仅 transcript + lineage 匹配才置位;statusline 子命令的落盘 / 无 sessionId / chain 字节透传 / 退出码透传 / 看门狗 / 非 JSON / workflow 白名单,快照的 mtime 缓存与陈旧丢弃,页脚与流式卡两个变体的渲染字符串,以及「无 quota 时与现状逐字节相同」。在一台接飞书的 daemon 上跑过 live:私聊里模型不调
botmux send也收到最终回复卡,转写里本轮输入是裸文本,该轮流式卡标「已完成」,页脚出现ctx N% · 5h N% · 7d N%,tmux attach后本机原有的 statusline 文案未被替换;多人群回归确认 sender 与壳仍在。卡片是飞书客户端渲染,未附截图。🤖 Generated with Claude Code
https://claude.ai/code/session_01YMAYq1f2eZstsgAu2HKFmN