一次有用的恢复应找回能辨认的会话、会话引用的准确文件,以及预期任务状态;还必须避免重复执行备份之外已经发生的动作。压缩包散列一致或数据库能启动,都不足以证明这四件事。

本文区分三种证据: 已在官方容器执行的 Hermes API 备份/导入往返测试、已执行的本地 SQLite 快照组件测试,以及仍未执行的完整 CLI/任务恢复方案。两次已执行测试都使用合成数据,没有服务商密钥、模型调用、真实消息或生产数据。应用测试明确拦截了服务启动,不是正常 gateway 部署。

Railway 上的真实应用恢复:已通过什么

在 2026-10-04T01:58:28Z,防护测试代码在 Railway 临时 Linux VM 的官方 nousresearch/hermes-agent:main 镜像内运行。容器退出码为 0,结果记录 passed: true。使用的是开发 main 通道,不是稳定版认证。准确 digest 为:

nousresearch/hermes-agent@sha256:4ab33c59d8ae5a1660f24b3065626853d09a2b42f28e77e85de99798be08a663

镜像 revision 和九个已审查核心源码散列都匹配 158fd638da1629c8e62caf9ade1515d162def8ab。主机是 x86_64 Ubuntu 26.04.1 LTS、Docker 29.1.2;容器使用 Python 3.14.7、SQLite 3.53.1。应用结果与控制台记录保留准确身份、检查及边界。

测试调用真实 SessionDB、cron 存储、run_backup 和 run_import API,将 9 个归档成员恢复到另一个合成 home。独立核验确认:

  • 记录: 1 个会话、2 条消息;角色、内容和持久 message ID 全部一致,源与目标数据库完整性均返回 ok
  • 文件: 工作区 sentinel 和引用脚本在导入前后 SHA256 相同,备份 ZIP 散列也已重新独立核验
  • 任务状态: 任务 f306ea79f5f3 保留原 ID,仍为 paused、enabled: false、no_agent: true、本地投递,且无上次/下次执行时间
  • 隔离: 容器网络为 none,根文件系统只读,不发布端口,不含真实凭据或主机 Docker socket,移除全部 capabilities 并开启 no-new-privileges;正常镜像入口被绕过

真实应用控制台包含以下内容:

Backup complete: /lab/api-roundtrip/synthetic-full-backup.zip
Import complete: 9 files restored in 0.0s
PASS: real Hermes API round-trip; service start stubbed; no cron execution

其中 0.0s 是应用对极小合成导入的舍入显示,不是恢复性能基准。结果记录了 context 为 import 的一次服务启动拦截。Gateway 启动函数被有意替换为测试桩,没有调度器或任务运行。找回暂停任务定义只证明持久化,不证明正确执行或防止重复动作。本次没有验证模型生成结果、完整 CLI 入口、控制台、SSH 隧道、主机重启或外部动作。

下面仍是独立操作者演练方案,其命令块并未在这次 API 测试中逐字执行,不能因往返成功而推断其余门槛也通过。

证据范围示意图:九个归档文件恢复一个会话、两条消息和暂停任务;网关及 cron 执行仍未测试
自制证据范围示意图,不是截图。绿色检查对应保留的 API 结果;任务执行仍需单独验收。可下载实际控制台原始记录,或查看完整公开结果。

较早执行的本地快照组件测试

2026-10-04,我们对提交 158fd638da1629c8e62caf9ade1515d162def8ab 中未经修改的 backup_sqlite.py、上游预检测试及少量附加测试代码进行了执行。环境是 Python 3.12.14、SQLite 3.53.1。这不代表完整当前版本 Hermes 可以用同一个 Python 版本安装。

调用方式为 python -I -S run_lab.py,只使用 Python 标准库、新建的合成目录和本地子进程。执行前核对了源文件 Git blob 标识。机器可读结果 包含固定提交、散列、参数、退出码及边界;实验环境绝对路径已作归一化。

实际观察:

  • 直接复制主数据库文件漏掉了只存在于 WAL 中的已提交记录;真实上游快照组件保留了该记录,SQLite 完整性检查返回 ok
  • 保留数量设为二时,创建三个快照后保留最新两个;只破坏合成输入后,命令退出码为 1,原有已发布快照保持不变
  • 缺少 state.db 时退出码为 0,但 path: null。这是空操作,不是成功生成可恢复备份
  • 人为锁住源数据库后,在设置的 0.2 秒期限内安全失败,没有降级为直接复制文件
  • SIGTERM 令子进程返回 -15,旧 .bak 不变,没有发布新 .bak;但遗留了 .partial 暂存文件。重试产生有效快照,却不会清掉旧暂存文件

这些结果只证明真实快照组件,不证明 Hermes 会话模式、完整 ZIP 备份/导入、调度器、云存储或灾难恢复。不能因为 .partial 文件存在就将其当成备份恢复;确认没有备份进程仍在使用后,才能审查遗留暂存文件。

1. 选对恢复单位

在已核对的源码版本中,完整备份命令是 hermes backup,完整恢复命令是 hermes import,不是 hermes restore。快速快照是另一套操作。先看已安装版本的帮助:

