停掉控制面之后,AI 任务还在跑:我们为什么把它拆成三层
用一个界面对着某台机器上的某个终端窗口,是大多数人开始用 AI 写代码的方式。任务变多之后会出现三个具体问题:任务分散在多台机器上、每个任务需要独立且可丢弃的环境、长任务不能因为浏览器关了或者界面断了就死掉。 这篇讲 TaskHandoff 在这三个问题上做的架构选择:为什么控制面和执行面要拆开,状态到底归谁管,以及由此付出的代价。 问题:任务一多,你就开始手动当调度器 假设你手上有三台机器:一台笔记本、一台常
用一个界面对着某台机器上的某个终端窗口,是大多数人开始用 AI 写代码的方式。任务变多之后会出现三个具体问题:任务分散在多台机器上、每个任务需要独立且可丢弃的环境、长任务不能因为浏览器关了或者界面断了就死掉。
这篇讲 TaskHandoff 在这三个问题上做的架构选择:为什么控制面和执行面要拆开,状态到底归谁管,以及由此付出的代价。
问题:任务一多,你就开始手动当调度器
假设你手上有三台机器:一台笔记本、一台常开的家用服务器、一台公司内网主机。你在它们上面分别跑 Codex 或 OpenCode。于是你每天在做这些事:
- 记得哪台机器上有哪个仓库、哪个分支、哪个 worktree;
- 手动把环境装好,或者忍受容器里从零开始装工具;
- 反复切 SSH 窗口看任务进度,因为任务只在那个终端的上下文里存在;
- 关掉笔记本之后,不知道家用服务器上的任务到底还在不在跑。
这些动作里没有一个是真正的开发工作,但它们都会消耗注意力,而且出错时很难回溯。
我们的做法:三层,各管一件事
浏览器 / 桌面端 / 移动端 / 聊天平台
│
▼
Control Plane(UI、API、聊天网关)
│
▼
Node Agent(节点资源与实例生命周期)
│
▼
Controlled Instance(工作区、应用、AI 会话)
关键不在分层本身,而在每层各自拥有什么状态:
- Control Plane 拥有节点清单、实例看板、跨实例的 AI 会话视图、聊天凭据与消息路由。它是你操作的地方,也是多端共用的入口。
- Node Agent 拥有节点本地配置、运行时资源、目录清单和实例生命周期。实例的启停由它执行。
- Controlled Instance 承载工作区、应用、AI 会话和触发器,可以独立运行。
因为实例生命周期归 Node Agent,停止或重启 Control Plane 不会顺带停掉节点上由 Node Agent 管理的 Docker 实例;控制面板恢复后会重新同步状态。
同理,AI 会话的对话状态以 AI Session 本身为准,控制面负责的是凭据、绑定、命令解析和路由。所以关掉网页不会中断任务——这条被写进了文档 FAQ,因为它是最常被问的问题。
一篇会话绑定固定工作目录,创建后不会自动改变(这条也在文档里)。要换个 worktree 干活,是「在目标 worktree 上新建会话」,而不是「把当前会话挪过去」。这是同一套状态所有权思路的延续:会话不追着环境跑。
停掉控制面之后会发生什么
这条不用实测也能从状态所有权推出来,文档里也已经把它写成了 FAQ——因为问的人最多:
- 关掉浏览器页面:任务继续跑,重新打开后恢复权威状态。
- 停止或重启 Control Plane:由 Node Agent 管理的 Docker 实例继续跑,控制面板恢复后重新同步。
- 真正会中断任务的操作:停止或删除实例、关闭会话、停止 Node Agent。
这里有个最容易误判的坑,我们也把它写进了文档:页面显示「正在重连」不等于后台任务停了。 如果界面一直卡在重连,问题通常在网络路径上——最常见的原因是反向代理没有转发 /api/events 的 WebSocket,而不是 Agent 挂了。把「显示状态」和「权威状态」分开看,能省掉不少误判。
取舍与被否决的方案
为什么不把实例生命周期直接放在控制面? 那样控制面重启就会波及正在跑的长任务,而且单点变成了「UI 挂掉 = 所有任务挂掉」。把生命周期的所有权下移到节点,代价是控制面离线期间你只能在节点上继续跑已经存在的实例——新建实例、改配置这类操作都要等控制面回来。
断线重连的语义要写清楚。 界面断线重连时会重新获取权威会话快照。最容易造成误解的是那个「正在重连」提示——它不代表后台任务已经停止。我们把这条明确写进文档,而不是靠用户自己猜:会话状态在 Agent 那边,界面的连接状态只是显示层的问题。
运行时不锁死在 Docker。 Docker Runtime 是当前主要的隔离运行时,一个节点可以跑多个实例;Local Runtime 直接用宿主机环境,同一宿主用户只能有一个本地实例,也不支持 Windows。两者共享同一套实例模型,运行时通过 adapter 扩展,所以将来接 Kubernetes 不需要另做一套 UI 流程。
结果:行为上发生了什么变化
不写数字,只列可验证的行为差异:
- 关掉网页、甚至停掉控制面,节点上正在跑的任务不受影响;重新打开后能拿回权威状态。
- 多台机器上的实例出现在同一个看板里,可以按节点、项目、状态筛选,不用逐个 SSH 上去看。
- 长任务可以在移动端或聊天里跟进;Telegram、微信、钉钉、飞书的桥接把消息和审批路由到指定实例的会话。
- 实例可以独立运行,节点负责它的生命周期,控制面挂了也不会连带停掉它。
可复用的四条原则
- 把生命周期所有权放在离资源最近的那一层。 编排层可以崩、可以升级,但它不该拥有「谁在运行」这个事实。
- 区分「显示状态」和「权威状态」。 界面上的连接状态、重连提示,都不等于后台任务的状态;接错这两个概念会带来大量虚假告警和误判。
- 让长任务的存活不依赖交互层。 判定标准很简单:关掉浏览器、重启 UI 服务,任务还在不在跑。
- 状态所有权写进文档,而不是留在实现里。 用户遇到「正在重连」时,需要一句话告诉他这意味着什么。
想自己跑一遍
- 三层结构和每个概念的职责:https://docs.thandoff.com/guide/
- 部署到自己的机器(Docker Compose 或 Debian/Ubuntu systemd 一键安装):https://docs.thandoff.com/guide/install
- 接入远程节点:https://docs.thandoff.com/integrations/remote-nodes
- 「关闭网页会不会停任务」「停 Control Plane 会不会停实例」:https://docs.thandoff.com/faq
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.