feat(cot): 思考气泡纳入中间叙述,并为无思考的 turn 补占位节点 - #1311
Conversation
|
你好!评审群已自动创建:pr 1311 思考气泡纳入中间叙述补占位节点 但你暂未拉入群中——自动拉群名单里你的 GitHub 账号尚未关联飞书信息。请自行把 GitHub 账号和飞书信息补进名单文档:https://bytedance.larkoffice.com/wiki/WJ1nwWbtxi89erkNGNbcgkt9nUe ,补好后后续复审会自动拉你进群。 这是自动流程,如有疑问请联系维护者。谢谢! |
|
感谢这个 PR,问题定位和动机都写得很清楚——「extended thinking 默认关闭 ⟹ 一整轮没有 验证结果(先说结论:功能本身是对的)
一个建议合入前处理的问题:沉默轮会冒出裸 sentinelbotmux 有个约定:消息不是发给自己时,最终回复只输出 但气泡这条通道没有 sentinel 过滤( 扫了 200 个真实 transcript / 1493 个 turn 统计频率:
合计 219/1493 ≈ 14.7%,不算边角情况。 建议在 一个想请维护者拍板的取舍(不是缺陷)气泡是独立的一条飞书消息、和最终回复卡不同条,所以纯聊天轮(无工具无思考)的气泡内容就是答案原文,用户会把同一句话看两遍。实测线上占比 121/1493 ≈ 8.1%。 你在描述里已经明确写了这是有意取舍(「收尾的最终答案也会进入气泡尾部……这比静默丢掉每一句中间叙述便宜得多」),所以我没把它当缺陷;只是 8.1% 这个量级值不值,属于产品判断,留给维护者定。如果结论是要治,一个不破坏流式前提的方向是:仅当该轮存在至少一个 tool_call 时才把 顺带一提
以上是自动评审的初步意见,最终以维护者审阅为准。除 sentinel 那条外其余都不阻塞,改好后我很乐意重跑一遍验证。 |
更正:我上一条评论里有两处是错的复审阶段实测下来,我前面给的修法和 F2 影响面数字都有问题,在这里更正。抱歉给了错误的方向。 ① 修法更正:不能在
|
| 输入 | 结果 |
|---|---|
BOTMUX_NOTHING_TO_SEND |
"" → 跳过该 entry |
BOTMUX_NO_REPLY(legacy token) |
"" → 跳过 |
改好了。\n\nBOTMUX_NOTHING_TO_SEND |
改好了。(保留 prose) |
| 普通旁白 | 原样保留 |
我打算输出 BOTMUX_NOTHING_TO_SEND 说明用法(inline 提及) |
原样保留,不误杀 |
所以建议的做法是:observeCotEntries 里对 kind === 'text' 的 entry 调 stripTrailingBridgeSentinelLine,结果为空就跳过该 entry,否则用 stripped 后的文本。
② 补一个边界:这个过滤建议挂 !adoptMode
bridge-fallback-gate.ts:30-33 有一条既有规则:
Adopt mode never suppresses: 在
/adopt里被接管的 CLI 不知道 botmux 存在,transcript drain 是它到飞书的唯一通道。
也就是 adopt 会话里模型吐出字面 BOTMUX_NOTHING_TO_SEND 是合法正文,gate 自己在 adopt 模式下就故意不剥。气泡侧的过滤如果不跟着这条走,会把 adopt 会话的真实答案从气泡里抹掉 —— 变成反方向的同类 bug。
③ F2 影响面数字更正:8.1% → 约 2.0%
我先前说「纯聊天轮占 121/1493 ≈ 8.1%」,这个数偏高。我用的口径是 tools == 0,但把口径拆开重数(同 200 个 transcript、1494 轮)后:
| 口径 | 条数 |
|---|---|
tools==0(我先前用的) |
121 (8.1%) |
tools==0 && thinking==0 |
101 (6.8%) |
| 上一行 且 至少有一个非空 text 块 | 30 (2.0%) |
原因:那 101 轮里有 71 轮压根没有任何非空 text 块(典型是「调用了 botmux send 后最终回复为空」那种),它们 entries 为 0、根本不会建气泡,不该算进 F2 影响面。
真实影响面是 约 2%(另一位 reviewer 独立数出 2.3%,同量级)。F2 的结论不变(仍是有意取舍、非阻断),但 2% 和 8% 在「值不值得处理」上是不同量级的判断输入,所以更正一下。
未变的部分
F1(沉默轮冒出裸 sentinel 气泡)的阻断结论和量级不变,且经两人独立统计确认:约 14%(219/1493 与 207/1453 两次独立测算)。前面列的验证结果(tsc 绿、57/57、邻域 243/243、反变异 5 枪全红、占位 id 不撞、60KB 净增 9 轮、codex 侧结构性不受影响)也都不变。
以上仍是自动评审意见,最终以维护者审阅为准。
补充:
|
| 结论 | 依据 | |
|---|---|---|
| F1 沉默轮泄漏裸 sentinel | 建议合入前处理 | 两人独立测算约 14% 的轮次受影响 |
| F2 纯聊天轮答案重复 | 非阻断(产品取舍) | 影响面约 2%,作者已在描述中说明是有意取舍 |
| 修法 | observeCotEntries 对 kind==='text' 调 stripTrailingBridgeSentinelLine,空则跳过;且挂 !adoptMode |
落点与 helper 边界均已实测 |
功能实现本身、测试质量(反变异 5 枪全红)、CI(9/9 绿)、跨 CLI 影响面都没有问题。仍是自动评审意见,最终以维护者审阅为准。
394c2c0 to
5cf90d3
Compare
|
感谢两轮评审,尤其是第二条里对自己修法的更正——依赖方向那条我确认了: F1 已按建议修复,落点与守卫都照评审给的方案:
实现是一个纯函数 F2(纯聊天轮答案重复)没有动,按评审的定性留给维护者拍板。也谢谢把口径拆开重数(8.1% → 2.0%),这个数比我原先预估的低不少;如果维护者认为该治,我可以按「仅当该轮存在工具或思考节点时才纳入尾部 text」的方向再提一版,那样不破坏流式前提。 本次同时 rebase 到了最新 master(#1309 已合入, |
Claude Code 默认关闭 extended thinking,一个 turn 常常一条 thinking 都没有: 转写里只有 text 块与工具调用。而 extractCotEntries 只认 thinking/tool_use/ tool_result,text 块被整体丢弃,气泡于是渲染成一排孤立的工具节点——既没有 可读的开场,工具节点也因为 lastReasoningId 未设置而挂不到任何 reasoning 父节点上。 三处修改: - CotEntry 新增 `text` kind,承载模型在工具之间写给用户的旁白。它与 `thinking` 在气泡里同为 reasoning 段落,但协议上分开:一是让「模型在想」 与「模型在说」可区分,二是占位判据要靠它。转写是流式消费的,「这是不是 最后一段」在提取时无从判断,因此收尾的最终答案也会进气泡尾部——这比静默 丢掉每一句中间叙述便宜得多。 - turn 首个 entry 就是工具调用时,先补一个「思考中…」占位 reasoning 节点。 一次修好两件事:气泡有了开场,后续工具节点也有了父节点。判据就是 lastReasoningId 未设置,因此真实 thinking 或旁白领头的 turn 永远看不到它, 同一 turn 内也只会插入一次。 - observeCotEntries 过滤 nothing-to-send sentinel(评审 F1)。sentinel 本身 就是个 text 块,气泡开始收 text 之后它会直接渲染成裸 token,沉默轮更是 整个气泡只剩这一个词。过滤放在 observeCotEntries 而非 extractCotEntries: claude-transcript 位于依赖链底层(gate → turn-queue → transcript),在那里 引入 gate 会成环;而 worker.ts 本就 import 了 gate,且它是 Claude/Codex 共用的累积核心,在此过滤可让 sentinel 既不进 timeline、也不占 60KB 上限、 也不进 IPC payload。复用已导出的 stripTrailingBridgeSentinelLine,两种 token 与 inline 提及的边界由其既有测试固化。 该过滤挂 `!adoptMode`:adopt 会话里被接管的 CLI 不知道 botmux 存在,gate 在 adopt 下故意保留字面 token,回复卡也照原样显示。气泡若单方面抹掉,同 一轮的两条消息就会自相矛盾。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXTEzQtcEcQkJzv1gNnxfa
5cf90d3 to
a25cb77
Compare
deepcoldy
left a comment
There was a problem hiding this comment.
三轮复审 0 阻断,可合。
已在最新 master 上本地 rebase(未推作者远端、未改写作者提交),2 处 import 并列冲突按并集解;以逐词净编辑比对确认为纯 rebase,以反向删除确认两侧符号均承重。tsc 通过、目标套件 71/71 + 263/263 + 187/187、build 通过、head CI 9/9 全绿。
本地全量红名单已过三层过滤:6 项为本机 root+bwrap 与双 bun 安装的既有环境噪声(干净 master 同形复现),7 项为高负载下的 Hook 超时抖动(两棵树各红在不同文件,且改前改后失败测试数相同),无一触及 CoT 代码。
TraeX 以前只把用户消息与回合终态喂给飞书,模型 working 阶段的 reasoning 与 工具调用/结果不进思考气泡。本次读取 TraeX append-only history_mutation (模型可见对话的权威时间线),把 reasoning / function_call / custom_tool_call 及其 output 归一成既有的结构化 CoT 条目,复用同一套回合归属/节流/message_cot 链路,使 TraeX 也产出飞书原生思考气泡。 忽略 replace 操作与诊断镜像以防重放重复;数组型工具输出做形状归一; 图片/未知/空输出补空 tool_result,让工具节点有完成收尾;probe 不产 CoT。 事件归属到 native turn 的乱序问题:新增跨 drain 的用户记录镜像状态机做配对绑定, 共享队列相应新增 turn_bind 事件、sourceTurnId/preserveCollecting 字段、 已关闭回合墓碑与 transcript 起始时间重放闸。 影响面:codex-bridge-queue 为 codex/traex/cursor/pi/grok 等多 CLI 共用。 已确认 sourceTurnId/preserveCollecting 仅由 TraeX 产出,其余 CLI 下恒为 undefined、路由退回既有分支,行为与改动前等价。 验证(针对合并后的树,主干含 #1311 同链路改动): tsc --noEmit rc=0;7 个相关测试文件 357/357 通过;反变异确认测试承重 (供给侧置空 11 红、渲染侧占位置空 2 红,均已还原);PR CI 9/9 全绿。 Co-authored-by: Tan Yuanhong <le0tan@users.noreply.github.com>
|
🚀 Released in v3.23.0 |
* feat(cot): 思考气泡纳入中间叙述,并为无思考的 turn 补占位节点 Claude Code 默认关闭 extended thinking,一个 turn 常常一条 thinking 都没有: 转写里只有 text 块与工具调用。而 extractCotEntries 只认 thinking/tool_use/ tool_result,text 块被整体丢弃,气泡于是渲染成一排孤立的工具节点——既没有 可读的开场,工具节点也因为 lastReasoningId 未设置而挂不到任何 reasoning 父节点上。 三处修改: - CotEntry 新增 `text` kind,承载模型在工具之间写给用户的旁白。它与 `thinking` 在气泡里同为 reasoning 段落,但协议上分开:一是让「模型在想」 与「模型在说」可区分,二是占位判据要靠它。转写是流式消费的,「这是不是 最后一段」在提取时无从判断,因此收尾的最终答案也会进气泡尾部——这比静默 丢掉每一句中间叙述便宜得多。 - turn 首个 entry 就是工具调用时,先补一个「思考中…」占位 reasoning 节点。 一次修好两件事:气泡有了开场,后续工具节点也有了父节点。判据就是 lastReasoningId 未设置,因此真实 thinking 或旁白领头的 turn 永远看不到它, 同一 turn 内也只会插入一次。 - observeCotEntries 过滤 nothing-to-send sentinel(评审 F1)。sentinel 本身 就是个 text 块,气泡开始收 text 之后它会直接渲染成裸 token,沉默轮更是 整个气泡只剩这一个词。过滤放在 observeCotEntries 而非 extractCotEntries: claude-transcript 位于依赖链底层(gate → turn-queue → transcript),在那里 引入 gate 会成环;而 worker.ts 本就 import 了 gate,且它是 Claude/Codex 共用的累积核心,在此过滤可让 sentinel 既不进 timeline、也不占 60KB 上限、 也不进 IPC payload。复用已导出的 stripTrailingBridgeSentinelLine,两种 token 与 inline 提及的边界由其既有测试固化。 该过滤挂 `!adoptMode`:adopt 会话里被接管的 CLI 不知道 botmux 存在,gate 在 adopt 下故意保留字面 token,回复卡也照原样显示。气泡若单方面抹掉,同 一轮的两条消息就会自相矛盾。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXTEzQtcEcQkJzv1gNnxfa * feat(continuation): 增加只读长程任务受限续跑 Co-authored-by: TRAE CLI <traecli@bytedance.com> * fix(continuation): 补齐恢复接线与模型继承 Co-authored-by: TRAE CLI <traecli@bytedance.com> * fix(continuation): 阻断 provider 外部能力 Co-authored-by: TRAE CLI <traecli@bytedance.com> * ci: 重试 Bun 偶发超时用例 Co-authored-by: TRAE CLI <traecli@bytedance.com> * fix(session): 隔离跨身份打断并保全消息 (#1348) fix(session): 隔离跨身份打断并保全消息 普通群多真人/机器人共用同一 anchor 时,旧逻辑以 lastCaller 作为活动轮 权威,跨身份输入可能直接注入正在执行的 CLI turn,或在并发/超时路径丢消息。 - 为活动 CLI turn 引入不可变权威元组 ActiveTurnAuthority (caller / controller=session owner / turnId / dispatchAttempt),terminal 前冻结 - worker 在 message 入队前硬性拦截跨身份 steer(真安全闸);daemon 预路由 + worker 竞态兜底(onOrdinaryImInputRejected)两路收敛到持久化 crossPrincipalInterruptions - 跨身份输入走显式分类:独立任务(git worktree 隔离 cwd,否则排队) / 对当前任务建议 (原 owner 在轮结束后明确采纳才执行);真人与机器人同一套规则 - host ask 卡片超时从「确认送达」起算、锁定唯一答复人、未送达不被 GC、可重启恢复 Co-Authored-By: Claude <noreply@anthropic.com> * fix(release): 把临时签名钥匙盒注册进搜索域,修复 stable CLI 签名失败 stable 发版首跑时 sign-darwin-binaries 在证书 import 成功后,于 set-key-partition-list 报 errSecItemNotFound ("The specified item could not be found in the keychain.")。 macOS CI 是无图形会话,security create-keychain 新建的临时钥匙盒不会自动进入 当前用户的搜索域;security import -k 与 set-key-partition-list 都经搜索域解析 刚导入的私钥,因而找不到。electron-builder 的桌面 job 内部会自行注册,所以同一张 Developer ID 证书在桌面链路签名正常,手写 CLI 脚本漏了这一步。 在 import 之前用 security list-keychain -d user -s 把临时钥匙盒设为唯一用户 搜索钥匙盒,并在 cleanup 中先恢复原列表再删除临时钥匙盒。对最终签名身份/产物无 影响,只修复 CI 上证书装配后签名命令够不到私钥的问题。同时在 release 门禁里加断言 防止该注册步骤被悄悄删掉。 Co-Authored-By: Claude Code <noreply@anthropic.com> * docs(api): 重写 /api/trigger 文档,拆开执行模式与投递目标两个维度 (#1387) 旧文把「执行模式」(同步/异步/即发即忘)与「投递目标」(虚拟会话/真群/已有话题/ 已有会话)当成同一件事,写出「两种模式都不进飞书群」——这句不准确,是排查中多次 被追问的来源。实际上任务进不进群只由 target 决定,与同步/异步无关。 本次不是打补丁,是按「两个正交维度」重排的 14 节骨架,中英各一份。 补进去的(旧文零提及): - §4 投递目标整节:真 chatId / chatId+rootMessageId / sessionId 三种落点 - §4.5 交付矩阵:真群 + wait/async 会开话题、会刷流式卡,但最终答案永不落飞书, 只回给 HTTP 调用方——最易踩的一格 - §4.6 「自动新建群」只在 webhook 接入点上,不在本端点 - §4.7 与交互式会话的两处差异:不建 worktree(有源码注释佐证,写作「刻意」)、 工作目录阶梯更短(源码无说明性注释,按中性表述并注明尚未定论) - §3 请求体全字段:presentation.*、envelope.rawText/headers/payload、options.dryRun - §6 空回答契约:sentinel 只有异步能结算成 completed,同步会一路挂到 504 - §10.1 action + message 全量对照(10 组) - §11 状态码与 20 个错误码按归属分表,含 chat_not_allowed → 500 的已知不一致 - §12 512 KiB 上限、代理侧无超时(客户端必须自设) - §13 客户端伪代码,含 5 类分诊与「不透明 404 不等于确认不存在」 同时写明三个陷阱字段:dedupKey / status 无消费方(但进 requestHash,改了即 409)、 suppressFinalOutput 在 wait/async 下按设计失效。 影响面:纯文档,零源码改动,不涉及任何 CLI / 后端 / 会话类型。 验证:docs-site 构建 exit 0,两页均产出(zh 176.6 kB / en 181.1 kB); checkDeadLinks 先证能打响(故意插死链后 exit 1 并点名文件),还原后复跑绿。 Co-authored-by: Claude Code <noreply@anthropic.com> * feat(traex): 展示 working 思考与工具调用 (#1336) TraeX 以前只把用户消息与回合终态喂给飞书,模型 working 阶段的 reasoning 与 工具调用/结果不进思考气泡。本次读取 TraeX append-only history_mutation (模型可见对话的权威时间线),把 reasoning / function_call / custom_tool_call 及其 output 归一成既有的结构化 CoT 条目,复用同一套回合归属/节流/message_cot 链路,使 TraeX 也产出飞书原生思考气泡。 忽略 replace 操作与诊断镜像以防重放重复;数组型工具输出做形状归一; 图片/未知/空输出补空 tool_result,让工具节点有完成收尾;probe 不产 CoT。 事件归属到 native turn 的乱序问题:新增跨 drain 的用户记录镜像状态机做配对绑定, 共享队列相应新增 turn_bind 事件、sourceTurnId/preserveCollecting 字段、 已关闭回合墓碑与 transcript 起始时间重放闸。 影响面:codex-bridge-queue 为 codex/traex/cursor/pi/grok 等多 CLI 共用。 已确认 sourceTurnId/preserveCollecting 仅由 TraeX 产出,其余 CLI 下恒为 undefined、路由退回既有分支,行为与改动前等价。 验证(针对合并后的树,主干含 #1311 同链路改动): tsc --noEmit rc=0;7 个相关测试文件 357/357 通过;反变异确认测试承重 (供给侧置空 11 红、渲染侧占位置空 2 红,均已还原);PR CI 9/9 全绿。 Co-authored-by: Tan Yuanhong <le0tan@users.noreply.github.com> * fix(cli): 支持关闭 Codex 系额度换模型提醒并兼容网关 (#1364) Dashboard 新增开关 hideCodexRateLimitModelNudge(默认开启,仅显式 false 关闭),开启时为 Codex / TraeX / CoCo 注入进程级配置 notice.hide_rate_limit_model_nudge=true,关闭额度接近上限时的换模型弹窗。该覆盖只作用于本次启动,不写入用户全局配置。 网关兼容:aiden 按精确值白名单放行、cjadk 改写为 --config 长参、ttadk 裸透传;CoCo 走 --config 长参。 已知未覆盖:经 Aiden 网关启动的会话拿不到该保护,且无兜底处理,已在中英文案中明确披露。 验证:本地 rebase 到 master 零冲突,结果树与 merge-tree 预算逐字节一致;bun run build exit 0;相关 10 个测试文件 870/870 通过;移除 worker 侧透传行的反变异探针精准转红 1 条。 * perf(dashboard): 会话四视图去掉整树重渲染,并修 listen 探测的 close 死锁 (#1378) 会话四视图(看板/话题/表格/板)去掉整树重渲染,并修 listen 探测的 close 死锁。 三处根因同形「只改一点状态却重建整棵树」:换 key 重放动画、三视图零 memo、 看板卡片没有等价函数。详情开关 board 1408→736、topics 1769→967、table 556→382, 切视图 kanban 6185→460。 顺带修 http.Server.close() 只停 accept、不断已建立连接,导致端口探测无声卡死 (前几天 dashboard 起不来的真因),改为先 closeAllConnections()。 自审发现并修掉两条由新 memo 比较器引入的回归:比较器只比引用,看不见组件读到的 模块级可变状态 —— ①切语言时卡片文案停在旧语言(卡片自身加 useT() 订阅); ②名字表异步回填后卡片仍显示原始 id(loadNameMaps 只写 Map 不发通知,页面 revision 被 prev.row!==next.row 挡掉,故把它作为 namesVersion 加进比较器判据)。两条同根因 但 useT() 治不了第二条,各自配了用例。第三条(安静卡片相对时间冻住)非阻断,留 backlog。 验证:tsc 0 错;受影响 8 个测试文件 77/77 绿;CI 13/13 全绿(含 bun-test)。 新增回归测试逐行反变异正交:删 useT() 只红切语言那条,删 namesVersion 判据只红 名字表那条。性能复测:选中单张卡片的卡片重渲染次数 400→1,两条修法零代价。 * fix(schedule): 修复任务变更后 Dashboard 状态不同步 (#1352) 监听器改用独立的已发布快照做对账基准,不再拿内存业务缓存当 before —— 本进程写入与抢先的读都会刷新缓存,导致差异为空、事件被吞。 三处改动: 1. 独立不可变快照(剥离 preconditionRef,代以 hasPrecondition),与业务缓存解耦 2. 字段消失时在 updated 事件里显式置 null,避免 JSON/SSE 丢弃 undefined 后 merge 型消费方留着旧值 3. 用 announced 集合记录生命周期,Dashboard 已提前宣布的行改发 updated 而非 created,保住展示字段 另收紧标题生成超时测试的时序:旧版的 existsSync 提前返回会在负载下静默跳过全部断言,改为冻结时钟 + 硬断言 + 真实清理。 * fix(lark): 识别话题内附件 (#1386) 飞书富文本(post)消息中,部分客户端上传的附件不是内联节点,而是与本地化正文并列的顶层 `files[]` 数组。 此前 `extractResources()` 只扫内联节点,这类消息的附件一个都识别不到。 改动:在 post 分支扫完内联节点后,追加扫描顶层 `files[]`,转成既有 `MessageResource` 形状, 复用同一 numberer 与 `pushIfNew` 去重。纯追加 10 行,post 之外的分支未改动。 验证:`bun run build` 绿;相关测试 37 文件 589 通过 / 3 跳过 / 0 失败;CI 全部 9 项绿。 * feat(groups): 支持按群配置新话题默认模型 (#1359) 为每个 Bot 支持按群配置新话题的默认模型与思考强度:bots.json 新增 groupDefaultModels[chatId][cliId] = {model, reasoningEffort}(兼容旧的纯字符串写法),Dashboard 群管理弹窗每个在群 Bot 一行,复用 Bot 默认页的模型/思考强度下拉,可选「继承 Agent 配置」。 保存链路复用 bots.json 跨进程文件锁 + 原子写,落盘前校验模型名与该 CLI+模型是否支持该 effort,不支持返 400 不落盘。仅新话题在 createSession 时把当时群配置快照进会话行(私聊/chat-scope/adopted/doc 虚拟会话不盖),之后改群配置不影响老话题。spawn 时优先级为:显式触发模型 > 群快照 > Bot 实时配置 > 会话旧值;effort 仅在会话无显式值时盖戳,并复用既有「模型不支持该 effort 就清空」的 fail-soft。公开未鉴权 /api/groups 走白名单红化,新字段自动剥除。 影响面:core/session-model、worker-pool、services/session-store、im/lark 群配置与 Dashboard 群管理;仅 codex / claude-code 两种 CLI 应用,其余 CLI 与私聊会话行为不变。 验证:在最新 origin/master 基线上 rebase 解冲突(仅 src/services/session-store.ts 三处机械并存),净编辑与 rebase 前逐字节相同;tsc --noEmit 0、bun run build 0、13 个相关测试文件 680 pass / 1 skip;反变异四枪全红(isTopic 闸 / 盖戳恒真 / 保存期 effort 校验 / worker-pool effort 盖戳);CI 9 项全绿。 Co-Authored-By: Claude Code <noreply@anthropic.com> * feat(lark): 支持飞书群标签页新增与管理 (#1388) 新增飞书群标签页管理能力:Lark 侧 /tabs、/tab 命令与 botmux tabs CLI,支持查看、按 URL 幂等新增、重命名、删除与排序。 - 路由:/tabs 走 Lark 预路由拦截器,不进 DAEMON_COMMANDS,避免落入无对应 case 的 switch 造成静默 no-op 与空会话。 - 幂等:按规范化 URL 复用已有标签页,仅按需刷新显示名。 - 排序:要求请求集合是当前全部 Tab ID 的无重复排列(含内置消息标签页),否则拒绝。 - 权限:新增两条可选 scope 校验 im:chat.tabs:read 与 im:chat.tabs:write_only;变更类操作需 owner 或获授权操作人。 验证:tsc --noEmit 通过、bun run build 通过、相关 9 个测试文件 481/481 通过、CI 9/9 全绿。 * fix(session): 修复共享工作目录接力原子性 (#1389) * fix(session): 修复共享工作目录接力原子性 * test(bun): 补齐异步推进下一计时器兼容 * fix(session): 避免共享目录队头阻塞 * fix(session): 明确共享目录队列满反馈 * fix(test): 收口子进程 IPC 清理竞态 * docs(session): 对齐队列满重试说明 --------- Co-authored-by: barry <barry.kangzhiping@ksher.com> * fix(setup): 「选择已有应用」登录态失效可重新扫码,不再死循环 (#1392) `botmux setup` →「添加新机器人」→「飞书应用来源: 选择已有应用」在本机飞书 Web 登录态「半失效」时会卡死循环:⚠️ 拉取应用列表失败: HTTP 400 /developers/v1/app/list: {"code":99991641,"msg":"Something went wrong, please log in again.", "error":{"Code":4101,"LogoutReason":15}} 已返回「飞书应用来源」,可重试或改走其他方式。 再选一次,一模一样的 400,无限重复。 根因是两处叠加: 1. `prepareFeishuWebSession` 的粗检探的是 ask.feishu.cn,与 open.feishu.cn console 跨域——console 已经不认这份 cookie 了,粗检照样判「有效」并原样复用 缓存; 2. 这条分支既没有 `forceQrLogin`,也没把 passport 登出信号识别成登录态失效, catch 里只是把原始 400 打出来返回 `failed`。于是每轮都拿同一份死 cookie 去 撞同一堵墙。 修法复用既有设施,不新造判定:用 `openPlatformWebSessionExpired`(dashboard 改名/头像链路同一个判定器,刻意不把顶层通用 code=99991641 单独当失效)识别登出 信号,TTY 下给一次「重新扫码 / 返回应用来源」,选重扫就带 `forceQrLogin: true` 重来一轮;重扫后仍失效不再追问,避免换一个死循环。覆盖三个出口: `createOpenPlatformApiClient` 的 missing_csrf、`listOpenPlatformApps`、 `fetchOpenPlatformAppSecret`。顺带在拿到 client 后打印当前飞书账号,重扫换号时 列表变化不至于莫名其妙。 同时修掉一个连带的 UX 死胡同:选完应用才失效时,用户明确选了「返回应用来源」 却仍被要求手动粘贴 AppSecret——登录态都没了,粘贴框是死路。现在用户主动返回就 真的返回,只有「问不成」(非 TTY)才保留手动粘贴兜底。 影响面:只动交互式 `botmux setup` 的「选择已有应用」这一条分支, `pickExistingAppCredentials` 全仓唯一调用方在 `src/cli.ts` 的来源菜单。 scripted `setup add` / `--json` 不经过 `promptBotConfig`/`obtainCredentials` (走 `buildBotFromAddFlags` + `inspectCachedFeishuOpenPlatformSession`),dashboard onboarding 走自己的 `feishu-login.ts`,均未触碰。非 TTY 语义不变:不弹二维码、 不改 back/failed 导航契约。 Co-authored-by: Claude Code <noreply@anthropic.com> * fix(traex): escape file mentions in native titles (#1397) * fix(herdr): 启动命令被 session-scope 等包装时按原始 CLI 识别托管 agent (#1390) * fix(herdr): 启动命令被 session-scope 等包装时按原始 CLI 识别托管 agent #1229 起 Linux 上每个新会话的启动命令都被包成 `/usr/bin/env … systemd-run --user --scope … -- <cli> <args>`。tmux/zellij/zmx/pty 原样执行不受影响,但 HerdrBackend.startPaneAgent() 用 basename(bin) 查 Herdr 的 agent kind,看到 env 就拒绝启动,herdr 后端在 3.19.2 之后完全不可用。 Herdr 的 `agent start --kind` 只在 workspace PATH 里找同名可执行文件,后端本就靠临时 launcher 脚本 exec 真正的命令,所以只需把身份识别和执行命令分开: - SpawnOpts 新增可选 cliBin:所有 launch wrapper 之前的 CLI 可执行文件,仅作身份提示 - worker 在 backend.spawn() 传入 cliAdapter.resolvedBin - HerdrBackend 用 cliBin 查 kind、命名 launcher、决定 PATH 前缀;launcher 仍 exec 完整包装命令。被包装时不再向 agent start 转发 argv;macOS 上遇到包装保持 fail-closed 单测覆盖 session-scope 包装(单行/多行 prompt)、非 Herdr kind 包装、macOS 拒绝、 未包装时 argv 转发不变;e2e 新增真机包装启动用例。 * fix(herdr): 用 pane run 显式执行 launcher,不再依赖 PATH 顺序识别托管 agent 线上验证发现上一提交仍起的是裸 claude:Herdr 的 `agent start --kind` 只是往 pane 的 交互式 shell 里输入裸命令名,rc 文件(如 zshrc 重新 prepend ~/.local/bin)会把真 CLI 目录排到 launcher 目录之前,botmux 的参数、系统提示和 session-scope 包装全部丢失。 仓库 e2e 因 fixture 强制 /bin/sh non_login 而看不出来。 改为:创建 workspace 后用 `pane run` 以绝对路径执行自删的 launcher(tty 预输入对 rc 尚未加载完的 pane 同样生效),同步轮询 `agent list` 直到 Herdr 在该 pane 识别出 期望的 kind,再 `agent rename` 成托管名并 `agent get` 校验 pane。识别到其它 kind、 超时、命名失败都关闭本次 workspace。不再需要 macOS 特判与 `--` 参数转发。 `pane run` 成功时 stdout 为空,requiredJsonCommand 增加 allowEmpty。单测按新协议 重写;e2e 新增「rc 把诱饵 pi 目录 prepend 到 PATH 时仍执行我们的 launcher」用例, launcher 临时目录含空格与单引号以覆盖引号传递。 * feat(lark): 支持动态单卡问答与执行过程 (#1383) * feat(lark): 支持动态单卡答复与独立状态卡开关 * feat(lark): 同卡整合问答与执行过程 * fix(lark): 沙盒会话回退默认答复模式 * fix(lark): 独立分配单卡工具与文字过程的展示预算 * fix(lark): 工具执行时保留单卡正文中的最近旁白 * style(lark): 将单卡旁白图标改为思考气泡 * fix(lark): 仅在整卡超限时截断执行过程 * fix(lark): 修复单卡附件引用与代码片段边界 * fix(lark): 避免将行内代码误判为代码围栏 * test(lark): 补充单卡预算借让回归测试 * test(ci): 修复进程就绪与后台任务清理竞态 --------- Co-authored-by: wangjiahui <wangjiahui.xjtu@bytedance.com> * chore(env): 新增 Cloud Agent 开发环境配置 (#1391) * chore(env): 新增 Cloud Agent 开发环境配置(bun/bubblewrap/构建) * chore(env): 安装脚本清理 /etc 悬空 symlink 以修复 v3 沙箱 --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> * feat(codex): 空闲会话自动升级 Codex 版本 (#1306) Dashboard「全局设置 → 实验性配置」新增「自动升级会话 Codex 版本」开关,对应 `dashboard.autoUpgradeCodexSessions`,默认关闭、仅显式 true 启用、修改热生效。 启用后每 1 小时比较会话进程正在运行的 Codex 版本与本机已安装版本,仅在安全空闲时拆除旧进程并以空 prompt 恢复原线程;只升级不降级,不下载安装 Codex,也不复用 daemon 侧 24 小时 npm 检查缓存(两者数据源与所在进程不同)。 验证:本 PR 触及的 10 个测试文件 814 passed / 0 failed;`tsc --noEmit` 无错误;`bun run build` 通过;必需检查 build 与 test (1..3/3) 全部通过。 Co-Authored-By: Claude Code <noreply@anthropic.com> * feat(session): 跨身份消息用飞书卡片和 send --as 二选一 (#1406) * feat(claude-code): per-bot env 经进程级 --settings 文件注入,修被用户全局 settings env 静默覆盖 (#1407) 背景:bot 在 bots.json 配的 env(如 ANTHROPIC_BASE_URL/AUTH_TOKEN/MODEL) 此前只经 pane 进程 env 传递,而 Claude 启动时会把 settings 源的 env 覆盖到 进程 env 之上——用户级 ~/.claude/settings.json 里若有自己的供应商配置, bot 的供应商配置会被静默改写(实测 bot 配了 deepseek 仍跑 kimi)。 改动: - claude 家族适配器 buildArgs 新增 settingsEnv/settingsFilePath 入参, per-bot env 合入进程级 --settings 负载(优先级高于用户/项目 settings 文件);含 AUTH_TOKEN 等密钥故写 0600 文件传路径,绝不内联进 argv; 无 settingsFilePath 时不内联、退化为旧行为 - worker 仅在 claude 家族(cliAdapter.claudeDataDir 非空)且有 per-bot env 时计算文件路径:数据重定向/沙盒下放有效 CLI 数据根(bwrap 已 bind 的 canonical 路径),其余放 per-bot BOT_HOME/launch-settings - 其它 20+ 适配器不受影响(新增字段为可选,全部忽略);wrapperCli (aiden 等剥 --settings)场景自然退化为旧行为 验证: - bun run build 通过 - 新增 test/claude-launch-settings.test.ts 4 例通过(文件 0600/密钥不进 argv/无路径不内联/disableCliBypass 兼容) - claude 相关测试组 7 文件 52 例通过 - 真实 claude 二进制 e2e:进程 env 指向 dummy A、--settings 文件指向 dummy B,实测请求打到 B 且携带 B 的 token 与 model * feat(dashboard,worker): 直达入口恢复无边框工作台,并修 Web 终端滚动条样式 (#1325) * feat(dashboard): 直达入口进沉浸式工作台——/workbench、短票与卡片链接恢复无边框壳 #948 之后 `#/agent-workbench` 只在桌面/移动客户端壳里无边框,浏览器一律套 Dashboard 外壳。这让 `/dashboard` 卡片「打开工作台」按钮、`botmux dashboard` 打印的工作台链接、 常驻链接和短票兑换这些直达入口全部落进了带侧栏的壳,与「一整屏工作台」的定位相悖。 改法:新增 `core/workbench-shell.ts` 定义沉浸式标记 `botmuxWorkbenchShell=immersive`, 三个直达入口(`/workbench` 与 `/workbench/dock` 跳板、`?t=` 登录一跳、 `/workbench-ticket/<ticket>` 兑换)302 到带标记的 hash;`workbenchSpaUrl` 生成的 无凭证 / 平台托管链接同样带标记。前端 client-shell 把标记与 `botmuxClientShell` 一样在启动时从 hash 提升进查询串(hash 导航不丢),app 壳判定认到标记就渲染 无边框壳。标记只决定工作台壳,不复用客户端壳的导航过滤与路由拦截;侧边栏 「驾驶舱」进入不带标记,保持正常壳。 验证:tsc 通过;相关 15 个测试文件 471 项全绿(新增 client-shell 标记读取 / 提升、 auth 一跳目标、票据 302 目标、源码钉死);隔离 HOME 起补丁版 dashboard,浏览器 冷登录实测 `/workbench?t=`、`/workbench/dock?t=`、短票兑换均落无边框壳且 hash 导航后保持,`/?t=#/agent-workbench` 侧栏形态仍为正常壳。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(worker): Web 终端滚动条改为细的半透明样式,不再在内嵌页里显示白底粗条 飞书内嵌 webview(以及任何用经典非悬浮滚动条的桌面环境)会把 xterm viewport 的 原生滚动条画成白底粗条,压在暗色终端上非常扎眼。给 `#terminal .xterm-viewport` 补滚动条样式:轨道透明、拇指细且半透明、悬停加深。标准 `scrollbar-*` 覆盖 Firefox 与 Chromium 121+(后者以标准属性优先),`::-webkit-scrollbar` 覆盖旧 Chromium 与 WebKit。viewport 是 xterm 唯一的真实滚动容器,只改它。中性灰在 明暗两套终端主题上都可读。 影响面:仅 worker 提供的 Web 终端页样式(桌面浏览器、工作台 iframe、飞书内嵌页 同一份 HTML);不触及 PTY / 适配器 / 卡片。 验证:bun run build 通过;本机 fleet 重启后在工作台里打开终端 iframe, 计算样式 scrollbar-width=thin、scrollbar-color 生效,截图确认细条半透明。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: wanglingfeng.linvon <wanglingfeng.linvon@bytedance.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> * fix(feedback): 反馈卡无提问者身份时 fail closed (#1419) 反馈卡回调的 requester 分支在 delivery 未存提问者身份时会跳过整条归属校验(fail-open),改为先判空并拒绝,与 reviewers / everyone 两个分支的 fail-closed 语义对齐。 验证:在最新主干上 rebase 干净无冲突(净改动 2 文件 +22);tsc --noEmit 0 错误;反馈相关 15 个测试文件 146/146 通过;变异验证(移除新增守卫)使新增用例转红,确认该用例可证伪;CI 9/9 通过。 * feat(webhook): 支持新建群固定群名和模板命名 (#1415) * feat(webhook): 支持新建群固定群名和模板命名 * fix(webhook): 修正新建群名模板路径与截断 * feat(lark): 支持可信建群服务声明默认免提及模式 (#1414) 可信建群服务可在群描述写入 HMAC 签名声明,使该群默认按 ambient 处理免提及回复; 签名以每个 bot 自己的 larkAppSecret 为密钥,绑定 appId+chatId+模式,按群按 bot 生效。 仅影响「是否需要 @」的寻址判定,不放宽任何权限闸。 * feat(session): 跨身份消息分流收进实验性开关并默认关闭 (#1424) * feat(session): 跨身份消息分流收进实验性开关并默认关闭 飞书卡片往返会丢掉 HTML 注释、并剥掉按钮的 value/behaviors,导致跨身份消息 分流(XPI)在线上不可用:`--as` marker 过不了飞书、裸关键词兜底又因按钮文字 被拼进正文而失效,消息无法 settle,进而触发暂存提示的自激环。 在缺陷修复前,先把整个特性收进实验性开关 `dashboard.crossPrincipalInterruption` (默认关闭,env 覆写 `BOTMUX_XPI_ENABLED`)。关闭时 daemon 侧不再分流、worker 侧 不再拒绝,第二个身份的消息按普通消息直接投递,即该特性引入之前的行为。 `--as` 的解析与卡片回调保持可用,历史卡片仍可被回答。 Co-Authored-By: Claude Code <noreply@anthropic.com> * test(session): 补一条用例钉住 worker 侧开关委托给共享访问器 变异对照补做正交验证时发现的覆盖缺口:worker.ts 的三处闸都按调用点做 源码钉定,而 `crossPrincipalIsolationOn()` 若被写死成常量,所有调用点文本 原样保留 ⟹ 16 条用例全绿。两个方向都验了:写死 true(恒隔离)与写死 false(恒不隔离)原先均存活。 新增用例钉住函数体委托给 `isCrossPrincipalInterruptionEnabled()`,并禁止 裸 return true/false;同时钉住该标识符确实从 global-config.js 导入,避免 用同名本地桩满足委托。补后两个方向的变异都转红,且红在这条新用例上。 Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com> * fix(continuation): 合并后补齐消息联合类型成员分隔符 --------- Co-authored-by: Roccoon <i@hyw.moe> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: TRAE CLI <traecli@bytedance.com> Co-authored-by: Barry <101912920+yousay123@users.noreply.github.com> Co-authored-by: 申晗 <shenhan.sh@bytedance.com> Co-authored-by: Tan Yuanhong <leotan.yh@gmail.com> Co-authored-by: Tan Yuanhong <le0tan@users.noreply.github.com> Co-authored-by: Xiong Zhuochen <50195037+xiongz-c@users.noreply.github.com> Co-authored-by: Rememorio <junevanlong@foxmail.com> Co-authored-by: Normalight <1520289655@qq.com> Co-authored-by: Alan-Paul <841934306@qq.com> Co-authored-by: ammend <1114694992@qq.com> Co-authored-by: barry <barry.kangzhiping@ksher.com> Co-authored-by: 张加恩 <105278771+xinna2001@users.noreply.github.com> Co-authored-by: powtick <64835612+powtick@users.noreply.github.com> Co-authored-by: sakurs2 <51690980+sakurs2@users.noreply.github.com> Co-authored-by: wangjiahui <wangjiahui.xjtu@bytedance.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: anarkh lee <710809606@qq.com> Co-authored-by: Yang Juecheng <995140153@qq.com> Co-authored-by: Linvon <linvon@vip.qq.com> Co-authored-by: wanglingfeng.linvon <wanglingfeng.linvon@bytedance.com> Co-authored-by: kunshanyep-beep <kunshanyep@gmail.com> Co-authored-by: 亚甲基蓝 <50371442+pigeoner@users.noreply.github.com> Co-authored-by: ejj.cc <hangeryj@gmail.com>
问题
Claude Code 默认关闭 extended thinking,于是一个 turn 常常一条
thinking都没有——转写里只有text块与工具调用。而extractCotEntries只认thinking/tool_use/tool_result,text块被整体丢弃。结果是气泡渲染成一排孤立的工具节点:
lastReasoningId从未被设置,拿不到parentMessageId,全部平铺在顶层。模型在工具之间写给用户的旁白,此前在三个通道里都消失了:CoT 提取跳过它,最终回复卡的
trailingAssistantText只取最后一个tool_use之后的收尾文本(那是有意的,避免 narration collage),流式卡则是终端画面而非结构化消息。改动
1.
CotEntry新增textkind,承载中间叙述。它与thinking在气泡里同为 reasoning 段落,但协议上保持分开:一是让「模型在想」与「模型在说」可区分,二是占位判据要靠它。转写是流式消费的,「这是不是最后一段」在提取时无从判断,因此收尾的最终答案也会进入气泡尾部。这比静默丢掉每一句中间叙述便宜得多,单条不设上限,由 worker 既有的累计上限兜底。空白
text块跳过——空节点在气泡里就是一段空白。2. turn 首个 entry 就是工具调用时,先补一个「思考中…」占位 reasoning 节点。 一次修好两件事:气泡有了开场,后续工具节点也有了父节点。判据就是
lastReasoningId未设置,因此真实 thinking 或旁白领头的 turn 永远看不到它,同一 turn 内也只会插入一次(跨增量更新同样成立)。占位复用 index 0 的 id——恰好在这种情况下它是空闲的,也正是 prologue 的REASONING_START开启该段落时用的 id。测试
test/cot-message.test.ts新增 4 例:叙述条目按转写顺序渲染成 reasoning 节点、纯工具开局插入占位且所有工具节点挂同一父节点、真实 thinking 领头时不插占位、跨增量更新只插一次。test/thinking-transcript.test.ts的两条旧断言写死「text 块不提取」,随行为更新,并补一条空白 text 块被跳过的用例。🤖 Generated with Claude Code
https://claude.ai/code/session_01EXTEzQtcEcQkJzv1gNnxfa