supervisor-auto-rollback-design.md 16.1 KB

Deploy Supervisor 蓝绿实例自动回滚机制设计

日期: 2026-07-30(同日更新:完成 Supervisor 概念改名消歧,见下方命名说明) 性质: 详细设计,回应用户"自动回滚机制现在就设计一下,省得到 PH5 阶段有那个阶段的问题"。落地 self-development-feasibility.md 缺口 2(应用变更+生效)与缺口 3(用户更正版:不要求人工评审,要求 supervisor 自动监督回滚)。 依赖: docs/done/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. 组件与进程边界

三个独立操作系统进程,任何一个崩溃不影响另外两个:

  1. Live 实例:当前正在服务的 graphlemon 后端(Graph Engine + 控制台 API),监听固定端口 LIVE_PORT,使用固定路径的生产 SQLite 库 PROD_DB_PATH。
  2. Candidate 实例:本次自我修改合并后的新版本,Deploy Supervisor 拉起来做验证,监听临时端口,使用一次性 DB 副本,与生产 DB 完全隔离,验证期间无论 Candidate 干什么都不影响 Live。
  3. 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 机制(部署新版本)

  1. Deploy Supervisor 拉取合并后的主分支到全新目录(不是原地 git pull,避免影响可能仍在读取源码的 Live 进程),生成 deployment 记录,status=staged。
  2. 复制 PROD_DB_PATH 到 candidate_db_path(一次性副本,用完即删,绝不让 Candidate 直接碰生产库)。
  3. 用 supervisor/isolation/isolated_workspace.isolate_workspace() 家族同款隔离原则拉起 Candidate 进程:subprocess.Popen,shell=False,固定 argv 形态,监听 candidate_port(生产端口 +1,或探测一个空闲端口),指向 candidate_db_path。status=candidate_starting。
  4. 跑健康检查(见第 6 节),status=health_check_running。
    • 全部通过 → status=health_check_passed,进入 5。
    • 任一失败/超时 → status=health_check_failed → 走回滚路径 A(第 7.1 节)。
  5. 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,不删除,直到监控窗口正常结束。

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 从未被动过,回滚成本最低:

  1. 终止 Candidate 进程(SIGTERM,参考 hermes_watchdog.py 的终止哲学)。
  2. 删除 candidate_db_path 一次性副本。
  3. git revert 本次合并 commit(不是 reset/force-push,保留历史可追溯,这一步复用 workspace_security.validate_subprocess_command() 的 argv 校验)。
  4. 发一个 bug_record,root_cause_category=core_engine,把健康检查失败的具体信号/日志作为 evidence 挂上去,供下一轮 Planner/Coder 重新尝试时参考。
  5. status=health_check_failed,记录结束,Live 全程未受影响,调度立即可以继续处理下一个任务。

7.2 路径 B:Post-cutover 回滚(监控期间检测到 anomaly)

Live 已经是 Candidate,需要真正"换回去":

  1. 记录触发回滚的信号快照(哪个 signal_type、哪次采样、什么值)到 deployment.rollback_reason。
  2. 当前 Live(即出问题的新版本)停止接受新触发,同样允许在途节点走到可恢复检查点。
  3. 用 previous_instance_dir(第 4 步保留的上一版本 checkout)重新拉起进程,绑定 LIVE_PORT,连接 PROD_DB_PATH(生产库这次全程没有被换过,只是换了跑在它前面的代码进程,数据不受影响——这是 blue-green 相对"原地改代码"最大的安全收益)。
  4. 对"复活的旧版本"跑一遍 7.1 同款的三项健康检查,确认旧版本本身没问题(正常情况下必过,因为它是上一次已经 finalized 的版本)。
  5. 通过 → status=rolled_back,当前 Live = 旧版本,本次自我修改的 commit 在主分支上被 git revert,发 bug_record(同路径 A 第 4 步,evidence 换成监控期间的异常信号),调度恢复正常。
  6. 不通过(极端情况:连旧版本都起不来,比如磁盘满了这类环境问题)→ 第 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. 未覆盖 / 开放问题(留给实现阶段决定,不阻塞本设计定稿)

  1. 单用户桌面场景假设:第 4 节 cutover 假设不需要处理并发外部流量/连接排空,只有 Cowork 控制台这一个使用者,断线可重连。如果未来 graphlemon 要支持多用户/多终端同时连接,这里的"直接切端口"简化模型需要重新评估。
  2. candidate_db_path 一次性副本的体积/耗时:如果生产 DB 随着长期使用变得很大,每次 cutover 前整库复制可能变慢,需要的话可以后续改成"复制 schema + 近期数据子集"而不是整库,MVP 阶段先用整库复制,简单可靠优先。
  3. git revert 冲突:如果本次自我修改之后主分支又有新的(比如另一个并发自我修改任务的)提交,revert 可能冲突,需要 Deploy Supervisor 能识别 revert 失败并转 blocked_pending_human(不在当前状态机图里显式画出,应该并入 7.3 兜底路径,而不是新开一个分支——这样能保持状态机简单)。