fix(web): await the platform-session probe before the onboarding hosting/welcome fork#33201
Conversation
…ing/welcome fork The local-mode auth middleware chose between the onboarding hosting and welcome screens off a bare `hasPlatformSession`. That flag is set by a fire-and-forget platform-session probe (the local gateway auth paths return before the session is known), so when the middleware runs during the probe window it reads an ambiguous `false` and sends a returning platform user -- one with zero local assistants -- to the new-user welcome flow instead of the hosting picker. Wait for `platformSessionResolved` before forking, mirroring the `waitForAuthReady` gate already used for `isLoading` and the prechat funnel fix (#33123) that introduced the resolved flag for exactly this race.
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
✦ APPROVE
Value: Returning platform users in local mode reliably land on the hosting picker instead of the new-user welcome flow — closes the same ambiguous-false race as #33123 at the auth-middleware seam that was missed by that PR.
What this does: Adds a waitForPlatformSessionResolved() gate before the hosting-vs-welcome fork in auth-middleware.ts, mirroring the existing waitForAuthReady() pattern in the same file. Reads hasPlatformSession only after the probe has settled.
Analysis
- Race shape matches #33123 exactly — local gateway
initSessionreturns before the fire-and-forgetprobePlatformSessionsettles, so any consumer readinghasPlatformSessionin that window sees an ambiguousfalse. #33123 introducedplatformSessionResolvedas the tri-state truth signal and gated the prechat funnel on it; this PR closes the missed sibling reader atauth-middleware.ts:48. - Boundedness —
probePlatformSessionflipsplatformSessionResolved=truein afinally(verified in #33123auth-store.ts), so the wait can't hang. In the no-platform-assistant local pathinitSessionflips it synchronously, so no added latency there either. - Subscribe-then-sync-check pattern is the canonical Zustand idiom (matches
waitForAuthReady4 lines above). Subscribe first, then read state synchronously to handle the already-resolved case. Zustand doesn't fire subscribers on initial subscribe, so no double-resolve risk. - Test coverage targets the exact race — case 1 asserts the middleware does NOT decide while
platformSessionResolved=false, then verifies it routes tohostingonce the probe settles with a live session. Would fail against pre-fix code (which would have immediately thrown welcome redirect). - Anti-pattern grep on diff: zero production
ascasts, zero!in production code (settled!is test-only, guarded by adjacentexpect(settled).not.toBeNull()), zero@ts-ignore, zeroeslint-disable. Three test-scaffolding casts (as unknown as Parameters<...>,as AuthUserfixture) are conventional. - Cross-platform impact — renderer-internal routing logic, no wire/IPC surface change, safe across macOS/iOS/web.
Vellum Constitution — Trust-seeking: the asymmetry between two hasPlatformSession readers (one gated on resolved, one not) was the kind of silent contract drift that erodes trust — this PR makes the contract uniform.
Prompt / plan
While investigating "login doesn't work in the local Electron app," I traced the local-mode auth bootstrap end-to-end and found a concrete, code-provable race in the auth middleware's onboarding routing. This PR fixes that race. (No live macOS repro — this is a code-level root-cause fix verified by a unit test.)
What was broken
In local mode with no assistants yet, the auth middleware forks the user to either the onboarding hosting picker or the new-user welcome flow based on
hasPlatformSession:But
hasPlatformSessionis set by a fire-and-forget platform-session probe (probePlatformSession) — the local gateway auth paths ininitSessionreturn control before the probe settles. When the middleware runs inside that window,hasPlatformSessionreads an ambiguousfalse, so a returning platform user is sent to the new-user welcome flow instead of the hosting picker.This is the same "ambiguous false" race that
platformSessionResolvedwas introduced to fix in the prechat onboarding funnel (#33123) — but that gate was never applied toauth-middleware.ts. The store distinguishes "no session" from "not resolved yet" viaplatformSessionResolved; the middleware ignored it.The fix
Wait for
platformSessionResolvedbefore readinghasPlatformSession, mirroring the existingwaitForAuthReady()gate already used forisLoading. The probe always settles (probePlatformSessionflips the flag in afinally), so the wait is bounded. In the no-platform-assistant local pathinitSessionsetsplatformSessionResolved: truesynchronously, so there is no added latency there.Test plan
apps/web/src/lib/auth/auth-middleware.test.ts(3 cases):platformSessionResolved: false), the middleware does not decide yet; once the probe settles with a live session it redirects tohosting(this fails against the old code, which would have redirected towelcomeimmediately).welcome.hosting.bun test src/lib/auth/auth-middleware.test.ts→ 3 pass.bun run lintandbunx tsc --noEmit(with regenerated API client, via the pre-commit hook) → clean.Root cause analysis
platformSessionResolvedwas added in fix(web): gate onboarding Google step on a live platform session, not a cached id #33123 to fix the exact ambiguous-falserace in the prechat funnel. The fix was applied at the funnel call site but not toauth-middleware.ts, which makes the same hosting-vs-welcome decision off the same flag. The middleware predates the resolved-flag concept and was never revisited.hasPlatformSessionmaking routing decisions, only one of them gated onplatformSessionResolved. The asymmetry was the tell.hasPlatformSessionfor a navigation/routing decision must first ensureplatformSessionResolvedis true (or treat unresolved as unknown). Both readers now do.probePlatformSession(which already documents the resolved-flag semantics) and this fix, than by a project-wide rule that would risk going stale.Notes
apps/webis a bundler package (moduleResolution: "Bundler"), so the test omits.jsimport extensions per repo convention.Link to Devin session: https://app.devin.ai/sessions/15bca57bd4c64a3085cfb80e1f26355a
Requested by: @ashleeradka