Skip to content

fix(desktop): profile switches publish atomically without ever failing closed (#46651, #89622) - #89797

Merged
teknium1 merged 1 commit into
mainfrom
feat/atomic-profile-switch-v2
Aug 19, 2026
Merged

fix(desktop): profile switches publish atomically without ever failing closed (#46651, #89622)#89797
teknium1 merged 1 commit into
mainfrom
feat/atomic-profile-switch-v2

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Re-lands what the reverted atomic-publish series (#89483 → reverted in #89785) was trying to achieve — mode-safe, atomic profile-switch publication for #46651 — without the fail-closed decline path that killed profile clicks (#89622), and fixes the underlying pruner race the original series exposed. Verified flawless in live Electron use before this PR was opened.

Two changes on top of the restored (working) baseline:

  • Atomic, fail-open publication ([Bug] Desktop (a376ca0): image upload fails on remote profile — "image not found: <local_path>" #46651): the switch resolves the target's connection descriptor concurrently with the socket work and publishes $activeGatewayProfile + $connection in one nanostores batch() frame — no request or plugin mode-listener can observe the new gateway beside the old profile's descriptor. Unlike the reverted series, a failed descriptor lookup fails open: the switch still lands, the previous descriptor stays (boot/reconnect resyncs later), and the failure is logged instead of swallowed.
  • Activation lease vs. the socket pruner ([Bug]: Profile switching is broken! #89622's root race): ensureGatewayForProfile / ensureGatewayForAgent lease their entry (activationLeaseUntil, 30s bound) for the duration of the dial; pruneSecondaryGateways spares leased entries. A switch target is not yet active, has no live sessions, and holds no request lease, so a prune recompute during a cold pool spawn used to dispose the dialing entry. An orphaned lease self-expires.

The agent door logs a non-landing activation (target removed mid-dial) instead of resolving silently — observable, but never fail-closed.

Changes

  • store/gateway.ts: activationLeaseUntil on Secondary; both ensure doors lease during the dial and release when it settles; pruner spares live leases (bounded).
  • store/profile.ts: concurrent descriptor resolve + single batch() publication frame on both switch doors; fail-open descriptor contract documented with an explicit do-not-reintroduce-fail-closed warning; swallowed failures now console.warn.
  • Tests: new gateway-activation-prune-lease.test.ts (mid-dial prune survival on both doors, lease release, lease expiry); one pin updated for the concurrent read-only descriptor probe.

Validation

Result
Live E2E — 10 sequential switches across 3 profiles (cold + warm) all land, overlay clears (0.3s warm / 1.2s cold)
Live E2E — rapid two-click interleave last click wins
Live E2E — 6-click stress alternation final target lands, no wedge
Live E2E — cold spawns under prune pressure fresh pool backends spawn, dial survives
Targeted vitest (lease + profile + activation suites) 25/25
tsc -p . / eslint on touched files clean

Live E2E = real Electron app over CDP, isolated HERMES_HOME, backend pinned to this branch. The reverted series failed this exact harness (every switch declined); the pre-series baseline passed it; this PR passes it with the atomic-publication guarantees added back.

Infographic

Profile switch v2 — atomic, fail open, leased

… mid-dial entries (#89622, #46651)

Re-lands the goals of the reverted atomic-publish series (#89483, reverted
in #89785) without the fail-closed decline path that killed profile clicks,
and fixes the underlying pruner race the original series exposed.

Two changes relative to the restored (pre-series) behavior:

- Publication is atomic and mode-safe (#46651): the switch resolves the
  target's connection descriptor CONCURRENTLY with the socket work and
  publishes $activeGatewayProfile + $connection in one nanostores batch()
  frame, so no request or plugin mode-listener observes the new gateway
  beside the previous profile's descriptor. Unlike the reverted series, a
  failed descriptor lookup fails OPEN — the switch still lands, the previous
  descriptor stays, boot/reconnect resyncs it later, and the failure is
  logged instead of swallowed.
- The live-work pruner can no longer kill a switch mid-dial (#89622's root
  race): ensureGatewayForProfile / ensureGatewayForAgent lease their entry
  (activationLeaseUntil, 30s bound) for the duration of the dial and
  pruneSecondaryGateways spares leased entries. A switch target is not yet
  active, has no live sessions and holds no request lease, so any prune
  recompute during a cold pool spawn used to dispose the dialing entry.
  Bounded lease: an orphaned one self-expires.

The agent door logs a non-landing activation (target removed mid-dial)
instead of resolving silently, but never fails the switch closed.

Live E2E (Electron over CDP, 3 local profiles, cold + warm): 10 sequential
switches, rapid two-click interleave (last click wins), 6-click stress run —
all land, overlay always clears, fresh pool backends spawn on cold switches.

Tests: gateway-activation-prune-lease suite (mid-dial prune survival on both
doors, lease release, lease expiry); the invalidated-registry-identity pin
updated for the concurrent (read-only) descriptor probe.
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on c69a0c2 — fix(desktop): atomic profile-switch publication v2 — fail op

⚠️ 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 3m34s vs 4m35s (-22.2%). 3 job(s) slower, 16 faster,

  • JS & TS checks / apps/desktop / check:test:ui:shard-2of3: -66.0s
  • JS & TS checks / apps/desktop / check:lint: -55.0s
  • JS & TS checks / apps/shared / check: -36.0s
  • JS & TS checks / ui-tui / check: -34.0s
  • JS & TS checks / apps/bootstrap-installer / check: -34.0s

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/desktop Electron desktop app (apps/desktop/*) labels Aug 19, 2026
@frizikk

frizikk commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

This PR and #89690 address complementary failure modes, and both are needed for the complete release-build fix.

This branch protects a dialing target from the live-work pruner and makes profile/connection publication atomic. However, its current head still pins nanostores@1.4.0. In that version, batch() is marked @__NO_SIDE_EFFECTS__, so the production Rollup build can remove the entire callback containing $activeGatewayProfile.set(...) and setConnection(...).

I reproduced that behavior independently: the target descriptor and authenticated WebSocket connection succeeded, but the release bundle omitted the publication callback and the Desktop remained on the previous connection descriptor.

Therefore #89797 fixes the activation/pruner race, while #89690 is still required to ensure the new batch() publication survives production tree-shaking. Landing only this PR would leave the packaged-app failure unresolved.

@teknium1
teknium1 merged commit 2507bc6 into main Aug 19, 2026
43 checks passed
@teknium1
teknium1 deleted the feat/atomic-profile-switch-v2 branch August 19, 2026 07:03
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/*) P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants