Skip to content

feat(backup): scheduled auto-backups with retention + hermes backup --list - #43058

Open
tgmerritt wants to merge 1 commit into
NousResearch:mainfrom
tgmerritt:feat/auto-backup-schedule
Open

feat(backup): scheduled auto-backups with retention + hermes backup --list#43058
tgmerritt wants to merge 1 commit into
NousResearch:mainfrom
tgmerritt:feat/auto-backup-schedule

Conversation

@tgmerritt

Copy link
Copy Markdown
Contributor

What does this PR do?

Implements the automatic half of #12238 (Built-in Automatic Backup & Version Control).

The backup building blocks already shipped — hermes backup (full zip), hermes import (restore), --quick state snapshots, pre-update archives — but getting periodic backups today still means hand-wiring a cron job, which is exactly the barrier the issue calls out for non-developer users. A single disk failure still wipes months of accumulated skills and memory for anyone who never set that up.

This adds an anacron-style scheduled backup, off by default, driven by the config block the issue proposed:

backup:
  enabled: true       # default: false — opt-in
  schedule: daily     # hourly | daily | weekly | <hours as integer>
  keep_last: 7        # auto archives to retain (oldest pruned first)
  dir: ~/backups      # optional — default: ~/.hermes/backups/

maybe_create_auto_backup() mirrors the curator's gating pattern exactly: cheap when disabled or not due (one config read + one small JSON stat), with the real cadence enforced by a last_run_at stamp in backups/.auto_backup_state.json. The gateway cron ticker polls it hourly alongside the curator, so any long-running gateway gets periodic snapshots with zero user setup — no OS cron, identical behavior on Linux/macOS/Windows. Archives reuse _write_full_zip_backup() (same exclusions, same WAL-safe SQLite copies) and restore with the existing hermes import.

Design decisions worth flagging for review:

  1. Opt-in default. Silent disk consumption felt like the wrong default to ship; flipping to default-on is a one-line change if you prefer.
  2. Failures stamp last_run_at too — a persistently failing destination (e.g. unmounted drive) retries once per interval instead of walking the full tree on every hourly poll.
  3. Pruning only touches auto-*.zip — pre-update/pre-migration/manual archives in the shared backups/ directory are never deleted, and keep_last floors at 1 (same rationale as _prune_pre_update_backups).
  4. backup.dir instead of cloud destinations. Pointing it at a mounted drive or a cloud-synced folder (Dropbox/Syncthing) covers the off-machine need without a provider integration; S3/R2 targets from the issue would be a separate, much larger PR.
  5. Scope: this covers the issue's backup acceptance criteria (scheduled runs, keep_last pruning, restore, --list). Per-skill history/rollback and memory diff are a version-control subsystem, not a backup mechanism — better served by a dedicated follow-up PR if there's appetite.

I had Claude Fable 5 do this work - specifically to test it's capabilities when analyzing a request on Github, an unknown codebase (to me), and to use some of my Claude-time to give back to the open source community. This may or may not be in the style or format that the maintainers wish - and it's my highest goal NOT to actually get involved in the software but rather push Fable itself to see how effective it can be with limited context - if the maintainers accept this PR, that's an implicit nod of approval to Fable as I gave it very very little to work from


Related Issue

Partially addresses #12238 (backup acceptance criteria — see scope note above)

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

  • hermes_cli/backup.py — new "Scheduled auto-backup" section: maybe_create_auto_backup(), backup: config parsing (enabled/schedule/keep_last/dir), _prune_auto_backups(), state persistence, plus list_backup_archives()/run_backup_list() for --list.
  • gateway/run.py_start_cron_ticker() polls maybe_create_auto_backup() hourly (AUTO_BACKUP_EVERY), same pattern and error-handling as the curator tick.
  • hermes_cli/subcommands/backup.py + hermes_cli/main.py--list flag and dispatch.
  • website/docs/reference/cli-commands.md--list option row and a "Scheduled auto-backups" section documenting the config block and the gateway-driven cadence.
  • tests/hermes_cli/test_auto_backup.py — 25 new tests: schedule parsing (named/numeric/garbage), disabled-by-default, interval gating (incl. naive-timestamp and unparseable-state recovery), archive creation + hermes import validation compatibility, keep_last pruning that spares non-auto archives, failure stamping (no poll-hammering), custom dir, --list classification/ordering/output.

How to Test

  1. scripts/run_tests.sh tests/hermes_cli/test_auto_backup.py — 25/25 pass.
  2. Regression: scripts/run_tests.sh tests/hermes_cli/test_backup.py tests/hermes_cli/test_subcommands_batch.py — all pass except TestProfileRestoration::test_import_creates_profile_wrappers, which fails identically on unmodified main in my environment (the test's alias-collision check consults the real PATH, and my machine has an unrelated ~/.local/bin/researcher binary). Pre-existing, unrelated to this change.
  3. Manual smoke (isolated HERMES_HOME): set the config block with schedule: hourly, call maybe_create_auto_backup()auto-<timestamp>.zip created with config/skills/.env inside; immediate second call returns None (gated); hermes backup --list shows the archive with kind/date/size.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run the relevant test suites via scripts/run_tests.sh (see How to Test, incl. one pre-existing environmental failure on main)
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Ubuntu 24.04 (aarch64)

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (the example file doesn't carry optional feature blocks like approvals:/updates:; documented in website/docs/reference/cli-commands.md)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — pure-Python pathlib/zipfile/JSON; the in-gateway scheduler specifically avoids OS cron so Windows gets the same behavior
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A (CLI + gateway internals only)

🤖 Generated with Claude Code

@liuhao1024

Copy link
Copy Markdown
Contributor

Verification review — clean ✅

Reviewed the full diff (~550 lines across 4 files + 286-line test file). Well-structured anacron-style auto-backup feature gated by backup.* config.

What I checked:

  • Disabled by default — opt-in via backup.enabled: true
  • Atomic state writes_save_auto_backup_state uses tmp + os.replace, safe against partial writes
  • Failure stampinglast_run_at is written even on failure, preventing retry-hammering on persistently broken configs
  • Pruning safetykeep_last floor is 1 (never prunes the just-created archive); only touches auto-*.zip so manual/pre-update archives are unaffected
  • Schedule parsing — handles named schedules, numeric hours, string numbers, and garbage input with sensible fallbacks (daily)
  • Naive datetime handlinglast_run_at without timezone is treated as UTC
  • Config integration — piggybacks on gateway cron ticker (poll rate doesn't affect actual cadence)
  • hermes backup --list — correctly classifies auto/pre-update/pre-migration/manual archives
  • Test coverage — comprehensive: disabled gate, first-run creation, interval gating, hourly schedule, unparseable state, pruning, list output

No issues found. Clean feature implementation.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard labels Jun 9, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused scheduled-backup implementation and accompanying tests. The feature is still needed on current main, but two path-scope issues need correction before it is safe to salvage.

Problems

  • hermes_cli/backup.py:1113 and :1179 use get_default_hermes_root(). For HERMES_HOME=<root>/profiles/<name>, that helper deliberately returns <root> (hermes_constants.py:123-150), while load_config() reads the active profile config (hermes_cli/config.py:747-749). A named profile can enable the feature yet write/list root-store archives.
  • hermes_cli/backup.py:1117 permits a custom destination under HERMES_HOME. _write_full_zip_backup() only excludes static directory names (hermes_cli/backup.py:1157-1172), so a destination such as HERMES_HOME/scheduled-backups is included on the next run. Existing regression coverage explicitly protects against this growth class for backups/ (tests/hermes_cli/test_backup.py:1908-1923).

Suggested changes

  • Scope scheduled-backup source, state, default destination, and listing to get_hermes_home(); add a named-profile test.
  • Reject an in-source custom destination or exclude its subtree during the zip walk; test two runs.
  • Add the new top-level backup defaults to hermes_cli/config.py::DEFAULT_CONFIG.

Automated hermes-sweeper review.

Comment thread hermes_cli/backup.py
if not _auto_backup_enabled(cfg):
return None

hermes_root = hermes_home or get_default_hermes_root()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_default_hermes_root() intentionally climbs from HERMES_HOME=<root>/profiles/<name> to <root> (hermes_constants.py:123-150), but this feature reads the active profile's config. Use get_hermes_home() here so a named gateway archives its own state rather than the shared root.

Comment thread hermes_cli/backup.py
if not hermes_root.is_dir():
return None

backup_dir = _auto_backup_dir(cfg, hermes_root)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A custom backup.dir can be an unexcluded child of hermes_root; _write_full_zip_backup() will include prior archives from that directory on the next run. Reject an in-source destination or pass it as an excluded subtree, and add a two-run regression test.

Comment thread hermes_cli/backup.py
files. Includes the configured ``backup.dir`` when it differs from the
default location.
"""
home = hermes_home or get_default_hermes_root()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep archive listing profile-scoped as well. With a named HERMES_HOME, this resolves to the machine root and exposes archives outside the active profile.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
@pxxD1998

Copy link
Copy Markdown

Hi @tgmerritt — thanks for putting this together. I'm interested in the scheduled backup feature, especially being able to configure the destination (for example, a mounted drive or cloud-synced folder) and retain a defined number of backups.

I saw the automated hermes-sweeper review from July 14 covering profile scoping, preventing an in-source backup directory from being recursively archived, and adding the defaults to DEFAULT_CONFIG. Are you still planning to continue this PR?

If you are no longer able to work on it, would you be comfortable with me preparing focused follow-up commits that address those review items while preserving your original commit history and authorship? I'd prefer to collaborate with you rather than open a competing PR. Thanks!

@tgmerritt

tgmerritt commented Jul 29, 2026 via email

Copy link
Copy Markdown
Contributor Author

@GottZ

GottZ commented Jul 29, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

You are asking whether this PR is still active and how to continue its reviewed work without creating a competing implementation.

Case context, measured live from our triage graph (2026-07-29T17:57:02+00:00):

  • This PR has been open for 50 days; the median open PR is 39 days old and p90 is 91 days — which is above the median wait, but inside the usual range.
  • Our graph currently records no duplicate candidate for this one — it is queued on its own merits.

If you want to move this one along: keep the diff scoped and rebase onto current main so the change stays cheap to verify.

@alt-glitch alt-glitch added comp/gateway Gateway runner, session dispatch, delivery sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages area/config Config system, migrations, profiles labels Jul 29, 2026
@pxxD1998

pxxD1998 commented Aug 1, 2026

Copy link
Copy Markdown

Hi @tgmerritt — thank you again for giving me the go-ahead to help with this.

I prepared a candidate branch rebased onto current upstream main. It keeps your feature work as the first commit in the rebased history and preserves its authorship, full commit message, and existing Claude Fable 5 co-author attribution. I added three follow-up commits limited to the review and safety fixes:

  • scope the scheduled-backup source, state, default destination, and listing to the active profile;
  • prevent recursive archiving when a custom destination is located inside HERMES_HOME;
  • add disabled-by-default backup defaults to DEFAULT_CONFIG and cli-config.yaml.example;
  • namespace a shared external backup.dir by logical profile name under <dir>/<profile-name>/;
  • preserve the logical profile name when named-profile directories are symlinks.

Candidate branch:
https://github.com/pxxD1998/HMA_hermes-agent/tree/feat/scheduled-backup-salvage-20260730

Comparison against current upstream main:
main...pxxD1998:HMA_hermes-agent:feat/scheduled-backup-salvage-20260730

Verification on WSL2 / Ubuntu 24.04:

  • focused backup/config tests: 146 passed;
  • Ruff, Python compilation, git diff --check, and the Windows-footgun scan: passed;
  • a manual integration check using two symlinked profiles created separate valid archives, state files, and listings beneath one shared external destination;
  • the full suite completed with 23,840 passed, 11 failed. The same 11 test IDs reproduced on a detached clean main worktree at the same base, so this candidate did not introduce additional failures in that environment. I am not representing the full suite as green.

No replacement PR has been opened. If you would like to keep #43058 as the active PR, you or a maintainer can use this candidate as a tested rebased reference for updating its branch. If a replacement PR would be easier, I can prepare one that clearly credits and builds on #43058—but I will not open one unless you or a maintainer asks me to.

@tgmerritt
tgmerritt force-pushed the feat/auto-backup-schedule branch from 69936b9 to 9eadc26 Compare August 3, 2026 19:57
…-list

The backup building blocks already exist (hermes backup, hermes import,
--quick snapshots, pre-update archives), but the "automatic" half of
job, which is exactly the barrier the issue calls out for non-developer
users. A single disk failure still wipes months of accumulated skills
and memory for anyone who never set that up.

Add an anacron-style scheduled backup, off by default and driven by a
new config block:

  backup:
    enabled: true       # default false — opt-in
    schedule: daily     # hourly | daily | weekly | <hours as integer>
    keep_last: 7        # auto archives to retain (oldest pruned first)
    dir: ~/backups      # optional override (default: ~/.hermes/backups)

maybe_create_auto_backup() mirrors the curator's gating pattern: cheap
when disabled or not due (one config read + one JSON stat), real cadence
enforced by a last_run_at stamp in backups/.auto_backup_state.json. The
gateway cron ticker polls it hourly alongside the curator, so any
long-running gateway gets periodic snapshots with zero user setup — no
OS cron, works the same on Linux/macOS/Windows. Archives reuse
_write_full_zip_backup (same exclusions, same WAL-safe SQLite copies)
and restore with the existing hermes import.

Details:
- Failures stamp last_run_at too, so a persistently failing destination
  retries once per interval instead of walking the full tree every poll.
- Pruning only touches auto-*.zip; pre-update/pre-migration/manual
  archives in the same directory are never deleted. keep_last floors at
  1 for the same reason as _prune_pre_update_backups.
- hermes backup --list shows every archive (auto, pre-update,
  pre-migration, manual) with date/size/path across the default and
  configured directories.
- backup.dir lets users point at a mounted drive or cloud-synced folder
  for off-machine copies without a cloud-provider integration.

Partially addresses NousResearch#12238 (the backup acceptance criteria; per-skill
history/rollback and memory diff are a version-control subsystem better
served by a separate PR).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tgmerritt
tgmerritt force-pushed the feat/auto-backup-schedule branch from 9eadc26 to cc8c8ab Compare August 3, 2026 20:06
@tgmerritt

Copy link
Copy Markdown
Contributor Author

Rebased onto current main — all conflicts resolved. Ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants