fix(mobile): Active now tray context filter, stale titles, status meta - #4830
Conversation
Preserve cached titles on heartbeat and sessions.list so cloud renames do not oscillate back to the CLI title after list enrichment.
Emit organizationId on every row (null for unattributable), authorize org access first, and fail filtered callers when the enrichment DB fails. Unfiltered callers keep the unenriched passthrough.
Return undefined from remoteMeta when updatedAt is missing so the row shows the live dot alone; align spoken accessibility meta the same way.
Carry organizationId through WS merges, keep enriched titles sticky, and add pure filter/rename helpers for the context-filtered tray.
Key activeSessions.list by context, hold a standing WS lease across owner swaps, filter unattributed heartbeat rows client-side, gate Home's session queries on the loaded context, and optimistically retitle tray rows on rename.
Use Reflect.apply passthrough for ensureOrganizationAccess selects so root typecheck accepts the enrichment-failure mock.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummarySince the previous review, the only changes are two new workflow-learnings documentation files ( Files Reviewed (2 files)
Previous Review Summary (commit e4540ec)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit e4540ec)Status: No Issues Found | Recommendation: Merge Executive SummaryReviewed all 20 changed files across the mobile Active-now tray context filter, stale-title, and status-meta fixes plus the web router/hook changes; no high-confidence security, correctness, or logic issues found on changed lines, with high confidence in the review scope. Files Reviewed (20 files)
Reviewed by claude-sonnet-5 · Input: 22 · Output: 3K · Cached: 403.6K Review guidance: REVIEW.md from base branch |
Summary
Fixes three defects in the mobile Agents Active now tray, all in the
activeSessions.listpath:activeSessions.listtook no input and returned every live session for the user. It now accepts an optionalorganizationId(undefined= unfiltered liveness callers,null= personal, uuid = org, authorized via the existingensureOrganizationAccess) and filters after the enrichment join in JS, because a live session with nocli_sessions_v2row is unattributable and counts as personal — a case SQL cannot distinguish from "belongs to another org". The router emitsorganizationIdon every row (nullincluded), so a cached row without it is exactly "arrived via WS push, not yet attributed"; the mobile client hides such rows in any filtered context until the next fetch attributes them (they re-appear one enrichment round-trip later — accepted). The tray query key now varies with context (one sharedbuildActiveSessionsInputfor the hook and the live-sync owner, so keys cannot drift), and Home gates its session queries on the loaded org context so an org user never flashes personal rows on cold start.titleis now enriched fromcli_sessions_v2(DB-authoritative, falling back to the worker title when NULL); the mobile cache keeps the enriched title sticky across heartbeats; and rename optimistically patches every cached tray context with snapshot + rollback, symmetric with the stored-list patch. Web's two WS merge paths (applyActiveSessionsHeartbeat,sessions.list) keep cached titles unconditionally via a smallpreserveCachedTitleshelper — without it, DB-authoritative titles would have made the web sidebar oscillate DB → CLI title on every heartbeat (regression prevention, not scope creep).BUSY/IDLE/RETRY) in the timestamp slot.remoteMetanow returnsundefinedwhenupdatedAtis absent, so the row renders the live dot alone; the spoken accessibility meta matches. No substitute value, no fixed-width slot.Socket lease (reviewer focus): the live-sync owner is recreated per context, and its
attach()is the app's onlyretain()caller — so a per-context swap would drop the retain count to zero and close the shared user-web WebSocket on every context switch. The mount now holds a standinguseEffect(() => connection.retain(), [connection])lease above the per-context owner; the socket stays up across switches and across the cold pre-isLoadedwindow.Failure semantics: on an enrichment DB failure, unfiltered callers (web,
resolveSession) keep the existing unenriched passthrough, while a filtered context fails the query (TRPCError) — attribution is unknowable without the join, and guessing would silently show out-of-context sessions. Mobile lands in the already-shipped retryable inline state when rows are on screen; a cold empty cache shows an empty tray (pre-existing, no new UI).Verification
E2E — iPhone 17 Pro simulator, iOS 26.5, against this worktree's local stack; real model calls on
kilo-auto/efficient. Flows exercised: personal/org context filtering across load, pull-to-refresh and ≥2 heartbeat cycles; 7 personal↔org context switches; org-attribution flip via SQL;cli_sessions_v2row deletion with the CLI still live (unattributable path); rename from both entry points (history list row → native prompt; session-detail header → RenameModal); NEEDS INPUT via a real permission prompt onefficient; socket-lease check via atmux pipe-panecapture of thecloudflare-session-ingestpane across every switch.step1-d6-clean.png)step2-org-tray.png; unattributable personal row hidden in org (step3-org-hidden.png)step4-tray-renamed.png→ 35 s heartbeat watch →step4-tray-after-heartbeat.png("Tray rename alpha" never reverted; DB confirmed)use-session-mutations.test.tsunit coverage is authoritative for that stateBUSY/IDLE/RETRYin meta (cold/PTR/heartbeat/rowless)Tray-test-1 ready test, CLOUD— no status word, no fabricated time (step3-row-deleted-still-listed.png)…, CLOUD, 1 hour ago, from CLI(timestamped) vs…, CLOUD(rowless, live dot only)Tray rename alpha, needs input, CLOUDSocket lease (Task 3c standing
retain()) — PASS: zeroWeb socket disconnected/connectedpairs across all 7 context switches in thecloudflare-session-ingestcapture. Three lone disconnects all correlate with app-process SIGSEGVs insideXCTAutomationSupport(Maestro/XCUITest driver instability under parallel-workflow load), never with a switch commit — the lease-churn signature (a synchronous pair per switch) cannot and did not occur.The verifier restored its baseline byte-for-byte (worktree porcelain, diff and index all MATCH, 0 changed paths), deleted every test DB row it created (verified
0|0|0|0), stopped the dev stack, shut down and released the simulator, and released the device slot.cd apps/web && NODE_ENV=test pnpm test src/routers/active-sessions-router— 32 tests (context filter both directions, unattributable→personal, non-member rejection,organizationIdon every row incl.toHaveProperty('organizationId', null), DB title with NULL fallback, D11 failure branches)cd apps/web && NODE_ENV=test pnpm test src/components/cloud-agent-next— 298 tests (cached title preserved on heartbeat for known ids, payload title for new ids, connection-scoped replace intact)cd apps/mobile && pnpm format && pnpm typecheck && pnpm lint && pnpm check:unused && pnpm test— 2363 tests (org-id carry across merges incl.nullpreserved asnull, filter matrix, two-heartbeat re-admission guard, title stickiness, dual-cache rename rollback,remoteMetamatrix, eyebrow-right rendering guards)pnpm typecheck+pnpm lint— cleanVisual Changes
No visual redesign. The tray is pixel-identical except: rows are filtered to the selected personal/org context; a renamed session keeps its new title across heartbeats instead of flashing the stale CLI title; and a row without a timestamp shows the live dot alone — no status word, no fabricated time, no fixed-width slot. Evidence screenshots from the E2E run: org-only tray (
step2-org-tray.png), renamed tray after a 35 s heartbeat watch (step4-tray-after-heartbeat.png), rowless-session meta (step3-row-deleted-still-listed.png).BUSY/IDLE/RETRYwordReviewer Notes
isEnrichedgate like mobile's): web drops enrichment fields on every heartbeat, so a gate would never hold. A CLI-side retitle reaches web on the next list fetch / 10s refetch — same freshness class as mobile's accepted trade-off.mobile-session-manager.resolveSessiondeliberately keeps callingactiveSessions.listwith no input (unfiltered liveness check); filtering it would misclassify org sessions as read-only.useActiveSessions× filter × live-sync mount) is intentionally not unit-tested — the vitest harness cannot import the RN/TRPC providers (apps/mobile/src/test/render-with-providers.tsxheader); covered by the pure-helper tests + E2E.