Tobatsu 2.0 · 机制讲清楚

continue / resume / fork 到底怎么做的

点一下"Continue task",系统在背后干了什么 —— 以及 Claude Code 和 Codex 这两个引擎的"续接"不一样在哪。

2026-06-16 更正版 · 逐行复核后修正了上一版关于 "fork 复制 workspace" 的错误说法(实为原地复用)

一、三个词分别指什么

  • Continue(续做):在一个**做完/失败**的任务上点"Continue task",给一句新指令 → 系统建一个子任务,接着上一次的活往下干。app.py:1498 task_action_continue
  • Fork(这个词名不副实):当前代码不复制工作目录 —— 子任务原地复用父任务那个 workspace 目录(同路径、同机器)。所以"fork"实际只发生在 codex 的会话线上,workspace 没被 fork。subprocess_runner.py:1422 _workspace_for_run(created=False,直接拿父 workspace_path)
  • Resume(续接会话):不从零开始,而是接上 agent 自己那段对话/记忆 → agent"记得"之前干了啥,直接在上面加新指令。

合起来就是系统里那个策略名:fork_then_native_resume —— 先分叉工作目录,再续接引擎的原生会话app.py:1600

二、点一下"Continue",端到端发生了什么

原任务 done / failed 建子任务 同引擎 · 同 workspace 血缘 钉在原来那台机器 在原机器上,runner 三步走 ① 复用父 workspace(原地,不复制;要求父目录还在盘上) ② native resume:接上引擎自己的会话 ③ 注入你的新指令 → 接着跑 关键:必须回到原来那台机器跑 —— 工作目录和会话记录都在它本地

子任务 source="continue"parent_task_id=原任务、required_host_id=原机器(app.py:1605,1626)。同一血缘已有在跑的续做 → 直接拒绝,防止并发踩踏(app.py:1541 _reject_in_flight_lineage_continue)。引擎沿用原任务的(app.py:1534)。

三、两个引擎的"续接会话"不一样(核心)

都叫 native resume,但 Claude Code 和 Codex 的会话存法、续法完全两套:

Claude CodeCodex
会话存在哪本机的对话记录文件(transcript),用一个 session id 标识Codex app-server 里的一个 thread(服务端会话)
怎么续接claude --resume <session_id>
重放那段对话,接着说
fork thread:服务端把源 thread 分叉成一个新 thread(branch),在新 thread 上继续
硬校验引擎契约校验:这次能不能 native resume同上 + fork 必须返回 thread id,返回原 id 直接报错
实现位置engine_profiles.py:66,95 (native_session_id 渲染 resume 命令)codex_appserver.py:124 run_codex_fork_continuation → :407 fork_continuation_thread

一句话记:Claude 是"用 id 把本地那段对话重放",Codex 是"把服务端那条会话线分叉出一条新线"。两边都通过 TOBATSU_NATIVE_SESSION_ID 把会话标识传进去(subprocess_runner.py:131)。

四、Fork 到底有没有分叉 workspace(重要更正)

  • 更正(2026-06-16 逐行核对后):workspace 并没有被"分叉/复制"。当前代码是原地复用父任务的 workspace 目录 —— `_workspace_for_run` 直接拿父 run 的 workspace_path 返回 created=False,不 copy。subprocess_runner.py:1435-1441
  • 所以"改动不污染原始记录"在当前实现里不成立:子任务和父任务用同一个目录,改动直接落在父目录上。之前那份说"复制独立副本"、引用 _restore_workspace_bundle_for_continuation / _for_fork_context 的版本是错的 —— 那两个函数当前代码里根本不存在
  • 真正被 fork 的只有 Codex会话线(thread):服务端把源 thread 分叉成新 thread。Claude 这边连会话都不 fork,只是用 id --resume 续同一条。workspace 两个引擎都只是复用。
  • 硬约束:原地复用要求父 workspace 目录还在盘上;一旦任务归档清掉了 workspace,continue/fork 直接报 continuation workspace is unavailablesubprocess_runner.py:1437

五、续不了会怎样 / 注意点

  • 降级:如果引擎/会话不满足 native resume 契约 → 不是彻底从零,而是 复用同一个 workspace 目录 + 开一个新会话,把你的新指令塞进去(有文件、但没有原生记忆)。subprocess_runner.py:213 ContinuationExecutionPlan(allows_native_resume / native_resume_reason)
  • 容器模式额外校验:image-owned 纯容器下,会先校验引擎能不能 resume 再决定(subprocess_runner.py:567 _validate_continuation_native_resume_engine_contract)。
  • ask-fork 变体:Codex 的"Ask"分支也能 fork 续接(doc: v10-ask-fork-e2e)。

总结(已更正):continue = 新子任务接着干;workspace = 原地复用父目录(复制,改动落同一目录,且依赖父目录还在);resume = 接上引擎自己的记忆 —— Claude 用 id 续同一条本地会话Codex 把服务端会话线 fork 成新 thread。"fork"这个词实际只落在 codex 的会话线上。续不了就降级成"复用目录的新会话"。