Skip to content

feat(cot): 思考气泡纳入中间叙述,并为无思考的 turn 补占位节点 - #1311

Merged
deepcoldy merged 1 commit into
deepcoldy:masterfrom
lRoccoon:pr/cot-text-block
Sep 14, 2026
Merged

deepcoldy merged 1 commit into
deepcoldy:masterfrom
lRoccoon:pr/cot-text-block

Conversation

@lRoccoon

@lRoccoon lRoccoon commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

问题

Claude Code 默认关闭 extended thinking,于是一个 turn 常常一条 thinking 都没有——转写里只有 text 块与工具调用。而 extractCotEntries 只认 thinking / tool_use / tool_resulttext 块被整体丢弃。

结果是气泡渲染成一排孤立的工具节点:

  • 没有任何可读的开场,看不出模型在干什么;
  • 工具节点因为 lastReasoningId 从未被设置,拿不到 parentMessageId,全部平铺在顶层。

模型在工具之间写给用户的旁白,此前在三个通道里都消失了:CoT 提取跳过它,最终回复卡的 trailingAssistantText 只取最后一个 tool_use 之后的收尾文本(那是有意的,避免 narration collage),流式卡则是终端画面而非结构化消息。

改动

1. CotEntry 新增 text kind,承载中间叙述。它与 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 块被跳过的用例。

Test Files  2 passed (2)
     Tests  57 passed (57)

🤖 Generated with Claude Code

https://claude.ai/code/session_01EXTEzQtcEcQkJzv1gNnxfa

@lRoccoon
lRoccoon requested a review from deepcoldy as a code owner September 7, 2026 16:45
@deepcoldy

Copy link
Copy Markdown
Owner

你好!评审群已自动创建:pr 1311 思考气泡纳入中间叙述补占位节点

但你暂未拉入群中——自动拉群名单里你的 GitHub 账号尚未关联飞书信息。请自行把 GitHub 账号和飞书信息补进名单文档:https://bytedance.larkoffice.com/wiki/WJ1nwWbtxi89erkNGNbcgkt9nUe ,补好后后续复审会自动拉你进群。

这是自动流程,如有疑问请联系维护者。谢谢!

@deepcoldy

Copy link
Copy Markdown
Owner

感谢这个 PR,问题定位和动机都写得很清楚——「extended thinking 默认关闭 ⟹ 一整轮没有 thinking ⟹ 气泡退化成一排没有父节点的孤立工具图标」这条因果链我实测复现了,占位节点一次修好「开场」+「父节点」两件事的思路也很干净。下面是自动评审跑出来的初步意见,供参考。

验证结果(先说结论:功能本身是对的)

  • tsc --noEmit rc=0
  • test/cot-message.test.ts + test/thinking-transcript.test.ts 57/57 绿(与描述一致)
  • 邻域套件 bridge-turn-queue / bridge-turn-journal / codex-bridge-queue / codex-transcript 243/243 绿
  • 对新增断言做了 5 枪反变异,全部转红(关掉 text 提取 / 渲染层不认 text / 占位永不插 / 占位每次都插 / 空白 text 不跳过),说明新测试是真承重的,不是摆设
  • 另外核实了两点你可能也想知道的:① 占位抢 index 0 的 id 确实不会撞(构造「工具在 0、旁白在 1」的对抗序,占位拿 -1、旁白拿 -2,无重复 id);② 60KB 累计上限压力很小(实测 285 → 294 个轮次触顶,净增 9 个)

一个建议合入前处理的问题:沉默轮会冒出裸 sentinel

botmux 有个约定:消息不是发给自己时,最终回复只输出 BOTMUX_NOTHING_TO_SEND,下游 bridge-fallback-gate.ts 会把它剥成空串,用户什么都看不到。

气泡这条通道没有 sentinel 过滤cot-message.ts / claude-transcript.ts 里零命中)。sentinel 本身就是个 text 块,所以这个 PR 开始收 text 块之后它会直接进气泡。走真实 BridgeTurnQueue.ingest 路径实测:

entries:       [{ kind: 'text', text: 'BOTMUX_NOTHING_TO_SEND' }]
bubbleCreated: 1        ← 当前 master 这里是 0,压根不会建气泡
deltas:        ['BOTMUX_NOTHING_TO_SEND']   ← 用户可见

扫了 200 个真实 transcript / 1493 个 turn 统计频率:

形态 master 本 PR 条数
纯沉默轮(无工具,sentinel 是唯一 text) 不建气泡 冒出一个只写着 token 的气泡 32
干活轮尾部带 sentinel 正常气泡 气泡末尾多挂一个 token 节点 187

