Skip to content

feat(fork): /fork — 会话分身(并行分身模式,Codex + Claude 原生 fork) - #704

Open
xiaoxueSunn wants to merge 8 commits into
deepcoldy:masterfrom
xiaoxueSunn:feat/session-fork
Open

feat(fork): /fork — 会话分身(并行分身模式,Codex + Claude 原生 fork)#704
xiaoxueSunn wants to merge 8 commits into
deepcoldy:masterfrom
xiaoxueSunn:feat/session-fork

Conversation

@xiaoxueSunn

Copy link
Copy Markdown
Contributor

这个 PR 做什么

给 botmux 加 /fork:把一个正在跑的会话,在当前上下文节点复制出一条带完整上下文的独立会话,源会话原封不动继续。上下文复制交给 CLI 原生 fork(Claude --fork-sessioncodex fork),botmux 只做「后端能力适配 + 与现有框架的套壳关联」,不碰上下文文件

本 PR 只交付并行分身模式(Fork)/fork --create <群名> 建新群放分身。原地断点(Branch,非话题群 close-换-fork)是自创用法、有破坏性,另立 PR 并先走产品讨论。

设计文档(含完整设计、能力门控、边界、真实环境测试记录):
https://bytedance.larkoffice.com/docx/YENydJcPgoJMjaxEhfIcMiCbnbc

怎么实现的(优雅 = 复用 transferSession + CLI 原生 fork)

  • forkSession()(worker-pool.ts)= transferSession(/relay 底层)的非破坏性兄弟:复用它的前置守卫,去掉全部破坏性步骤(不冻结源卡、不 kill 源 worker、不删注册表、不改写源路由),改为 mint 一个 child 会话 + forkWorker(resume=true) 拉起。源会话全程不碰。
  • 双 id 解耦:child 的 cliSessionId 先指向源的 CLI id,首次 spawn 用 CLI 原生 fork 派生新 id,worker 回读后写回 child、清除一次性 pendingForkSession 标记;之后 refork 正常 resume 自己。
  • 适配层:Claude buildArgs 加 --fork-session;Codex 把 resume 子命令换成 fork
  • 能力门控:Claude 系 + Codex 终端 ✅;Codex App / 开了 Hybrid RPC 的 Codex / 纯远端后端 → typed-refusal(不误跑)。
  • 命令/forkDAEMON_COMMANDS + command-handler;--create@ 默认当前 bot(fork 只能复制给同一个 CLI)。

真实环境端到端测试(2026-08-02,隔离单 bot 切到本分支 build)

Codex(seed0630):/fork --create → 原生 codex fork <源id> → 独立新 rollout(带 forked_from_id)→ 分身在新群答出暗号 → 源 rollout 不动。同源多次 fork 均独立。
Claude(Relay-Claude2):/fork --create → 原生 --fork-session → child transcript 继承暗号 → 源 17 行不动 → child 拿自己新 id + 🔱 血缘标题。

测试中发现并修复 5 个问题(详见文档附录 C):建群前置守卫顺序、失败时孤儿群清理、裸 /fork 提示文案、--create 冗余 @selfpendingForkSession 清除的 worker/daemon 抢写竞态。

仍未验证

  • 父子双活并发写:父会话与分身同时各发消息、各写各的 transcript 不互相 clobber。已顺序验证父子独立;并发压测未做(需两个 live worker 同时活跃,单机 CLI 复现不了)。建议合并前或紧接着补一次并发验收。

影响范围

  • 新增面集中在 forkSession() + /fork command case + adapter 的 forkSession 参数 + i18n;对非 fork 路径无行为改变(forkSession 标志对普通会话恒为 false)。
  • 触及公共层 worker-pool.ts / command-handler.ts / worker.ts / adapters,已 pnpm build 通过、Codex+Claude 两条后端路径真机验证。

🤖 Generated with Claude Code

xiaoxueSunn and others added 8 commits August 2, 2026 17:14
Non-destructive sibling of transferSession: fork a running session into a
second independent botmux session at a different anchor, source untouched.

- forkSession() (worker-pool): reuse transferSession front guards, drop all
  destructive steps (freeze/kill/delete/rewrite source), mint a child session
  and forkWorker(resume=true) it.
- Native CLI fork threading: adapter buildArgs gains forkSession flag ->
  Claude --fork-session, Codex fork subcommand. One-shot
  Session.pendingForkSession marker cleared once the child's own cliSessionId
  is persisted.
- Capability gate isForkCapableSession: Claude family + Codex terminal only;
  codex-app / Codex-under-RPC / remote backends refused.
- lineage title + forkedFrom provenance on the child.

Command wiring (/fork case + picker card) and real-env acceptance still pending.

Co-Authored-By: Claude <noreply@anthropic.com>
Single-bot fork-to-new-group entry (fork is single-session, so unlike
/relay --create there is no multi-bot leader election / peer coordination):

- /fork case in command-handler: owner check, capability gate
  (isForkCapableSession) with typed refusal, resolve the single @-bot (must be
  this bot, since fork copies this session's transcript), createGroupWithBots,
  then forkSession into the new chat. Source session left untouched.
- cmd.fork.* i18n (zh + en): no_bot / no_sender / no_session / not_owner /
  no_mentions / resolve_failed / unsupported_backend / mid_turn /
  not_started_yet / adopt_not_forkable / created / failed.
- No-arg picker path guides to --create for now (picker is a follow-up).

Co-Authored-By: Claude <noreply@anthropic.com>
Real-env test (seed0630) surfaced two issues:
- Orphan group bug: /fork --create ran createGroupWithBots before checking
  the source was a real, resumable session, so a refusal (e.g. /fork typed at
  the group top-level while the session lives in a 话题) left an empty orphan
  group. Move the real-session + idle guards ahead of group creation.
- Clearer refusal: new cmd.fork.no_source_here tells the user to invoke /fork
  inside the thread the session lives in, not the group top level.

Co-Authored-By: Claude <noreply@anthropic.com>
…e-/fork message

- Orphan-group cleanup: if forkSession fails after createGroupWithBots (narrow
  TOCTOU race), best-effort disbandChat the just-created empty group so no
  failure path leaves an orphan chat; if disband fails (ownership transferred
  to the user), tell them explicitly via cmd.fork.orphan_group_left.
- Clearer bare /fork: the no-arg picker path is not built yet, but the old copy
  reused cmd.fork.no_session ("must be invoked in a thread with a session"),
  which is misleading when the user DOES have a session. New cmd.fork.picker_pending
  says the picker is pending and points at /fork --create.

Co-Authored-By: Claude <noreply@anthropic.com>
Word it as a product message (in-place fork not supported here, use --create, in-group Branch coming later) instead of exposing "not built yet".

Co-Authored-By: Claude <noreply@anthropic.com>
Fork copies THIS session, so the invited bot is always the current bot. No
@mention now defaults to the current bot (common "fork myself to a new group"
case — no need to @ the bot you are already talking to). An explicit @mention
that resolves to a DIFFERENT bot is refused (cmd.fork.wrong_bot).

Co-Authored-By: Claude <noreply@anthropic.com>
The daemon-side cli_session_id handler cleared the one-shot marker, but the
worker process writes the sessions file directly in persistCliSessionId and,
having reloaded the row from disk with the marker still set, its write raced
and clobbered the clear — leaving pendingForkSession=true on the child, so a
later refork would re-fork the parent instead of resuming the child. Clear it
in the same worker-side write that sets cliSessionId.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@xiaoxueSunn
xiaoxueSunn requested a review from deepcoldy as a code owner August 2, 2026 12:23
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.

1 participant