Files
hermes-agent/apps
Minsang Lee 0287dfb0c2 fix(bot-mode): a bot row opens the conversation you were last having
Clicking a bot in the roster always reopened its pinned canonical Bot Chat.
Start a new conversation with bot A, click bot B, click back to A — the new
conversation was gone, replaced by the pinned transcript. A bot row is a
workspace entry point, so it has to land on the live conversation.

Two independent causes, both fixed here:

1. The pin overrode newer work.
   `openBotCanonicalChat` opened the pin unconditionally. It now prefers the
   bot's freshest VISIBLE session — but only AFTER `profiles.list` has
   verified through `preferred_session` that the pin is alive and is a real
   canonical Bot Chat. That ordering matters: with a dead or unverified pin,
   adopting the profile's latest row would claim an unrelated user
   conversation as the bot's chat, and the hide sweep would then hide it.
   The existing "no pin" / "dead pin" safety tests cover exactly that and
   still pass. The pin keeps owning plumbing (creation, hide sweep, DM
   delivery); it just stops shadowing newer conversations.

   Guards on the candidate (`newerVisibleBotChat`): the canonical chat can
   never shadow itself, an empty draft never displaces a real conversation,
   and a gateway that omits `message_count` is treated as real history
   rather than discarded.

2. The workspace did not follow the bot.
   The three `host.openSession` calls on the bot path relied on the SDK
   default `keepAllProfilesScope: true`, so `$activeGatewayProfile` stayed on
   whatever profile was active before the click. Sessions created afterwards
   were then filed under the previous bot's profile — measured: four new
   chats started from three different bots all persisted into one profile's
   state.db. Clicking a bot IS a profile switch, so these pass `false`.

Note on the call shape: `previewSession` is `bot.preferred_session || last`,
so on a pinned bot it resolves to the PIN (preview identity must match click
identity). Feeding that as the "newer" candidate makes the whole preference
dead code — it always sees the pin and short-circuits on "same id". The
freshest visible session therefore arrives as its own argument. The first
attempt at this fix had that bug and passed its tests, which is why
`bot-row-opens-latest.test.mjs` mirrors the production call site argument for
argument rather than constructing a convenient one.

Tests: 362 pass (was 348). Each new guard was verified by sabotage — reverting
any one of the three behaviours above makes the suite fail (1, 3, and 1 tests
respectively), so none of them is a test that passes either way.
2026-08-21 13:42:54 -07:00
..