dsh-release-guard
August 17, 2026 · View on GitHub
healthCheck decide supervise rollback
一个把“Canary 发布 + 自动回滚”规范封装为 DSH 插件服务的轻量组件。它提供决策(decide(),适合作为外部发布控制面的内部服务、被其他插件或 Agent 调用),也提供监控控制(supervise(),周期探活并把决策自动落地为新会话路由);两者都不修改运行中的会话,只影响之后创建的新会话。
解决的问题
- 自写静态插件如果直接进入 host composition,逻辑坏了会导致 dsh 启动异常;
- 通过 agent preset 挂载插件后,需要一个可复用的健康检查与回滚决策;
- 社区缺少“先验证、再放量、坏了自动回滚”的通用插件服务。
功能一览
- preset 健康检查:尝试创建
agentPresets.standingKeyFor(id)的 standing mount,能挂载视为健康; - Canary 决策:
decide({ stable, canary })返回应继续使用 canary 还是回滚到 stable; - Supervisor 监控控制:
supervise({ stable, canary })周期探活 + 防抖切换 + 自动回滚/恢复,并把决策自动落地为新会话路由; - broken 状态透出:
listPresets()直接暴露 roster 中的broken原因; - 不改 Cordis:只依赖
ctx.agentPresets现有能力,不修改框架; - 可测试:仓库内提供无头功能测试、canary 回滚测试、坏包模拟测试,以及覆盖
dsh plugin真实安装流程的验收测试。
安装
前置
需要 @deepseek-ai/dsh-agent-presets 已挂载,否则插件无法激活(inject: [agentPresets] 找不到服务):
- web profile:
dsh-web-appbundle 已自带agent-presets行,无需额外操作; - headless / 自建 profile:在 profile 的
cordis.patch.yml手动挂载:
- insert:
- id: agent-presets
name: '@deepseek-ai/dsh-agent-presets'
config:
default: stable
通过 dsh plugin 安装(推荐,自动成为 bundle 层)
dsh plugin 会把 pnpm 参数转发到 profile 目录,并在成功后自动把声明了 dsh.bundle.patch 的依赖并入 dsh.profile.bundles(见 profile 的 package.json)。本地路径用相对或绝对路径均可(相对路径会锚定到你的调用目录):
# 从本仓库根目录
dsh plugin --profile web add ./dsh-release-guard
# 或任意目录用绝对路径
dsh plugin --profile web add /path/to/dsh-release-guard
验证组合树:
dsh --profile web --dump-config | grep dsh-release-guard
卸载:
dsh plugin --profile web remove dsh-release-guard
手动挂载(不装包)
把 index.mjs 放进 profile 目录,在 profile 的 patch 层加入(此时才能用相对路径,它从 profile 目录解析):
- insert:
- id: dsh-release-guard
name: ./index.mjs
inject: [agentPresets]
坑:bundle 的
cordis.patch.yml必须写裸包名name: dsh-release-guard,不能写./index.mjs——patch 里的相对路径一律按 profile 目录解析,而装包后文件在node_modules/dsh-release-guard/下,./index.mjs会报Cannot find module .../profiles/<name>/index.mjs。
服务:ctx.releaseGuard
listPresets()
列出当前 preset roster 与 broken 状态。
const presets = await ctx.releaseGuard.listPresets()
// [{ id: 'stable', trust: 'system' }, { id: 'canary', trust: 'user', broken: '...' }]
healthCheck(id)
对指定 preset 执行健康检查。
const result = await ctx.releaseGuard.healthCheck('canary')
// { id: 'canary', healthy: true } 或 { id: 'canary', healthy: false, reason: '...' }
decide({ stable, canary })
返回 Canary 发布决策。
const result = await ctx.releaseGuard.decide({ stable: 'stable', canary: 'canary' })
// 继续灰度:
// { active: 'canary', rollback: false, ... }
// 自动回滚:
// { active: 'stable', rollback: true, reason: '...' }
外部控制面拿到 result.active 后,通过 session.create({ agentPreset: result.active }) 路由新会话,或切换默认 preset。
supervise({ stable, canary, intervalMs?, requiredStableTicks?, onFlip? })
Supervisor 监控控制:启动周期监控循环,把决策自动落地为新会话路由(无需外部控制面轮询)。
const supervisor = await ctx.releaseGuard.supervise({
stable: 'stable',
canary: 'canary',
intervalMs: 200, // 探活周期(默认 200ms)
requiredStableTicks: 2, // 防抖:连续 N 次观测才切换(默认 2)
onFlip: (decision) => {}, // 可选:每次路由切换回调
})
supervisor.state() // { checks, active, pending, pendingTicks, lastDecision, flips }
supervisor.stop() // 停止监控循环
supervisor.removePreset(id) // 回收辅助:agentPresets.remove(删磁盘目录)
行为:
- 每 tick 用
standingKeyFor()挂载层实测 stable/canary(unmemoized 重读,磁盘变更下一 tick 即见;resolve()/list()只覆盖发现层,检测不到 apply 期坏包); - 决策语义与
decide()一致:stable 必须健康;canary 健康→canary,否则回滚; - 防抖:active 变化需
requiredStableTicks次连续观测才执行,瞬时抖动不切换; canary 修复后同样防抖自动恢复(flip 回 canary); - 控制动作:
settings.update('agent-presets', { default: active })——只影响之后创建的新会话, 运行会话保持加入时的代际(dsh 代际语义); - 回收:被取代代际在进程内无法卸载(dsh 无 standing-mount unload API),
removePreset()删磁盘 + 进程重启(进程内ctx.appExit请求退出 + 外部编排拉起)是唯一彻底回收。
参考 Erlang/OTP release_handler 三阶段语义:prep(健康检查)→ load(翻转路由默认)
→ post(防抖确认 N 轮;失败自动回滚/恢复)。详见 docs/DSH_SPEC_11_ERLANG_HOT_UPDATE.md。
Model Experience
None. 本插件只暴露 Host 侧服务,不注册模型可见的 prompt、工具 schema 或结果。
KV Cache effect
None. 插件不向模型请求前缀增加任何内容。
Known Limitations and Deferred Work
- 只做决策,不自动执行
session.create路由切换;路由切换由外部控制面完成。 (supervise()例外:自动翻转 settings 路由默认,但仍只影响新会话,不做已运行会话迁移。) supervise()依赖ctx.settings(dsh-settings)与ctx.timer(cordis-plugin-timer), 二者由 dsh-base 组合提供;插件inject: [agentPresets, settings, timer]。- host 层坏包无法由本插件自动回滚(设计边界):
releaseGuard服务本身在 host 平面,若坏包直接进入 host composition,进程启动即 fail-loud、服务不可达——即使decide()能在崩溃前算出回滚决策,也没有活进程执行它(坏包测试 Part D 实证)。这正是“先 canary 验证、再进 host”发布纪律的动机。host 层坏包的真实防线是:- fail-loud + 错误可诊断:启动失败且错误携带坏包签名(坏包测试 Part A/C);
- 运行中的实例不受影响:host 层 patch 热更新失败时 HMR 保留旧树继续运行(
watchUserPatches); - 外部编排层回滚:进程管理器/发布脚本在启动失败后切回上一版本——进程级启动失败没有“进程内回滚”概念。
- 缓解:guard wrapper(
tests/fixtures/plugin-guard.mjs)——对“未经验证就得上 host”的插件,用包装器行替代裸行:loader 只加载包装器,包装器动态加载真实插件并 try-catch,坏插件降级为pluginGuard.status() = { healthy: false, error }而非拖垮进程;host 存活期间decide()持续可用(plugin-guard 测试已验证)。只兜激活期错误,apply 内已启动的 timer/进程无法完全隔离。 - 已并入:supervisor 监控控制 =
supervise()——原参考实现(tests/fixtures/supervisor.mjs)的healthCheck/decide消费逻辑已并入本服务(supervise()),参考实现保留作独立对照。 - 健康检查基于“能否创建 standing mount”,不覆盖运行期工具调用错误率等运行时健康信号。
- 不处理多实例/多 dshweb 进程间的分布式决策;当前面向单进程控制面。
- 需要
@deepseek-ai/dsh-agent-presets已挂载,否则插件无法激活。