diff --git a/cli.py b/cli.py index f9d0abae02..559094a527 100644 --- a/cli.py +++ b/cli.py @@ -2114,8 +2114,9 @@ def _run_state_db_auto_maintenance(session_db) -> None: :func:`hermes_cli.config.load_config` (the authoritative loader that deep-merges DEFAULT_CONFIG, so unmigrated configs still get default values). Honours ``auto_prune`` / ``retention_days`` / - ``vacuum_after_prune`` / ``min_interval_hours``, and delegates to the - DB. Never raises — maintenance must never block interactive startup. + ``vacuum_after_prune`` / ``min_vacuum_interval_days`` / + ``min_interval_hours``, and delegates to the DB. Never raises — + maintenance must never block interactive startup. """ if session_db is None: return @@ -2164,6 +2165,7 @@ def _run_state_db_auto_maintenance(session_db) -> None: session_db.maybe_auto_prune_and_vacuum( retention_days=int(cfg.get("retention_days", 90)), min_interval_hours=int(cfg.get("min_interval_hours", 24)), + min_vacuum_interval_days=int(cfg.get("min_vacuum_interval_days", 30)), vacuum=bool(cfg.get("vacuum_after_prune", True)), sessions_dir=_hermes_home_maint / "sessions", ) diff --git a/gateway/run.py b/gateway/run.py index 905a7763c5..532ff9c2a9 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -6001,6 +6001,9 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew self._session_db._db.maybe_auto_prune_and_vacuum( retention_days=int(_sess_cfg.get("retention_days", 90)), min_interval_hours=int(_sess_cfg.get("min_interval_hours", 24)), + min_vacuum_interval_days=int( + _sess_cfg.get("min_vacuum_interval_days", 30) + ), vacuum=bool(_sess_cfg.get("vacuum_after_prune", True)), sessions_dir=self.config.sessions_dir, ) diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index 80adf77ff4..4545575db2 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -2625,6 +2625,9 @@ DEFAULT_CONFIG = { # 100MB, so it only runs at startup, and only when prune deleted # ≥1 session. "vacuum_after_prune": True, + # Minimum days between successful VACUUM rewrites. Pruning can still + # run on its normal cadence while SQLite reuses the freed pages. + "min_vacuum_interval_days": 30, # Minimum hours between auto-maintenance runs (avoids repeating # the sweep on every CLI invocation). Tracked via state_meta in # state.db itself, so it's shared across all processes. diff --git a/tests/test_hermes_state.py b/tests/test_hermes_state.py index 9e6d75ffb2..ca1926e422 100644 --- a/tests/test_hermes_state.py +++ b/tests/test_hermes_state.py @@ -1887,6 +1887,28 @@ class TestVacuum: assert result["vacuumed"] is True assert vacuum_calls == [True] + def test_auto_maintenance_retries_after_failed_vacuum(self, db, monkeypatch): + monkeypatch.setattr(db, "prune_sessions", lambda **_kwargs: 3) + vacuum_calls = [] + + def fail_first_vacuum(): + vacuum_calls.append(True) + if len(vacuum_calls) == 1: + raise RuntimeError("vacuum failed") + + monkeypatch.setattr(db, "vacuum", fail_first_vacuum) + + first = db.maybe_auto_prune_and_vacuum(min_interval_hours=0) + + assert first["vacuumed"] is False + assert db.get_meta("last_vacuum") is None + + second = db.maybe_auto_prune_and_vacuum(min_interval_hours=0) + + assert second["vacuumed"] is True + assert vacuum_calls == [True, True] + assert db.get_meta("last_vacuum") is not None + class TestOptimizeFts: def test_optimize_returns_index_count(self, db): diff --git a/tests/test_session_vacuum_config.py b/tests/test_session_vacuum_config.py new file mode 100644 index 0000000000..d231996b59 --- /dev/null +++ b/tests/test_session_vacuum_config.py @@ -0,0 +1,41 @@ +from pathlib import Path +from unittest.mock import MagicMock + + +def test_default_config_exposes_vacuum_interval(): + from hermes_cli.config import DEFAULT_CONFIG + + assert DEFAULT_CONFIG["sessions"]["min_vacuum_interval_days"] == 30 + + +def test_cli_auto_maintenance_forwards_vacuum_interval(monkeypatch, tmp_path: Path): + import cli + import hermes_cli.config + import hermes_constants + + session_db = MagicMock() + session_db.get_meta.return_value = "already-done" + monkeypatch.setattr( + hermes_cli.config, + "load_config", + lambda: { + "sessions": { + "auto_prune": True, + "retention_days": 90, + "vacuum_after_prune": True, + "min_interval_hours": 24, + "min_vacuum_interval_days": 17, + } + }, + ) + monkeypatch.setattr(hermes_constants, "get_hermes_home", lambda: tmp_path) + + cli._run_state_db_auto_maintenance(session_db) + + session_db.maybe_auto_prune_and_vacuum.assert_called_once_with( + retention_days=90, + min_interval_hours=24, + min_vacuum_interval_days=17, + vacuum=True, + sessions_dir=tmp_path / "sessions", + ) diff --git a/website/docs/user-guide/sessions.md b/website/docs/user-guide/sessions.md index 71b53a2310..91a63a2bed 100644 --- a/website/docs/user-guide/sessions.md +++ b/website/docs/user-guide/sessions.md @@ -714,7 +714,7 @@ Key tables in `state.db`: - Gateway sessions auto-reset based on the configured reset policy - Before reset, the agent saves memories and skills from the expiring session - Opt-in auto-pruning: when `sessions.auto_prune` is `true`, ended sessions inactive for `sessions.retention_days` (default 90) are pruned at CLI/gateway startup -- After a prune that actually removed rows, `state.db` is `VACUUM`ed to reclaim disk space (SQLite does not shrink the file on plain DELETE) +- After a prune that actually removed rows, `state.db` is `VACUUM`ed to reclaim disk space when at least `sessions.min_vacuum_interval_days` (default 30) have elapsed since the last successful `VACUUM` (SQLite does not shrink the file on plain DELETE) - Pruning runs at most once per `sessions.min_interval_hours` (default 24); the last-run timestamp is tracked inside `state.db` itself so it's shared across every Hermes process in the same `HERMES_HOME` Default is **off** — session history is valuable for `session_search` recall, and silently deleting it could surprise users. Enable in `~/.hermes/config.yaml`: @@ -724,6 +724,7 @@ sessions: auto_prune: true # opt in — default is false retention_days: 90 # keep ended sessions active within this window vacuum_after_prune: true # reclaim disk space after a pruning sweep + min_vacuum_interval_days: 30 # don't rewrite the DB more often than this min_interval_hours: 24 # don't re-run the sweep more often than this ``` diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/sessions.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/sessions.md index 3bc6692de2..19c7b710eb 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/sessions.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/sessions.md @@ -579,7 +579,7 @@ state.db 后可安全删除。 - Gateway session 根据配置的重置策略自动重置 - 重置前,agent 保存即将过期 session 中的记忆和技能 - 可选自动清理:当 `sessions.auto_prune` 为 `true` 时,在 CLI/gateway 启动时清理早于 `sessions.retention_days`(默认 90)天的已结束 session -- 实际删除了行的清理操作完成后,`state.db` 会执行 `VACUUM` 以回收磁盘空间(SQLite 在普通 DELETE 后不会缩小文件) +- 实际删除了行的清理操作完成后,如果距离上次成功执行 `VACUUM` 已达到 `sessions.min_vacuum_interval_days`(默认 30)天,`state.db` 会执行 `VACUUM` 以回收磁盘空间(SQLite 在普通 DELETE 后不会缩小文件) - 清理最多每 `sessions.min_interval_hours`(默认 24)小时运行一次;上次运行时间戳记录在 `state.db` 内部,因此在同一 `HERMES_HOME` 下的所有 Hermes 进程间共享 默认为**关闭**——session 历史对 `session_search` 召回很有价值,静默删除可能会让用户感到意外。在 `~/.hermes/config.yaml` 中启用: @@ -589,6 +589,7 @@ sessions: auto_prune: true # 选择启用——默认为 false retention_days: 90 # 保留已结束 session 的天数 vacuum_after_prune: true # 清理后回收磁盘空间 + min_vacuum_interval_days: 30 # 数据库重写的最短间隔天数 min_interval_hours: 24 # 清理间隔不短于此值 ```