Skip to content

fix(session): 隔离跨身份打断并保全消息 - #1348

Open
yousay123 wants to merge 6 commits into
deepcoldy:masterfrom
yousay123:fix/chat-interruption-isolation
Open

fix(session): 隔离跨身份打断并保全消息#1348
yousay123 wants to merge 6 commits into
deepcoldy:masterfrom
yousay123:fix/chat-interruption-isolation

Conversation

@yousay123

@yousay123 yousay123 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

改动内容

本改动包含用户可见行为变化:跨身份输入不再直接 steer 活动轮,而是进入显式、可恢复的分类流程。

  • 为活动 CLI 轮引入不可变的权威元组,分别记录真实发起者 caller 与稳定任务控制人 controller
  • daemon 与 worker 两侧统一执行跨身份准入:同一身份或任务控制人可继续控制当前轮,其他身份不得直接 steer 活动轮。
  • 将被隔离的输入持久化为显式分类流程,真人与机器人使用同一规则,不再把机器人输入默认归类为“建议”。
  • 分类、控制人确认、控制人忙碌等待采用分阶段超时;计时从卡片实际送达后开始,超时后保留可恢复出口,避免静默丢弃。
  • host ask 固定精确答复人,未送达 ask 可跨重启恢复;输入队列增加可信身份边界,禁止跨身份拼接。
  • raw_input 携带由 session owner 派生的稳定 controller;派生逻辑收敛为 daemon/worker 共用的单一实现。
  • 在 worker crash、主动重启、worker replacement、terminal/revoke 等生命周期边界精确清理或保留活动权威。

为什么改

普通群的 chat-scope 会话可能由多名真人或机器人共用同一 anchor。历史 lastCaller 只能说明上一轮是谁发言,不能证明当前在飞轮的控制权;把它当作活动权威可能让另一身份的输入注入正在执行的轮,也可能在并发与超时路径中造成消息丢失。

本改动固定以下不变量:

  1. 活动轮的 caller/controller/turnId/dispatchAttempt 在 terminal 前保持不可变;任何所有权变化只影响下一次 reserve。
  2. 只有同一 caller 或已认证的稳定 controller 可以 steer 当前轮。
  3. 跨身份输入必须进入显式、可恢复、可追踪的隔离流程,不得静默销毁。
  4. 超时从用户真正看到交互入口后开始,不能早于送达。

影响范围

  • 影响飞书普通消息、raw passthrough、daemon↔worker IPC、worker 输入队列、ask 卡片与持久化、session 生命周期公共路径。
  • 同时覆盖真人和机器人 principal;caller-less 的 legacy/non-IM 串行路径保持原行为。
  • controller 始终从 session owner 派生,不从 lastCaller、creator 或入站 envelope 实时推导。
  • 未包含部署或配置变更。

验证

  • 合入当前 master30fdc7f39)后,bun run build(Bun 1.4.2)通过。
  • PR 直接触及的 14 个测试文件同进程执行:545 passed / 0 failed
  • 按语义、源码锁、worker payload 三类影响面并入直接改动测试文件后,对照集共 223 个文件;当前 base 为 4528 passed / 10 skipped / 0 failed,本分支为 4530 passed / 10 skipped / 0 failed
  • worker-ordinary-im-init-concurrency.integration 在当前 base 为 5 项、本分支为 6 项;ask-broker 为 53→55,ask-resume-restart 为 13→14;两侧均 0 failed。新增的 active-turn-authority 15 项、cross-principal-interruption-store 4 项均通过。
  • acknowledges an OpenCode argv activation token without a dispatch attempt 在合入 #1335 之前的 base(9387fa190)可复现约 8 秒超时;合入 #1335 后在当前 base 本地为 5 passed / 0 failed,已不再复现。
  • 当前 GitHub Actions 中 build、三个 binary、bun-test、Node shard 2/3 与 3/3 均通过;shard 1/3 唯一失败为未改动的 test/tmux-pipe-backend-exit.test.tslets the process exit with a LIVE backend and no teardown call at all,表现为 FIFO 清理时序失败。
  • 对 24 组安全关键分支做逻辑变异验证;每组均先验为绿,变异后命中对应承重用例,恢复后重新通过。
  • git diff --check 通过。

合并后验证计划

  • 先只部署到单个 Bot,不直接扩到其它机器。
  • 在活动轮期间分别验证任务控制人输入与另一 principal 并发输入:前者应直接进入控制通道,后者应进入分类入口且消息守恒。
  • 行为验收通过后再评估扩大部署范围,并补充实际卡片与消息流截图。

@yousay123
yousay123 requested a review from deepcoldy as a code owner September 9, 2026 12:16
@yousay123

yousay123 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

CI 与基线更新(2026-09-10):

  • 分支已合入当前 master30fdc7f39,包含 #1335),合并提交为 e234cabf1;本地 build 与 PR 直接触及的 14 个测试文件(545 项)通过。
  • 旧 base(9387fa190)上的 OpenCode argv activation 约 8 秒超时,在当前 base 本地已不再复现(5 passed)。
  • 当前 GitHub Actions 中 build、三个 binary、bun-test、Node shard 2/3 与 3/3 均通过;shard 1/3 唯一失败为本 PR 未改动的 test/tmux-pipe-backend-exit.test.tslets the process exit with a LIVE backend and no teardown call at all,表现为进程退出后的 FIFO 清理时序失败。
  • 当前 base/head 的 223 文件双侧对照均为 0 failed(4528→4530 passed,10 skipped)。

提交者仍无权重跑上游 Actions。烦请有权限的维护者基于当前证据重跑失败作业或审阅后决定合并;若该失败稳定复现,再作为独立问题定位,不在本 PR 中夹带修改。

@deepcoldy

Copy link
Copy Markdown
Owner

你好 👋 这是 botmux 的自动评审流程。我们已经为这个 PR 建好了评审群:https://applink.feishu.cn/client/chat/open?openChatId=oc_7878b63455923963b5f013dfba8a414d ,评审 bot 会在群里开始 review。

不过你暂时还没被拉进群——我们的自动拉群名单里还没有你的飞书信息。麻烦你把 GitHub 账号和飞书信息补进这份名单文档:https://bytedance.larkoffice.com/wiki/WJ1nwWbtxi89erkNGNbcgkt9nUe ,补好之后后续复审会自动把你拉进群。感谢你的贡献!(本条为自动流程发出,最终评审结论以维护者审阅为准。)

@deepcoldy

Copy link
Copy Markdown
Owner

自动评审初步意见(最终以维护者审阅为准):

