From 2858ab59a892b00d2e55eb211e8cdd61cc10d88b Mon Sep 17 00:00:00 2001 From: Offending Commit Date: Thu, 13 Aug 2026 13:40:29 -0500 Subject: [PATCH] fix(state): Support evolving SessionDB options --- README.md | 3 ++ hermes_plugin_kit/__init__.py | 62 +++++++++++++++++++++++++++++++++-- tests/test_kit.py | 17 +++++++++- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 11c37d7..852c23c 100644 --- a/README.md +++ b/README.md @@ -440,6 +440,9 @@ live `~/.hermes/state.db`. Production helpers issue no raw SQL and delegate ordering, pagination, structured message encoding, locking, and migration to Hermes itself. An incompatible Hermes build raises `SessionDBCompatibilityError` naming the missing contract. +Default-valued options added by newer Hermes releases are omitted when an +older public method signature does not accept them. Requesting a non-default +option that the running Hermes does not support still fails explicitly. ## Calling host-managed capabilities diff --git a/hermes_plugin_kit/__init__.py b/hermes_plugin_kit/__init__.py index 310fc76..d31cd24 100644 --- a/hermes_plugin_kit/__init__.py +++ b/hermes_plugin_kit/__init__.py @@ -181,6 +181,38 @@ def _call_session_db( return method(*args, **kwargs) +def _call_session_db_evolving( + db: Any, + method_name: str, + *args: Any, + optional_defaults: dict[str, Any], + **kwargs: Any, +) -> Any: + """Call an evolving Hermes API without forwarding absent optional features.""" + method = _session_db_method(db, method_name) + try: + signature = inspect.signature(method) + except (TypeError, ValueError) as exc: + raise SessionDBCompatibilityError( + f"hermes-agent SessionDB is incompatible: cannot inspect public method {method_name}()" + ) from exc + accepted = set(signature.parameters) + accepts_kwargs = any( + parameter.kind is inspect.Parameter.VAR_KEYWORD + for parameter in signature.parameters.values() + ) + forwarded: dict[str, Any] = {} + for name, value in kwargs.items(): + if name in accepted or accepts_kwargs: + forwarded[name] = value + elif name not in optional_defaults or value != optional_defaults[name]: + raise SessionDBCompatibilityError( + "hermes-agent SessionDB is incompatible: public method " + f"{method_name}() does not accept requested argument {name}={value!r}" + ) + return _call_session_db(db, method_name, *args, **forwarded) + + @contextmanager def open_session_db( db_path: str | Path | None = None, @@ -250,9 +282,28 @@ def list_sessions( session_key: str | None = None, ) -> list[dict[str, Any]]: """List rich session rows using Hermes-supported filters and pagination.""" - return _call_session_db( + return _call_session_db_evolving( db, "list_sessions_rich", + optional_defaults={ + "source": None, + "sources": None, + "exclude_sources": None, + "cwd_prefix": None, + "limit": 20, + "offset": 0, + "include_children": False, + "min_message_count": 0, + "project_compression_tips": True, + "order_by_last_active": False, + "include_archived": False, + "archived_only": False, + "id_query": None, + "search_query": None, + "compact_rows": False, + "include_pinned": False, + "session_key": None, + }, source=source, sources=sources, exclude_sources=exclude_sources, @@ -284,10 +335,17 @@ def read_session_messages( after_id: int | None = None, ) -> list[dict[str, Any]]: """Read a session transcript using Hermes' ordering and paging rules.""" - return _call_session_db( + return _call_session_db_evolving( db, "get_messages", session_id, + optional_defaults={ + "include_inactive": False, + "limit": None, + "offset": 0, + "latest": False, + "after_id": None, + }, include_inactive=include_inactive, limit=limit, offset=offset, diff --git a/tests/test_kit.py b/tests/test_kit.py index 16e50b2..de17dea 100644 --- a/tests/test_kit.py +++ b/tests/test_kit.py @@ -123,10 +123,25 @@ def test_incompatible_public_signature_has_instructive_error(self) -> None: db = types.SimpleNamespace(get_messages=lambda session_id: []) with self.assertRaisesRegex( hpk.SessionDBCompatibilityError, - r"get_messages\(\) does not accept the required arguments", + r"get_messages\(\) does not accept requested argument limit=1", ): hpk.read_session_messages(db, "s1", limit=1) + def test_older_public_signature_ignores_default_newer_options(self) -> None: + calls = [] + + def get_messages(session_id, include_inactive=False, limit=None, offset=0): + calls.append((session_id, include_inactive, limit, offset)) + return [] + + db = types.SimpleNamespace(get_messages=get_messages) + self.assertEqual(hpk.read_session_messages(db, "s1"), []) + self.assertEqual(calls, [("s1", False, None, 0)]) + with self.assertRaisesRegex( + hpk.SessionDBCompatibilityError, "does not accept requested argument latest=True" + ): + hpk.read_session_messages(db, "s1", latest=True) + def test_missing_hermes_import_has_instructive_error(self) -> None: real_import = __import__