fix(tui): keep in-session agent rebuilds on their own profile store

_sync_bot_capabilities (Bot Chat capability refresh, run at every turn start) and
_reset_session_agent (/new, tools.set) swap a fresh AIAgent into a LIVE session but
called _make_agent with neither the session's state.db handle nor its HERMES_HOME.
_make_agent resolves prompt/skills/toolsets through get_hermes_home() and defaults
session_db to the process-wide LAUNCH _get_db() handle, so a named-profile session
silently migrated onto the launch profile: every later turn appended to
~/.hermes/state.db under the same session id while the desktop replayed
profiles/<name>/state.db and showed a stale transcript.

Route both rebuilds through _rebuild_session_agent, which binds the session's
profile scopes and inherits the outgoing agent's handle (same session, same file)
so no second refcount is taken and teardown still releases it exactly once.
This commit is contained in:
HexLab98
2026-09-06 15:36:42 +07:00
committed by Teknium
parent dffcf84e25
commit 8da23bdae4
2 changed files with 46 additions and 4 deletions
+44 -2
View File
@@ -4,6 +4,7 @@ globals at install time (method_ctx.bind_module), so they reference server.py gl
from __future__ import annotations
import contextlib
import threading
from .method_ctx import bind_module
@@ -356,6 +357,47 @@ def _preview_restart_callbacks(parent: str, task_id: str) -> dict:
"status_callback": lambda kind, text=None: progress(text if text is not None else kind)}
def _rebuild_session_agent(sid: str, session: dict, **kwargs):
"""Rebuild a LIVE session's agent on the profile that session belongs to.
``_make_agent`` resolves prompt/skills/toolsets through ``get_hermes_home()`` and falls back to the
process-wide LAUNCH ``state.db`` when handed no handle, so an unscoped in-session rebuild silently moves
a named-profile session onto the launch profile: its later turns append to ``~/.hermes/state.db`` under
the same session id while the desktop replays the profile store and shows a stale transcript (#104079).
The outgoing agent already holds this session's handle — inherit it (same session, same FILE) instead of
acquiring a second refcount, and carry ownership across so teardown still releases it exactly once.
"""
old_agent = session.get("agent")
profile_home = session.get("profile_home")
session_db = getattr(old_agent, "_session_db", None)
# No live agent to inherit from (rebuild before the deferred build ran): open the profile's store the
# same FAIL-CLOSED way _start_agent_build does rather than letting _make_agent reach for the launch db.
opened = session_db is None and bool(profile_home)
scopes = _bind_build_profile_scopes(profile_home) if profile_home else None
try:
if opened:
session_db = _open_profile_session_db(profile_home)
agent = _make_agent(sid, session["session_key"], session_db=session_db, **kwargs)
except BaseException:
if opened and session_db is not None:
with contextlib.suppress(Exception):
session_db.close()
raise
finally:
if scopes is not None:
_release_build_profile_scopes(scopes)
# Only a DEDICATED handle carries ownership; the shared launch handle outlives every agent and
# _transfer_db_to_agent refuses it.
owned = opened or bool(getattr(old_agent, "_owns_session_db", False))
if owned and _transfer_db_to_agent(agent, session_db):
if old_agent is not None:
old_agent._owns_session_db = False
elif opened:
with contextlib.suppress(Exception):
session_db.close()
return agent
def _reset_session_agent(sid: str, session: dict) -> dict:
tokens = _set_session_context(session["session_key"])
try:
@@ -364,8 +406,8 @@ def _reset_session_agent(sid: str, session: dict) -> dict:
# resurrect them. Global process state is never touched (see _apply_model_switch).
for k in ("model_override", "create_reasoning_override", "create_service_tier_override", "one_turn_model_restore"):
session.pop(k, None)
new_agent = _make_agent(
sid, session["session_key"], session_id=session["session_key"],
new_agent = _rebuild_session_agent(
sid, session, session_id=session["session_key"],
platform_override=_session_source(session),
context_cwd_is_launch_artifact=_context_cwd_is_launch_artifact(session))
finally:
+2 -2
View File
@@ -275,8 +275,8 @@ def _sync_bot_capabilities(sid: str, session: dict) -> None:
try:
tokens = _set_session_context(sid, cwd=_session_cwd(session))
try:
new_agent = _make_agent(sid, session["session_key"], session_id=session["session_key"],
platform_override=_session_source(session))
new_agent = _rebuild_session_agent(sid, session, session_id=session["session_key"],
platform_override=_session_source(session))
finally:
_clear_session_context(tokens)
new_agent._session_title_hint = "Bot Chat"