diff --git a/gateway/hosted_room_policy_checkpoint.py b/gateway/hosted_room_policy_checkpoint.py index 2d79bc98aa..f4e9adb822 100644 --- a/gateway/hosted_room_policy_checkpoint.py +++ b/gateway/hosted_room_policy_checkpoint.py @@ -114,7 +114,7 @@ class HostedRoomPolicyCheckpoint: self.db_path.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(self.db_path, timeout=10) conn.row_factory = sqlite3.Row - apply_wal_with_fallback(conn, db_label="state.db (room policy checkpoint)") + apply_wal_with_fallback(conn, db_label="shared-state.db (room policy checkpoint)") return conn @staticmethod diff --git a/gateway/hosted_rooms.py b/gateway/hosted_rooms.py index ba9be1b7d6..7874243db2 100644 --- a/gateway/hosted_rooms.py +++ b/gateway/hosted_rooms.py @@ -423,7 +423,7 @@ def local_authority_gateway_id() -> str: _connect = partial( - connect, db_label="state.db (hosted_rooms)", ready=_schema_is_current, + connect, db_label="shared-state.db (hosted_rooms)", ready=_schema_is_current, initialize=lambda conn: _initialize_schema(conn), lock_retries=_JOURNAL_MODE_LOCK_RETRIES) diff --git a/tests/gateway/test_hosted_rooms.py b/tests/gateway/test_hosted_rooms.py index 65c2272423..64a1cc5773 100644 --- a/tests/gateway/test_hosted_rooms.py +++ b/tests/gateway/test_hosted_rooms.py @@ -1339,3 +1339,26 @@ def test_room_log_page_bound_counts_bytes_not_characters(tmp_path, monkeypatch): assert [event["seq"] for event in page["events"]] == [1] assert page_bytes(page) <= budget assert page["has_more"] is True + + +def test_default_db_path_never_names_the_master_session_store(tmp_path, monkeypatch): + """Hosted-room coordination lives beside, never inside, the master ``state.db``. + + Every profile gateway starts the hosted-room worker, so a store resolved to the + root session DB made every profile process a long-lived writer on state.db — + the multi-profile restart corruption in #102120 / #103339 / #103490. The store + is shared across profiles (one file at the install root) but is not the + SessionDB file the root gateway owns. + """ + root = tmp_path / ".hermes" + profile_home = root / "profiles" / "bot1" + profile_home.mkdir(parents=True) + + monkeypatch.setenv("HERMES_HOME", str(profile_home)) + from_profile = rooms.default_db_path() + monkeypatch.setenv("HERMES_HOME", str(root)) + from_root = rooms.default_db_path() + + assert from_profile == from_root, "one coordination file per install" + assert from_root.parent == root + assert from_root.name != "state.db" diff --git a/tests/tui_gateway/test_groups_methods.py b/tests/tui_gateway/test_groups_methods.py index 4ab00cdaba..54e6635609 100644 --- a/tests/tui_gateway/test_groups_methods.py +++ b/tests/tui_gateway/test_groups_methods.py @@ -6,6 +6,7 @@ from types import SimpleNamespace import pytest +from gateway.hosted_rooms import default_db_path as hosted_rooms_default_db_path import tui_gateway.server as srv from tui_gateway import methods_groups @@ -346,7 +347,7 @@ def test_register_peer_route_probes_scope_and_persists_via_service(home, monkeyp } class FakeService: - db_path = home / "state.db" + db_path = hosted_rooms_default_db_path() def register_peer_route(self, **kwargs): captured["registered"] = kwargs @@ -376,7 +377,7 @@ def test_register_peer_route_probes_scope_and_persists_via_service(home, monkeyp def test_register_rejects_plaintext_non_loopback(home, monkeypatch): class FakeService: - db_path = home / "state.db" + db_path = hosted_rooms_default_db_path() monkeypatch.setattr(srv, "get_hosted_room_service", lambda: FakeService()) response = srv._methods["groups.peer.register"]( @@ -398,7 +399,7 @@ def test_register_requires_roomlink_protocol_v2(home, monkeypatch): from gateway.hosted_room_peer import catalog_mapping class FakeService: - db_path = home / "state.db" + db_path = hosted_rooms_default_db_path() monkeypatch.setattr(srv, "get_hosted_room_service", lambda: FakeService()) response = srv._methods["groups.peer.register"]( @@ -914,7 +915,7 @@ def test_disband_stops_and_revokes_before_tombstoning(home, monkeypatch): calls = [] class FakeService: - db_path = home / "state.db" + db_path = hosted_rooms_default_db_path() def stop_room(self, room_id, **_kwargs): calls.append(("stop", room_id)) @@ -933,7 +934,7 @@ def test_failed_remote_revocation_keeps_room_recoverable(home, monkeypatch): _create_room() class FakeService: - db_path = home / "state.db" + db_path = hosted_rooms_default_db_path() def stop_room(self, _room_id, **_kwargs): return 1 @@ -958,7 +959,7 @@ def test_disband_does_not_revoke_routes_while_stop_is_unacknowledged( calls = [] class FakeService: - db_path = home / "state.db" + db_path = hosted_rooms_default_db_path() def stop_room(self, _room_id, **kwargs): calls.append(("stop", kwargs["require_acknowledged"]))