Files
hermes-agent/apps
Jack Lau d0e0951cf1 fix(desktop): publish the agent activation atomically too
`ensureGatewayAgent` is the (connectionId, profile) door the SDK's `ensureAgent`
goes through, and it landed on main after the profile path was made atomic. It
published in the order the profile path used to:

    await ensureGatewayForAgent(connection, target)   // $gateway flips here
    $activeGatewayProfile.set(target)
    await syncConnectionToActiveAgent(connection, target)   // $connection here

The trailing await is the same mixed-state window: $gateway and
$activeGatewayProfile already name the agent's backend while $connection still
describes the previous one, so any request or plugin mode-listener firing in
that window announces the wrong mode to the new backend.

Both doors now share one seam:

* `prepareGatewayForAgent` mirrors `prepareGatewayForProfile`: dial the socket,
  publish nothing, return the synchronous activation thunk. A local/null
  connection falls through to the profile seam, so the two paths cannot drift.
  `ensureGatewayForAgent` becomes `(await prepareGatewayForAgent(...))()`,
  exactly how `ensureGatewayForProfile` relates to its own prepare.
* `syncConnectionToActiveAgent` splits into `resolveConnectionForActiveAgent`,
  which resolves only. `ensureGatewayAgent` resolves the descriptor and dials
  the socket concurrently, then activates, moves the profile pointer and sets
  the descriptor with no awaits between them.

The best-effort contract on this path is unchanged on purpose: a descriptor
lookup that fails still leaves the previous `$connection` in place rather than
aborting the switch, which is what the profile path does instead. That
difference is deliberate and called out for review rather than quietly
harmonised.

Tests: `profile-agent-activation.test.ts` gains
`never publishes the agent gateway before its connection descriptor`, the mirror
of the profile-path test, asserting a pending `getConnectionFor` leaves all
three atoms on the old backend and that they flip together once it resolves. The
existing mutex and resync tests move onto the prepare/publish mocks, which also
repairs them: that file mocked `@/store/gateway` without `prepareGatewayForProfile`,
so its profile-path cases called an undefined mock after the rebase.
2026-08-18 15:04:28 -07:00
..