Deploy Supervisor 蓝绿实例自动回滚机制设计
日期: 2026-07-30(同日更新:完成 Supervisor 概念改名消歧,见下方命名说明)
性质: 详细设计,回应用户"自动回滚机制现在就设计一下,省得到 PH5 阶段有那个阶段的问题"。落地 self-development-feasibility.md 缺口 2(应用变更+生效)与缺口 3(用户更正版:不要求人工评审,要求 supervisor 自动监督回滚)。
依赖: docs/supervisor-design-comparison.md(方案选型结论:复用 graphloop 隔离/manifest/apply 库,见 supervisor/isolation/;Deploy Supervisor 自身决策逻辑必须是确定性代码,不接入 LLM)。
状态: 设计已回填进正式 SDD(01-M/03-C/04-P/05/06/sdd-mvcp-contract.json),代码尚未实现(supervisor/isolation/ 除外,那是通用库不是本机制专属)。
命名说明(2026-07-30):graphlemon_architecture.md 里已经有一个"Supervisor(调度子 Agent)"概念(Coder Work Graph 内部的常驻监控节点,负责监控 Work Node 执行状态、触发 reflection/重新调度)。本文档描述的是完全不同的另一个组件——一个独立于 Graph Engine 之外、专门负责 graphlemon 自我修改后蓝绿部署与自动回滚的进程。为避免和架构文档里的既有概念混淆,本文档统一称之为 Deploy Supervisor(部署监督进程),所有标识符前缀使用 c_deploy_supervisor_*,与既有的 Supervisor(调度子 Agent)概念完全区分开:两者职责不同(一个管 Work Node 执行进度,一个管整个 graphlemon 进程的部署/回滚)、进程边界不同(一个在 Graph Engine 进程内,一个在 Graph Engine 进程外)、生命周期不同(一个随 Work Graph 任务存在,一个是常驻的独立进程)。
1. 触发范围
本机制只在 target_folder_path == graphlemon 平台自身安装路径(self-target 场景,self-development-feasibility.md 缺口 1 已定义的判断条件)且 Validator 对该次修改判定 PASS 之后触发。改别的 target project 时,走既有的"worktree 合并回主分支"流程即可,不涉及蓝绿实例——因为改的代码不是正在跑的这个进程本身。
触发时点:Validator PASS → worktree 分支 merge 回 graphlemon 自身仓库主分支(既有机制,self-development-feasibility.md 步骤 6)→ Deploy Supervisor 从这里接管,直到新代码安全生效或安全回滚为止。
2. 组件与进程边界
三个独立操作系统进程,任何一个崩溃不影响另外两个:
- Live 实例:当前正在服务的 graphlemon 后端(Graph Engine + 控制台 API),监听固定端口
LIVE_PORT,使用固定路径的生产 SQLite 库PROD_DB_PATH。 - Candidate 实例:本次自我修改合并后的新版本,Deploy Supervisor 拉起来做验证,监听临时端口,使用一次性 DB 副本,与生产 DB 完全隔离,验证期间无论 Candidate 干什么都不影响 Live。
- Deploy Supervisor 进程:独立于以上两者的确定性 Python 脚本(stdlib only,无 LLM 调用),是本设计的主体。职责:拉起/健康检查/切换/监控/回滚 Candidate 与 Live,自身不做任何"业务判断"(比如不判断代码写得好不好),只做"这几个信号是否符合预设阈值"这种算术判断。
Deploy Supervisor 与 Live/Candidate 之间没有共享内存/共享失败域——这正是用户"第三方 agent"要求的字面落地:Deploy Supervisor 挂了不会拖死 Live,Live 挂了 Deploy Supervisor 还能观察到并处理。
3. 数据模型(已回填 01-M-模型设计.md)
两个新实体:
deployment
| 字段 | 类型 | 说明 |
|---|---|---|
deployment_id |
string, PK | |
git_commit_hash |
string | 本次候选版本对应的 commit |
previous_commit_hash |
string | 回滚目标(上一个 status=finalized 的 deployment 的 git_commit_hash) |
status |
enum | 见第 5 节状态机 |
candidate_port |
int, nullable | 健康检查阶段临时端口 |
candidate_db_path |
string, nullable | 一次性 DB 副本路径,验证结束后清理 |
started_at / cutover_at / monitoring_closed_at / rollback_at |
timestamp, nullable | |
rollback_reason |
string, nullable | 见第 6 节信号类型 |
previous_instance_dir |
string | 保留的上一个已知良好版本 checkout 目录,直到本次 deployment 监控窗口正常关闭才允许清理 |
deploy_health_signal
| 字段 | 类型 | 说明 |
|---|---|---|
signal_id |
string, PK | |
deployment_id |
string, FK | |
sampled_at |
timestamp | |
signal_type |
enum: process_alive \ |
http_health \ |
status |
enum: ok \ |
degraded \ |
detail |
string | 供人工事后排查,不参与自动判断 |
4. Cutover 机制(部署新版本)
- Deploy Supervisor 拉取合并后的主分支到全新目录(不是原地
git pull,避免影响可能仍在读取源码的 Live 进程),生成deployment记录,status=staged。 - 复制
PROD_DB_PATH到candidate_db_path(一次性副本,用完即删,绝不让 Candidate 直接碰生产库)。 - 用
supervisor/isolation/isolated_workspace.isolate_workspace()家族同款隔离原则拉起 Candidate 进程:subprocess.Popen,shell=False,固定 argv 形态,监听candidate_port(生产端口 +1,或探测一个空闲端口),指向candidate_db_path。status=candidate_starting。 - 跑健康检查(见第 6 节),
status=health_check_running。- 全部通过 →
status=health_check_passed,进入 5。 - 任一失败/超时 →
status=health_check_failed→ 走回滚路径 A(第 7.1 节)。
- 全部通过 →
- Cutover 本体(本地单用户桌面应用,不需要处理并发外部流量/负载均衡,这一条假设需要用户确认,见第 9 节开放问题):
- Live 停止接受新的 Org Node 触发(不接受新
c_org_node_trigger调用),但允许已在执行的work_node_execution走到可安全中断的检查点(已有的持久化恢复机制保证中断后可恢复,不强求"优雅排空")。 - Live 进程退出。
- Candidate 关闭一次性 DB 副本连接,改连
PROD_DB_PATH,改绑LIVE_PORT。 - Candidate 成为新的 Live。
status=live,cutover_at=now。 - 原来的 Live checkout 目录保留为
previous_instance_dir,不删除,直到监控窗口正常结束。
- Live 停止接受新的 Org Node 触发(不接受新
5. 完整状态机
staged
-> candidate_starting
-> health_check_running
-> health_check_failed [回滚路径 A:见 7.1,不影响 Live,直接结束]
-> health_check_passed
-> cutover_in_progress
-> live
-> monitoring (进入监控窗口)
-> anomaly_detected -> rollback_in_progress
-> rolled_back [回滚路径 B:见 7.2,Live 换回旧版本]
-> rollback_failed -> blocked_pending_human [兜底,见 7.3]
-> monitoring_window_clean -> finalized (旧 previous_instance_dir 可清理)
finalized 是唯一的"安全终态";blocked_pending_human 是唯一需要人工介入的终态,且触发条件严格限定为"自动回滚机制本身失败",不是"代码写得不好"——这与用户对缺口 3 的更正(不要求人工评审代码内容)不冲突,是不同层面的安全网。
6. 健康检查 / 监控信号(全部确定性,不接入任何 LLM 判断)
Pre-cutover 健康检查(health_check_running,有界超时,全部通过才能 cutover)
| 检查项 | 方法 | 判定 |
|---|---|---|
process_alive |
启动后轮询 os.kill(pid, 0),超时 HEALTH_CHECK_STARTUP_TIMEOUT |
超时未存活 = fail |
http_health |
请求 Candidate 的 /api/health,超时 HEALTH_CHECK_HTTP_TIMEOUT |
非 200 或超时 = fail |
canary_task |
对 Candidate 跑一个固定的、结果可预期的最小任务(例如创建一个临时 project + 一个已知会通过的 trivial task,断言最终 task.status 与预期字段一致),超时 HEALTH_CHECK_CANARY_TIMEOUT |
结果不符/超时/异常 = fail |
任一 fail 立即进入回滚路径 A,不重试(重试属于"给坏版本第二次机会",不符合 fail-closed 原则;要重试整个自我修改,走 Planner 重新出一版)。
Post-cutover 监控(monitoring,窗口内持续采样)
| 信号 | 方法 | 判定阈值(MVP 固定值,不做用户可配) |
|---|---|---|
process_alive |
每 MONITOR_SAMPLE_INTERVAL 探活 |
连续崩溃/重启 ≥ MAX_CRASH_COUNT 次 / CRASH_WINDOW 内 = anomaly |
http_health |
同上频率请求 /api/health |
连续 MAX_CONSECUTIVE_HTTP_FAIL 次失败 = anomaly |
canary_task |
每 CANARY_INTERVAL 跑一次固定 canary 任务 |
单次失败即 anomaly(canary 本身就是"已知应该过"的任务,失败没有灰色地带) |
validator_approval_rate |
从 execution_event/work_node_execution 表按滚动窗口统计 Validator 节点 approved 占比,与 deployment 之前的历史基线比较 |
偏离基线超过 VALIDATOR_RATE_DEVIATION_THRESHOLD(绝对值,如 ±30 个百分点)= anomaly。这一条直接对应用户点名的场景:"改坏了的 validator"——如果新版本的 Validator 变得异常宽松(approve 率骤升)或异常严格(approve 率骤降到几乎全部拒绝),都会被这条信号捕获,且判断是纯算术,不需要另一个 LLM 来"评价 Validator 判得对不对" |
监控窗口:MONITORING_WINDOW_HOURS(固定值,如 24h)或 MONITORING_MIN_TASK_COUNT(固定值,如 10 个真实任务)先到者为准,之后无 anomaly 则 monitoring_window_clean → finalized。
7. 回滚路径
7.1 路径 A:Pre-cutover 回滚(健康检查未过)
Live 从未被动过,回滚成本最低:
- 终止 Candidate 进程(
SIGTERM,参考hermes_watchdog.py的终止哲学)。 - 删除
candidate_db_path一次性副本。 git revert本次合并 commit(不是reset/force-push,保留历史可追溯,这一步复用workspace_security.validate_subprocess_command()的 argv 校验)。- 发一个
bug_record,root_cause_category=core_engine,把健康检查失败的具体信号/日志作为 evidence 挂上去,供下一轮 Planner/Coder 重新尝试时参考。 status=health_check_failed,记录结束,Live 全程未受影响,调度立即可以继续处理下一个任务。
7.2 路径 B:Post-cutover 回滚(监控期间检测到 anomaly)
Live 已经是 Candidate,需要真正"换回去":
- 记录触发回滚的信号快照(哪个 signal_type、哪次采样、什么值)到
deployment.rollback_reason。 - 当前 Live(即出问题的新版本)停止接受新触发,同样允许在途节点走到可恢复检查点。
- 用
previous_instance_dir(第 4 步保留的上一版本 checkout)重新拉起进程,绑定LIVE_PORT,连接PROD_DB_PATH(生产库这次全程没有被换过,只是换了跑在它前面的代码进程,数据不受影响——这是 blue-green 相对"原地改代码"最大的安全收益)。 - 对"复活的旧版本"跑一遍 7.1 同款的三项健康检查,确认旧版本本身没问题(正常情况下必过,因为它是上一次已经
finalized的版本)。 - 通过 →
status=rolled_back,当前 Live = 旧版本,本次自我修改的 commit 在主分支上被git revert,发bug_record(同路径 A 第 4 步,evidence 换成监控期间的异常信号),调度恢复正常。 - 不通过(极端情况:连旧版本都起不来,比如磁盘满了这类环境问题)→ 第 7.3 节。
7.3 兜底:回滚本身失败
status=rollback_failed → blocked_pending_human。这是本设计里唯一保留的人工介入点,且明确限定触发条件仅为"自动恢复机制本身失效"(旧版本也起不来、DB 不可写这类基础设施问题),不是"代码判断谁对谁错"。控制台需要能清楚展示这个区别,避免用户以为又回到了"需要人工评审代码改得好不好"的老路。
8. 与既有 SDD 的对接点(2026-07-30 已回填,见第二次更新记录)
01-M-模型设计.md:新增deployment、deploy_health_signal两个实体(第 3 节)。已完成。03-C-控制与接口.md:新增c_deploy_supervisor_stage/c_deploy_supervisor_health_check/c_deploy_supervisor_cutover/c_deploy_supervisor_monitor/c_deploy_supervisor_rollback五个控制操作;c_org_loop_route扩展root_cause_category=core_engine的路由分支。已完成。04-P-流程与状态流转.md:新增p_deploy_supervisor_blue_green流程,状态转移表照搬第 5 节。已完成。05-实施约束与依赖.md:新增本机制的 MVP 固定参数表(第 9 节),比照现有p_usage_limit_wait参数(5→10→20→30min backoff、8h/9d 超时)"MVP 阶段硬编码、不做用户可配"的先例,登记同一张表里;新增 self-target worktree 无条件强制规则。已完成。06-验收与测试锚点.md:新增 REQ-15,覆盖:健康检查全过正常 cutover、健康检查失败路径 A、监控期 anomaly 触发路径 B、回滚本身失败兜底到blocked_pending_human四类 happy/failure 用例。已完成。sdd-mvcp-contract.json:同步以上四类变更,重新跑jsonschema.validate()。已完成。self-development-feasibility.md:缺口 2/3 的"推荐方案"段落指向本文档。已完成(2026-07-30 更新记录)。
9. MVP 固定参数(草案值,需要在实现前用真实测量校准,参考 A-1 的验证方法论)
| 参数 | 草案值 | 说明 |
|---|---|---|
HEALTH_CHECK_STARTUP_TIMEOUT |
30s | Candidate 进程从拉起到能响应探活 |
HEALTH_CHECK_HTTP_TIMEOUT |
10s | 单次 /api/health 请求 |
HEALTH_CHECK_CANARY_TIMEOUT |
120s | 一次最小 canary 任务从提交到出结果 |
MONITOR_SAMPLE_INTERVAL |
5min | process/http 采样间隔 |
CANARY_INTERVAL |
1h | 监控期 canary 任务运行间隔 |
MAX_CRASH_COUNT / CRASH_WINDOW |
2 次 / 10min | 判定进程反复崩溃 |
MAX_CONSECUTIVE_HTTP_FAIL |
3 次 | 判定健康端点持续不可用 |
VALIDATOR_RATE_DEVIATION_THRESHOLD |
±30 个百分点 | 判定 Validator 行为异常 |
MONITORING_WINDOW_HOURS |
24h | 监控窗口时长上限 |
MONITORING_MIN_TASK_COUNT |
10 个真实任务 | 监控窗口任务数上限,先到者结束 |
以上数值全部是工程经验草案,不是实测结果——和 05-实施约束与依赖.md §5.4.2 现有的额度等待退避参数一样,需要在真正实现并跑几轮真实自我修改后重新校准,不建议直接当最终值写进 contract.json 的强约束里(建议标注为"可调,需实测校准"而不是"硬编码不可变",这一点和额度等待参数刻意标注为"MVP-fixed"略有不同,因为额度等待的时间线是 Anthropic 平台文档定义的外部事实,而这里的阈值是纯粹的工程经验值)。
10. 未覆盖 / 开放问题(留给实现阶段决定,不阻塞本设计定稿)
- 单用户桌面场景假设:第 4 节 cutover 假设不需要处理并发外部流量/连接排空,只有 Cowork 控制台这一个使用者,断线可重连。如果未来 graphlemon 要支持多用户/多终端同时连接,这里的"直接切端口"简化模型需要重新评估。
candidate_db_path一次性副本的体积/耗时:如果生产 DB 随着长期使用变得很大,每次 cutover 前整库复制可能变慢,需要的话可以后续改成"复制 schema + 近期数据子集"而不是整库,MVP 阶段先用整库复制,简单可靠优先。git revert冲突:如果本次自我修改之后主分支又有新的(比如另一个并发自我修改任务的)提交,revert 可能冲突,需要 Deploy Supervisor 能识别 revert 失败并转blocked_pending_human(不在当前状态机图里显式画出,应该并入 7.3 兜底路径,而不是新开一个分支——这样能保持状态机简单)。