fix(desktop): refresh routed-profile Bot Chat transcripts (salvage #99333)

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>
This commit is contained in:
Teknium
2026-09-02 03:36:51 -07:00
parent ff8c1aca7a
commit 2ce6f5c538
2 changed files with 39 additions and 9 deletions
+17
View File
@@ -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)
+22 -9
View File
@@ -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