diff --git a/hermes_state.py b/hermes_state.py index 3cdffe3744..70a7c698c2 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -2877,6 +2877,16 @@ def _reapply_durability_barriers(conn: sqlite3.Connection) -> bool: return False +def apply_durability_barriers(conn: sqlite3.Connection) -> bool: + """Apply state-store durability barriers without changing journal mode. + + This is the public entry point for secondary users of ``state.db`` that + must inherit its owner's journal mode while retaining per-connection + durability settings. + """ + return _reapply_durability_barriers(conn) + + @contextmanager def _exclusive_repair_db_guard(db_path: Path): """Yield one live connection that excludes writers for repair surgery. diff --git a/tests/tools/test_async_delegation.py b/tests/tools/test_async_delegation.py index 5a15608a82..dd8c07ab9f 100644 --- a/tests/tools/test_async_delegation.py +++ b/tests/tools/test_async_delegation.py @@ -66,11 +66,8 @@ def _drain_for(delegation_id, timeout=5.0): return None -def test_schema_init_preserves_shared_state_db_journal_mode(tmp_path, monkeypatch): +def test_schema_init_preserves_shared_state_db_journal_mode(tmp_path): """The delegation ledger is a guest in state.db, not its mode owner.""" - import hermes_state - - monkeypatch.setattr(hermes_state, "is_sqlite_wal_reset_vulnerable", lambda: False) conn = sqlite3.connect(tmp_path / "state.db") try: assert conn.execute("PRAGMA journal_mode=DELETE").fetchone()[0] == "delete" @@ -86,6 +83,23 @@ def test_schema_init_preserves_shared_state_db_journal_mode(tmp_path, monkeypatc conn.close() +def test_schema_init_preserves_shared_state_db_wal_mode(tmp_path): + """Schema initialization must not replace an existing WAL mode.""" + conn = sqlite3.connect(tmp_path / "state.db") + try: + assert conn.execute("PRAGMA journal_mode=WAL").fetchone()[0] == "wal" + + ad._initialize_schema(conn) + + assert conn.execute("PRAGMA journal_mode").fetchone()[0] == "wal" + assert conn.execute( + "SELECT name FROM sqlite_master WHERE type='table' " + "AND name='async_delegations'" + ).fetchone() == ("async_delegations",) + finally: + conn.close() + + @pytest.mark.macos_only def test_connect_preserves_wal_and_applies_macos_durability_barriers( tmp_path, monkeypatch diff --git a/tools/async_delegation.py b/tools/async_delegation.py index aa787804c8..ea0757143d 100644 --- a/tools/async_delegation.py +++ b/tools/async_delegation.py @@ -140,14 +140,17 @@ def _connect() -> sqlite3.Connection: def _initialize_schema(conn: sqlite3.Connection) -> None: - from hermes_state import _reapply_durability_barriers + from hermes_state import apply_durability_barriers # state.db's owning SessionDB connection establishes the configured journal # mode. This secondary durability ledger must preserve that mode: applying # WAL here on every short-lived connection requires an exclusive lock when # the file is not already WAL and can collide with live transcript/FTS - # writers. The ledger works in either WAL or DELETE mode. - _reapply_durability_barriers(conn) + # writers. The ledger works in either WAL or DELETE mode; if it opens a new + # file first, the default rollback journal remains valid until SessionDB + # establishes the configured mode. sqlite3.connect(timeout=10) above also + # gives its small transactions a busy handler for ordinary contention. + apply_durability_barriers(conn) conn.execute( """CREATE TABLE IF NOT EXISTS async_delegations ( delegation_id TEXT PRIMARY KEY,