Skip to content

fix(desktop): bots wake on the right gateway — session RPCs route to the owning profile (#89206) - #89475

Merged
teknium1 merged 1 commit into
mainfrom
fix/bot-wake-profile-routing
Aug 18, 2026
Merged

teknium1 merged 1 commit into
mainfrom
fix/bot-wake-profile-routing

Conversation

@teknium1

Copy link
Copy Markdown
Collaborator

Summary

Bot Mode bots wake reliably: session-scoped RPCs (session.resume / session.activate / session.usage) now always reach the backend that owns the session's profile, instead of whatever socket happened to be active at dispatch time.

Root cause (diagnosed from zero trust's debug bundle + @DanBennettUK's #89206 trace): a routing split-brain. The renderer's $activeGatewayProfile atom and the gateway registry's actual socket selection could diverge — eviction fallbacks (idle reap, connection removal, profile delete) moved the socket back to the primary while the atom kept naming the evicted bot, so ensureGatewayProfile's "already active" fast path skipped the re-swap forever and every resume went out on the default backend. Evidence: loki/hulk/teknium-kun backends booted, passed HTTP+WS probes, then sat at zero traffic until the idle reaper killed them, while the renderer burned its bounded retries into "automatic retries gave up".

Changes

  • store/gateway.ts — applyActive() publishes the active route's bare profile ($activeGatewayRoute + new onActiveRouteChanged registry callback), in the same synchronous step that selects the socket. Every eviction/fallback path already funnels through applyActive, so the published profile can never linger on a deselected backend.
  • app/gateway/hooks/use-gateway-boot.ts — wires onActiveRouteChanged to mirror the registry's route into $activeGatewayProfile (registry leads; renderer follows).
  • store/session-request-router.ts (new) — requestForSessionProfile(): session-scoped RPCs re-resolve their route at request time; when the active gateway serves the owning profile the ambient dispatcher is kept (reauth-aware reconnect), otherwise the RPC is pinned to the profile's own socket via requestGatewayForProfile.
  • app/session/hooks/use-session-actions/index.ts — resumeSession's three session-scoped RPCs go through the router.
  • store/session-request-router.test.ts (new) — 7 tests covering route-publish lockstep, eviction fallback, and wrong-socket dispatch.

Validation

Check Result
Sabotage: revert route publish lockstep + eviction tests FAIL
Sabotage: revert request-time routing wrong-socket dispatch test FAILS
New router tests 7/7
store gateway/profile suites (10 files) 147/147
session hooks suites 622/622
gateway hooks suites 22/22
npm run check:lint (3 tsconfigs + eslint) 0 errors

Fixes the remaining half of #89206 (the hydration-gate half landed in #89394).

Infographic

Bots wake on the right gateway

…#89206)

Bot Mode wake-ups died on a routing split-brain: session.resume /
session.activate / session.usage were dispatched on whatever socket was
active at request time, while the bot's own backend sat healthy and idle
(zero traffic until the idle reaper killed it). Two divergence sources,
both fixed:

1. Registry-owned route truth. applyActive() now publishes the active
   route's bare profile ($activeGatewayRoute + onActiveRouteChanged), and
   use-gateway-boot mirrors it into $activeGatewayProfile. Previously,
   eviction fallbacks (idle reap, connection removal, profile delete)
   moved the SOCKET back to the primary while the profile atom kept
   naming the evicted bot — ensureGatewayProfile's "already active" fast
   path then trusted the stale atom and skipped the re-swap forever.

2. Request-time routing for session-scoped RPCs. resumeSession's RPCs go
   through requestForSessionProfile (store/session-request-router.ts):
   when the active route serves the session's owning profile the ambient
   dispatcher is kept (reauth-aware reconnect); when it diverges — a
   concurrent switch won the mutex, a failed dial left the old socket
   active, an eviction re-pointed the route — the RPC is pinned to the
   owning profile's own socket via requestGatewayForProfile.

Diagnosed from zero trust's debug bundle (loki/hulk/teknium-kun backends
READY then idle-reaped, renderer stuck on "Waking up… → retries gave
up") and DanBennettUK's #89206 trace (profile socket accepts, closes
with messages=0, no resume RPC observed).

Both layers sabotage-proven: reverting the route publish fails the
lockstep/eviction tests; reverting the request-time routing fails the
wrong-socket dispatch test.
@github-actions

github-actions Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on f1bf53b — fix(desktop): route session RPCs to the profile that owns th

⚠️ Warnings

OSV vulnerability scan · View job

7 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 3m53s vs 10m35s (-63.3%). 10 job(s) slower, 8 faster, 1 unchanged.

  • JS & TS checks / apps/desktop / check:test:ui:shard-2of3: +36.0s
  • JS & TS checks / apps/desktop / check:test:ui:shard-3of3: +26.0s
  • JS & TS checks / ui-tui/packages/hermes-ink / check: +9.0s
  • JS & TS checks / web / check: -8.0s
  • Detect affected areas: +8.0s

@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 18, 2026
@teknium1
teknium1 merged commit ae6578a into main Aug 18, 2026
41 checks passed
@teknium1
teknium1 deleted the fix/bot-wake-profile-routing branch August 18, 2026 21:09
@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference; please use your judgment.

  • correctness: The lockstep design is sound — applyActive() publishes the bare profile (secondaries.get(activeKey)?.profile ?? primary) in the same synchronous step as socket selection, so eviction fallbacks can't leave a stale published profile. Optional chaining on g.config?.onActiveRouteChanged?.() correctly tolerates callbacks firing before configureGatewayRegistry.
  • correctness: The residual TOCTOU between sessionRpcNeedsProfileRoute()'s check and dispatch is benign since the divergent path pins via requestGatewayForProfile(profile, ...) rather than the ambient socket.
  • tests: Router tests exercise the real registry modules with only the transport class mocked — behavioral, not snapshot/source-reading. Good coverage of the three failure layers including sabotage-verified regression cases.
  • nit: $activeGatewayProfile now has two writer classes (registry mirror + whatever pre-existing code set it, e.g. profile switches). Worth confirming no legacy write path can race the mirror and briefly re-introduce divergence; the registry-leads invariant makes this low-risk.
  • nit: requestForSession helper in resumeSession closes over sessionProfile; fine here, but future session-scoped RPCs outside this closure won't get routing automatically — consider exporting the helper pattern if more call sites appear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants