Skip to content

fix(session): 让 delete 与 daemon 关闭状态对齐 - #563

Merged
deepcoldy merged 3 commits into
deepcoldy:masterfrom
kaopupengpeng:fix/session-delete-close-semantics
Jul 26, 2026
Merged

fix(session): 让 delete 与 daemon 关闭状态对齐#563
deepcoldy merged 3 commits into
deepcoldy:masterfrom
kaopupengpeng:fix/session-delete-close-semantics

Conversation

@kaopupengpeng

Copy link
Copy Markdown
Contributor

背景

botmux delete 原先只在 CLI 侧直接终止进程/tmux 并写入关闭状态,不会同步移除 daemon 内存中的 activeSessions 条目。删除当前会话时,如果先 kill 再通知 daemon,执行命令的 CLI 本身会随 backing session 一起退出,IPC 没有机会发出。

方案

  • daemon 在线时,botmux delete 先调用已有的 POST /api/sessions/:sessionId/close,由 daemon 统一执行 worker/backend 回收、activeSessions 删除、持久化关闭、订阅清理和生命周期事件。
  • 宿主终端调用继续使用 dashboard HMAC;读隔离/沙箱内的 CLI 只能用当前轮换 capability 关闭它自己所属的精确会话。
  • 已发现在线 daemon 时,IPC 拒绝或连接失败会 fail closed,不再退回本地强杀;只在所属 daemon 不在线时保留原本的本地收口。
  • delete all 将当前会话放到最后,避免自删除提前终止批处理。TUI 删除也走同一条路径。
  • daemon close 先同步提交内存删除 + 持久化关闭屏障,再等待文档退订,避免异步清理期间的会话复活或崩溃恢复。

本 PR 不新增 botmux session close-self 命令或新 IPC 路由。

验证

  • corepack pnpm build 通过。
  • TypeScript tsc --noEmit 通过。
  • 定向回归:8 个测试文件,170/170 通过,覆盖 CLI daemon-first、在线失败 fail-closed、离线收口、自删除排序、IPC 鉴权矩阵和 close barrier。
  • 全量单测:9928/9960 通过,32 项失败均已完成环境/基线归因:
    • 25 项因系统 Git 2.20 不支持 git init -b 失败;使用 Git 2.31 对对应的 3 个文件复跑为 31/31 通过。
    • 剩余 7 项为当前 shell//proc PID namespace 的进程发现与超时用例;已在未修改的 origin/master (ec4d3407) 临时工作树上按相同签名复现 7/7。

@kaopupengpeng
kaopupengpeng requested a review from deepcoldy as a code owner July 22, 2026 14:55

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

发现 1 个会造成“删除成功但实际没删”的兼容性问题,建议修复后再合并。其余 daemon-first、fail-closed、鉴权和 close barrier 方向均合理;本地 pnpm build 通过,8 个定向测试文件 175/175 通过。

Comment thread src/cli.ts Outdated
closeSessionForDelete 原先在会话缺 larkAppId 且仅一个 daemon 在线时,
会把会话路由给该唯一 daemon。但 daemon 的 sessionStore.closeSession
只写当前 appId 的 sessions-<appId>.json,不会碰 legacy sessions.json,
导致 getSession 跨文件读到 wasOpen=true、closeSession 静默 no-op,
daemon 返回 {ok:true},CLI 不走本地收口,legacy 记录仍是 active。

修复:无 larkAppId 的会话一律走本地收口 closeSessionOffline,
其 saveSession 能正确写到 legacy sessions.json。

补回归测试:legacy 会话 + 单 daemon 在线 → CLI 不调 daemon、本地收口。

Copy link
Copy Markdown
Owner

维护者跟进:已将修复提交 e1bbf103 快进推送到本 PR 原分支 kaopupengpeng:fix/session-delete-close-semantics,未合并。

修复内容:缺少 larkAppId 的 legacy 会话不再被猜测路由到唯一在线 daemon,而由 CLI 本地收口并写回 sessions.json;新增 legacy sessions.json + 单 daemon 在线 回归,验证不会调用 daemon、记录会真实变为 closed

复核结果:

  • pnpm build 通过(含 TypeScript、dashboard bundle、公开域名与 dist 审计)
  • session-delete-cliipc-close-routesession-delete-close-barrierdashboard-ipc:89/89 通过
  • PR 当前 head:e1bbf103bb83ac6afde5abebf70902bc1d4325eb

