dsh-agent-checkpoint
存档点管理器:最后的保活机制 + 试错回滚工具。定时/事件/主人指令创建健康存档点(记忆库+灵魂+校验和),验证器确保存档可启动,出事后一键恢复最近健康点。
- Stars
- 0
- Language
- TypeScript
- Created
- Sep 7, 2026
- Updated
- Sep 20, 2026
Introduction
dsh-agent-checkpoint
一句话:给 agent 一个「存档 / 读档」工具面——把记忆库(storages 顶层 *.json)+ 灵魂(AGENTS.md)+ 每文件 SHA-256 + 元数据打包成一个经过自检的目录,出事后一条 checkpoint_restore 回到最近健康点。
为什么值得用:改 AGENTS.md、改插件、跑不可逆实验之前,先有一份「可证明没坏」的快照——存档写完立刻做 SHA-256 逐文件比对 + JSON/storage 结构校验,健康与否进返回值与日志;恢复前把被覆盖的现存文件先拷进 .pre-restore-<stamp>/,所以回滚本身也是可逆的。定时侧还带启动补档:进程重启吞掉内存 timer 时,距最近存档已超周期就立即补一档(这条是踩过坑才有的,见「设计要点」)。
能力
| 工具 | 用途 |
|---|---|
checkpoint_create | 创建存档点(reason 必填留痕)。返回 {ok, id, fileCount, healthy};写 manifest.json(原子写)+ files/ 子树 |
checkpoint_list | 列出全部存档点(含健康状态):id / createdAt / reason / healthy / files(每个都现场复算 SHA-256) |
checkpoint_verify | 验证存档健康(SHA-256 比对 + JSON 解析 + storage unit 结构)。id 缺省 = 全部;返回逐条 issues |
checkpoint_restore | 从存档点恢复。id 缺省 = 最近的健康点;覆盖前自动备份到 .pre-restore-* |
checkpoint_cleanup | 清理旧存档(保留最近 keep 个,缺省用配置 keepCount) |
行为侧(无工具):
- 定时自动存档:每
autoIntervalMs(默认 6h)产出一档reason: "auto";外加启动补档(距最近存档 ≥ 周期 → 进程启动即立即补,不等 interval 从头计)。 - 服务面:
ctx.provide('checkpoint', { create(reason) })——供其他插件消费(压缩前自动存档等),失败不阻塞调用方。当前消费方:dsh-compact-provider。 - 可选 git 保险库联动:
gitVaultCommand非空时,每次存档成功后追加执行该 argv 命令(best-effort,超时 60s,失败仅 warn,不影响存档)。
快速开始
1) 装依赖(自研插件家园 self-plugins/,在目标 profile 的 package.json 加 link 依赖):
"dsh-agent-checkpoint": "link:<工作区>/self-plugins/dsh-agent-checkpoint"
2) 挂组合(profile 的 cordis.patch.yml):
- insert:
- id: agent-agent-checkpoint
name: dsh-agent-checkpoint
config:
keepCount: 10
autoIntervalMs: 21600000 # 6h;0 = 关闭定时与补档
3) 30 秒验证:调 checkpoint_list → 期望返回 ok: true 与 checkpoints 数组(全新环境为空数组);再调 checkpoint_create(reason: "验收")→ 期望返回 {ok: true, id: "YYYYMMDD-HHmmss-<6位>", fileCount: ≥1, healthy: true},随后 ls ${DSH_HOME}/checkpoints/<id>/ 应能看到 manifest.json 与 files/。
注意:
checkpoint_create/checkpoint_restore/checkpoint_cleanup有真实副作用(写目录 / 覆盖文件 / 删目录),不是只读探测——在不属于你的环境里请显式传reason留痕后再动手。
配置
| 项 | 默认 | 说明 |
|---|---|---|
checkpointDir | '' → ${DSH_HOME}/checkpoints | 存档点根目录;DSH_HOME 缺省回落进程 cwd |
storagesDir | '' → ${DSH_HOME}/storages | 被快照的记忆库目录(只取顶层 *.json,非递归) |
soulFile | '' → <cwd>/AGENTS.md | 被快照的灵魂文件 |
keepCount | 10 | 保留存档点数;每次存档成功后按 createdAt 降序清理更旧的 |
autoIntervalMs | 21600000(6h) | 自动存档周期;0 = 关闭定时轮巡与启动补档 |
gitVaultCommand | [] | 存档成功后追加执行的 argv 数组(如 ["wsl.exe","-d","Ubuntu","--","bash","scripts/git-vault-commit.sh"]);空 = 不启用 |
相对路径按进程 cwd 解析(配置为相对路径时的行为,见 src/pure.ts:resolvePaths)。
落盘与自证(出问题时先看这里)
本插件不写 *-trace.jsonl 侧车轨迹——日志走 ctx.logger('checkpoint'),而宿主 logger 不落盘,所以「自动存档到底有没有发生」只能由存档产物证明。这是已知的可维护性缺口(docs/semantic.md §10 U5)。
落盘产物 = 存档目录与索引(目录即清单):
${DSH_HOME}/checkpoints/
├── <id>/ # id = YYYYMMDD-HHmmss-<uuid 前 6 位>
│ ├── manifest.json # { version:1, id, createdAt(ISO), reason, files:{ rel:{sha256,size} } }
│ └── files/ # 快照正文,rel 固定两种形态:
│ ├── storages/<名字>.json # ← storagesDir 顶层每个 *.json
│ └── AGENTS.md # ← soulFile
└── .pre-restore-<stamp>/ # 恢复前备份(位于 checkpointDir 内,平铺;不在清理范围内)
| 字段 | 含义 |
|---|---|
manifest.json.version | 结构版本(当前恒为 1) |
manifest.json.reason | 触发者留痕:manual / auto / 压缩前自动存档等(调用方传入) |
manifest.json.files[rel].sha256 / .size | 该文件的校验和与字节数——checkpoint_verify 用它重算比对 |
.pre-restore-<stamp>/<rel 转义名> | 被覆盖文件的旧副本;文件名 = rel 的 / 换成 __(备份目录是平铺的) |
一条命令答五问(本插件只答得全 ③④,其余见注):
d=$(ls -1d "$DSH_HOME"/checkpoints/2* | tail -1); python3 -c "
import json,sys; m=json.load(open(sys.argv[1]))
print(m['id'], m['createdAt'], m['reason'][:60], len(m['files']), list(m['files']))" "$d/manifest.json"
# ① 跑的是哪个构建 → manifest 无 build 字段(缺口);改用 mtime 对照:stat -c %y lib/index.js vs web 进程启动时间
# ② 谁发起 / 存了什么 → reason(manual / auto / 压缩前自动存档…)+ files 的 rel 键即入库对象
# ③ 断在哪一段 → 无阶段枚举;目录缺失或半写(只有 manifest.json 没有 files/)= 存档中断
# ④ 结果质量 → files 条数 + 每个 rel 是否非空;再调 checkpoint_verify 复算 SHA-256(重算不一致 = 产物被改)
# ⑤ 耗时与预算 → 无 durationMs;用相邻两档 createdAt 的间隔 vs autoIntervalMs 判「定时/补档是否按周期跑」
行为级验证(无需落盘):checkpoint_list 返回真清单、checkpoint_verify 返回 healthy: true + 空 issues——这两个工具就是本插件的自证入口。
旁注:
${DSH_HOME}/checkpoint-standalone.mjs是外部脚本,不是本插件所写(插件只产checkpoints/子树);排查时勿把两者混为一谈。
生效判据与回退
生效判据(三选一,按可靠性排序):
- 进程级:
lib/index.js的 mtime ≤ web 进程启动时间,且src/index.ts不新于lib/index.js(源码改了没构建 = 跑的还是旧产物); - 生态级:
plugin_boot_status(dsh-plugin-bootreport)的 live/stale 清单把本插件列为 live ⇒ 进程在跑当前构建; - 行为级(最直接):现读调
checkpoint_list能返回清单;调checkpoint_create后manifest.json的createdAt与源文件重算的 sha256 一致。
注意:重新构建 ≠ 生效——产物 mtime 新只证明「构建过」,进程启动时间晚于产物 mtime 才算「在跑它」(AGENTS §5.11 §6;曾有修复躺了 41 分钟未生效)。web profile 开 HMR 时
lib变更会重载该行,否则走preflight_check→daemon_restart。日志旁证(
checkpoint 就绪:dir=… keep=…/存档 <id>: N 文件 healthy=…/清理旧存档 N 个)走宿主 logger、不落盘,不得作为唯一证据。
回退(三档,外加处置数据的第四档):
- 源码级:
git -C self-plugins/dsh-agent-checkpoint log --oneline→git revert <commit>(或git checkout <上一提交>)→npm run build→ 预检 → 重启; - 组合级:profile patch 给该行加
disabled: true,或plugin_stop dsh-agent-checkpoint→ 5 个工具消失、定时与启动补档停止、ctx.checkpoint服务不再提供(dsh-compact-provider的压缩前存档随之静默跳过,不影响压缩本身); - 运行期:本插件无内存态业务状态——停用即干净,无残留需清理;
- 数据级(本插件自己的回滚能力):
checkpoint_restore(id缺省 = 最近健康点,覆盖前自动写.pre-restore-*,故恢复可逆)。
测试
npm run build && npm test # build = tsc;test = node --test "tests/*.test.mjs"
23 例离线测试,全部 pass(实测 # tests 23 / # pass 23 / # fail 0),跑的是构建产物——tests/pure.test.mjs 从 ../lib/pure.js 导入,所以改源码后必须先构建(npm test 脚本本身不含 tsc)。
覆盖范围(tests/pure.test.mjs,8 个纯函数,正常路径 + 失败/退化路径):
resolvePaths— 空配置走默认、显式配置优先、DSH_HOME缺失/空串回落 cwd 且仍为绝对路径;formatStamp— 定长YYYYMMDD-HHMMSS补零(存档目录名字典序排序依赖它);validateStorageUnit— 合法 unit 通过;非法/空/截断 JSON、非对象、缺unit、tables非对象逐条给出原因且不抛;shouldAutoBackfill— 无记录/NaN 补档、恰好到周期补、差 1ms 不补、intervalMs<=0恒不补、时钟回拨不补;planRestore— 显式 id 命中即不搜索其它档、未命中报错而不静默改投、无 id 时按顺序逐个验健康、空列表 →missing;restoreDest— 白名单映射;尸体样本:storages/../AGENTS.md.bak等逃逸形态必须返回null(修前实测返回盘根\AGENTS.md.bak,此用例当时为红);selectStaleCheckpoints— 保留语义、keep 超长不删、空列表、keep=0全删、幂等收敛、负 keep 的 JSslicequirk 钉住现状;backupFileName—storages/memory.json→storages__memory.json(备份目录平铺)。
无需网络、无需真实外部依赖(无 IO、无时钟依赖;gitVaultCommand 不在测试中触发)。未覆盖:ctx 级集成(造 ctx 跑 apply 断言工具注册 / 定时器接线 / 真实落盘)——见 docs/semantic.md §10。
设计要点
- 纯逻辑与 IO 分离:
src/pure.ts收全部决策(路径解析 / 补档判定 / 恢复计划 / 落点白名单 / 清理切片 / storage 结构校验),不引node:fs、不收ctx、不读时钟;index.ts只做接线(环境读取、落盘、子进程)。决策因此可离线单测,失败路径能被机器锁住。 - 恢复落点白名单(真实缺陷的修复):
rel只认AGENTS.md与storages/<纯文件名>,其余一律拒收。修前storages/../AGENTS.md.bak归一化后落在storagesDir之外——恢复是破坏性动作,一份被篡改/手改的manifest.json就能写到工作区任意路径。正常 manifest 由readdir裸文件名拼出,永不含分隔符 ⇒ 白名单对合法存档零影响。 - 显式 id 不做健康校验(已知信任缺口):传
id时只按 id 找目录直接恢复;只有「id缺省」路径才逐个verifyCheckpoint挑最近健康点。即一个坏 id 可以从坏存档恢复——语义待定调(§10 U2),文档如实登记。 - 无效存档照落盘、只在日志与返回值标注:
createCheckpoint自检后不删除 unhealthy 存档(保活场景宁可有坏档不可无档)。旧 README 写的「无效存档不落库」与实现不符,已按源码订正。 - 启动补档是必要的自愈:纯内存
setInterval随进程重启归零,重启间隔短于周期时永远到不了触发点(曾出现「周期 6h 而实例总在 6h 内重启 ⇒ 一次 auto 都没有」)。补档判定读存档目录manifest.createdAt(持久化产物),不依赖内存。 - 清理只认清单内目录:
cleanupOld基于listCheckpoints()——只含可解析manifest.json的目录。故.pre-restore-*备份永不会被清理误删;代价是备份会无限累积(§10 U6)。 checkpoint_cleanup的负keep危险:实现是list.slice(keep),keep=-5且存档数 < 5 时等于一次删光所有存档。已用「文档化 quirk」用例钉住现状,未加 clamp(§10 U7)——别传负数。- 不发布、不订阅任何 cordis 事件:外部(守护、面板)无法订阅「存档完成」;定时走
ctx.effect,timer.unref?.()保证不阻进程退出。 - 反定位:不是上下文治理/压缩(那是
dsh-agent-compact/dsh-agent-context);不是 git 版本管理(只是可选触发保险库脚本);不做损坏修复(游戏存档思路:回到健康点);不含会话级快照(不动sessions/*.jsonl.zstd);不含进程保活(那是守护类插件的事,本插件管数据存活)。
相关文档
| 文档 | 内容 |
|---|---|
docs/semantic.md | 权威契约:定位与反定位、术语表、概念模型与不变量、契约(含调用点清单)、边界与信任、可证伪验收清单(A1–A14)、实践修订记录、未决问题(U1–U8) |
| alice-digital-life | 本插件所属生态的中心索引(全部自研插件) |
技能 plugin-maintainability | 机制自证(五问一条命令可答)与可维护性工程的方法论 |
技能 semantic-doc-first / dsh-plugin-testability | 语义文档优先开发;决策逻辑抽纯层 + 失败路径机器锁住 |
License
MIT © jonah791
本插件属于我的数字生命爱丽丝(alice-digital-life)的 DSH 自研插件生态——50 个插件按生命/认知/感知/行动/通信/治理/呈现七层组织。