Skip to content

perf(mobile): cut foreground refetch bursts in the mobile data layer - #6640

Merged
iscekic merged 14 commits into
mainfrom
kwf/mobile-perf-data-layer-bea9
Sep 28, 2026
Merged

iscekic merged 14 commits into
mainfrom
kwf/mobile-perf-data-layer-bea9

Conversation

@iscekic

@iscekic iscekic commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Changelog for users

  • Returning to the app issues fewer requests, so the screen you return to is less likely to wait behind a request burst.
  • The session history keeps the rows you were reading while it refreshes its first page.
  • While a review is running, the reviews list refreshes only its newest page and keeps its newest rows when you page far back.
  • A review added while you are reading no longer skips the row that moves between the first and second page.
  • A session history that opens without a network connection shows its retryable error instead of cached rows as loaded.
  • The session list can load older sessions again after it shrinks to fit the screen.
  • The Kilo Pass card refreshes with the profile screen instead of on its own while the tab is not visible.
  • The session history leaves a small gap between the refresh status line and the first row.

Changelog for maintainers

Review first: the new default staleTime, the session page-one reconcile, and the review-list paging cap change when network work starts; the new mounted tests pin the one-listener and per-edge request contracts.

  • src/lib/query-client.ts:225 — accepted. New queries default to a 30 s staleTime; refetchOnWindowFocus stays false and call sites that pass staleTime: 0 still override.
  • src/lib/hooks/use-screen-tracking.ts:65 — accepted. The 500 ms generation interval and its exported constant are gone; the hook subscribes to the authenticated-owner store and reads currentGeneration() on that change.
  • src/lib/hooks/use-screen-tracking.ts:50-56 — accepted. The hook keeps only the derived stale boolean and compares it in a ref before dispatching, so an unchanged flag never re-renders the root layout.
  • src/app/_layout.tsx:1080-1094 — accepted. The root layout drops its private AppState listener for the temp-file reap and runs the reap on the shared useAppLifecycle background-to-active edge; the cold-start reap and deferred scheduleCacheMaintenance stay.
  • src/app/(app)/_layout.tsx:41-57 — accepted. The logout and push mounts merge into one ForegroundReconciliationMount on the shared store, running both in one ordered pass (logout then push); the push-token rotation subscription stays.
  • src/app/(app)/_layout.tsx:73-90 — accepted in part. No extra minimum interval: both attempts are already single-flight with 60 s spacing, and push reconciliation skips when the stored token, locale and app version already match the device.
  • src/app/(app)/agent-chat/[session-id].tsx:100-101 — accepted. The foreground/focus refresh now invalidates only the opened session's cliSessionsV2.get key and modelPreferences, not the whole cliSessionsV2 prefix, so the stored list pages behind the route are untouched.
  • src/app/(app)/agent-chat/[session-id].tsx:96-99 — no change required, as the finding states; the openStart ref stays.
  • src/components/kilo-pass/kilo-pass-subscription-card.tsx:90 — accepted. The card's own AppState listener and refetch refs are deleted; the profile layout's useRouteForegroundRefresh([[['kiloPass']]]) owns the refresh. A mounted test fails if a listener returns.
  • src/lib/hooks/use-agent-sessions.ts:150 — accepted. refetchOnWindowFocus now defaults to false; the hook owns the app-foreground edge and reconciles page one (reconcileFirstPage plus refetchQueries with cancelRefetch: false). Pull-to-refresh, retry and the Share Gate reconcile too, and the render hold now spans isPaused/isError so the list never blanks.
  • src/lib/hooks/use-code-reviews.ts:57 — accepted. The list builder applies withInfiniteRetention(..., 10); the 5 s in-flight poll moves off the infinite query to a page-one probe (offset 0, staleTime: 0) merged back with mergeReviewFirstPage.
  • src/lib/hooks/use-organization-queries.ts:157 and :186 — accepted. The credit-transactions and invoices builders are extracted and both wrapped in withInfiniteRetention(INFINITE_QUERY_MAX_PAGES).
  • apps/mobile/src/lib/hooks/use-agent-sessions.ts:471 — accepted. storedFetchedSinceMount now latches true only when dataUpdatedAt advanced and at least one page was delivered, so a reconcileFirstPage reset no longer counts as a delivery and a failed fresh open keeps the retryable full-screen error.
  • apps/mobile/src/lib/hooks/use-code-reviews.ts:105 — accepted. getNextPageParam refuses the next page once pages.length reaches REVIEW_LIST_MAX_PAGES, so the newest-first list keeps page one and maxPages never evicts it.
  • apps/mobile/src/lib/hooks/use-code-reviews.ts:159 — accepted. selectReviewFirstPageAction compares the probe's page one with the cache; a changed row set with more than one retained page refetches the retained pages at their offsets (cancelRefetch: false) instead of merging, so no row is skipped at the boundary.
  • apps/mobile/src/components/agents/session-list-content.tsx:259 — accepted. onTouchMove releases the pagination park alongside onScrollBeginDrag, so a list that fits the viewport and never emits a drag can still re-open pagination.
  • Tests: mounted tests pin the session fresh-open flag and the touch-move park release; unit tests pin the forward-paging bound and selectReviewFirstPageAction.

One more change rides in this diff beyond the findings list: the artifact mirror defers its first and foreground runs by MIRROR_BURST_SETTLE_MS (5 s) so its session-list read stays out of the launch and foreground bursts.

Proof note: the new p2 boundary check replaces the earlier offset-0 review-list poll excerpt, which no longer describes the changed merge path; the request-count lines below stand.

E2E proof

p8-burst.log:72  POST /api/trpc/organizations.list,kiloclaw.listAllInstances,kiloChat.getToken?batch=1 200 in 40ms
p8-burst.log:86  POST /api/trpc/cliSessionsV2.list?batch=1 200 in 28ms
p9-burst.log:12  POST /api/trpc/organizations.list,cliSessionsV2.list?batch=1 200 in 76ms
p11-foreground.log:25  POST /api/trpc/organizations.list,user.getNotificationPreferences,user.getAuthProviders,user.getContextBalance,user.getCreditBlocks,organizations.list,kiloPass.getPurchasePresentation,kiloPass.getState?batch=1 200 in 102ms

p2-after-top.png

e14-list-immediate-return.png

e5.png

e14.png

e11-home-after-foreground.png

e1-agents-live.png

e13-multi-foreground.png

e14-session.png

e3-offline.png

e4-after-foreground.png

e17-fg-settled.png

p1-access-denied.png

p9-foreground.png

e3-unavailable.png

e3-retry-recovered.png

p5-error-state.png

p8.png

p5.png

p10-session-detail.png

p3-offline.png

e2.png

p10.png

p8-home-after-foreground.png

p9-empty-history.png

p11-loading.png

p2.png

p3-after-foreground.png

p8-agents.png

p9-empty-foreground.png

p9-history-foreground.png

p9-history-top.png

p11-after-foreground.png

p11.png

p10.png

Owner request

Surface: mobile-app

Fix the mobile performance audit findings below. The audit is static: every finding was confirmed by reading the source, not by profiling. Confirm each finding in the current tree before you change it. If a finding is already fixed or no longer applies, say so and skip it; do not invent work.

Audit revision: the report was written against apps/mobile at commit 3fbc18a3b2 on main (2026-09-22). The worktree is newer. Re-locate every cited line by content, never by line number.

Constraints:

  • Change product behaviour only where a finding asks for it. A performance fix must not change what the user sees, except where the finding says the perceived performance improves.
  • No new dependency, and no new test-only runtime support, unless the owner allowed it.
  • Keep the existing test suites green: pnpm --filter @kilocode/mobile test, plus the mounted suite the section runbook names.
  • Proof: quote the decisive log lines from the run in the scenario note. Add a screenshot when the change has a visible surface. A performance fix with no visible surface is proved from a log line that shows the reduced work, for example a request count, a fetch count, or a parse count.

This item: mobile-perf-data-layer

Fixes

Fix 1. Set a default staleTime on the query client

  • Severity S3 · Checks P5 · Effort S, with care
  • Files src/lib/query-client.ts
  • Impact With staleTime at 0, every foreground transition, focus regain, and invalidation refetches a full payload. This multiplies the cost of the 29 foreground subscriptions and the route-level invalidations across the app.
  • Detail G7 and the systemic note in every area summary

Fix 2. Stop polling for the telemetry account generation every 500 ms, and stop mirroring the navigation state tree

  • Severity S2 · Checks P4, P1 · Effort S
  • Files src/lib/hooks/use-screen-tracking.ts, src/app/_layout.tsx
  • Impact A 500 ms interval runs from launch to kill because the hook is mounted by the root layout, to detect a change that only happens on sign-in and sign-out. The same hook also re-renders the root layout on every navigation event, because it mirrors the whole navigation state tree in useState and only reads one boolean from it.
  • Detail G7, findings for use-screen-tracking.ts

Fix 3. Give foreground work one owner and one minimum interval

  • Severity S3 · Checks P5, U3 · Effort M
  • Files src/app/_layout.tsx, src/app/(app)/_layout.tsx, src/app/(app)/agent-chat/[session-id].tsx, src/app/(app)/agent-chat/instance-picker.tsx, src/components/kilo-pass/kilo-pass-subscription-card.tsx
  • Impact 29 independent AppState subscriptions fire on a foreground transition. Several start network work and several invalidate queries, so returning to the app produces a request burst that competes with the refetch the user is waiting for. The instance picker polls every 10 seconds and also refetches on focus, so a foreground return fetches twice. The Kilo Pass card refetches on foreground from its own listener while the profile layout invalidates the same keys, and the card's listener ignores focus.
  • Detail G4, findings for (app)/_layout.tsx and _layout.tsx; G4 and G5, foreground findings

Fix 4. Reconcile the first session-history page on foreground instead of refetching every retained page

  • Severity S2 · Checks P5 · Effort S
  • Files src/lib/hooks/use-agent-sessions.ts
  • Impact The stored-sessions infinite query retains up to 20 pages of 30 sessions and refetches on focus. An app foreground can therefore re-issue up to 20 requests and parse 600 rows exactly while the app is painting its first frame. reconcileFirstPage already exists and is used elsewhere.
  • Detail G7, finding for use-agent-sessions.ts

Fix 9. Apply the infinite-query retention contract everywhere it is missing, and keep polls off infinite queries

  • Severity S2 · Checks P5 · Effort S
  • Files src/lib/hooks/use-code-reviews.ts, src/lib/hooks/use-organization-queries.ts
  • Impact While a review runs, the list polls every 5 seconds, and a refetch of an infinite query re-requests every retained page. With 10 pages loaded that is 10 requests and 500 parsed reviews every 5 seconds. The list builder is the one in this area that never applies withInfiniteRetention. Both lists keep every page they load for the life of the screen, with no maxPages. No jank today because both load on an explicit tap, but the bound is missing from the one area whose own documentation states it.
  • Detail G7, finding for use-code-reviews.ts

Fix 15. Narrow the foreground invalidation of the whole cliSessionsV2 prefix

  • Severity S3 · Checks P5 · Effort S
  • Files src/app/(app)/agent-chat/[session-id].tsx
  • Impact A foreground transition or focus regain invalidates every mounted cliSessionsV2 query, which includes the session-list pages behind the pushed route. With no default staleTime, each one refetches.
  • Detail G4, finding for agent-chat/[session-id].tsx

Files this item owns

This item owns exactly the files listed below. Every other audited file belongs to a different workflow item; do not edit a file outside this list.

  • src/lib/query-client.ts
  • src/lib/hooks/use-screen-tracking.ts
  • src/app/_layout.tsx
  • src/app/(app)/_layout.tsx
  • src/app/(app)/agent-chat/[session-id].tsx
  • src/components/kilo-pass/kilo-pass-subscription-card.tsx
  • src/lib/hooks/use-agent-sessions.ts
  • src/lib/hooks/use-code-reviews.ts
  • src/lib/hooks/use-organization-queries.ts

Note: src/app/(app)/agent-chat/instance-picker.tsx is named in fix 3 but belongs to the item mobile-perf-list-virtualization, which applies the poll and focus part of fix 3 with its own list change. Do not edit that file.

Findings in the source

Each bullet is the audited evidence for one file. Read the file, confirm the finding, then apply the fix.

src/lib/query-client.ts

  • [S3] [P5] src/lib/query-client.ts:225 — the query client sets no default staleTime, so every useQuery without an explicit one is stale on mount and refetches on focus. The audit states this once for the whole app (G7, G8, and every area summary), and fix 1 is that note.
    Impact: with staleTime at 0, every foreground transition, focus regain, and invalidation refetches a full payload. This multiplies the cost of the 29 AppState subscriptions and of the route-level invalidations across the app.
    Fix: set a default staleTime on the client. Set it with care: the persisted cache hydrates on launch and screens paint cached rows before the network answers (U4), so a non-zero default keeps that first paint and removes the immediate duplicate refetch. Keep every explicit invalidation path working, and keep the disabled blanket refetchOnWindowFocus (query-client.ts:227) as it is.
    Backend counterpart (section 11.3): cliSessionsV2.list, cliSessionsV2.getWithRuntimeState, and the agentProfiles procedures can gain an updatedAt or version/ETag so the client revalidates cheaply instead of refetching every list page on each foreground transition.

src/lib/hooks/use-screen-tracking.ts

  • [S3] [P4] src/lib/hooks/use-screen-tracking.ts:65 — setInterval(..., SCREEN_TRACKING_GENERATION_POLL_MS) runs at 500 ms for as long as the process lives, because the hook is called from the root layout (src/app/_layout.tsx:594). The only edge it detects is a change of the telemetry account generation, which happens on sign-in/sign-out and not otherwise.
    Impact: two JS-timer wakeups per second, forever, from launch to kill — battery and JS-thread wakeups that the user never sees the benefit of, on every screen including the ones that do not render anything.
    Fix: expose a subscription from src/lib/telemetry/controller.ts next to currentGeneration() (the auth epoch / sign-out store already publishes the same transition) and subscribe to it; failing that, drop the poll to a much longer interval and pause it while the app is backgrounded via the existing useAppStateActive store.
  • [S3] [P1] src/lib/hooks/use-screen-tracking.ts:50-56 — the navigationRef state listener calls setNavState(navigationRef.current?.getRootState()) on every navigation state event. getRootState() returns a fresh object, so useState always sees a new reference and re-renders, even though the render path only reads navState?.stale === false (:58).
    Impact: one extra render of the root layout — and of the navigator JSX it creates — per navigation event, on top of the render the navigation itself causes; the mount effect adds another one because update() runs immediately.
    Fix: keep only the derived boolean in state and bail out on an unchanged value, e.g. capture const next = navigationRef.current?.getRootState() and setNavState(previous => previous?.stale === next?.stale ? previous : next), or read stale from the ref on demand instead of mirroring the whole state tree.

src/app/_layout.tsx

  • [S3] [P5] src/app/_layout.tsx:1080-1094 — the root layout owns the app's only global foreground hook, and it handles cache maintenance only. Every other foreground concern subscribes on its own elsewhere (29 subscriptions across src/**).
    Impact: no place can answer "what does a foreground transition cost", so the cost grows silently with each new feature.
    Fix: make this listener the single fan-out point and move the other subscriptions onto it (or onto a small lib/foreground.ts owner). Keep the deferred scheduleCacheMaintenance behaviour.

src/app/(app)/_layout.tsx

  • [S3] [P5] src/app/(app)/_layout.tsx:41-57 and :73-89 — two independent AppState listeners start network work on every foreground transition (attemptLogoutReconciliation, attemptPushRegistrationReconciliation), and both run alongside the query-invalidation hooks mounted in the same tree (useUnreadCountsInvalidation, useSecurityLifecycleInvalidation, useRouteForegroundRefresh, useForceUpdate).
    Impact: returning to the app fires a burst of parallel requests and cache invalidations. On a slow network the burst competes with the refetch the user is actually waiting for, and each invalidation re-renders the screens observing those keys.
    Fix: give foreground work one owner. A single AppState subscription that runs a small ordered queue (cheap local work first, network last, one minimum interval between passes) removes the burst without changing any individual behaviour.
  • [S4] [P5] src/app/(app)/_layout.tsx:73-90 — push registration reconciliation runs on every foreground with no minimum interval and no "already registered" short-circuit visible at this layer.
    Impact: a native registration round trip per foreground on a device that already has a token.
    Fix: keep the listener but gate it behind the same queue as above, and skip when the stored token and the native token already match.

src/app/(app)/agent-chat/[session-id].tsx

  • [S3] [P5] src/app/(app)/agent-chat/[session-id].tsx:100-101 — useRouteForegroundRefresh([[['cliSessionsV2']], [['modelPreferences']]]) invalidates the whole cliSessionsV2 procedure prefix on every foreground transition and on every focus regain after the first. With no default staleTime, each invalidation refetches every mounted cliSessionsV2 query, which includes the session-list pages behind the pushed route.
    Impact: opening a session and returning to the list can refetch the list, the metadata, and the runtime state in the same frame.
    Fix: narrow the invalidated key to the opened session's query key, or let SessionDetailContent own the metadata refresh (it already refetches getWithRuntimeState on focus).
  • [S4] [P5] src/app/(app)/agent-chat/[session-id].tsx:96-99 — the open clock (openStart) is a useRef mutated during render when the session id changes.
    Impact: none today; the mutation is idempotent per id.
    Fix: none required.

src/components/kilo-pass/kilo-pass-subscription-card.tsx

  • [S3] [P4] src/components/kilo-pass/kilo-pass-subscription-card.tsx:90 — the card registers its own AppState listener that refetches getPurchasePresentation and getState on every active transition, duplicating the route-level mechanism this app already standardized on: (3_profile)/_layout.tsx:10 invalidates the [['kiloPass']] prefix through useRouteForegroundRefresh, which is the documented replacement for the disabled blanket refetchOnWindowFocus (query-client.ts:227).
    Impact: each foreground/background cycle pays two extra server requests for data the route-level invalidate is already refreshing, and because the listener never checks focus it also fires while the profile tab is blurred (a frozen tab still runs its subscriptions), refetching for a screen the user is not looking at.
    Fix: delete the card's listener and rely on the profile layout's useRouteForegroundRefresh([[['kiloPass']]]); if a card-scoped refresh is genuinely needed, route it through useRouteForegroundRefresh so it is focus-aware and de-duplicated by key.