原 P2 技术问题已修复;仍按话题约定,等待申晗对兼容性/端到端语义确认后再决定合并。

上一轮修复只在 descriptor 查找处按 larkAppId 决定是否路由给 daemon,但
current-session 的 injected-port 兜底(BOTMUX_DAEMON_IPC_PORT)是独立计算的,
不看 larkAppId——于是"无 larkAppId 的会话不得路由给 daemon"这条不变量只挡住了
一扇门,injected-port 是没上锁的第二扇门。虽然实际不可达(daemon 托管的当前会话
必带 larkAppId),但这是靠"当前会话必有 appId"的隐式耦合,不是显式保证。

改为在函数入口用一个权威守卫:无 larkAppId 一律走本地离线收口,descriptor 和
injected-port 两条路都被同一条不变量覆盖。合法的沙箱当前会话关闭(必带 larkAppId)
不受影响。

回归测试:新增"legacy current session + injected daemon port"用例,验证 daemon
永不被命中、legacy 文件被正确标 closed。已确认该用例在修复前会失败(会话被 injected
port 路由给 daemon)、修复后通过。

验证:pnpm build、tsc --noEmit 通过;4 个定向测试文件 90/90 通过(原 89 + 新增 1)。

Co-Authored-By: Riff <noreply@riff.dev>
@deepcoldy

Copy link
Copy Markdown
Owner

补充修复 · 1c35ec03(Claude review 复核)

在 review 复核时发现上一轮修复(e1bbf103)留了一个同源旁路,本 commit 收口。

问题

e1bbf103 让「无 larkAppId 的 legacy 会话不路由给 daemon」——但这个判断只加在了 descriptor 查找那一处:

const daemon = s.larkAppId ? online.find(...) : undefined;   // 挡住了这扇门
const injectedPort = isCurrentSession ? resolveDaemonIpcPort(...) : undefined;  // 这扇门没锁
const ipcPort = daemon?.ipcPort ?? injectedPort;

injectedPort(current session 的 BOTMUX_DAEMON_IPC_PORT 兜底)是独立计算的,不看 larkAppId。于是「无 larkAppId → 不路由 daemon」这条不变量只挡住了一扇门,injected-port 是第二扇没上锁的门:一个 legacy 会话若同时是当前会话,仍会被 injected port 路由给 daemon,撞上同一类「200 OK 但 sessionStore.closeSession 只写 sessions-<appId>.json、legacy 记录 no-op」的静默失败。

实际几乎不可达(Session.larkAppId 是必填、daemon 托管的当前会话必带 appId),但它依赖「当前会话必有 appId」的隐式耦合而非显式保证——对要合码的 PR 不够稳。

修复

把守卫上提到函数入口,让 descriptor 和 injected-port 两条路径共用同一条权威不变量

if (!s.larkAppId) { closeSessionOffline(s); return { ok: true, via: 'offline' }; }
const daemon = online.find(d => d.larkAppId === s.larkAppId);

合法的沙箱当前会话关闭(必带 larkAppId,靠 injected port 到达 daemon)不受影响。

回归测试

新增用例 closes a legacy current session locally even with an injected daemon port:legacy 会话 + BOTMUX_SESSION_ID + BOTMUX_DAEMON_IPC_PORT 三者齐备,断言 daemon 永不被命中、legacy 文件被正确标 closed

已确认该用例修复前会失败(会话经 injected port 打到 /api/sessions/.../close)、修复后通过——是真的回归护栏,不是自证。

验证

  • pnpm build:通过
  • tsc --noEmit:通过
  • 4 个定向测试文件:90/90 通过(原 89 + 新增 1)

改动面:src/cli.ts(+14/-6)、test/session-delete-cli.test.ts(+43)。

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

复核通过,无 blocking finding。

  • 1c35ec03!s.larkAppId 守卫提升到 daemon 路由入口,descriptor 与 BOTMUX_DAEMON_IPC_PORT injected fallback 共用同一不变量;合法的带 appId 当前会话路径不受影响。
  • 新增 legacy current-session + injected-port 回归覆盖了上轮遗漏的旁路。
  • PR head 定向回归:4 文件 90/90 通过。
  • 与当前 origin/master 做真实 --no-commit merge:无冲突;合并态同组回归 99/99、pnpm build 通过(含 tsc、dashboard bundle、domain/dist audit)。
  • 工作树已恢复干净,未改代码、未合并。

代码层面 merge-ready;按话题约定由 Claude2 执行 merge。

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