worker fleet · docker runtime reset

把任务运行时从主机里拿出来。

新方向不是继续给 worker2 / worker3 手装依赖。worker 仍然从 DB 抢任务, 但真正跑模板的那一步,改成每个 job 起一个干净的 Docker runtime container。

decision

这次改的是执行边界

1一个 job 一个容器,跑完就销毁。
basic先用一个基础 runtime 覆盖主流 MB / Mai。
digestworker 拉固定镜像摘要,不靠机器手装。
rerun旧 host runtime PASS 要用容器重跑。

architecture

任务还是从 DB 来,命令进容器跑

Tobatsu per-job Docker runtime architecture Upstream creates jobs in DB. Host controller claims jobs and starts a Docker runtime container. Container mounts workspace, NAS, scratch, auth homes, and codex appserver socket. NEW CLAIM DOCKER UPSTREAM MB / Mai real trigger STORE Neon DB tasks · jobs · pin HOST Worker Controller claim · drain · kill PER JOB Runtime Container tobatsu-runtime-basic codex · python · node ffmpeg · fonts · skills REGISTRY Private GHCR pull by digest NAS Skills / Evidence fonts · manifest DATA DISK Scratch / Docker not system disk AUTH Codex / Claude Home mounted · not baked SOCKET Codex Appserver unix:// bind mount SECRETS Runtime Env inject at start LEGEND 新的执行边界 host 服务 持久/数据盘 外部服务

判断点:worker 不是被 Docker 取代。被替换的是“跑模板命令”的那一层。controller 还在 host 上抓任务,但模板实际在容器里跑。

before / after

原来的 subprocess,只剩启动容器这一个职责

之前

  • worker 抢到 job。
  • host 上直接起 `codex` / `python` / `ffmpeg`。
  • 缺包就 ssh 上去补。
  • worker2 / worker3 靠探针比对。

现在目标

  • worker 抢到 job。
  • host subprocess 只负责 `docker run`。
  • 模板依赖都在 image 里。
  • 两台机器拉同一个 digest。
worker claim job
  -> docker run --rm \
       --name tobatsu-job-<job_id> \
       --env-file <runtime-env> \
       -v <workspace>:/workspace \
       -v /mnt/tobatsu-cfs:/mnt/tobatsu-cfs \
       -v /mnt/tobatsu-data:/mnt/tobatsu-data \
       -v <codex-home>:/home/tobatsu/.codex \
       -v <claude-home>:/home/tobatsu/.claude \
       tobatsu-runtime-basic@sha256:<digest> \
       tobatsu-run-job <task_id>

hard gates

cc review 点名的 4 个硬要求

要求 为什么重要 验收证据
Codex appserver socket 容器里的 `127.0.0.1` 不是 host。Codex Ask 不能继续走 host loopback。 容器内 `TOBATSU_CODEX_APPSERVER_URL=unix://...`,socket bind-mounted,Ask probe 通过。
DB 连接预算不变 Docker 不会让 Neon 连接变少。每个 job container 还是会开 DB 连接。 每执行仍按 `POOL_SIZE=1 / MAX_OVERFLOW=1` 预算,pooler gate 保留。
挂载不能少 只挂 workspace 和 NAS 不够。热 scratch 不能写到 container layer。 workspace、NAS、data-disk scratch、`CODEX_HOME`、claude home 都在 mount readback。
旧 PASS 要重跑 旧结果证明了路由,但不是 Docker runtime 下的最终完成。 测试表标 `RE-RUN-REQUIRED`,容器路径重跑后再盖最终 PASS。

sessions

Continue、Ask、Fork 不放进容器层里

per-job container 是一次性执行壳。真正要续上的状态,必须放在容器外: DB、NAS、workspace、`CODEX_HOME`、Claude home、host appserver socket。

