fix(gateway): new chats honor their profile in global-remote mode (#39993)

Follow-up to #39921. That PR scoped session.resume + prompt.submit to a
session's profile, but a BRAND-NEW chat (session.create) under a non-launch
profile was still built and persisted against the dashboard's launch profile.
Two visible symptoms in app-global remote mode (one dashboard, many profiles):

  1. "who are you" in profile S replied as the launch (default) profile/agent —
     the agent was built with the launch HERMES_HOME, so config/SOUL/identity
     came from the wrong profile.
  2. "session not found" on later resume — _ensure_session_db_row persisted the
     row into the launch profile's state.db via _get_db(), so the session lived
     in the wrong db, the unified list mis-tagged it (it showed up under BOTH
     profiles), and resume routed to the wrong one.

Fix — carry the owning profile through the create path too:

- session.create accepts an optional `profile`; resolves its home and stores
  `profile_home` on the session (alongside what resume already set).
- _start_agent_build binds that profile's HERMES_HOME while building the agent
  (config/skills/model/identity resolve to it) and hands the agent the profile's
  state.db so turns persist there.
- _ensure_session_db_row writes the row into the profile's state.db, not the
  launch db — fixing the duplicate row + mis-tag + resume 404.
- desktop sends the new-chat profile on session.create.

None/launch profile → unchanged (single-profile and per-profile-remote setups
take the same path). Verified live against a one-dashboard / multi-profile
remote: a new chat under `work` builds as work's agent (correct SOUL identity),
persists ONLY to work's state.db (launch db stays empty), the unified list tags
it `work` exactly once, and it resumes cleanly.

tests/test_tui_gateway_server.py: _make_agent mocks updated for the session_db
param added in #39921's build path.
This commit is contained in:
brooklyn! 2026-06-05 12:44:45 -05:00 committed by GitHub
parent 1d9c3ebae0
commit 6f6eb871d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 58 additions and 6 deletions

View file

@ -331,7 +331,14 @@ export function useSessionActions({
// so single-profile users are unaffected).
await ensureGatewayProfile($newChatProfile.get())
const cwd = $currentCwd.get().trim() || getRememberedWorkspaceCwd()
const created = await requestGateway<SessionCreateResponse>('session.create', { cols: 96, ...(cwd && { cwd }) })
// Pass the owning profile so a new chat under a non-launch profile (global
// remote mode) builds its agent + persists against THAT profile's home/db.
const newChatProfile = $newChatProfile.get()
const created = await requestGateway<SessionCreateResponse>('session.create', {
cols: 96,
...(cwd && { cwd }),
...(newChatProfile ? { profile: newChatProfile } : {})
})
const stored = created.stored_session_id ?? null
if (

View file

@ -3577,7 +3577,7 @@ def test_session_create_close_race_does_not_orphan_worker(monkeypatch):
release_build = threading.Event()
build_entered = threading.Event()
def _slow_make_agent(sid, key, session_id=None):
def _slow_make_agent(sid, key, session_id=None, session_db=None):
build_started.set()
build_entered.set()
release_build.wait(timeout=3.0)
@ -3685,7 +3685,7 @@ def test_session_create_no_race_keeps_worker_alive(monkeypatch):
self.base_url = ""
self.api_key = ""
monkeypatch.setattr(server, "_make_agent", lambda sid, key: _FakeAgent())
monkeypatch.setattr(server, "_make_agent", lambda sid, key, session_db=None: _FakeAgent())
monkeypatch.setattr(server, "_SlashWorker", _FakeWorker)
monkeypatch.setattr(
server,
@ -3769,7 +3769,7 @@ def test_session_create_continues_when_state_db_is_unavailable(monkeypatch):
emits = []
monkeypatch.setattr(server, "_make_agent", lambda sid, key: _FakeAgent())
monkeypatch.setattr(server, "_make_agent", lambda sid, key, session_db=None: _FakeAgent())
monkeypatch.setattr(server, "_SlashWorker", _FakeWorker)
monkeypatch.setattr(server, "_get_db", lambda: None)
monkeypatch.setattr(server, "_session_info", lambda _a, *a2: {"model": "x"})

View file

@ -678,10 +678,24 @@ def _start_agent_build(sid: str, session: dict) -> None:
worker = None
notify_registered = False
home_token = None
profile_home = current.get("profile_home")
try:
tokens = _set_session_context(key)
# Build against the session's profile (global-remote): bind its
# HERMES_HOME so config/skills/model resolve to it, and hand the
# agent that profile's db so turns persist to the right state.db.
session_db = None
if profile_home:
home_token = set_hermes_home_override(profile_home)
try:
from hermes_state import SessionDB
session_db = SessionDB(db_path=Path(profile_home) / "state.db")
except Exception:
session_db = None
try:
agent = _make_agent(sid, key)
agent = _make_agent(sid, key, session_db=session_db)
finally:
_clear_session_context(tokens)
@ -725,6 +739,8 @@ def _start_agent_build(sid: str, session: dict) -> None:
current["agent_error"] = str(e)
_emit("error", sid, {"message": f"agent init failed: {e}"})
finally:
if home_token is not None:
reset_hermes_home_override(home_token)
with _sessions_lock:
replaced = _sessions.get(sid) is not current
if replaced:
@ -850,7 +866,22 @@ def _ensure_session_db_row(session: dict) -> None:
key = session.get("session_key")
if not key:
return
db = _get_db()
# Persist into the session's own profile db (global remote mode), not the
# launch profile's — otherwise the row lands in the wrong state.db, the
# unified list mis-tags it, and resume 404s ("session not found").
profile_home = session.get("profile_home")
if profile_home:
from hermes_state import SessionDB
try:
db = SessionDB(db_path=Path(profile_home) / "state.db")
except Exception:
logger.debug("failed to open profile db for session row", exc_info=True)
return
close_db = True
else:
db = _get_db()
close_db = False
if db is None:
return
try:
@ -862,6 +893,12 @@ def _ensure_session_db_row(session: dict) -> None:
)
except Exception:
logger.debug("failed to persist desktop session row", exc_info=True)
finally:
if close_db:
try:
db.close()
except Exception:
pass
def _set_session_cwd(session: dict, cwd: str) -> str:
@ -2960,6 +2997,13 @@ def _(rid, params: dict) -> dict:
resolved_cwd = _completion_cwd(params)
_enable_gateway_prompts()
# ``profile`` (app-global remote mode): a new chat started under a non-launch
# profile must build its agent + persist against THAT profile's home/state.db,
# not the dashboard's launch profile. Stored on the session so _start_agent_build
# and each turn re-bind HERMES_HOME. None/own profile → launch (unchanged).
profile = (params.get("profile") or "").strip() or None
profile_home = _profile_home(profile)
ready = threading.Event()
now = time.time()
@ -2981,6 +3025,7 @@ def _(rid, params: dict) -> dict:
"inflight_turn": None,
"last_active": now,
"pending_title": title or None,
"profile_home": str(profile_home) if profile_home is not None else None,
"running": False,
"session_key": key,
"show_reasoning": _load_show_reasoning(),