合计 219/1493 ≈ 14.7%,不算边角情况。

建议在 extractCotEntries 里跳过纯 sentinel 的 text 块——离源头最近,codex 侧将来若也发 text 同样受益。bridge-fallback-gate.ts 已经导出了 BRIDGE_NOTHING_TO_SEND_SENTINELBRIDGE_NO_REPLY_SENTINEL_LEGACY,复用它们即可,不必新写字面量(两个 token 都要考虑)。

一个想请维护者拍板的取舍(不是缺陷)

气泡是独立的一条飞书消息、和最终回复卡不同条,所以纯聊天轮(无工具无思考)的气泡内容就是答案原文,用户会把同一句话看两遍。实测线上占比 121/1493 ≈ 8.1%。

你在描述里已经明确写了这是有意取舍(「收尾的最终答案也会进入气泡尾部……这比静默丢掉每一句中间叙述便宜得多」),所以我没把它当缺陷;只是 8.1% 这个量级值不值,属于产品判断,留给维护者定。如果结论是要治,一个不破坏流式前提的方向是:仅当该轮存在至少一个 tool_call 时才把 text 送进气泡(纯聊天轮本来也没有「一排孤立工具节点」这个病,不在你要解决的问题域里)。

顺带一提

  • 用户自己说的话不会泄漏进气泡——extractCotEntries 不看 event.type,单看它容易担心,但上游 ingest 对 user 事件卡了 isPureToolResultUserEvent(要求每个 block 都是 tool_result),真人 prompt 和「tool_result + text 混合块」都进不来。这点你的实现是安全的,只是建议在注释里点一句这个不变量靠上游守,免得后人重构 ingest 时无意打破。
  • 跨 CLI 影响面是收敛的:codex-transcript.tskind: 'text' 零命中,上面两条在 codex 上结构性不会发生;而占位节点住在共享渲染层,所以 codex 的「工具开局」轮次也顺带修好了。

以上是自动评审的初步意见,最终以维护者审阅为准。除 sentinel 那条外其余都不阻塞,改好后我很乐意重跑一遍验证。

@deepcoldy

Copy link
Copy Markdown
Owner

更正:我上一条评论里有两处是错的

复审阶段实测下来,我前面给的修法F2 影响面数字都有问题,在这里更正。抱歉给了错误的方向。

① 修法更正:不能在 extractCotEntries 里过滤(会造成依赖环)

我先前建议「在 extractCotEntries 里复用 bridge-fallback-gate 的 sentinel 常量」——我没查依赖方向就给了这个方案,它不可行

claude-transcript.ts     imports 仅 node:fs / node:path / jsonl-cursor / types   ← 依赖链最底层
bridge-fallback-gate.ts:52  →  bridge-turn-queue.js
bridge-turn-queue.ts:42     →  claude-transcript.js

claude-transcript.ts 里 import bridge-fallback-gate 会成环。

改到 observeCotEntriesworker.ts)更合适worker.ts:66 已经 import 了 bridge-fallback-gate(同模块再加一个符号无环),而且它是 Claude / Codex 共用的累积核心 —— 在这里过滤,sentinel 既不进 timeline、也不占 60KB 累计上限、也不进 IPC payload。

现成可用的是已导出的 stripTrailingBridgeSentinelLine,我拿它跑了六种形态确认覆盖完整:

输入 结果
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 侧结构性不受影响)也都不变。

以上仍是自动评审意见,最终以维护者审阅为准

@deepcoldy

Copy link
Copy Markdown
Owner

补充:!adoptMode 守卫的链路已完整验证(两人独立走通),附实现落点

前一条评论里我提到过滤应挂 !adoptMode,当时是从注释推的。现在这条链路两位 reviewer 各自完整走了一遍,确认 adopt 会话真的到得了气泡通道,所以这个守卫是必需的,不是冗余。

链路(逐跳有据)

  1. adopt 会话会挂 bridge watcher:setupAdoptTranscriptBridgesworker.ts:12358)调 startBridgeWatcher(cfg.bridgeJsonlPath, { cliPid, cliCwd }) —— 不传 mode,而 startBridgeWatcherworker.ts:5701)是 opts?.mode ?? 'baseline-existing',所以 watcher 正常挂上。
  2. watcher 会喂 CoT 通道:回调 → bridgeIngestworker.ts:5571)→ bridgeQueue.ingest(events, path, observeThinkingAttribution)observeCotEntries。adopt 的 text block 会作为 kind:'text' 进 timeline。
  3. gate 在 adopt 模式下故意保留字面 token:bridge-fallback-gate.ts:191return adoptMode ? withoutMemoryCitation : stripTrailingBridgeSentinelLine(withoutMemoryCitation)

