Skip to content

[Bug]: Full pre-update backup aborts when HERMES_HOME contains a non-SQLite .db file #75724

Description

@akivavh

Bug Description

On Windows, a full pre-update backup (hermes update --backup) treats every file whose suffix is .db as SQLite. If HERMES_HOME contains an unrelated non-SQLite .db file—for example, a Windows cache file copied into a Kanban workspace—sqlite3.backup() fails with file is not a database.

_write_full_zip_backup() then aborts the entire archive, deletes the partial ZIP, and returns None. The updater reports that the backup was skipped and continues. As a result, explicitly requesting a full pre-update backup can leave no full pre-update restore artifact.

The latest occurrence was reproduced on Hermes Agent v0.19.1, upstream commit e444d165, on 2026-08-01. The same causal signature was recorded locally on 2026-07-26 and 2026-07-28.

Steps to Reproduce

A minimal deterministic reproduction of the backup helper is:

from pathlib import Path
from tempfile import TemporaryDirectory

from hermes_cli.backup import _write_full_zip_backup

with TemporaryDirectory() as td:
    td = Path(td)
    home = td / "home"
    home.mkdir()
    (home / "foreign.db").write_text("not sqlite", encoding="utf-8")

    output = td / "pre-update.zip"
    result = _write_full_zip_backup(output, home)

    print(result)
    print(output.exists())

Current result:

None
False

The user-facing trigger is:

  1. Have a non-SQLite file ending in .db anywhere under a non-excluded path in HERMES_HOME.
  2. Have an update available.
  3. Run hermes update --backup.
  4. Observe file is not a database followed by Full-zip backup aborted.
  5. Observe that the partial pre-update ZIP is deleted and the update continues.

Expected Behavior

The backup implementation should distinguish actual SQLite databases from unrelated files that merely use the .db suffix.

A non-SQLite .db should not invalidate the entire full backup. It should either be copied as a normal file or skipped individually with an explicit warning. Fail-closed behavior should remain for a genuine Hermes SQLite database whose consistent snapshot cannot be created.

If no full backup can be produced, the updater should clearly state that no full rollback archive exists.

Actual Behavior

Every .db suffix is routed through the SQLite backup API. A foreign non-SQLite file therefore causes the full ZIP operation to stop. The partial ZIP is deleted, create_pre_update_backup() returns None, and hermes update continues.

In the latest occurrence, the partial archive had reached approximately 4.5 GB before being deleted. No full pre-update ZIP remained. This is especially risky when the quick snapshot also omits an oversized state.db, because there may then be no current full-state restore artifact.

Affected Component

Setup / Installation

Messaging Platform (if gateway-related)

N/A (CLI only)

Debug Report

Not uploaded because the debug bundle may contain unrelated local profile/session data. This report instead includes a deterministic sanitized reproduction, exact environment/version information, and the relevant sanitized log signature below.

Operating System

Windows 11 Pro 10.0.26200 (build 26200), 64-bit

Python Version

3.11.15

Hermes Version

Hermes Agent v0.19.1 (2026.7.30), upstream e444d165, git installation, up to date

Additional Logs / Traceback (optional)

2026-08-01 00:48:57,987 WARNING hermes_cli.backup: SQLite safe copy failed for <HERMES_HOME>\kanban\boards\<board>\workspaces\<workspace>\product\%SystemDrive%\ProgramData\Microsoft\Windows\Caches\cversions.2.db: file is not a database
2026-08-01 00:48:57,987 WARNING hermes_cli.backup: Full-zip backup aborted: SQLite snapshot failed for kanban\boards\<board>\workspaces\<workspace>\product\%SystemDrive%\ProgramData\Microsoft\Windows\Caches\cversions.2.db

Same causal signature also recorded on:
- 2026-07-26 01:48:41
- 2026-07-26 02:01:38 (a second non-SQLite Windows cache .db filename)
- 2026-07-28 18:57:10

Root Cause Analysis (optional)

At commit e444d165, hermes_cli/backup.py selects the SQLite-safe-copy path solely with:

if abs_path.suffix == ".db":

_safe_copy_db() opens the source through sqlite3 and calls conn.backup(). A non-SQLite file returns False. _write_full_zip_backup() then sets sqlite_snapshot_failed, breaks the archive loop, deletes out_path, and returns None.

_run_pre_update_backup() treats that None as “no files found or write failed” and continues the update, as designed.

I searched existing issues and PRs; none matched this trigger and mechanism. Related but not duplicate:

Proposed Fix (optional)

Route a file through _safe_copy_db() only when it is a known Hermes SQLite database or is positively identified as SQLite. Treat an arbitrary non-SQLite .db as a regular backup file (or skip that file explicitly) without discarding the whole archive.

Please preserve the existing fail-closed invariant for genuine Hermes databases. Suggested regression coverage:

  1. A foreign non-SQLite foreign.db does not abort or delete the full archive.
  2. A genuine SQLite database is still copied through the SQLite backup API.
  3. A corrupt known Hermes database still fails closed.
  4. A failed full pre-update backup reports unambiguously that no full rollback archive was created.

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existsarea/install-updateInstaller, updater, packaging, wheels, doctorcomp/cliCLI entry point, hermes_cli/, setup wizardsweeper:risk-compatibilitySweeper risk: may break existing users, config, migrations, defaults, or upgradestype/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions