fix(mobile): harden offline and query integrity flows (w6c) - #5416
Open
iscekic wants to merge 14 commits into
Open
fix(mobile): harden offline and query integrity flows (w6c)#5416iscekic wants to merge 14 commits into
iscekic wants to merge 14 commits into
Conversation
Add an additive two-edge reconnect-exhaustion signal so mobile can show an explicit recovery action instead of an indefinite Reconnecting label. The auth-failure stop path fires the same terminal edge.
Render Connection lost with a Retry action when the user-web transport exhausts reconnect attempts, instead of an indefinite Reconnecting label.
The settings overview now renders a skeleton, a permissions error with retry, or the offline variant instead of permission-denied copy when the org-role query is unresolved or paused.
Add a per-key generation guard so a failing older mutation cannot roll back cache state a newer mutation owns, and serialize same-entity mutations with scope ids for security, org, and model-preferences hooks.
…tations Add the generation guard and the named network-order mechanisms to the remaining mutation hooks: session list, code-reviewer, PR discussion threads, and push-token/notification preferences.
Add useRouteForegroundRefresh to invalidate route query keys on the focused AppState foreground transition and on focus regain after the first. Disable the blanket refetchOnWindowFocus default so frozen background tabs no longer refetch.
Mount useRouteForegroundRefresh at the app-wide, home-tab, profile-tab, and pushed-route surfaces from plan section 10. Each path-prefix key uses the nested tRPC form so invalidation prefix-matches tRPC v11 keys. Remove the profile credits card's bespoke focus invalidation, now subsumed by the profile-tab owner. Add a real-QueryClient key-matching test.
Gate the Agents list AppState foreground listener on the tab's focus state so a frozen background tab no longer refetches. The focused tab refreshes the stored list and invalidates the active-sessions tray. Add a mounted test covering focused, unfocused, and blur-after-mount foreground transitions.
Extend the offline banner, query-client lifecycle, and mutation outbox tests with EV-03-style chaos scenarios: NetInfo flapping drives onlineManager and the banner debounce correctly, and an outbox row survives a simulated relaunch with its operationKey reused.
Contributor
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (5 files)
Previous Review Summaries (2 snapshots, latest commit 0010a39)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 0010a39)Status: 3 Issues Found | Recommendation: Address before merge Fix these issues in Kilo Cloud Overview
Issue Details (click to expand)CRITICAL
WARNING
Files Reviewed (56 files)
Previous reviewStatus: 2 Issues Found | Recommendation: Address before merge Fix these issues in Kilo Cloud Overview
Issue Details (click to expand)CRITICAL
WARNING
Files Reviewed (57 files)
Reviewed by grok-4.6 · Input: 103.8K · Output: 13.3K · Cached: 288.6K Review guidance: REVIEW.md from base branch |
Thread the reconnect attempt through refreshAndConnect so reconnects that refresh auth before connecting no longer reset the attempt counter to zero. Live user-web sessions now reach the exhaustion cap and fire the two-edge signal, instead of showing Reconnecting forever.
Stamp the generation inside snapshotAndUpdate after cancelQueries resolves, so the stamp order matches the write order and an older mutation cannot clobber a newer list write.
Read navigation.isFocused() in the AppState callback instead of a focusedRef that a frozen tab never updates. A frozen unfocused Agents tab no longer refetches or invalidates the active-sessions tray on foreground.
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.
Summary
A live session that stops reconnecting now shows "Connection lost" with a Retry button, so the user can recover instead of watching "Reconnecting…" forever.
The Security Agent settings screen now shows a load error with a Retry button when it cannot reach the service, instead of telling an owner that only owners can manage the agent.
The SDK connection gains an additive reconnect-exhaustion contract: an optional
maxReconnectAttemptscap, a two-edgeonReconnectExhaustionChangecallback, and aretryReconnectmethod. Exhaustion sets when the retry cap is reached or a terminal auth failure stops retries, and clears on every recovery path;refreshAndConnectnow threads the attempt through so a live session that refreshes auth on reconnect still reaches the cap. All additions are optional, so web and extension consumers compile and behave unchanged.Files
packages/cloud-agent-sdk/src/base-connection.ts— addsmaxReconnectAttempts,onReconnectExhaustionChange, andretryReconnect; sets exhaustion at the retry cap and on a terminal auth failure, clears it on every recovery path, and threads the reconnect attempt throughrefreshAndConnect.packages/cloud-agent-sdk/src/user-web-connection.ts— forwardsmaxReconnectAttempts, mirrors the exhaustion snapshot, exposesisReconnectExhausted,onReconnectExhaustionChange, andretryConnection, and resets the snapshot when the connection stops.The session connection indicator gains an explicit
exhaustedstate: a remote session whose reconnects are exhausted shows "Connection lost" with a Retry action instead of "Reconnecting…" forever. The state resolver consumes the exhaustion signal, and a newuseUserWebConnectionHealthhook binds readiness and exhaustion reactively through the shared connection.Files
apps/mobile/src/components/agents/session-connection-indicator-state.ts— adds theexhaustedstate and thereconnectExhaustedinput.apps/mobile/src/components/agents/session-connection-indicator.tsx— renders the "Connection lost" label and a Retry pressable that callsretryConnection, with separate accessibility elements.apps/mobile/src/lib/hooks/use-user-web-connection-state.ts— addsuseUserWebConnectionHealthbinding both readiness and exhaustion.useSecurityAgentCapabilityreturns a discriminatedstatus(loading | error | denied | allowed) instead of a bare canManage boolean, so callers can tell a failed or pending permission load from a resolved denial. The settings overview screen renders distinct loading, error, offline, and denied states, so an owner never sees permission-denied copy on a network error or an offline launch. A settled role stays authoritative: a failed background refetch does not demote an already-resolved capability to error.Files
apps/mobile/src/lib/hooks/use-security-agent.ts— adds the discriminated capability status and exposeshasDataandisPendingfrom the role query.apps/mobile/src/components/security-agent/settings-overview-screen.tsx— branches on capability status and committed connectivity for the offline and permission-error screens.The offline banner store now exposes a committed
BannerStatetri-state (online, offline, unknown) and notifies on every committed change, so screens can gate on a settled connectivity state. The hook layer keeps one module-level store for the whole app, so every consumer shares a single NetInfo subscription, and adds auseCommittedConnectivityStatushook.Files
apps/mobile/src/lib/offline-banner-state.ts— exportsBannerState, adds thestatesnapshot, and notifies on every committed change.apps/mobile/src/lib/hooks/use-offline-banner-state.ts— replaces the per-mount store lifecycle with one lazy shared store and addsuseCommittedConnectivityStatus.Every optimistic mutation now stamps its write with a per-cache generation from
nextMutationGenerationand rolls back only whenisLatestMutationGenerationconfirms it is still the latest, so an older failure cannot stomp cache state a newer mutation owns. Mutations that share one cache serialize their network calls through ascope.idor achainSave, and each changed mutation declares its onError policy in a comment. The accepted residual is that when two writes both fail, the older optimistic value can show until the settle invalidation refetches.Files
apps/mobile/src/lib/hooks/mutation-generations.ts— new pure per-key generation counter and latest-generation check.apps/mobile/src/lib/hooks/use-code-reviewer.ts— stamps toggle and save rollbacks with generations and chains both onto the shared scope+platform save key.apps/mobile/src/lib/hooks/use-model-preferences.ts— stamps favorite rollbacks with generations and chains favorite writes on one key.apps/mobile/src/lib/hooks/use-organization-mutations.ts— stamps member, rename, and balance rollbacks with generations and serializes org writes with a scope id.apps/mobile/src/lib/hooks/use-security-agent-mutations.ts— stamps config save and toggle rollbacks with generations and serializes them with a scope id.apps/mobile/src/lib/hooks/use-security-remediation.ts— stamps remediation-cancel rollbacks with generations.apps/mobile/src/lib/hooks/use-session-mutations.ts— stamps session delete and rename rollbacks with generations after the cancel, and chains both per session.apps/mobile/src/lib/pr-review/discussion/use-review-discussion-mutations.ts— stamps thread resolve, unresolve, and reaction rollbacks with a shared generation key, chains resolve and unresolve per thread, and scopes reactions per thread.apps/mobile/src/components/notifications-screen.tsx— stamps push-token and preference rollbacks with generations and serializes both with static scope ids.The global query client sets
refetchOnWindowFocusto false, and a newuseRouteForegroundRefreshhook invalidates only the active route's query prefixes on foreground and focus regain. Owner mounts across layouts refresh each route's domain, and the root layout keeps app-wide freshness for the signed-in user, organizations, and kilo-chat. The Agents list reads focus live vianavigation.isFocused()so a frozen unfocused tab does not refetch on foreground.Files
apps/mobile/src/lib/query-client.ts— setsrefetchOnWindowFocus: false.apps/mobile/src/lib/hooks/use-route-foreground-refresh.ts— new hook invalidating path-prefix keys on app-active and focus regain.apps/mobile/src/app/(app)/_layout.tsx— adds the app-wide freshness mount.apps/mobile/src/app/(app)/(tabs)/(0_home)/_layout.tsx— refreshes the active-sessions domain.apps/mobile/src/app/(app)/(tabs)/(3_profile)/_layout.tsx— refreshes the user, organizations, reviewer, security-agent, and kilo-pass domains.apps/mobile/src/app/(app)/pr-review/[owner]/[repo]/[number]/_layout.tsx— refreshes the PR-review domain.apps/mobile/src/app/(app)/pr-review/index.tsx— refreshes the PR-review domain.apps/mobile/src/app/(app)/agent-chat/[session-id].tsx— refreshes the session and model-preference domains.apps/mobile/src/app/(app)/agent-chat/model-picker.tsx— refreshes the model-preference domain.apps/mobile/src/app/(app)/device-sessions.tsx— refreshes the user domain.apps/mobile/src/app/(app)/kilo-pass.tsx— refreshes the kilo-pass domain.apps/mobile/src/components/agents/session-list-screen.tsx— gates the foreground refetch on tab focus and invalidates the active-sessions tray.The credits card's own focus-based refetch is removed because the profile route mount now owns that refresh.
Files
apps/mobile/src/components/profile-credits-card.tsx— removes the focus-based refetch.Tests: 25 test files updated — 20 in the mobile app and 5 in the SDK, adding chaos scenarios for offline flapping, process kill with persisted state, cold-launch offline, hibernation zombie-socket replacement, reconnect exhaustion, and mutation interleave.
Generated: none.
Verification
iOS ran three cases in one round; a fourth case was not run.
The round reproduced no defect on the unfixed build; all three cases passed on the first run.
No recording exists.
Human steps: none.
Visual Changes
Agents tab (session list) after cold-launch restore
After a cold relaunch, the user sees the Agents tab with its session list restored from the cache as a settled empty state. The
Agentsheader at the top and theNo sessions yetblock in the middle of the screen show the restored list.Agents tab after foreground return
When the app returns to the foreground, the Agents tab refreshes and shows no
Connection lostorReconnecting…label. The empty session list fills the app window, and no connection label appears on the screen. The capture catches the foreground transition, but the screen content stays readable.Security Agent settings (personal scope)
The owner sees a Settings screen with the Security Agent row and its switch, and no owner-denial copy. The
Security Agentrow and its switch sit under theSettingsheader, above the repository hint text.Reviewer Notes
Notes: none.