Continue Ask Fork state placement under Docker runtime Continue, Ask and Fork use external session state. Job container mounts or connects to DB, NAS workspace, CODEX_HOME, Claude home and codex appserver socket. CONTINUE 继续任务 source ids · workspace ASK Ask Session codex fork · claude resume FORK Fork / Refork session metadata PER JOB Runtime Container stateless execution shell mounts context connects socket runs CLI writes evidence container exits state remains outside DB source ids task · run · session NAS workspace evidence · outputs SOCKET Codex Appserver unix:// bind mount HOME CODEX_HOME per-host · mounted HOME Claude Home shared session auth RULE 容器不保存会话状态 Continue 靠 DB source ids + workspace。 Codex Ask 靠 host appserver socket。 Codex Fork 靠 CODEX_HOME + appserver。 Claude resume 靠 Claude home。 容器退出后,下次重新 mount 回来。 LEGEND 执行壳 / 焦点 持久状态 挂载 / host 服务

如果 session 状态留在 container writable layer,Continue 和 Fork 一销毁就断。正确做法是容器每次重新连外部状态。

runtime contracts

三类会话功能的实现口径

功能 Docker 下怎么做 必须挂载 / 注入
Continue 不是继续同一个进程。controller 找到 source task/run,新容器 mount source/current workspace,再按 source ids 续跑。 DB source ids、source workspace、current workspace、callback env、runtime env。
Codex Ask Session host codex appserver 继续长驻。容器通过 bind-mounted UNIX socket 请求 fork / Ask。 `CODEX_HOME`、appserver socket、`TOBATSU_CODEX_APPSERVER_URL=unix://...`。
Codex Fork / Refork fork 由 host appserver 处理,session metadata 写 DB / evidence。容器只是请求方。 `CODEX_HOME`、appserver socket、workspace evidence path。
Claude Resume Claude 没 appserver。容器里调用 Claude CLI resume,状态来自 mounted Claude home。 Claude home/session/auth、workspace、runtime env。

image hosting

镜像放哪里,怎么不把盘打爆

镜像托管

  • 默认选私有 GHCR / private registry。
  • Docker Hub 可以用,但 runtime 细节会被发现。
  • 不把密钥 bake 进 image。
  • worker 用只读 pull token。
  • 生产拉 digest,不拉漂移 tag。

磁盘规则

  • 不在 worker2 / worker3 上 build 大镜像。
  • 用 build host 或 CI 构建。
  • Docker `data-root` 必须在数据盘。
  • 不能让 `/var/lib/docker` 吃系统盘。
  • container writable layer 终态后清理。
{
  "data-root": "/mnt/tobatsu-data/docker"
}

这条是硬闸。Docker image 层、pull 缓存、容器可写层都很大;没把 Docker root 移到数据盘前,不应该在 worker 上拉 basic runtime。

rollout

重新开始时,按这个顺序走

顺序 动作 不做什么
01 把 Docker reset 写进 goal / implementation / engineering / test-plan。 不继续 host 手装路线。
02 准备 build host / 私有 registry / image digest 规则。 不在 worker 上 build。
03 worker2 / worker3 先验 Docker data-root 在数据盘。 不拉大镜像到系统盘。
04 实现 controller 的 container runner。 不让模板命令直接跑 host。
05 解决 codex appserver UNIX socket mount。 不用 host network 绕过。
06 跑 cheap canary,确认容器路径 claim / run / callback。 不直接恢复 MB/Mai fire。
07 用容器路径重跑 MB creative / delivery 等主线模板。 不把旧 host PASS 当最终 PASS。

what survives

前面的工作不是全丢,但要重新定位

保留为控制面证据

  • `required_host_id` 能把任务钉到指定 host。
  • `fleet-test` 能隔离 fleet 验收任务。
  • 临时 clone template 的 E2E 模型成立。
  • MB/Mai 必须走真实上游入口。
  • callback / readback / rollback 闸序保留。

不再当完成证据

  • host apt / pip / font 手装路线。
  • worker2 / worker3 host parity probe。
  • 旧 host runtime 下跑出的 template PASS。
  • 把环境问题留给 E2E 撞出来。

一句话:路由证据保留,运行时证据重跑。最终报告必须清楚区分这两类,不能把旧 PASS 包装成 Docker runtime 完成。