hermes backup --help
hermes import --help
hermes cron --help

完整归档可能包含配置、凭据、会话、状态数据库、profiles、脚本及 Hermes 数据根目录中的普通文件,且不会自动加密。不要上传到公开 issue 或代码仓库。外部项目工作目录不会因为路径出现在会话里就被备份,必须另行盘点。

快速快照只选择部分状态/配置,不代替完整脚本、工作区和技能归档。在此版本,快速快照包含 cron/executions.db,却不包含 cron/deliveries.db,所以不能把它当作定时工作和投递状态的完整迁移。

完整备份使用默认 Hermes 数据根目录(含 profiles),完整导入使用当前 home/profile。先核对两个实际解析出的路径;不能假设命名 profile 自动限制完整备份范围,也不能假设设置一个环境变量就不会写到别处。

2. 隔离是硬性门槛,不只是换目录

恢复环境未核验前,停止在完整导入之前。 当前导入代码可能在解压后安装并启动 gateway,没有 --no-start 选项。仅换一个 HERMES_HOME 不能阻止该副作用。

后续完整演练至少需要:

  • 临时环境,仅有合成 HOME 和状态,不继承服务商令牌、消息平台登录、生产挂载或 SSH 材料
  • 已核验固定版本的服务行为,导入不能启动未审查的 gateway/调度器;恢复沙箱不能访问主机服务管理器或 Docker socket
  • 导入和检查期间不能执行外部动作;建立或改变网络限制是另需操作者批准的设置步骤,不是本文悄悄执行的命令
  • 相互独立、初始为空的源和恢复目录,以及位于两者之外的小型证据/副作用目录
  • 源和目标调度器绝不同时运行,在一致性检查点前都暂停

替换服务启动函数的测试桩可以测试导入内部逻辑,但应标成 API 测试,不能说完整 CLI/服务已通过。上文带显式服务测试桩的 API 往返已经通过,未修改的完整 CLI/服务路径仍未执行。不要虚构 --no-start,也不要把 --force 当作隔离开关。

3. 用真实应用创建能核对的合成状态

以下是固定版本 Hermes 安装完成、隔离验收通过后才执行的拟定命令,目前未运行。使用该安装的 Python,确保导入的是 Hermes 真代码。SOURCE_HOME、TARGET_HOME、ARCHIVE 必须是临时实验中的绝对路径,不能指向已有 profile。源目录应位于原生 ~/.hermes 树和 <root>/profiles/<name> 结构之外,避免完整备份自动扩大到父根目录。运行 Python fixture 和 cron 命令前,都先设置 HERMES_HOME="$SOURCE_HOME";先新建源目录,目标保持为空。

用真正的 SessionDB API 创建会话,不自造一个看起来像 Agent 状态的表:

import os
from pathlib import Path
from hermes_state import SessionDB

home = Path(os.environ["HERMES_HOME"])
(home / "workspace").mkdir(parents=True, exist_ok=True)
(home / "workspace" / "restore-marker.txt").write_text(
    "synthetic restore marker: cobalt-harbor-57\n", encoding="utf-8"
)
with SessionDB(db_path=home / "state.db") as db:
    db.create_session("lab-session", "cli", model="synthetic",
                      cwd=str(home / "workspace"))
    db.append_message(session_id="lab-session", role="user",
                      content="Read the synthetic restore marker")
    db.append_message(session_id="lab-session", role="assistant",
                      content="Fixture response, not a model-generated result")
    rows = db.get_messages_as_conversation("lab-session")
    assert len(rows) == 2
    assert all(row["message_uid"] for row in rows)
    print(rows)

备份前记录实际 message UID、角色/内容、会话 ID、文件 SHA256、数据库 schema/完整性结果。插入的消息是 fixture,不是模型作答证据。只在新源目录创建一次;恢复后重新造数据不算恢复成功。

通过 Hermes 创建真实但暂停、不用模型的任务。脚本位于当前 home 的 scripts,不是 cron/scripts:

export HERMES_HOME="$SOURCE_HOME"
mkdir -p "$HERMES_HOME/scripts"
printf 'print("hermes-lab-sentinel")\n' > "$HERMES_HOME/scripts/sentinel.py"
hermes cron create 'every 1h' --name lab-sentinel \
  --no-agent --script sentinel.py --deliver local \
  --paused --paused-reason 'Restore rehearsal'
hermes cron list --all
sha256sum "$HERMES_HOME/workspace/restore-marker.txt" \
  "$HERMES_HOME/scripts/sentinel.py"

记录真实返回的任务 ID。要求它处于暂停/禁用状态、没有下次执行时间,同时保存计划、脚本、no_agent 和本地投递设置。只有脚本却不加 --no-agent 仍可能调用模型。本阶段不运行任务。

4. 获取应用一致的完整备份

停止或静止所有写源状态的进程,包括 gateway、cron 和控制台修改操作。SQLite 一致快照只保证单个数据库,不会令 jobs.json、多个数据库和工作区文件形成一次共同事务。

