Conversation
_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.
SummaryFixes the same #104079 stale-transcript symptom from the TUI side: Findings (all Non-blocking)
VerdictSafe fix with tests proving writes land in the profile store, not the launch store. No blocking issues. |
|
Already on main as 8da23bd. Thank you. |
Summary
Investigating #104079 turned up two things.
The reported
active=0/active=1duplicate pairs are not a bug. They are the designed signature of in-place compaction:SessionDB.archive_and_compact()soft-archives the pre-compaction rows (active=0, still searchable) and inserts the compacted transcript as freshactive=1rows, so the protected tail legitimately exists twice inmessages. The issue's detection query groups bysession_id, role, timestamp, contentwithout filteringactive, so it counts every compaction generation as a duplicate. Live reads are unaffected —get_messages()filtersactive=1, and_dedupe_display_rowscollapses generations for display.stamp_db_persisted_markers(#98450) and the in-place commit rollback (#99477) already cover the real double-INSERT class, which produces duplicateactive=1rows instead.The cross-profile write drift in the issue's evidence #4 is real, and this PR fixes it. Two paths swap a fresh
AIAgentinto a live session:_sync_bot_capabilities(tui_gateway/model_switch.py) — runs at every turn start for Bot Chat sessions and rebuilds when the capability fingerprint changes._reset_session_agent(tui_gateway/agent_callbacks.py) —/newand thetools.setRPC.Both called
_make_agentwith neither the session'sstate.dbhandle nor itsHERMES_HOME._make_agentresolves prompt/skills/toolsets throughget_hermes_home()and defaultssession_dbto the process-wide launch_get_db()handle, which ignores the profile ContextVar override entirely. A named-profile session therefore migrated onto the launch profile mid-conversation: every later turn appended to~/.hermes/state.dbunder the samesession_idwhile the desktop replayedprofiles/<name>/state.db, which is exactly the reported "96 messages in the default profile's store" and "chat history reverted to an older point". Compression does not cause this — it only raises the flush volume, which is why the symptom clusters around a compaction._start_agent_buildalready gets this right (_bind_build_profile_scopes+ fail-closed_open_profile_session_db, added for #88532/#50233); the two rebuild sites were never brought along.Both now route through
_rebuild_session_agent, which binds the session's profile scopes for the build and inherits the outgoing agent's handle — same session, same file, so no second refcount is acquired and ownership moves across so teardown still releases it exactly once. With no live agent to inherit from it opens the profile store the same fail-closed way the deferred build does, rather than letting_make_agentreach for the launch handle.Test plan
Two invariant tests added to
tests/hermes_state/test_named_profile_session_db.py, the existing home of this bug class ("a named-profile session must never touch the launchstate.db"), one per rebuild site. They assert the rebuilt agent holds the session's own handle, thatget_hermes_home()inside the build is the profile home, that ownership moved off the outgoing agent, and — writing through the rebuilt agent against realSessionDBhandles on a tempHERMES_HOME— that the row lands inprofiles/worker/state.dbwith the launch store still empty.assert None is <SessionDB ...>(no handle passed → the launch default).scripts/run_tests.sh tests/hermes_state/test_named_profile_session_db.py→ 7 passed.scripts/run_tests.sh tests/test_tui_gateway_server.py→ 637 passed.scripts/run_tests.sh tests/tui_gateway/ tests/hermes_state/→ the only failures (test_projects_rpc.py,test_hud_surface_note.py,test_live_db_guard_ancestry.py,test_session_cwd_follow.py) reproduce identically on an unmodified tree; they are a localgit initsandbox restriction, not related to this change.