还排除了一个会推翻上述结论的可能:如果 adopt 会话的 turn 一律带 isLocal: true,那 observeThinkingAttribution 开头的 if (turn.isLocal) return; 会先把 CoT 掐掉,守卫就成了冗余。查下来不成立 —— setLocalTurns 只有 codex 侧有,Claude 的 BridgeTurnQueue 没有这个方法;Claude 侧 isLocal按轮判定的(fingerprint 匹配不上 pending Lark turn 时才 headless/local,见 bridge-turn-queue.ts:369)。所以 adopt 会话里由飞书发起的轮次不是 isLocal,确实会到达气泡。

不加守卫的后果,比「气泡少一条」更麻烦:最终回复卡按 :191 在 adopt 下保留字面 token,而气泡把它抹掉了 → 同一轮的两条消息内容互相矛盾。

实现落点建议observeCotEntries 里读模块级 lastInitConfig?.adoptModeworker.ts:2179 声明,在 observeCotEntries(4317)之前,作用域可达)—— adopt 则保留原文,非 adopt 才 strip。


至此双审对本 PR 无剩余异议,汇总一下供维护者参考:

结论 依据
F1 沉默轮泄漏裸 sentinel 建议合入前处理 两人独立测算约 14% 的轮次受影响
F2 纯聊天轮答案重复 非阻断(产品取舍) 影响面约 2%,作者已在描述中说明是有意取舍
修法 observeCotEntrieskind==='text'stripTrailingBridgeSentinelLine,空则跳过;且挂 !adoptMode 落点与 helper 边界均已实测

功能实现本身、测试质量(反变异 5 枪全红)、CI(9/9 绿)、跨 CLI 影响面都没有问题。仍是自动评审意见,最终以维护者审阅为准

@lRoccoon

lRoccoon commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

感谢两轮评审,尤其是第二条里对自己修法的更正——依赖方向那条我确认了:claude-transcript.ts 只 import node:fs / node:path / jsonl-cursor / types,位于依赖链底层,在那里引 bridge-fallback-gate 确实会成环。

F1 已按建议修复,落点与守卫都照评审给的方案:

  • 过滤放在 observeCotEntriesworker.ts):该模块本就 import 了 gate(无新增环),且是 Claude / Codex 共用的累积核心 —— sentinel 因此既不进 timeline、也不占 60KB 累计上限、也不进 IPC payload。
  • 复用已导出的 stripTrailingBridgeSentinelLine,不新写字面量;两种 token(含 legacy)与 inline 提及不误杀这几条边界,由它在 bridge-fallback-gate.test.ts:205 的既有测试固化,没有重复造测试。
  • 挂了 !adoptMode 守卫。这条我单独确认过必要性:bridge-fallback-gate.ts 在 adopt 下 return adoptMode ? withoutMemoryCitation : stripTrailingBridgeSentinelLine(...),回复卡保留字面 token;气泡若单方面抹掉,同一轮的两条消息会自相矛盾。adoptMode 从模块级 lastInitConfig 读,声明在 observeCotEntries 之前,作用域可达。

实现是一个纯函数 cotEntryWithoutSentinel(entry):非 text 原样返回;text 剥完为空返回 null(跳过该 entry),否则返回剥好的文本。

F2(纯聊天轮答案重复)没有动,按评审的定性留给维护者拍板。也谢谢把口径拆开重数(8.1% → 2.0%),这个数比我原先预估的低不少;如果维护者认为该治,我可以按「仅当该轮存在工具或思考节点时才纳入尾部 text」的方向再提一版,那样不破坏流式前提。

本次同时 rebase 到了最新 master(#1309 已合入,b26d2290),冲突都是 subject 字段与 text kind 的并列,已逐处合并两边。rebase 后 tsc --noEmit rc=0,cot-message + thinking-transcript + bridge-fallback-gate 三套 137/137 绿

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

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

三轮复审 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 代码。

@deepcoldy
deepcoldy merged commit e8c9bbd into deepcoldy:master Sep 14, 2026
15 of 18 checks passed
deepcoldy pushed a commit that referenced this pull request Sep 14, 2026
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>
@github-actions

Copy link
Copy Markdown

🚀 Released in v3.23.0

deepcoldy added a commit that referenced this pull request Sep 17, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants