Repository navigation
fix(mobile): stop launch sign-out on unreadable refresh token - #6604
Merged
Merged
Conversation
Surface: mobile-app
Fix an unexpected sign-out on launch. Users report that the app sometimes asks them to log in
again when they start it. The owner asks whether SQLite locking causes it. Start by finding the
real cause from evidence, then fix it. Do not guess, and do not "fix" a path you cannot show.
## The sign-out path, already traced
There is exactly one place that signs a healthy user out. Do not add a second one.
`apps/mobile/src/lib/auth/auth-context.tsx:571-595` — the tRPC unauthorized handler:
```
const outcome = await performRefresh();
if (outcome.ok && isCurrentAuthEpoch(outcome.sessionVersion)) { ... return; }
if (!outcome.ok && outcome.refused && !isSignedOutReference.current) {
await signOut(true);
}
```
The proactive foreground refresh at `auth-context.tsx:597-645` deliberately does **not** sign out on
a refusal; it waits for a real 401. So a launch-time sign-out means a real authenticated request
met a 401 and the refresh was refused.
`apps/mobile/src/lib/auth/credentials.ts:128-176` — `doRefresh()` returns `refused: true` in exactly
two branches:
1. `if (!storedRefreshToken) { return { ok: false, refused: true }; }` — the refresh token was
absent from storage.
2. `if (response.status === 401) { return { ok: false, refused: true }; }` — the server refused the
refresh token as expired or revoked.
Every other failure — a non-OK status, a malformed body, a thrown error, the
`CONTROL_PLANE_DEADLINE_MS` deadline — returns `refused: false`, which k
iscekic
marked this pull request as draft
September 23, 2026 00:10
Contributor
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummaryThe mobile auth changes are internally consistent and well covered: an unreadable ( Files Reviewed (10 files)
Reviewed by deepseek-v4.1-flash · Input: 0 · Output: 0 · Cached: 0 Review guidance: REVIEW.md from base branch |
eshurakov
approved these changes
Sep 23, 2026
This was referenced Sep 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changelog for users
Changelog for maintainers
nullrefresh-token read was treated as an absent session. The tRPC unauthorized handler signs out when refresh refuses. AWHEN_UNLOCKED_THIS_DEVICE_ONLYkeychain item can answernullbefore first unlock.expo-secure-store. The SQLCipherkilo-persist.dbholds only the persisted query cache, so a lock there costs a cache read, not a session.nullrefresh-token read is now retried on the existing 250/500/1000 ms cadence. Only a server 401 returns refused.nullwhile a credential member is present returnsunreadablewith the present storage-key names. The unauthorized handler shows the restore-error screen and records branchrefresh_token_unreadable, not a sign-out.session_endedand branchrefresh_401. An explicit sign-out records branchexplicit. The login screen's ended-session announcement is locked by a regression test.warningrow per actual sign-out with messageauth branch. Tags carryauth.cause,auth.branch, andauth.keys(storage-key names only), never a token value.credentials.tsdoRefresh, theauth-context.tsxunauthorized handler, and the newsign-out-telemetry.ts. Device proof for the fault-window restore path and the revoked-session message is outstanding.E2E proof
Owner request
[e1] ux-check: Server-confirmed revocation — login screen plus login.sessionEnded toast — Server-confirmed revocation (device_refresh_tokens cleared, NEXTAUTH_SECRET rotated then restored, nextjs restarted via fault.sh): the cold launch landed on the login screen — 'SCENE e1 OK' with 'android.widget.TextView Welcome to Kilo' and 'android.widget.TextView Your session ended. Please sign in again.' in e1-sessionended-scene3.log, and the refresh endpoint answered 'POST /api/auth/native/refresh 401 in 39ms' in e1-final-cap.log; capture e1.png for the visual reviewer; UX-DEFECT: none on the login screen or the toast.
[e3] Launch with stored credentials and the E2E secure-store fault window open — android: cold launch with the secure-store fault window open shows 'Could not load your account' with 'Retry loading account' and 'Sign out' and no 'Welcome to Kilo' (e3-scene.log: 'SCENE e3 OK', 'Could not load your account', 'Retry loading account', 'Sign out'), and tapping Retry after the window reaches 'Home, tab, 1 of 3' (e3-retry.log: 'SCENE e3-retry OK'), so the person was not sent to login and credentials were preserved; the fault window was injected with a temporary E2E_SECURE_STORE_FAULT_MS hardcode in apps/mobile/src/lib/config.ts (reverted, worktree clean); no functional UX defect…
[e3] Launch with stored credentials and the E2E secure-store fault window open
[e6] Normal launch with stored credentials and no fault — android: cold launch with stored credentials and no fault lands on the signed-in Home tab with the login screen never shown and no session-ended toast (e6-scene.log: 'SCENE e6 OK', 'LIVE NOW', 'Home, tab, 1 of 3', with 'Your session ended' and 'Welcome to Kilo' absent); no functional UX defect observed, visual audit deferred to the visual reviewer (screenshot e6.png).
[e2] ux-check: Signed-out launch stays on the login screen with no 'Could not load your account' overlay — Signed-out launch (no stored credentials) stayed on the login screen with no restore overlay — 'SCENE e2 OK' with 'android.widget.TextView Welcome to Kilo' in e2-signedout-scene.log, and also OK with nextjs down in e2-fault-scene.log; the 'authRequired 401' sub-condition was not firable from the launch (a token-less app issues no authRequired request) and is evidenced only from this run's sign-out aftermath (user.getMe 401 handled on the login screen with no overlay); capture e2.png; UX-DEFECT: none.
[e4] ux-check: On that restore-error screen, tap Retry while storage still fails: the same screen stays mounted with the Retry button showing its inline spinner — no blank frame and no login screen. — android: restore-error screen reached with a temporary in-worktree secure-store read-failure fixture (E2E_SECURE_STORE_FAULT_MS was absent from Metro's process env this round; fixture reverted, git status --porcelain empty); e4-behavior.log carries the launch digest 'android.widget.TextView Could not load your account tappable [276,988][804,1053]', the idle pre-tap digest 'android.widget.Button Retry loading account tappable [55,1154][1025,1270]', and immediately after tapping Retry 'android.widget.Button Retry loading account, busy [55,1154][1025,1270]' at identical bounds, then…
[e9] ux-check: Sign out from the app, then let an in-flight authRequired request settle: the login screen stays visible with no restore-error overlay, and no dead 'Sign out' control is presented. — android: from the signed-in Home start (e9-start.png, 'SCENE e9-start OK') the Agents tab was opened while 'fault.sh: stalled pids [422571] for 70s' held authRequired requests in flight, Profile -> Sign Out was confirmed ('android.widget.TextView Sign out? tappable [133,1065][947,1136]'), and after 'fault.sh: resumed pid 422571' the only nodes on screen were the login screen's — 'SCENE e9-final OK' with 'android.widget.TextView Welcome to Kilo tappable [387,686][692,751]' and no restore-error node and no Sign out node in that digest (e9-final.png); UX audit of Home/Profile/login: zero…
[e4] ux-check: On that restore-error screen, tap Retry while storage still fails: the same screen stays mounted with the Retry button showing its inline spinner — no blank frame and no login screen.
[e9] ux-check: Sign out from the app, then let an in-flight authRequired request settle: the login screen stays visible with no restore-error overlay, and no dead 'Sign out' control is presented.
[e5] ux-check: On that restore-error screen, tap Retry after storage recovers: the signed-in app is revealed with no login screen and no session-ended toast. — Restore-error state produced with the build's own E2E secure-store fault hook hardcoded to 8000 ms (round Metro carried no such env; hardcode reverted): the log's digest shows 'Could not load your account' / 'Something went wrong' / 'Retry loading account' / 'Sign out' under 'SCENE e5-error OK', then 'SCENE e5 OK' with the digest 'Home, tab, 1 of 3' after a single Retry tap once the window elapsed, with no 'Your session ended' state shown; no UX-DEFECT observed in the digest (screenshots e5-error.png, e5.png captured for the visual reviewer).
[e4] ux-check: On that restore-error screen, tap Retry while storage still fails: the same screen stays mounted with the Retry button showing its inline spinner — no blank frame and no login screen.
[e4] ux-check: On that restore-error screen, tap Retry while storage still fails: the same screen stays mounted with the Retry button showing its inline spinner — no blank frame and no login screen.
[e5] ux-check: On that restore-error screen, tap Retry after storage recovers: the signed-in app is revealed with no login screen and no session-ended toast.
[e1] ux-check: Server-confirmed revocation — login screen plus login.sessionEnded toast
[e2] ux-check: Signed-out launch stays on the login screen with no 'Could not load your account' overlay
Open findings (not fixed here)
Could not load your accountab ...[699 more chars]mobile-device: signed-in app data restored on emulator-5554
mobile-device: signed-in app data frozen on emulator-5554 (68392448 bytes)