整体设计扎实——活动轮权威元组(caller/controller/turnId/dispatchAttempt)在 terminal 前不可变、worker 侧在入队前硬性拦截跨身份 message、daemon 预路由 + worker 竞态兜底两路收敛到同一份可恢复记录、ask 卡片「确认送达后才起超时 + 锁定唯一答复人 + 未送达不被 GC」都实现得很到位,worker/authority/ask/store 层的测试与变异也确实承重。下面是几条非阻断建议,供参考:

  1. 死代码(建议合入前清理)pendingCrossPrincipalSuggestions + confirmNextCrossPrincipalSuggestionsrc/daemon.ts ~18000–18115,约 115 行)已无任何生产者——持久化的 driveCrossPrincipalInterruptions 是唯一在跑的路径,旧的内存队列在 terminal 回调里被调用时永远在 if (!suggestion) return 早退。类型注释也写明 "Never persisted; do not add new producers"。两套并存的「采纳建议」实现容易让后来者误判,建议删掉旧的那条。

  2. 未落地的字段(建议实现或删除)cwdSerializationGroup / cwdSerializedTurnssrc/types.ts)目前只写不读——没有任何代码真正按 group 做 cwd 互斥。非 worktree 共用目录的「串行执行」实际是靠 activeInteractiveTurn 推迟 fork 实现的(正常工作),但这个推迟只在内存里:daemon 重启后 activeInteractiveTurn 丢失,prepareIndependentCrossPrincipalSession!isolatedWorktree && sourceDs.activeInteractiveTurn 变为 false,排队的独立会话会立即 fork(而父会话也在 resume),共用 cwd 并发。要么补上 group 的读取/互斥,要么先删掉这两个字段避免误导。

  3. 驱动层测试缺口(建议补):worker 权威闸、ask-broker、store FSM 都有不错的单测/集测(我本地变异 controller 控制腿、answerer 锁都会转红);但 daemon 侧编排——预路由闸、分类/等待/主人确认三段 ask、独立会话 fork、采纳后派发(driveCrossPrincipalInterruptions 这一大段)——目前没有直接单测覆盖(全仓 test/ 无 driveCrossPrincipalInterruptions 引用),变异 daemon 预路由闸的 controller 腿也不转红。真正的安全边界在 worker(已测),驱动层更多是消息流/UX,但建议在灰度前补几条驱动级用例。

  4. 小一致性mayControlActiveTurn 的 controller 控制腿只比对 incoming.caller,不看 incoming.controller;而 daemon 给每条消息都附了 trustedController。目前无 trustedCaller 的注入(doc-comment / doc-watch,turnId 非 om_)只会落到 doc:<fileToken> 专属会话、且该会话所有轮都无 caller(双双缺失 → 走 legacy 兼容),所以线上安全;但「非 om_ turnId 被 worker 以跨身份拒绝时,daemon 侧 rejectOrdinaryImDelivery 因无投递记录直接 return」是静默丢弃路径,靠「caller-less 注入永远只去 caller-less 会话」这一隐式不变量撑着。建议加一行注释/断言,或给这类受信注入显式 caller / 显式绕闸。

  5. 次要onTurnTerminal 仅在 status === 'completed' 时驱动后续,父轮 failed/ambiguous 会让排队中的独立会话/建议记录滞留到下次重启才重驱。

本地验证:bun run build 通过;PR 相关 13 个测试文件 539 用例全绿;worker 权威集测(跨身份 B/bot 被拒、文本不进 CLI、A 结束后 B 放行)通过。全量 unit 套件的少量红为本机 bun 1.4.0 vs 钉版 1.4.2 的版本断言与 tmux/沙箱环境噪声,与本改动无关。

再次说明:以上为自动评审初步意见,非阻断,最终以维护者审阅为准。

@deepcoldy

Copy link
Copy Markdown
Owner

复审完成(独立验证,非仅复核首审结论):

  • bun run build 通过;PR 相关 14 个测试文件 545 用例全绿(含 worker 真实进程跨身份集成用例:B/bot 被拒、文本不进 CLI、A 结束后 B 放行)。
  • 独立变异:移除 mayControlActiveTurn 的 controller 控制腿 → active-turn-authority 2 用例转红;恢复后复绿,控制腿承重。
  • 首审 5 条非阻断全部复核属实:
    • N1 死代码确认:pendingCrossPrincipalSuggestions 零生产者,~115 行可删。
    • N2 确认:cwdSerializationGroup/cwdSerializedTurns 只写不读;非 worktree 串行实际靠内存态 activeInteractiveTurn 推迟,而该字段重启后不恢复 → 排队独立会话可能提前 fork 与父轮共用 cwd。最值得优先处理的非阻断。
    • N3 确认(略有补充):worker-pool 的 rejection handoff 在 session-lifecycle-start 有覆盖,但 daemon 驱动层(三段 ask / 独立会话 fork / 采纳派发 / 预路由闸)无直接单测,建议灰度前补。
    • N4 确认:controller 腿只比对 incoming.caller;caller-less 拒识无投递记录时被 worker-pool 静默丢弃,安全性靠「caller-less 注入只落虚拟 doc: anchor」的结构不变量,建议加注释/断言。
    • N5 确认:onTurnTerminal 仅 completed 驱动后续,父轮 failed/ambiguous 时记录滞留至重启。

