Skip to content

fix(backup): floor pre-update backup_keep to 1 so the new backup survives#16555

Closed
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/pre-update-backup-keep-floor-16539
Closed

fix(backup): floor pre-update backup_keep to 1 so the new backup survives#16555
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/pre-update-backup-keep-floor-16539

Conversation

@briandevans

Copy link
Copy Markdown
Contributor

Summary

  • updates.backup_keep: 0 (or negative) silently deletes the freshly-created pre-update backup, leaving the user with no recovery point even though Saved: <path> was printed.
  • Floor the keep parameter to a minimum of 1 inside _prune_pre_update_backups so the just-written zip is always preserved.
  • Three regression tests + clarifying comment on the config default.

The bug

_prune_pre_update_backups(backup_dir, keep) only clamps keep < 0 up to 0:

if keep < 0:
    keep = 0
...
backups = sorted(..., reverse=True)   # newest-first, includes the just-written zip
for p in backups[keep:]:               # backups[0:] = ALL of them
    p.unlink()

The function is called from create_pre_update_backup() immediately after the new zip is written. So with keep=0:

  1. The zip is written to <HERMES_HOME>/backups/pre-update-<ts>.zip.
  2. _prune_pre_update_backups(..., keep=0) lists every pre-update-*.zip (one entry — the new one) and deletes it.
  3. The wrapper in main.py calls out_path.stat().st_size, which raises OSError (file missing) — caught and silently degraded to size_bytes = 0.
  4. The user sees Saved: ~/.hermes/backups/pre-update-….zip (0 B, 1.2s) and believes they have a recovery point.

This is exactly the kind of footgun config systems trigger because some apps treat 0 as "unlimited" / "no rotation". Here it does the opposite — every backup is destroyed right after creation, with no warning.

Reproduced locally:

>>> from hermes_cli.backup import create_pre_update_backup
>>> out = create_pre_update_backup(hermes_home=Path('/tmp/hh'), keep=0)
>>> out.exists()
False

The fix

Clamp keep to a minimum of 1 inside _prune_pre_update_backups:

if keep < 1:
    keep = 1

The pruner is only ever invoked immediately after a fresh backup is written, so the smallest sensible retention is 1 — anything below that means "throw away the file we just paid disk + CPU to produce." Operators who genuinely want no backups should set updates.pre_update_backup: false (which gates creation entirely) rather than relying on backup_keep: 0. The config docstring is updated to spell this out.

The floor only protects the just-created backup; older zips are still rotated out as before. test_keep_zero_still_prunes_older_backups proves this.

Test plan

  • Before (on 8ed599dc0, the merge commit of feat(update): auto-backup HERMES_HOME before hermes update #16539): the three new tests fail with keep=0 deleting the freshly-created backup.
  • After: 9/9 TestPreUpdateBackup tests pass; full tests/hermes_cli/test_backup.py suite green (84 tests).
  • Regression guard: temporarily reverted hermes_cli/backup.py → 3 new tests fail with "keep=0 silently deleted the freshly-created backup" / "Floor must preserve the new backup". Restored → all pass.
  • Behavior for the existing default (keep=5) is unchanged.

Tests added (parametrized in spirit by covering both 0 and negative):

  • test_keep_zero_does_not_delete_freshly_created_backup
  • test_keep_negative_does_not_delete_freshly_created_backup
  • test_keep_zero_still_prunes_older_backups — proves the floor doesn't regress rotation for older files

Related

  • Follow-up to feat(update): auto-backup HERMES_HOME before hermes update #16539 (feat(update): auto-backup HERMES_HOME before hermes update) — the new auto-backup feature merged earlier today (8ed599d).
  • No competing PRs found (gh search prs ... pre_update_backup / backup_keep / _prune_pre_update_backups / create_pre_update_backup all clean as of 2026-04-27 13:30Z).

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 27, 2026 13:19

Copilot AI 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.

Pull request overview

Fixes a retention edge case in the pre-update auto-backup rotation so misconfigured updates.backup_keep values (0/negative) can’t immediately prune the just-created backup, and adds regression coverage + config clarification to prevent recurrence.

Changes:

  • Floor _prune_pre_update_backups(..., keep) to a minimum of 1 to ensure the newly created pre-update zip is preserved.
  • Add three regression tests covering keep=0, keep<0, and verifying older backups are still pruned under the floor.
  • Clarify in DEFAULT_CONFIG comments that values below 1 are floored and how to disable backups properly.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
hermes_cli/backup.py Floors keep to >= 1 in _prune_pre_update_backups and documents the rationale to prevent pruning the freshly created backup.
tests/hermes_cli/test_backup.py Adds targeted regression tests to ensure keep=0/negative preserves the new backup while still rotating older zips.
hermes_cli/config.py Updates the config comment to document the floor-to-1 behavior and the correct way to disable pre-update backups.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard labels Apr 27, 2026
…ives

`updates.backup_keep: 0` (or any negative value) wiped the freshly-
created pre-update zip:

  _prune_pre_update_backups(backup_dir, keep=0):
      backups = sorted(..., reverse=True)   # newest first, includes
                                            # the zip we just wrote
      for p in backups[0:]:                 # = all of them
          p.unlink()

The wrapper in `main.py` then printed `Saved: <path>` for a file that
no longer existed (the size lookup is wrapped in `try/except OSError`
which silently degrades to "0 B"), leaving operators believing they had
a recovery point when they had none.

This is a real footgun because some config systems treat 0 as "keep
unlimited"; here it does the opposite — every backup is destroyed
right after creation.

Fix: clamp `keep` to a minimum of 1 inside `_prune_pre_update_backups`
since that helper is only invoked immediately after a fresh backup
is written.  Operators who genuinely want no backups should set
`updates.pre_update_backup: false` (which gates creation entirely)
rather than relying on `backup_keep: 0`.

Also extends the `backup_keep` config docstring to spell out the floor
and point at `pre_update_backup: false` as the off-switch.

## Tests

Three regression tests added in `TestPreUpdateBackup`:

  - `test_keep_zero_does_not_delete_freshly_created_backup` —
    asserts the file persists after `keep=0`
  - `test_keep_negative_does_not_delete_freshly_created_backup` —
    same for negative values
  - `test_keep_zero_still_prunes_older_backups` — proves the floor
    only protects the new backup; older ones are still rotated out

Verified the new tests fail on origin/main (without the floor) and
pass with it; full `tests/hermes_cli/test_backup.py` suite green
(84 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@teknium1

teknium1 commented May 4, 2026

Copy link
Copy Markdown
Contributor

Salvaged via #19730 onto current main - your commit authorship was preserved. Thanks!

@teknium1 teknium1 closed this May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants