fix(desktop): adopt saved primary profile before gateway boot - #75112
fix(desktop): adopt saved primary profile before gateway boot#75112weddle wants to merge 1 commit into
Conversation
Cold boot published gatewayReady before adoptPrimaryProfile()'s IPC read
returned: the shared gateway client fires setState('open') before
connect() resolves, while boot only awaited the profile adoption
afterwards. Any profile-scoped fetch triggered by gatewayReady (sessions,
projects) therefore raced the $activeGatewayProfile atom at its initial
'default' -- on a machine whose primary profile is a named one, the first
sidebar population was scoped to the wrong profile. Same ordering family
as NousResearch#50529.
Fix: split the persisted-profile read out of adoptPrimaryProfile() and
apply it before desktop.getConnection(), so the atom, the registry's
primaryProfile, and event tagging all carry the persisted profile before
any connection state can be observed. Applying the profile also aligns
the registry's active pointer (ensureGatewayForProfile fast path):
without that, state events tagged with the adopted primary fail the
activeKey match in reportGatewayState, the 'open' transition is dropped,
and the UI hangs at 'connecting'. HMR-survivor adoption and the
catch->default fallback are preserved.
Tests: the boot-ordering test asserts the profile IPC resolves and the
atom carries the persisted profile before getConnection(), and that the
gateway still publishes 'open' after the socket opens (regression for
the dropped-open hang).
|
Thanks for carrying the saved-primary-profile boot ordering fix forward from #50529. The premise remains valid on current main: cold boot calls The proposed pre-connect Automated hermes-sweeper review. |
Graph note (no action implied — a maintainer has already reviewed this thread). Our triage graph places this PR in a complex with 3 related pull requests ( Full neighbourhood: https://hermes-triage.gottz.de/?node=75112 This note exists so the relationship stays discoverable from the thread itself. |
What does this PR do?
Fixes a race at cold boot in the desktop app. The renderer connects the primary gateway before it reads which profile the desktop persisted, so fetches keyed on
gatewayReadycan run while$activeGatewayProfilestill holds its initial'default'.I measured the order on unpatched
mainwithatlasas the persisted profile.desktop.profile.get()runs once, aftergetConnection()has already resolved. At the moment the connection is obtained the atom reads"default". The shared gateway client publishessetState('open')insideconnect()before the promise resolves (apps/shared/src/json-rpc-gateway.ts), and boot awaitsadoptPrimaryProfile()later. The sidebar's first sessions and projects fetches race the adoption IPC and lose. Any machine whose primary profile is a named one paints its first sidebar under the wrong profile.PR #50529 identified this ordering problem. Its review asked for a rebase onto the current
adoptPrimaryProfile()helper with the seed applied before cold-bootgetConnection(). This PR implements that shape, with credit to #50529's author.adoptPrimaryProfile()intoreadPrimaryProfile()andapplyPrimaryProfile(). Cold boot applies it to the atom, to the registry'sprimaryProfile, and to event tagging beforedesktop.getConnection(). HMR-survivor adoption keeps its path, as does the catch that falls back to'default'.applyPrimaryProfile()also runs the primary fast path ofensureGatewayForProfile(), a synchronoussetActivethat is safe before connect. Skipping this step drops theopentransition: state events tagged with the adopted primary fail theactiveKeycomparison inreportGatewayStateand the UI sits at "connecting" forever. A test guards this.If the preferred path is updating #50529 itself, the tests here can move there.
Related Issue
Addresses the boot-ordering defect tracked in PR #50529's review: adopt the persisted primary profile before
getConnection(). In-flight work on the same hook covers different lifecycle paths: #74552 handles reconnect after sleep, #74388 gates session restore. I am glad to coordinate rebases with either.Type of Change
Changes Made
apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts: split the read of the persisted profile from applying it.startBoot()reads and applies beforegetConnection().setPrimaryGatewaywaits until the profile is known. Event tagging uses the adopted profile.applyPrimaryProfile()aligns the registry's active pointer.apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx: an ordering test (the persistedatlasmust be applied beforegetConnection(), and the profile IPC must precede the connection IPC) plus an open-state test (after the socket opens on a named-primary boot,$gatewayStatemust publish'open').How to Test
cd apps/desktop && npx vitest run --project ui src/app/gateway/hooks/use-gateway-boot.test.tsxpasses 7 tests.mainand run it. One test fails, six pass. The mockedgetConnectionasserts the atom already carries the persisted profile; on old code adoption runs after connect, so the assertion rejects insidegetConnection. The runner reportsexpected "vi.fn()" to be called once, but got 0 timesonprofileGet, the downstream trace of that rejection.mainby construction. It exists to guard the hazard described in change 2. I verified it by mutation: the fix with theensureGatewayForProfile()line deleted fails it withexpected 'idle' to be 'open'.active-profile.json, cold-launch, and confirm the first sidebar population belongs to that profile and the app reaches "open".Checklist
Code
uiproject 354 files, 3,134 tests, 0 failed;electronproject 867 passed, 0 failed. Local note: Node 26 produces roughly 200 spurious jsdomlocalStoragefailures on unmodifiedmain, so a clean local run needs CI's pinned Node 22.Documentation & Housekeeping
cli-config.yaml.example: N/AScreenshots / Logs
Instrumented boot on unpatched
main, persisted primaryatlas:With this PR the atom carries
atlasbeforegetConnection()runs, and the open transition still publishes.