确认完整备份解析出的数据根就是合成源目录后:

if HERMES_HOME="$SOURCE_HOME" hermes backup --output "$ARCHIVE" --keep 0; then
  printf 'Backup exit code: 0\n'
else
  rc=$?
  printf 'Backup failed, exit code: %s\n' "$rc" >&2
  exit "$rc"
fi
sha256sum "$ARCHIVE"
python -m zipfile -t "$ARCHIVE"
python -m zipfile -l "$ARCHIVE"

--keep 0 关闭归档修剪。执行后续命令前先记录备份命令自身的退出码。部分归档保留下来并不代表成功:当前实现可能保留它并返回 1,锁争用可能返回 2;非零立即停止。ZIP 格式检查不证明应用数据完整。

清单必须包含预期数据库、标记文件、脚本和 cron 任务。完整归档会排除部分运行时/缓存/代码树、符号链接和 SQLite sidecar。一些记忆提供者可加入 _external/ 项,导入时可能写到当前 home 之外的 HOME 下。本合成演练遇到意外外部项应停止,不把未审查归档解压进真实用户目录。另行备份的工作区也要记录路径和散列。

5. 导入空目标,核对有意义的数据

只有服务/网络隔离及归档检查全部通过后,才执行:

HERMES_HOME="$TARGET_HOME" hermes import "$ARCHIVE"

新空目标演练不要用 --force。记录退出码和警告。导入有选定文件的预检,但不是全文件事务;后续失败可能留下半恢复目标。保留失败目标供诊断,改用另一个全新空目标重试,不覆盖唯一好源。

启动任何工作进程前,要求逐项通过:

  1. 用真实 SessionDB 打开恢复会话;会话 ID、两个 message UID、角色和内容与备份前一致,同时做 SQLite 完整性检查,不能只数行数
  2. 按相对路径比较两个文件散列。会话里的工作目录可能仍指向旧源;执行工具前必须检查并有意重新映射,不能因误读旧源文件而“通过”
  3. 运行 HERMES_HOME="$TARGET_HOME" hermes cron list --all,原任务 ID、暂停状态、计划、脚本、本地/无模型设置均应保留;不要重建任务来掩盖丢失
  4. 确认没有意外启动 gateway/调度器,记录导入如何处理运行时身份文件,不手工复制旧 PID/锁文件回来
  5. 在另行批准的执行阶段,运行恢复后的 sentinel 一次并检查输出/历史:
HERMES_HOME="$TARGET_HOME" hermes cron run "$JOB_ID"
HERMES_HOME="$TARGET_HOME" hermes cron runs "$JOB_ID" --limit 20

显式 cron run 会越过暂停状态。本地输出应是 sentinel 文本,实际历史中应有该次执行。这只能证明恢复任务能在不调用模型的情况下执行,不能证明定时任务恰好执行一次。任务已暂停不等于手动执行天然安全。

6. 恢复调度前测试重复动作

Hermes 分开保存任务定义、执行历史和投递状态。恢复旧账本可能抹掉“任务已经做过”的证据,外部消息、付款和发布不会随归档倒退。

再准备一个刻意无害的 no-agent 脚本,唯一动作是在两个备份目录之外的独立本地副作用日志追加固定合成操作 ID。以下用例只在隔离运行时执行,保留实际前后行数:

  • 恢复期间不执行: 导入并检查,任务保持暂停,独立日志必须不变
  • 动作后恢复旧备份: 先备份,再在源上执行一次无害动作,保持源停止,恢复较早归档;独立日志应仍保留已经发生的动作,即使恢复历史没有它
  • 有意重试: 手动执行恢复任务;第二次副作用展示重放风险,不能误标为调度器 bug,也不能宣称手动重试自动去重
  • 有防护的重试: 使用明确设计、保存在备份之外的接收端操作键重做实验;检查接收端拒绝重复,副作用计数不变。这需要真实实现和结果,提示词里的规则不是持久幂等机制
  • 中断后的不确定状态: 本地副作用发生后、完成记录落盘前停止实验任务;重试前先核对独立日志。unknown/failed 不等于什么都没发生

这些用例尚未执行,不要用真实邮件或付款来试。Hermes 的 occurrence claim 和账本不能保证跨旧备份或两个复制 home 的端到端恰好一次。在逐项对账备份后的副作用、确认只有一个调度器拥有工作负载前,保持高风险工具禁用。

恢复验收记录

保留真实镜像/源码版本、合成源与目标身份、归档散列/清单、备份及导入退出码、时间、校验输出、原任务 ID、运行历史、独立副作用日志计数、失败行为和清理状态。备份年龄与恢复到经验证就绪的耗时分别记录,不从未运行方案编造恢复时间。

当前结果刻意限定为:固定版本快照组件和有防护的应用 API 备份/导入已经通过;完整 CLI/服务恢复、任务执行与重复动作门槛仍未关闭。 云主机丢失、加密异地恢复、真实模型工作、控制台和重启不在该组件测试覆盖范围内。私密控制台方案有独立验收项。