diff --git a/tests/tui_gateway/test_change_watcher.py b/tests/tui_gateway/test_change_watcher.py index 9c612c555f..1e0d297072 100644 --- a/tests/tui_gateway/test_change_watcher.py +++ b/tests/tui_gateway/test_change_watcher.py @@ -64,6 +64,23 @@ def test_state_db_move_broadcasts_sessions_changed(watcher_home): assert ("sessions.changed", {}) in events +def test_served_profile_store_move_broadcasts_sessions_changed(watcher_home, monkeypatch): + """A backend serving a sibling profile must see that profile's state.db + move too — otherwise a routed profile's Bot Chat never refreshes (#99333).""" + home, events = watcher_home + bot_home = home / "profiles" / "bot" + bot_home.mkdir(parents=True) + monkeypatch.setattr(server, "_served_profile_homes", set()) + monkeypatch.setattr("hermes_cli.profiles.get_profile_dir", lambda name: home / "profiles" / name) + assert server._profile_home("bot") == bot_home + server._broadcast_watched_changes(now=0.0) + + (bot_home / "state.db").write_text("x") + server._broadcast_watched_changes(now=10.0) + + assert ("sessions.changed", {}) in events + + def test_gateway_state_move_broadcasts_platforms_changed(watcher_home): home, events = watcher_home server._broadcast_watched_changes(now=0.0) diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 64438a3eb1..e2eee50d36 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -2457,7 +2457,18 @@ def _profile_home(profile: str | None) -> Path | None: # Already the launch profile? No override needed. if home.resolve() == Path(_hermes_home).resolve(): return None - return home if (home / "state.db").exists() or home.exists() else None + if (home / "state.db").exists() or home.exists(): + # Remember every sibling home this backend was asked to serve so the + # change watcher stats its store too (#99333 class). + _served_profile_homes.add(home) + return home + return None + + +# Profile homes served by this process besides the launch home — the only +# extra stores the sessions watcher must probe. Empty on single-profile +# installs, so their watcher stays byte-identical (two stats per tick). +_served_profile_homes: set[Path] = set() def _profile_scoped(handler): @@ -5203,15 +5214,17 @@ def _sessions_sig(): """Newest mtime across state.db and its WAL — the cross-process change signal. Messaging-gateway turns and cron runs are written by OTHER processes that never touch this gateway's transports; the shared SQLite - file is the one thing they all move (#58671).""" - home = _watcher_home() + file is the one thing they all move (#58671). A backend serving several + profiles owns one store per profile, so every served sibling home is + probed too — otherwise a routed profile's Bot Chat never refreshes.""" sig = None - for name in ("state.db", "state.db-wal"): - try: - mtime = (home / name).stat().st_mtime_ns - except OSError: - continue - sig = mtime if sig is None else max(sig, mtime) + for root in (_watcher_home(), *_served_profile_homes): + for name in ("state.db", "state.db-wal"): + try: + mtime = (root / name).stat().st_mtime_ns + except OSError: + continue + sig = mtime if sig is None else max(sig, mtime) return sig