Files
hermes-agent/tests/hermes_state/test_auto_vacuum_holder_gate.py
teknium1 1dc881b67a fix(state): automatic maintenance skips the VACUUM while another process holds state.db
maybe_auto_prune_and_vacuum() runs the same store rewrite as `hermes sessions optimize`
(VACUUM + TRUNCATE checkpoint) from CLI startup and the gateway constructor, with no holder
scan — so the manual command was gated while the automatic producer of the same #110054
failure was not. The VACUUM branch now runs the same foreign_state_db_holders admission and
SKIPS (debug log + a vacuum_skipped_holders count in the result) when a sibling writer holds
the store or a WAL sidecar. Housekeeping never refuses a turn; it only defers the rewrite to
the next run.
2026-09-20 20:13:33 -07:00

69 lines
2.4 KiB
Python

"""Automatic state.db maintenance never VACUUMs under a foreign writer (#110054).
``maybe_auto_prune_and_vacuum`` runs the SAME store rewrite as ``hermes sessions optimize``
(VACUUM + TRUNCATE checkpoint) from CLI and gateway startup. The manual command refuses while a
sibling writer holds the store; the automatic producer must skip for the same reason — a rewritten
generation under a live holder is what leaves every agent refusing turns. It only ever skips.
"""
import subprocess
import sys
import pytest
pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="holder scan is unavailable on Windows")
_HOLDER = (
"import sqlite3, sys\n"
"conn = sqlite3.connect(sys.argv[1])\n"
"conn.execute('SELECT count(*) FROM sqlite_master')\n"
"print('ready', flush=True)\n"
"sys.stdin.readline()\n"
)
def _seeded_db(tmp_path):
from hermes_state import SessionDB
db = SessionDB(db_path=tmp_path / "state.db")
db.create_session("old", "cli")
db.append_message("old", "user", "hello")
db.end_session("old", "done")
return db
def _auto_maintenance(db):
# retention_days=0 makes the ended row prunable; the freelist floor is disabled so only the
# holder scan can stop the VACUUM.
return db.maybe_auto_prune_and_vacuum(
retention_days=0, min_interval_hours=0, min_vacuum_interval_days=0,
min_vacuum_freelist_ratio=-1.0)
def test_auto_vacuum_skips_while_a_foreign_process_holds_the_store(tmp_path):
db = _seeded_db(tmp_path)
holder = subprocess.Popen(
[sys.executable, "-c", _HOLDER, str(db.db_path)],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
try:
assert holder.stdout.readline().strip() == "ready"
result = _auto_maintenance(db)
assert result["pruned"] == 1
assert result["vacuumed"] is False
assert result.get("vacuum_skipped_holders")
# Control: the same call VACUUMs once the holder is gone.
holder.stdin.close()
holder.wait(timeout=10)
db.set_meta("last_auto_prune", "0")
db.create_session("old2", "cli")
db.end_session("old2", "done")
again = _auto_maintenance(db)
assert again["vacuumed"] is True
assert "vacuum_skipped_holders" not in again
finally:
if holder.poll() is None:
holder.stdin.close()
holder.wait(timeout=10)
db.close()