From 2ce6f5c53847646f324b2293fd92ef08d36d4c7b Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 03:36:51 -0700 Subject: [PATCH] fix(desktop): refresh routed-profile Bot Chat transcripts (salvage #99333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two sites dropped profile ownership on the tile-transcript refresh path: - tui_gateway/server.py _sessions_sig statted only the launch home's state.db, so a turn landing in a served sibling profile's store never produced sessions.changed. The watcher now also probes every profile home _profile_home() has resolved for this backend (empty set on single-profile installs — behavior byte-identical there). - use-background-sync.ts reconcileTileTranscripts read the tile's transcript unscoped; it now passes the tile's ownerRoute scope, the same way reconcileActiveTranscript already does for the main pane, and keys the change signature by owner. Reimplemented minimal from PR #99333 (the PR head's commit identity does not match the GitHub author). Co-authored-by: StodsEcho5 <250208229+StodsEcho5@users.noreply.github.com> --- tests/tui_gateway/test_change_watcher.py | 17 +++++++++++++ tui_gateway/server.py | 31 +++++++++++++++++------- 2 files changed, 39 insertions(+), 9 deletions(-) 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