From a071fc80da30ffc4311ae1a6fb4f86f6947cf655 Mon Sep 17 00:00:00 2001 From: loulanyue <260355617@qq.com> Date: Sat, 29 Aug 2026 09:45:04 +0800 Subject: [PATCH] fix(state): quarantine 0-byte truncated state.db and record store provenance (#97568) --- hermes_cli/backup.py | 6 ++++-- hermes_state.py | 8 ++++++-- hermes_state_schema.py | 14 ++++++++++++++ tests/test_zeroed_state_db.py | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/hermes_cli/backup.py b/hermes_cli/backup.py index c831ea7b78..7d540991c8 100644 --- a/hermes_cli/backup.py +++ b/hermes_cli/backup.py @@ -449,15 +449,17 @@ def is_zeroed_sqlite_file( size = path.stat().st_size except OSError: return False - if size <= 0: + if size < 0: return False from hermes_cli.sqlite_safe_read import read_header_bytes_preopen head = read_header_bytes_preopen( path, length=max(16, probe_bytes), force=force ) - if not head: + if head is None: return False + if len(head) == 0: + return True if head.startswith(b"SQLite format 3"): return False return all(byte == 0 for byte in head) diff --git a/hermes_state.py b/hermes_state.py index 9e052a5fff..973e67d1a3 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -3952,14 +3952,18 @@ def is_zeroed_state_db( size = path.stat().st_size except OSError: return False - if size <= 0: + if size < 0: return False from hermes_cli.sqlite_safe_read import read_header_bytes_preopen head = read_header_bytes_preopen( path, length=max(16, probe_bytes), force=force ) - if not head or head.startswith(b"SQLite format 3"): + if head is None: + return False + if len(head) == 0: + return True + if head.startswith(b"SQLite format 3"): return False return all(byte == 0 for byte in head) diff --git a/hermes_state_schema.py b/hermes_state_schema.py index ce87cfe673..5d735ec817 100644 --- a/hermes_state_schema.py +++ b/hermes_state_schema.py @@ -8,12 +8,15 @@ own; methods access the host's attributes (``self._conn``, ``self.db_path``, module-level constants live in hermes_state_common. """ +import datetime import logging import json import sqlite3 import time +import uuid from typing import Dict, Optional, Sequence + from hermes_constants import get_hermes_home from hermes_state_common import ( DEFERRED_INDEX_SQL, @@ -1032,6 +1035,17 @@ class SessionSchemaMixin: "INSERT INTO schema_version (version) VALUES (?)", (SCHEMA_VERSION,), ) + # Record store provenance on creation so fresh vs wiped stores are distinguishable (#97568) + now_iso = datetime.datetime.now(datetime.timezone.utc).isoformat() + instance_id = str(uuid.uuid4()) + cursor.executemany( + "INSERT OR IGNORE INTO state_meta (key, value) VALUES (?, ?)", + [ + ("store_instance_id", instance_id), + ("store_created_at_utc", now_iso), + ], + ) + else: current_version = row["version"] if isinstance(row, sqlite3.Row) else row[0] # Data migrations that can't be expressed declaratively (row diff --git a/tests/test_zeroed_state_db.py b/tests/test_zeroed_state_db.py index 6a9bf2810b..470dd9dfae 100644 --- a/tests/test_zeroed_state_db.py +++ b/tests/test_zeroed_state_db.py @@ -41,6 +41,38 @@ def test_sessiondb_opens_fresh_after_zeroed_quarantine(tmp_path, monkeypatch): sdb.close() +def test_is_zeroed_state_db_zero_byte_quarantine(tmp_path, monkeypatch): + """#97568: a 0-byte file must be detected as zeroed and quarantined.""" + import hermes_state as hs + + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + db = tmp_path / "state.db" + db.write_bytes(b"") # 0-byte truncated file + assert hs.is_zeroed_state_db(db) is True + + sdb = hs.SessionDB(db_path=db) + try: + # Fresh DB should open and accept schema + assert db.exists() + assert not hs.is_zeroed_state_db(db) + # Quarantine retained for the 0-byte file + backups = list(tmp_path.glob("state.db.zeroed-*.bak")) + assert len(backups) == 1 + assert backups[0].stat().st_size == 0 + # Check store provenance was recorded in state_meta + row_instance = sdb._conn.execute( + "SELECT value FROM state_meta WHERE key = 'store_instance_id'" + ).fetchone() + row_created = sdb._conn.execute( + "SELECT value FROM state_meta WHERE key = 'store_created_at_utc'" + ).fetchone() + assert row_instance is not None and row_instance[0] + assert row_created is not None and row_created[0] + finally: + sdb.close() + + + def test_concurrent_quarantine_no_clobber(tmp_path): """#68805: two concurrent startups must not race on quarantine.