结论:0 阻断,技术上可合。 核心安全边界在 worker 闸且测试承重;建议 N1/N2 合并前后尽快处理,N3 在扩大部署前补齐。

@xu4wang

xu4wang commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

补充 CI 状态说明,便于审核。

当前唯一失败的用例与本 PR 改动无关:

test/tmux-pipe-backend-exit.test.ts:210
[unit] TmuxPipeBackend fifo teardown
       > lets the process exit with a LIVE backend and no teardown call at all
AssertionError: expected [ 'botmux-pipe-c663fab4329b9991.fifo' ] to deeply equal []

该断言是「进程退出后 FIFO 应已被清理」,失败形态是残留一个 fifo 文件,属拆除时序的不稳定用例。该文件不在本 PR 改动的 27 个文件之内(本 PR 只触及 src/core/src/daemon.tssrc/worker.tssrc/im/lark/ask-card.tssrc/utils/pending-input-queue.ts 与对应测试)。

另外那个失败的 test 是聚合 job,因 test (1/3) 失败而失败,不是第二个独立问题。

本地在 base / head 两个对照 worktree 上跑了受影响文件的并集(223 个文件):base 4528 通过、head 4530 通过、0 失败。完整基线仍以上游 CI 为准。

我们没有 rerun 权限(403),麻烦维护者重跑一下 test (1/3)

@deepcoldy

Copy link
Copy Markdown
Owner

交叉确认完成(独立验证二轮复审结论):

  • 合并状态:作者头 b0b34d6 与最新 origin/master (7f8139b) 做 mirror GitHub merge = 零冲突干净自动合并;落地净 diff 仍 27 文件 +2388/−85,与一审逐文件比对一致。
  • 增量确认:作者本次唯一净新增 = 1 个守卫测试(session-lifecycle-start +17 行,锁住 forkAdoptWorker 在 cliInstanceBinding 下的 throw);其余均为上游随 merge 带入。源码侧除合并外零改动。
  • 守卫变异:删除该 throw → 守卫测试准时转红;恢复后复绿。承重测试。
  • 测试:合并树上 bun run build 绿;PR 子集 + fix(bridge): Claude Code 的 <synthetic> 占位 assistant 行不再被当成正常完成,改判可重试失败 #1330 的 bridge-turn-queue/claude-transcript 共 16 文件 731 用例全绿(worker 真实进程跨身份集测首轮遇已知时序敏感 flaky 用例失败 1 次、重跑 6/6,与本改动无关)。
  • N1–N5:在新 head 上全部仍在、未处理,维持一审判断(N2 最高优先)。

结论:增量安全、零新问题,0 阻断可合不变。 交申晗拍板。

@deepcoldy

Copy link
Copy Markdown
Owner

你好!这个 PR 的评审飞书群已建好(点击加入评审群),但自动流程暂时没能把你拉进群——你的 GitHub 账号还不在我们的自动拉群名单里。

麻烦把 GitHub 账号和飞书信息自行补录到这份名单文档:https://bytedance.larkoffice.com/wiki/WJ1nwWbtxi89erkNGNbcgkt9nUe ,补好后后续复审会自动把你拉进群。评审意见我们仍会同步在本 PR 评论里,不影响评审进行。感谢贡献!(本条为自动流程发送)

@deepcoldy

Copy link
Copy Markdown
Owner

交叉确认完成(独立验证三轮复审结论):

结论:纯同步上游、零 PR 语义改动、零新问题,0 阻断可合不变。 交申晗拍板。

@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.

三轮双审(首审 + pi 复审 + 两次交叉确认)0 阻断可合:worker 入队前硬拦跨身份(真安全闸,实测承重)+ daemon 预路由/竞态兜底收敛到持久化记录 + ask 送达起算/锁唯一答复人。与最新 master 干净合并;必需 check(build / bun-binary linux·darwin·musl / test 1·2·3)全绿。唯一红的 bun-test 是 ci.yml continue-on-error: true 的非门禁 advisory 腿,两次同 SHA 运行红在不同真机 PTY/tmux 用例(tmux systemd、tmux-storm SIGKILL、child-env node-pty lost read),均不在本 PR 文件、属已知环境 flake。

非阻断跟进(不挡合):N2 cwdSerializationGroup 只写不读(重启后排队独立会话可能提前 fork 共用 cwd,建议优先补)、N1 删 ~115 行零生产者死代码、N3 扩部署前补 daemon 驱动层单测、N4 controller 腿只比 caller、N5 terminal 仅 completed 驱动。

@deepcoldy

Copy link
Copy Markdown
Owner

自动评审同步(初步意见,最终以维护者审阅为准):

本 PR 功能双审已 0 阻断通过、必需 CI 全绿。不过在评审期间上游 master 又合入了多个提交(最新 a95d024b7),其中 #1366「文档评论按评论线程隔离会话」也改了 forkAdoptWorker,与本 PR 给该函数 opts 增加的 trustedCaller 在同一位置产生了合并冲突,目前分支对最新 master 为 DIRTY,需要麻烦你 rebase/merge 一次最新 master。

冲突只有两处,且都是「两边各加了东西」的机械并集,没有逻辑对立。我已在本地按下面方式解过并验证(bun run build 通过、session-lifecycle-start 等相关测试全绿),供参考:

1) src/core/worker-pool.tsforkAdoptWorker
保留 #1366 引入的多行签名与 'accepted' | 'rejected' 返回值,同时保留本 PR 的 trustedCaller 字段,合并后应为:

export function forkAdoptWorker(
  ds: DaemonSession,
  opts?: {
    restoredFromMetadata?: boolean;
    prompt?: string;
    turnId?: string;
    trustedCaller?: TrustedCaller;
  },
): 'accepted' | 'rejected' {
  if (ds.session.cliInstanceBinding) throw new Error('External adoption cannot carry a Codex instance binding');
  // …函数体不变;本 PR 把 opts.trustedCaller 透传进 init IPC 的那行请保留:
  //   ...(opts?.trustedCaller ? { trustedCaller: opts.trustedCaller } : {}),

(即:返回类型从 void 变为 #1366'accepted' | 'rejected',opts 里保留本 PR 的 trustedCaller#1333cliInstanceBinding throw、#1366 的各 return 'rejected' 出口都保留。)

2) test/session-lifecycle-start.test.ts
两组测试是并列关系、都要保留,不要二选一:

把三个 it(...) 作为同一个 describe 下的兄弟用例即可。

补充两点:

  • CI 里只有 bun-test 一条腿偶发失败,它在 .github/workflows/ci.ymlcontinue-on-error: true 的非门禁 advisory 腿,最近两次同 SHA 运行分别红在 tmux/systemd 真机用例与 child-env 的 node-pty 丢读(均不在本 PR 文件、属已知环境 flake),可忽略;build / bun-binary(linux·darwin·musl) / test(1·2·3) 这几条必需 check 保持绿色即可。
  • 本 PR 评审给出的非阻断跟进(不挡合)仍然有效:cwdSerializationGroup 目前只写不读、建议补上真正的 cwd 互斥或先删字段(重启后排队的独立会话可能提前 fork 共用 cwd);另有约 115 行零生产者的旧内存队列死代码可清理。这些可以在本 PR 顺手做,也可以后续单独处理。

你 rebase/merge 最新 master 并推上来后,我们会复验必需 check 并继续走合并流程,谢谢!

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.

3 participants