fix(desktop): surface profile-switch failures instead of swallowing them - #89621
darkrhodes7 wants to merge 1 commit into
Conversation
|
Heads-up that this and #89609 (mine, opened ~20 minutes earlier) land in the same They are not the same bugA profile switch can fail to happen in two structurally different ways: A. The switch throws. Descriptor lookup rejects, the dial rejects, the IPC is unreachable. The B. The switch declines. if (!activate()) {
return
}That happens when the entry was torn down mid-dial, when a newer activation superseded this one's epoch, or when an eviction re-pointed the active key at the primary. Nothing throws. The IIFE resolves, This PR does not change any return type and does not touch that guard, so path B is still silent after it. #89609 widens the three seams to Concretely: your error atom cannot fire for a declined activation, and my boolean cannot explain a thrown one. Landing only one leaves half of #89586 in place. Your mutex timeout is the piece I deliberately left out
One thing worth checking there: If both landThey are complementary and the merge is small, but it is not automatic - both rewrite the same })().catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error ?? 'unknown error')
console.error(`[profile] gateway switch to "${target}" failed: ${message}`)
$gatewaySwitchError.set({ profile: target, message })
// `published` stays false, so the caller is told the switch did not happen.
})i.e. your body, with my I am happy to rebase #89609 on top of this one once it lands, rather than the other way round, since yours carries the UI surface and the i18n strings and is the more disruptive diff to rebase. Just say which order you want. One correctness note on the timeout
Agreed for |
4cb5381 to
ead6c9c
Compare
The rail's gateway switch used to die silently: every rejection inside ensureGatewayProfile (descriptor lookup, gateway preparation, dial) was caught by an empty .catch(), so a failed switch to another profile published nothing — no log, no UI state, no error. A dead click read as a broken rail, and the real reason (e.g. a failing descriptor IPC) was invisible in both the console and the swap overlay. - Publish failures to a new $gatewaySwitchError atom and log them to the console, so the swap overlay can render 'Couldn't switch to X' instead of silently doing nothing (en/ar/ja/zh/zh-hant keys added). - Bound the gatewaySwitch mutex wait (30s): a wedged in-flight switch (hung descriptor IPC or stuck dial) used to block EVERY later click forever, since each new switch awaited the previous promise. The activation epoch guard makes a stale switch's late publish a no-op, so failing open is safe. Applied to both the profile and agent paths. - Guard the mutex-release finally with identity: when the bounded wait lets a newer switch start while the old one is still wedged, the stale finally no longer nulls out the newer switch's promise or swap target. Tests: extend profile.test.ts — failed switch publishes the error and clears on retry; a wedged in-flight switch no longer blocks a later click.
ead6c9c to
fa62fd7
Compare
|
Closing — the seams this PR hardens were rewritten during the resolution of #89622, and the specific failure it surfaces no longer exists. Sequence on main: the atomic-publish series was reverted (#89785), profile switching was re-landed with fail-open semantics and an activation lease preventing mid-dial socket disposal (#89797), and the underlying release-build breakage turned out to be nanostores 1.4.0's On the current code a switch cannot silently decline (the decline path is gone), and descriptor-lookup failures are logged rather than swallowed. If you see a switch-failure UX gap remaining on latest main — e.g. a user-visible toast rather than a console warning — a fresh PR against the current seams would be welcome. Thanks for the work here; the silent-failure framing was correct and informed the final design. |
Summary
The desktop profile rail's gateway switch used to fail silently. Every rejection inside
ensureGatewayProfile(descriptor lookup, gateway preparation, socket dial) was caught by an empty.catch(), so a failed switch to another profile published nothing — no log, no UI state, no error. A dead click read as a broken rail, and the real reason (e.g. a failing descriptor IPC) was invisible in both the console and the swap overlay. This became visible in the field as "clicking Kuro/Vesper does nothing; only the All view still opens sessions."Changes
$gatewaySwitchErroratom + log to console — theChatSwapOverlaynow renders "Couldn't switch to<profile>—<message>" in place of the spinner when a switch fails, instead of a silent no-op. i18n keys added for en/ar/ja/zh/zh-hant + the sharedtypes.tscontract.gatewaySwitchmutex wait (30s) — a wedged in-flight switch (hung descriptor IPC or stuck dial) previously blocked every later click forever: each new switch awaited the previous promise with no timeout, so one stuck switch bricked the whole rail for the session. The activation epoch guard makes a stale switch's late publish a no-op, so failing open is safe. Applied to both the profile and agent paths.finallywith identity — when the bounded wait lets a newer switch start while the old one is still wedged, the stalefinallyno longer nulls out the newer switch's promise or swap target.Test Plan
apps/desktop: 44 tests pass acrossprofile.test.ts,gateway-switch,gateway-profile-request,profile-agent-activation,chat/index,i18n/languagestsc --noEmitcleanelectron/*(ssh, hardening, darwin staging) are Windows-host environment issues unrelated to this change — verified they reproduce on the base tree.Notes