From cfb268a6c9f1ce504a8fdb556f7a44eaab448b4b Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:55:33 -0700 Subject: [PATCH] refactor(state): table-drive the session upsert keep-existing tail, compact module-constant comments --- hermes_state.py | 54 ++++++++++++++++------------------------ hermes_state_sessions.py | 21 ++++++++-------- 2 files changed, 33 insertions(+), 42 deletions(-) diff --git a/hermes_state.py b/hermes_state.py index c4176cfeeb..aed5807cba 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -178,44 +178,36 @@ def _scrub_surrogates(value: Any) -> Any: return _sanitize_surrogates(value) if isinstance(value, str) else value -# Billing buckets that aren't a routable provider identity: a session that -# persisted only one of these (never ran /model) falls back to the config default. -# Shared by session_gateway_runtime and tui_gateway.server so they cannot drift. +# Billing buckets that aren't a routable provider identity: a session that persisted only +# one of these (never ran /model) falls back to the config default. Shared by +# session_gateway_runtime and tui_gateway.server so they cannot drift. _BARE_BILLING_PROVIDERS = frozenset({"auto", "custom"}) T = TypeVar("T") -DEFAULT_DB_PATH = get_hermes_home() / "state.db" +# Import-time snapshot lets _default_db_path() detect a re-pointed DEFAULT_DB_PATH +# (tests monkeypatch the constant directly). +DEFAULT_DB_PATH = _IMPORT_DEFAULT_DB_PATH = get_hermes_home() / "state.db" -# Back off from read-only opens after one fails: not retried per query, but short -# enough that transient fd pressure doesn't strand the read pool. +# Back off from read-only opens after one fails: not per query, but short enough that +# transient fd pressure doesn't strand the read pool. _READ_OPEN_RETRY_SECONDS = 60.0 - -# Transient SQLITE_IOERR retry budget for READ-ONLY opens: a WAL writer's -# checkpoint/reset/frame flush surfaces "disk I/O error" to a concurrent mode=ro -# reader for a millisecond-wide window (ro cannot do the -shm recovery). Never -# for writable opens: a writer owns the transition, so an IOERR there is real. -_READ_ONLY_IOERR_RETRY_ATTEMPTS = 3 -_READ_ONLY_IOERR_RETRY_BACKOFF_S = 0.05 - -# Import-time snapshot so _default_db_path() can detect a re-pointed -# DEFAULT_DB_PATH (tests monkeypatch the constant directly). -_IMPORT_DEFAULT_DB_PATH = DEFAULT_DB_PATH +# Transient SQLITE_IOERR retry budget for READ-ONLY opens: a WAL writer's checkpoint/reset/ +# frame flush surfaces "disk I/O error" to a concurrent mode=ro reader for a millisecond-wide +# window (ro cannot do the -shm recovery). Never for writable opens: there an IOERR is real. +_READ_ONLY_IOERR_RETRY_ATTEMPTS, _READ_ONLY_IOERR_RETRY_BACKOFF_S = 3, 0.05 def _default_db_path() -> Path: """Default state DB path at CALL time: a re-pointed ``DEFAULT_DB_PATH`` wins, else ``get_hermes_home()`` is resolved fresh so a runtime HERMES_HOME redirect works regardless of import order.""" - if DEFAULT_DB_PATH != _IMPORT_DEFAULT_DB_PATH: - return DEFAULT_DB_PATH - return get_hermes_home() / "state.db" + return DEFAULT_DB_PATH if DEFAULT_DB_PATH != _IMPORT_DEFAULT_DB_PATH else get_hermes_home() / "state.db" -# Live-DB guard knobs live HERE (not in hermes_state_guard): the hermetic conftest -# monkeypatches ``hermes_state._STATE_DB_GUARD_BYPASS`` (escape hatch for -# ``@pytest.mark.live_system_guard_bypass``) and ``_EXTRA_DENY_ROOTS`` (the -# pre-sandbox root, so custom-HERMES_HOME deployments are covered too). +# Live-DB guard knobs live HERE (not in hermes_state_guard): the hermetic conftest monkeypatches +# ``hermes_state._STATE_DB_GUARD_BYPASS`` (``@pytest.mark.live_system_guard_bypass`` escape hatch) +# and ``_EXTRA_DENY_ROOTS`` (the pre-sandbox root, so custom-HERMES_HOME deployments are covered). _STATE_DB_GUARD_BYPASS = False _STATE_DB_GUARD_EXTRA_DENY_ROOTS: Tuple[Path, ...] = () @@ -320,13 +312,12 @@ def format_session_db_unavailable(prefix: str = "Session database not available" return f"{prefix}: {cause}{hint if any(m in cause.lower() for m in _WAL_INCOMPAT_MARKERS) else ''}." -# Auto-repair at most once per DB path per process (no repair loops; serialises -# concurrent web_server / gateway opens on the same malformed file). +# Auto-repair at most once per DB path per process (no repair loops; serialises concurrent +# web_server / gateway opens on the same malformed file). _repair_attempted_paths: set[str] = set() _repair_attempt_lock = threading.Lock() - -# Cross-process schema-surgery lock timeout (``_repair_attempt_lock`` covers one -# interpreter only); sized for the slowest legitimate holder (VACUUM, multi-GB DB). +# Cross-process schema-surgery lock timeout (``_repair_attempt_lock`` covers one interpreter +# only); sized for the slowest legitimate holder (VACUUM, multi-GB DB). _REPAIR_LOCK_TIMEOUT_SECONDS = 120.0 _IS_WINDOWS = sys.platform == "win32" @@ -348,9 +339,8 @@ def divert_session_transcript_jsonl(session_id: str, messages) -> "Optional[Path return path -# Process-wide shared SessionDB registry (hermes_state_registry): long-lived -# in-process callers share ONE writer connection per resolved path via -# get_shared_session_db(); one-shots use SessionDB() with their own close(). +# Process-wide shared SessionDB registry: long-lived in-process callers share ONE writer +# connection per resolved path via get_shared_session_db(); one-shots use SessionDB() + close(). from hermes_state_registry import ( # noqa: F401 (re-export) close_shared_session_dbs, get_shared_session_db, release_or_close, ) diff --git a/hermes_state_sessions.py b/hermes_state_sessions.py index 8e34a0507b..71902f3dd4 100644 --- a/hermes_state_sessions.py +++ b/hermes_state_sessions.py @@ -183,6 +183,16 @@ _SAME_KEY_NAMESPACE_SQL = ( ) +# Upsert tail of _insert_session_row: every routing/metadata column keeps the value an +# earlier writer set (whitespace is part of the SQL text). +_UPSERT_KEEP_EXISTING_SQL = ",\n".join( + f" {col} = COALESCE(sessions.{col}, excluded.{col})" for col in ( + "session_key", "chat_id", "chat_type", "thread_id", "parent_session_id", "cwd", "profile_name", + "git_repo_root", "origin_json", "display_name", + ) +) + + def _inherit_col_sql(col: str, extra: str = "") -> str: """``col = COALESCE(sessions.col, (SELECT p.col FROM parent))`` (whitespace is part of the SQL text).""" pad = " " * (30 + len(col)) @@ -301,16 +311,7 @@ class SessionSessionsMixin: THEN NULL ELSE sessions.system_prompt END, - session_key = COALESCE(sessions.session_key, excluded.session_key), - chat_id = COALESCE(sessions.chat_id, excluded.chat_id), - chat_type = COALESCE(sessions.chat_type, excluded.chat_type), - thread_id = COALESCE(sessions.thread_id, excluded.thread_id), - parent_session_id = COALESCE(sessions.parent_session_id, excluded.parent_session_id), - cwd = COALESCE(sessions.cwd, excluded.cwd), - profile_name = COALESCE(sessions.profile_name, excluded.profile_name), - git_repo_root = COALESCE(sessions.git_repo_root, excluded.git_repo_root), - origin_json = COALESCE(sessions.origin_json, excluded.origin_json), - display_name = COALESCE(sessions.display_name, excluded.display_name)""", +""" + _UPSERT_KEEP_EXISTING_SQL, ( session_id, source, user_id, session_key, chat_id, chat_type, thread_id, model, json.dumps(model_config) if model_config else None, system_prompt_hash,