src/lib/hooks/use-agent-sessions.ts

  • [S3] [P5] src/lib/hooks/use-agent-sessions.ts:150 — refetchOnWindowFocus defaults to true on the stored-sessions infinite query, and SESSION_HISTORY_MAX_PAGES (:128, applied at :140) is 20. React Query re-requests every page retained in the cache on a refetch, so an OS foreground (Home and the Share Gate take the default) re-issues up to 20 cliSessionsV2.list requests of 30 sessions each; the Agents screen opts out of the native focus refetch but its own AppState/use-route-foreground-refresh invalidation drives the same full refetch.
    Impact: up to 20 sequential requests and 600 rows of JSON per app foreground once the user has scrolled the history — a burst of network, parsing and cache writes that lands exactly when the app is trying to paint its first frame.
    Fix: make the foreground path reconcile instead of refetch: reconcileFirstPage(queryClient, trpc.cliSessionsV2.list.pathFilter().queryKey) (already used by the departure trigger and the mutation settle) drops the cached pages and refetches page one, which is what a foreground user needs; the older pages can be re-fetched on demand when the user scrolls back.

src/lib/hooks/use-code-reviews.ts

  • [S3] [P5] src/lib/hooks/use-code-reviews.ts:57 — the review-list infinite query polls with refetchInterval every 5 s while any review on page one is in flight, and buildReviewListQueryOptions (:27-64) is the only list builder in this area that does not apply withInfiniteRetention. A refetch() of an infinite query re-requests every page still in the cache, and maxPages is what bounds that set (the reasoning is written out in use-agent-sessions.ts:109-126 and applied in use-security-findings.ts:42).
    Impact: while a review runs, the list screen re-requests one page per page the user has scrolled — 10 loaded pages means 10 requests every 5 s, each 50 reviews parsed on the JS thread — and the loaded pages stay in memory for the session.
    Fix: wrap the options in withInfiniteRetention(...) (a page count sized to the browsable list, as the session list does), and keep the in-flight poll off the infinite query: poll a separate single-page useQuery for the status, or poll page one only, so the 5 s interval does not fan out over retained pages.

src/lib/hooks/use-organization-queries.ts

  • [S4] [P8] src/lib/hooks/use-organization-queries.ts:157 and :186 — the credit-transactions and invoices infinite queries keep every page they load; neither passes maxPages, so they miss the retention contract infinite-retention.ts:17-23 states for the area ("every in-scope owner states the same retention contract").
    Impact: no user-visible jank today — both screens load pages from an explicit "Load more" tap, not from scroll — but the pages and their flattened entries accumulate for the life of the screen with no bound.
    Fix: wrap both option objects in withInfiniteRetention(...), the same way the session and findings lists do.

Proof

Proof for this item is a request count. Count the fetches a foreground transition and a session-list open issue with pnpm dev:capture nextjs, before and after. Quote both counts in the scenario note. The count must go down; the screens must still show the same data.

E2E proof — log excerpts

[p2] Code Reviewer list with a running review: newest page stays at the top and  -> pass :: Android emulator-5554; probe row 'Boundary shift probe' rendered first above 'Seed review 0001 … Running' (p2-after-top.log) and the post-shift boundary window is contiguous 0049/0050/0051 (p2-after-boundary.xml, p2-after-boundary-order.log) with page two re-requested at its own offset 50 (p2-offsets.log:56); fixture = code-reviews:review-list --count 120 + db.sh running status + db.sh probe insert; no UX-DEFECT observed.
/home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-review-dd74/e2e-mobile-app/p2-after-top.log
android.widget.TextView Seed review 0002 tappable [55,662][1025,708]
android.widget.TextView kilo-seed/review-list #3001 tappable [55,713][1025,750]
android.widget.TextView Completed tappable [55,759][192,796]
android.widget.TextView Just now tappable [210,759][325,796]
android.widget.Button Seed review 0003, kilo-seed/review-list #3002, Completed, Just now tappable [55,825][1025,1016]
android.widget.TextView Seed review 0003 tappable [55,852][1025,898]
android.widget.TextView kilo-seed/review-list #3002 tappable [55,903][1025,940]
android.widget.TextView Completed tappable [55,949][192,986]
android.widget.TextView Just now tappable [210,949][325,986]
android.widget.Button Seed review 0004, kilo-seed/review-list #3003, Completed, 1 minute ago tappable [55,1016][1025,1206]
android.widget.TextView Seed review 0004 tappable [55,1043][1025,1089]
android.widget.TextView kilo-seed/review-list #3003 tappable [55,1094][1025,1131]
android.widget.TextView Completed tappable [55,1140][192,1177]
android.widget.TextView 1 minute ago tappable [210,1140][374,1177]
android.widget.Button Seed review 0005, kilo-seed/review-list #3004, Completed, 2 minutes ago tappable [55,1206][1025,1396]
android.widget.TextView Seed review 0005 tappable [55,1233][1025,1279]
android.widget.TextView kilo-seed/review-list #3004 tappable [55,1284][1025,1321]
android.widget.TextView Completed tappable [55,1330][192,1367]
android.widget.TextView 2 minutes ago tappable [210,1330][389,1367]
android.widget.Button Seed review 0006, kilo-seed/review-list #3005, Completed, 3 minutes ago tappable [55,1396][1025,1586]
android.widget.TextView Seed review 0006 tappable [55,1423][1025,1469]
android.widget.TextView kilo-seed/review-list #3005 tappable [55,1474][1025,1511]
android.widget.TextView Completed tappable [55,1520][192,1557]
/home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-review-dd74/e2e-mobile-app/p2-after-boundary-order.log
text="Seed review 0045"
text="Seed review 0046"
text="Seed review 0047"
text="Seed review 0048"
text="Seed review 0049"
text="Seed review 0050"
text="Seed review 0051"
text="Seed review 0052"
text="Seed review 0053"
text="Seed review 0054"
text="Seed review 0055"
text="Seed review 0056"
/home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-review-dd74/e2e-mobile-app/p2-offsets.log
8:  offset: 0,
16: POST /api/trpc/codeReviews.listForUser?batch=1 200 in 58ms (next.js: 23ms, proxy.ts: 4ms, application-code: 30ms)
24:  offset: 0,
32: POST /api/trpc/codeReviews.listForUser?batch=1 200 in 59ms (next.js: 22ms, proxy.ts: 6ms, application-code: 32ms)
40:  offset: 0,
48: POST /api/trpc/codeReviews.listForUser?batch=1 200 in 22ms (next.js: 7ms, proxy.ts: 4ms, application-code: 11ms)
56:  offset: 50,
64: POST /api/trpc/codeReviews.listForUser?batch=1 200 in 22ms (next.js: 5ms, proxy.ts: 5ms, application-code: 12ms)
72:  offset: 0,
80: POST /api/trpc/codeReviews.listForUser?batch=1 200 in 39ms (next.js: 21ms, proxy.ts: 4ms, application-code: 13ms)
88:  offset: 0,
96: POST /api/trpc/codeReviews.listForUser?batch=1 200 in 57ms (next.js: 25ms, proxy.ts: 4ms, application-code: 28ms)
106:  offset: 0,
114: POST /api/trpc/codeReviews.listForUser?batch=1 200 in 30ms (next.js: 6ms, proxy.ts: 4ms, application-code: 21ms)
122:  offset: 0,
130: POST /api/trpc/codeReviews.listForUser?batch=1 200 in 58ms (next.js: 22ms, proxy.ts: 4ms, application-code: 32ms)

@iscekic
iscekic marked this pull request as draft September 23, 2026 11:12
@iscekic
iscekic marked this pull request as ready for review September 23, 2026 12:23
Comment thread apps/mobile/src/lib/hooks/use-agent-sessions.ts
Comment thread apps/mobile/src/lib/hooks/use-code-reviews.ts
Comment thread apps/mobile/src/lib/hooks/use-code-reviews.ts
Comment thread apps/mobile/src/components/agents/session-list-content.tsx Outdated
@kilo-code-bot

kilo-code-bot Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

Re-verified the PR-visible diff at HEAD b0e865589: the only commit since the previously reviewed revision (307749c2) aligns an out-of-scope screen-tracking file to main (net-zero against the base), and every core data-layer source file is byte-identical to the last reviewed revision, so no new issues were found.

Files Reviewed (20 files)
  • apps/mobile/src/app/(app)/_layout.tsx
  • apps/mobile/src/app/(app)/agent-chat/[session-id].tsx
  • apps/mobile/src/app/(app)/agent-chat/[session-id].mounted.test.tsx
  • apps/mobile/src/app/(app)/app-layout-foreground.mounted.test.tsx
  • apps/mobile/src/app/_layout.tsx
  • apps/mobile/src/components/agents/session-list-content.tsx
  • apps/mobile/src/components/agents/session-list-content.mounted.test.tsx
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-card.tsx
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-card.mounted.test.tsx
  • apps/mobile/src/lib/artifacts/artifact-mirror-sync-mount.tsx
  • apps/mobile/src/lib/artifacts/artifact-mirror-sync-mount.mounted.test.tsx
  • apps/mobile/src/lib/hooks/use-agent-sessions.ts
  • apps/mobile/src/lib/hooks/use-agent-sessions.test.ts
  • apps/mobile/src/lib/hooks/use-agent-sessions.combined.mounted.test.tsx
  • apps/mobile/src/lib/hooks/use-code-reviews.ts
  • apps/mobile/src/lib/hooks/use-code-reviews.test.ts
  • apps/mobile/src/lib/hooks/use-organization-queries.ts
  • apps/mobile/src/lib/hooks/use-organization-queries.test.ts
  • apps/mobile/src/lib/query-client.ts
  • apps/mobile/src/lib/query-client.test.ts

Incremental scope note: 3e0f...-unrelated main merges (9e6b3e3b8, 7131fe924, 9cac7c1c2) are already present in the base and are not PR-visible. use-screen-tracking.ts is not in the PR diff (net-zero against base).

Previous Review Summaries (7 snapshots, latest commit 307749c)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 307749c)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The only new commit (307749c) reverts an unrelated gastown backend-gate change that is net-zero against main; the PR-visible mobile data-layer diff is byte-identical to the previously reviewed revision, so no new issues were found.

Files Reviewed (incremental scope: 14 files)
  • services/gastown/src/dos/Agent.do.ts
  • services/gastown/src/dos/Town.do.ts
  • services/gastown/src/gastown.worker.ts
  • services/gastown/test/integration/*.test.ts (11 files)

The reverted gastown files are not part of gh pr diff 6640 (they are net-zero against the base), and the mobile files in the PR are unchanged since the last review.

Previous review (commit 1b1a45f)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The follow-up change is a behavior-preserving refactor of the page-one probe inputs into a single options object (with matching test updates); the previously bounded consecutive-failure poll is unchanged, and no new issues were found.

Files Reviewed (2 files)
  • apps/mobile/src/lib/hooks/use-code-reviews.ts
  • apps/mobile/src/lib/hooks/use-code-reviews.test.ts

Previous review (commit efaeca4)

Status: No Issues Found | Recommendation: Merge

The prior unbounded-poll finding on the page-one review probe is resolved: this change bounds consecutive failed polls and clears the bound on a delivered page, a scope change, or a review starting or ending. No new issues were found in the two changed files.

Files Reviewed (2 files)
  • apps/mobile/src/lib/hooks/use-code-reviews.ts
  • apps/mobile/src/lib/hooks/use-code-reviews.test.ts

Previous review (commit 0cb6b11)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
apps/mobile/src/lib/hooks/use-code-reviews.ts 155 The page-one probe now returns REVIEW_POLL_INTERVAL_MS on every failure with no cap, so a persistent failure keeps a 5 s request loop alive for the life of the screen.
Files Reviewed (18 files)
  • apps/mobile/src/components/agents/session-list-content.tsx
  • apps/mobile/src/components/agents/session-list-content.mounted.test.tsx
  • apps/mobile/src/lib/hooks/use-code-reviews.ts - 1 issue
  • apps/mobile/src/lib/hooks/use-code-reviews.test.ts
  • services/gastown/src/dos/Agent.do.ts
  • services/gastown/src/dos/Town.do.ts
  • services/gastown/src/gastown.worker.ts
  • services/gastown/test/integration/awaiting-approval.test.ts
  • services/gastown/test/integration/convoy-dag.test.ts
  • services/gastown/test/integration/http-api.test.ts
  • services/gastown/test/integration/mayor-idle.test.ts
  • services/gastown/test/integration/pr-poll-errors.test.ts
  • services/gastown/test/integration/reconciler.test.ts
  • services/gastown/test/integration/review-failure.test.ts
  • services/gastown/test/integration/rig-alarm.test.ts
  • services/gastown/test/integration/rig-do.test.ts
  • services/gastown/test/integration/town-container.test.ts
  • services/gastown/test/integration/town-deletion.test.ts

Fix these issues in Kilo Cloud

Previous review (commit b2c8cee)

Status: 4 Issues Found | Recommendation: Address before merge

Executive Summary

The page-one review probe resolves handler failures as data, so one transient error silently stops the 5 s in-flight poll; the session-list pagination park also discards the end-reach it suppresses, so a viewport-fitting list cannot resume paging.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/lib/hooks/use-code-reviews.ts 135 Probe resolves { success: false } as data, so refetchInterval stops after one transient failure instead of keeping the last good page.
apps/mobile/src/components/agents/session-list-content.tsx 254 The parked end-reach is dropped, not held; releasing the park on drag leaves a viewport-fitting list with nothing to replay, so pagination stays stuck.

SUGGESTION

File Line Issue
apps/mobile/src/lib/hooks/use-code-reviews.ts 226 The probe refreshes only page one while the screen renders all retained pages, so a running review on a later page no longer live-updates.
apps/mobile/src/lib/hooks/use-agent-sessions.ts 267 storedFetchedSinceMount latches per hook instance and is not reset on a filter/sort key change, so a failed new key can show stale cached rows as fresh.
Files Reviewed (22 files)
  • apps/mobile/src/app/(app)/_layout.tsx
  • apps/mobile/src/app/(app)/agent-chat/[session-id].mounted.test.tsx
  • apps/mobile/src/app/(app)/agent-chat/[session-id].tsx
  • apps/mobile/src/app/(app)/app-layout-foreground.mounted.test.tsx
  • apps/mobile/src/app/_layout.tsx
  • apps/mobile/src/components/agents/session-list-content.mounted.test.tsx
  • apps/mobile/src/components/agents/session-list-content.tsx - 1 issue
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-card.mounted.test.tsx
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-card.tsx
  • apps/mobile/src/lib/artifacts/artifact-mirror-sync-mount.mounted.test.tsx
  • apps/mobile/src/lib/artifacts/artifact-mirror-sync-mount.tsx
  • apps/mobile/src/lib/hooks/use-agent-sessions.combined.mounted.test.tsx
  • apps/mobile/src/lib/hooks/use-agent-sessions.test.ts
  • apps/mobile/src/lib/hooks/use-agent-sessions.ts - 1 issue
  • apps/mobile/src/lib/hooks/use-code-reviews.test.ts
  • apps/mobile/src/lib/hooks/use-code-reviews.ts - 2 issues
  • apps/mobile/src/lib/hooks/use-organization-queries.test.ts
  • apps/mobile/src/lib/hooks/use-organization-queries.ts
  • apps/mobile/src/lib/hooks/use-screen-tracking.test.ts
  • apps/mobile/src/lib/hooks/use-screen-tracking.ts
  • apps/mobile/src/lib/query-client.test.ts
  • apps/mobile/src/lib/query-client.ts

No memory leaks were found on the changed lines: the artifact-mirror launch/burst timers are cleared on unmount, the AppState/store subscriptions are removed on last unsubscribe, and the page-one probe is a single bounded cache entry.

Fix these issues in Kilo Cloud

Previous review (commit 85ebeeb)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental changes fix all four prior findings — the session-history fresh-open flag (storedFetchedSinceMount now requires both a dataUpdatedAt advance and a delivered page, so the reconcileFirstPage reset no longer masks the retryable full-screen error), the review-list retention bound (getNextPageParam refuses the next page at REVIEW_LIST_MAX_PAGES, so maxPages never evicts the newest page), the page-one/page-two boundary skip (selectReviewFirstPageAction refetches the retained pages at their offsets instead of merging a shifted page one), and the pagination-park release (onTouchMove joins onScrollBeginDrag). No new defects were found on the changed lines.

Files Reviewed (6 files)
  • apps/mobile/src/components/agents/session-list-content.tsx
  • apps/mobile/src/components/agents/session-list-content.mounted.test.tsx
  • apps/mobile/src/lib/hooks/use-agent-sessions.ts
  • apps/mobile/src/lib/hooks/use-agent-sessions.combined.mounted.test.tsx
  • apps/mobile/src/lib/hooks/use-code-reviews.ts
  • apps/mobile/src/lib/hooks/use-code-reviews.test.ts

Previous review (commit e5af22e)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/lib/hooks/use-agent-sessions.ts 471 refetch now reconciles page one; the setQueriesData reset advances dataUpdatedAt, so storedFetchedSinceMount becomes true without fresh rows and masks the documented fresh-open full-screen error. It also drops loaded pages on pull-to-refresh/retry.
apps/mobile/src/lib/hooks/use-code-reviews.ts 105 REVIEW_LIST_MAX_PAGES on a newest-first list: React Query's addToEnd drops pages[0] (the newest page) past the bound, after which the probe merge no-ops and offset 0 is never refreshed.
apps/mobile/src/lib/hooks/use-code-reviews.ts 159 Merging a fresh page one without updating pageParams leaves later pages' offsets stale; a shift at the page-one/page-two boundary can skip a row.

SUGGESTION

File Line Issue
apps/mobile/src/components/agents/session-list-content.tsx 259 Pagination parking is released only by onScrollBeginDrag, which a viewport-fitting list never fires; the repo's diff list wires onTouchMove for this.
Files Reviewed (22 files)
  • apps/mobile/src/app/(app)/_layout.tsx
  • apps/mobile/src/app/(app)/agent-chat/[session-id].mounted.test.tsx
  • apps/mobile/src/app/(app)/agent-chat/[session-id].tsx
  • apps/mobile/src/app/(app)/app-layout-foreground.mounted.test.tsx
  • apps/mobile/src/app/_layout.tsx
  • apps/mobile/src/components/agents/session-list-content.mounted.test.tsx
  • apps/mobile/src/components/agents/session-list-content.tsx - 1 issue
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-card.mounted.test.tsx
  • apps/mobile/src/components/kilo-pass/kilo-pass-subscription-card.tsx
  • apps/mobile/src/lib/artifacts/artifact-mirror-sync-mount.mounted.test.tsx
  • apps/mobile/src/lib/artifacts/artifact-mirror-sync-mount.tsx
  • apps/mobile/src/lib/hooks/use-agent-sessions.combined.mounted.test.tsx
  • apps/mobile/src/lib/hooks/use-agent-sessions.test.ts
  • apps/mobile/src/lib/hooks/use-agent-sessions.ts - 1 issue
  • apps/mobile/src/lib/hooks/use-code-reviews.test.ts
  • apps/mobile/src/lib/hooks/use-code-reviews.ts - 2 issues
  • apps/mobile/src/lib/hooks/use-organization-queries.test.ts
  • apps/mobile/src/lib/hooks/use-organization-queries.ts
  • apps/mobile/src/lib/hooks/use-screen-tracking.test.ts
  • apps/mobile/src/lib/hooks/use-screen-tracking.ts
  • apps/mobile/src/lib/query-client.test.ts
  • apps/mobile/src/lib/query-client.ts

No memory leaks were found on the changed lines: the removed 500 ms screen-tracking interval, the artifact-mirror foreground timeout, the AppState/store subscriptions, and the push-rotation listener are all cleaned up on unmount or dependency change, and all refs are bounded.

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash · Input: 0 · Output: 0 · Cached: 0

Review guidance: REVIEW.md from base branch main

@iscekic
iscekic marked this pull request as draft September 23, 2026 12:57
@iscekic
iscekic force-pushed the kwf/mobile-perf-data-layer-bea9 branch from 0a8adc5 to 85ebeeb Compare September 23, 2026 16:27
@iscekic
iscekic marked this pull request as ready for review September 23, 2026 16:47
@iscekic

iscekic commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

kilo-review — independent audit of the published diff.

Status: 1 Issues

File Line Issue
apps/mobile/src/lib/hooks/use-organization-queries.ts 2623 Credit-transactions and invoices now carry bare maxPages: 5 without a refusal bound, so paging past 125 entries makes React Query's addToEnd front-trim silently drop the newest rows of a newest-first (created_at desc) continuously rendered list.

@iscekic
iscekic marked this pull request as draft September 23, 2026 18:39
@iscekic

iscekic commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Accepted. Pushed ad88ed3 (kwf kwf-fix-review-fa5f).

What changed:
Review first: the new default staleTime, the session page-one reconcile, and the newest-first paging bounds when network work starts; the new mounted tests pin the one-listener and per-edge request contracts.

  • src/lib/query-client.ts:225 — accepted. New queries default to a 30 s staleTime; refetchOnWindowFocus stays false and call sites that pass staleTime: 0 still override.
  • src/lib/hooks/use-screen-tracking.ts:65 — accepted. The 500 ms generation interval and its exported constant are gone; the hook subscribes to the authenticated-owner store and reads currentGeneration() on that change.
  • src/lib/hooks/use-screen-tracking.ts:50-56 — accepted. The hook keeps only the derived stale boolean and compares it in a ref before dispatching, so an unchanged flag never re-renders the root layout.
  • src/app/_layout.tsx:1080-1094 — accepted. The root layout drops its private AppState listener for the temp-file reap and runs the reap on the shared useAppLifecycle background-to-active edge; the cold-start reap and deferred scheduleCacheMaintenance stay.
  • src/app/(app)/_layout.tsx:41-57 — accepted. The logout and push mounts merge into one ForegroundReconciliationMount on the shared store, running both in one ordered pass (logout then push); the push-token rotation subscription stays.
  • src/app/(app)/_layout.tsx:73-90 — accepted in part. No extra minimum interval: both attempts are already single-flight with 60 s spacing, and push reconciliation skips when the stored token, locale and app version already match the device.
  • src/app/(app)/agent-chat/[session-id].tsx:100-101 — accepted. The foreground/focus refresh now invalidates only the opened session's cliSessionsV2.get key and modelPreferences, not the whole cliSessionsV2 prefix, so the stored list pages behind the route are untouched.
  • src/app/(app)/agent-chat/[session-id].tsx:96-99 — no change required, as the finding states; the openStart ref stays.
  • src/components/kilo-pass/kilo-pass-subscription-card.tsx:90 — accepted. The card's own AppState listener and refetch refs are deleted; the profile layout's useRouteForegroundRefresh([[['kiloPass']]]) owns the refresh. A mounted test fails if a listener returns.
  • src/lib/hooks/use-agent-sessions.ts:150 — accepted. refetchOnWindowFocus now defaults to false; the hook owns the app-foreground edge and reconciles page one (reconcileFirstPage plus refetchQueries with cancelRefetch: false). Pull-to-refresh, retry and the Share Gate reconcile too, and the render hold now spans isPaused/isError so the list never blanks.
  • src/lib/hooks/use-code-reviews.ts:57 — accepted. The list builder applies withInfiniteRetention(..., 10); the 5 s in-flight poll moves off the infinite query to a page-one probe (offset 0, staleTime: 0) merged back with mergeReviewFirstPage.
  • src/lib/hooks/use-organization-queries.ts:157 and :186 — accepted. The credit-transactions and invoices builders are extracted and both wrapped in withInfiniteRetention(INFINITE_QUERY_MAX_PAGES).
  • apps/mobile/src/lib/hooks/use-organization-queries.ts:2623 — accepted. A forward fetch with a bare maxPages runs addToEnd(pages, page, maxPages), which drops index 0 — page one, the newest rows of these created_at desc lists. Both builders now take pages in getNextPageParam and refuse the next cursor once pages.length reaches INFINITE_QUERY_MAX_PAGES; the hooks read hasMore from query.hasNextPage, so Load more hides at the bound instead of paging into a silent front-trim. Unit tests pin the refusal and drive a real InfiniteQueryObserver to show page one survives and no sixth request is issued.
  • apps/mobile/src/lib/hooks/use-agent-sessions.ts:471 — accepted. storedFetchedSinceMount now latches true only when dataUpdatedAt advanced and at least one page was delivered, so a reconcileFirstPage reset no longer counts as a delivery and a failed fresh open keeps the retryable full-screen error.
  • apps/mobile/src/lib/hooks/use-code-reviews.ts:105 — accepted. getNextPageParam refuses the next page once pages.length reaches REVIEW_LIST_MAX_PAGES, so the newest-first list keeps page one and maxPages never evicts it.
  • apps/mobile/src/lib/hooks/use-code-reviews.ts:159 — accepted. selectReviewFirstPageAction compares the probe's page one with the cache; a changed row set with more than one retained page refetches the retained pages at their offsets (cancelRefetch: false) instead of merging, so no row is skipped at the boundary.
  • apps/mobile/src/components/agents/session-list-content.tsx:259 — accepted. onTouchMove releases the pagination park alongside onScrollBeginDrag, so a list that fits the viewport and never emits a drag can still re-open pagination.
  • Tests: mounted tests pin the session fresh-open flag and the touch-move park release; unit tests pin the forward-paging bound (including the org retention refusal) and selectReviewFirstPageAction.

One more change rides in this diff beyond the findings list: the artifact mirror defers its first and foreground runs by MIRROR_BURST_SETTLE_MS (5 s) so its session-list read stays out of the launch and foreground bursts.

Proof note: the new p2 boundary check replaces the earlier offset-0 review-list poll excerpt, which no longer describes the changed merge path; the request-count lines below stand.

@iscekic
iscekic force-pushed the kwf/mobile-perf-data-layer-bea9 branch from ad88ed3 to b2c8cee Compare September 23, 2026 19:27
@iscekic
iscekic marked this pull request as ready for review September 23, 2026 19:46
Comment thread apps/mobile/src/lib/hooks/use-code-reviews.ts Outdated
Comment thread apps/mobile/src/lib/hooks/use-code-reviews.ts
Comment thread apps/mobile/src/components/agents/session-list-content.tsx
Comment thread apps/mobile/src/lib/hooks/use-agent-sessions.ts
@iscekic
iscekic marked this pull request as draft September 23, 2026 20:16
The kwf base branch had re-created main's github connection role
migration as 0258_stormy_marvel_boy.sql, which left 0258/0259 missing
and the journal with a hole at 259. schema.ts already matches main and
carries no unique change, so restore main's migrations verbatim and drop
the duplicate.
@iscekic iscekic added the human-ready The PR is ready for human review. label Sep 25, 2026
@iscekic
iscekic marked this pull request as ready for review September 25, 2026 13:49
Comment thread apps/mobile/src/lib/hooks/use-code-reviews.ts
@iscekic iscekic removed the human-ready The PR is ready for human review. label Sep 25, 2026
The page-one probe is the only writer of the review list's cached page one,
so a probe whose endpoint keeps failing never turns itself off: `probing`
stays true off the stale cached page and the interval re-armed every 5 s (plus
the query's own retries) for the life of the screen.

Keep the interval surviving a failed poll (one transient error must not stop
live updates for the rest of the review), but count consecutive failed polls
in `useReviewList` and stop at `REVIEW_POLL_MAX_CONSECUTIVE_FAILURES`. The
count advances off the probe's settle signals (a failed refetch keeps
`dataUpdatedAt`, so "has data" is not "the last poll delivered") and restarts
on a delivered list or probe page, on a scope change, and when a review starts
or ends. AGENTS.md "Failure UX": retry within a bound.
@iscekic
iscekic force-pushed the kwf/mobile-perf-data-layer-bea9 branch from efaeca4 to 1b1a45f Compare September 25, 2026 14:22
@iscekic iscekic added the human-ready The PR is ready for human review. label Sep 25, 2026
The gastown auth, Durable Object lifecycle, gastown integration
tests, and session-ingest validation changes came from a backend gate
repair, not from this change. Restored to the branch merge base
(f1f708e).
@iscekic

iscekic commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator Author

Audit finding (apps/mobile/src/lib/hooks/use-organization-queries.ts, bare maxPages: 5): already fixed in b2c8cee. getNextPageParam for credit transactions and invoices now returns undefined when hasReachedRetentionBound(pages.length) is true, so a forward fetch never runs past INFINITE_QUERY_MAX_PAGES and the addToEnd front-trim never drops the newest rows.

…layer-bea9

# Conflicts:
#	apps/mobile/src/lib/hooks/use-screen-tracking.test.ts
#	apps/mobile/src/lib/hooks/use-screen-tracking.ts
@iscekic iscekic added merge-by-human the merge bot routed this PR to a human and removed human-ready The PR is ready for human review. labels Sep 26, 2026
@iscekic
iscekic merged commit 8270e75 into main Sep 28, 2026
28 checks passed
@iscekic
iscekic deleted the kwf/mobile-perf-data-layer-bea9 branch September 28, 2026 13:31
iscekic added a commit that referenced this pull request Sep 29, 2026
Restore the 561 files that this branch reverted relative to main (the
cloud-agent e2e CI automation from #6803 and the mobile foreground-refetch
tuning from #6640), leaving only the code-reviewer repository-selection
sender-map clear introduced on an account boundary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-by-human the merge bot routed this PR to a human

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants