diff --git a/gateway/session.py b/gateway/session.py index 3ba5b10c48..be74201c9f 100644 --- a/gateway/session.py +++ b/gateway/session.py @@ -3875,6 +3875,19 @@ class SessionStore: db = self._db if db is None or not hasattr(db, "rebuild_fts"): return False + # Guard against the same WAL split-brain risk as the automatic + # rebuild paths: skip when a foreign process holds state.db or + # its WAL sidecars open. + if hasattr(db, "_foreign_state_db_holders"): + foreign_holders = db._foreign_state_db_holders() + if foreign_holders: + logger.warning( + "Skipping Session DB FTS rebuild while foreign processes " + "hold the database or WAL sidecars (%s); canonical " + "transcript writes remain available.", + foreign_holders, + ) + return False try: rebuilt = db.rebuild_fts() except Exception as exc: diff --git a/hermes_state.py b/hermes_state.py index a7d706e296..c795815164 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -4286,6 +4286,12 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin) return False if not self._is_fts_write_corruption_error(exc): return False + # Set the one-shot flag before the foreign-holder check: even when + # the rebuild is skipped, the fail-open path that follows persists + # the FTS_STALE_KEY marker so the next process startup will retry + # via _recover_stale_fts (which has its own holder guard). Setting + # the flag here also avoids re-running the expensive psutil scan on + # every subsequent corrupted write through this instance. self._fts_runtime_rebuild_attempted = True foreign_holders = self._foreign_state_db_holders() if foreign_holders: