Skip to content

feat(mobile): honest state across sessions, security, and reviews (w4a) - #5324

Merged
iscekic merged 34 commits into
mainfrom
audit-w4a-state-honesty-837d
Aug 18, 2026
Merged

feat(mobile): honest state across sessions, security, and reviews (w4a)#5324
iscekic merged 34 commits into
mainfrom
audit-w4a-state-honesty-837d

Conversation

@iscekic

@iscekic iscekic commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

The mobile app now reports state honestly, and Security Agent and Code Reviewer saves can no longer silently overwrite each other.

  • A session that fails to load shows a clear terminal error — "Not found", "Access denied", or "Couldn't load this session" — with Copy and Back buttons, and a Retry button only when a retry can help. The endless loading skeleton is gone.
  • Leaving the new-session screen with a typed prompt asks "Discard draft?" before the draft is lost, on every way out (header back, Android back, and the iOS swipe gesture).
  • The new-session screen shows the effective environment profile (its name and its commands, MCP servers, skills, and agents), and a "Leave changes" / "Commit and push" choice for the agent's changes.
  • The home screen shows "unavailable" instead of a first-use welcome when the active-sessions list fails to load.
  • Security Agent refuses to enable until at least one repository is selected, and explains why.
  • A Security Agent dismissal that fails keeps the reason and comment, and shows a retry card so the work is not lost. A failed sync or dismissal shows a retry card on the dashboard and finding screens.
  • Two tabs saving Security Agent settings at the same time can no longer silently overwrite each other — the stale save is rejected with a clear message.
  • The Code Reviewer overview shows an "action required" banner when it is disabled for a fixable reason, distinct from empty and healthy states.
  • The Code Reviewer detail screen leads with the outcome — the conclusion and any failure reason first, then findings, the council, and the gate.
  • Offline or not-yet-known connectivity no longer shows a $0 balance or a "Connect" prompt as if they were truth.
  • An organization invite now says "Invite created" (the email is sent in the background), and a failed invite email shows "Email failed" with a Resend option.

A session that fails to start now surfaces a classified terminal error instead of an endless loading skeleton. The class is derived from the session manager's formatted message text (not-found, permission, transient, or unknown), and only the transient class offers Retry; every terminal error offers Copy and Back. The session detail route adds a permanent NOT_FOUND/UNAUTHORIZED state and retries a transient NOT_FOUND only on the spawned route at a flat 1s cadence.

Files
  • apps/mobile/src/components/agents/session-terminal-error.ts — new; classifies a terminal error from the message text and resolves the error a session must surface, with Copy always offered.
  • apps/mobile/src/components/agents/session-detail-content.tsx — renders the terminal error with Copy, Back to sessions, and Retry only for the transient class.
  • apps/mobile/src/app/(app)/agent-chat/[session-id].tsx — renders a permanent NOT_FOUND/UNAUTHORIZED state (no Retry) with Copy and Back, and retries a transient NOT_FOUND on the spawned=1 route only.
  • apps/mobile/src/components/agents/mobile-session-manager.ts — adds readFetchSessionErrorCode, isCloudPrepareRetryableError, and fetchSessionWithNotFoundRetry at a flat 1s cadence.

Leaving the new-session screen with a non-empty prompt now asks to discard the draft on every exit path. A single React Navigation beforeRemove listener covers header back, Android back, and the iOS swipe gesture, and the caller's discard runs before the captured navigation action is replayed. A committed remote spawn that settles without navigating re-arms the guard so an abandon after a failed spawn still confirms.

Files
  • apps/mobile/src/app/(app)/agent-chat/use-new-session-discard-guard.ts — new; registers the beforeRemove listener and the Discard/Keep editing alert.
  • apps/mobile/src/app/(app)/agent-chat/new.tsx — wires the guard, the durable draft, the discard bypass on a successful Start/spawn, and the discard handler.
  • apps/mobile/src/lib/use-new-session-share-remote.ts — threads onSpawnAdmitted/onSpawnFailed callbacks.
  • apps/mobile/src/components/agents/use-remote-spawn-dispatch.ts — fires onSpawnAdmitted at admission and onSpawnFailed when a spawn settles without navigating.

The new-session screen now shows the effective environment profile and a Leave changes / Commit and push choice. The effective profile resolves from a new read-only agentProfiles router (list for personal, listCombined for org), and the create body sends autoCommit plus an optional profileId. Start is blocked only while the profile is still loading; a failed profile never blocks Start.

Files
  • apps/mobile/src/components/agents/use-effective-agent-profile.ts — new; resolves the effective default profile from list or listCombined.
  • apps/mobile/src/components/agents/new-session-configure-form.tsx — renders the read-only profile row and the commit segmented control.
  • apps/mobile/src/lib/new-session-submit.ts — adds resolveNewSessionStartDisabled, which blocks Start only while the profile is loading.
  • packages/trpc/src/agent-profiles-mobile.ts — new; read-only list/listCombined/get router.
  • packages/trpc/src/mobile.ts — mounts agentProfilesMobileRouter in place of the full web router.

A persisted mutation outbox now survives relaunch so a crash mid-session-create reuses the same operation key instead of minting a duplicate session. Rows are account-scoped under outbox:<userId> with two taxonomies: safe-retry (idempotent under its operationKey) and reconcile-first (must reconcile before re-submit). The session creators write a safe-retry row before prepareSession and reuse the stored key on relaunch.

Files
  • apps/mobile/src/lib/persist/mutation-outbox.ts — new; write/load/remove/list over the encrypted KV store, epoch-fenced and serialized.
  • apps/mobile/src/lib/persist/use-mutation-outbox.ts — new; exposes getStoredOperationKey, writeSafeRetry, writeReconcileFirst, needsReconcile, and whenLoaded.
  • apps/mobile/src/components/agents/use-new-session-creator.ts — sends autoCommit and an optional profileId, writes a safe-retry row before the mutate, and reuses the stored key.
  • apps/mobile/src/components/agents/use-continue-cloud-create.ts — writes a safe-retry row before the mutate and reuses the stored key on the continue path.

The home screen now shows a retryable "Couldn't load active sessions" error instead of the first-use promo when the active-sessions poll fails before any data loads.

Files
  • apps/mobile/src/components/home/home-screen.tsx — renders the activeIsError state as a retryable error.

Security Agent config saves are now compare-and-set on a config_revision column, so two tabs saving at once can no longer silently overwrite each other. A null expectedRevision is a first insert; a non-null value updates only while the stored revision matches, otherwise the save conflicts with a clear message. Enabling now refuses an empty effective repository set, and the CAS write, the activation boundary, and the include-existing enqueue all commit in one transaction.

Files
  • packages/db/src/schema.ts — adds agent_configs.config_revision (integer, default 1, >= 1 check) and security_analysis_queue.admitted_config_revision (nullable integer).
  • apps/web/src/lib/security-agent/db/security-config.ts — adds writeSecurityAgentConfigWithRevision and saveSecurityAgentConfigWithRevision, which commit the CAS write, boundary timestamp, and enqueues in one transaction.
  • apps/web/src/lib/security-agent/router/shared-handlers.ts — threads expectedRevision through saveConfig, returns configRevision from getConfig, and refuses setEnabled with an empty effective repo set.
  • apps/web/src/lib/security-agent/core/schemas.ts — adds expectedRevision to SaveSecurityConfigInputSchema.
  • apps/web/src/lib/security-agent/db/security-analysis.ts — accepts an optional tx and admittedConfigRevision on enqueueBacklogFindings.
  • apps/web/src/components/security-agent/SecurityAgentContext.tsx — sends expectedRevision and adds refetchConfigOnConflictError.
  • apps/mobile/src/lib/security-agent.ts — adds the FlattenedSecurityAgentConfig helper type.
  • apps/mobile/src/components/security-agent/analysis-settings-screen.tsx — switches its config ref to the flattened type.
  • apps/mobile/src/components/security-agent/automation-settings-screen.tsx — switches its config ref to the flattened type.
  • apps/mobile/src/components/security-agent/repository-settings-screen.tsx — switches its config ref to the flattened type and opts out of the disabled-state redirect.
  • apps/mobile/src/components/security-agent/settings-overview-screen.tsx — disables the enable switch without an effective repo set and offers a repo-picker CTA.
  • apps/mobile/src/lib/hooks/use-settings-back-guard.ts — adds skipRedirect so the repositories screen stays reachable while disabled.
  • apps/mobile/src/lib/hooks/use-security-agent-mutations.ts — sends expectedRevision from the cached config and writes a reconcile-first row before a sync POST.

A failed finding dismissal now keeps its reason and comment and surfaces a retry card instead of losing the work. The dismiss draft persists per finding with lastError and retryable, and the dashboard and finding detail render one retry card per failed dismissal.

Files
  • apps/mobile/src/lib/hooks/use-security-dismiss-draft.ts — new; persists a per-finding dismiss draft and lists failed dismissals per scope.
  • apps/mobile/src/components/security-agent/dismiss-finding-screen.tsx — restores the draft, records the intent before submit, and writes the failure on error.
  • apps/mobile/src/components/security-agent/finding-detail-screen.tsx — renders the retry card for a failed dismissal.
  • apps/mobile/src/components/security-agent/dashboard-screen.tsx — renders retry cards for failed dismissals and reconcile cards for crashed syncs.
  • apps/mobile/src/components/security-agent/security-command-retry-card.tsx — new; Retry/Discard card for a failed command.
  • apps/mobile/src/lib/persist/drafts.ts — adds securityDismissDraftKey.

Security analysis admission and rendering are tightened so a stale result is never read as current. The analysis start always sends forceSandbox, and the analysis panel gates triage and sandbox evidence on a terminal state so a stale result left by a failed retry does not read as "Analyzed".

Files
  • apps/mobile/src/lib/hooks/use-security-findings.ts — sends forceSandbox: true on startAnalysis.
  • apps/mobile/src/components/security-agent/finding-analysis-panel.tsx — gates triage and sandbox evidence on a terminal analysis state.

Connectivity is now classified three ways — online, offline, and unknown — so a not-yet-settled NetInfo boot is never read as live data. The offline banner starts unknown and delays 5s, the connection lifecycle resumes only on a real offline→online recovery, and paused cold-launch queries stay loading instead of showing $0 or a Connect prompt. The query client also stops retrying permanent error codes and drops permission-denied queries from the persisted cache.

Files
  • apps/mobile/src/lib/connectivity-online.ts — three-way classification; a null reachability is never online.
  • apps/mobile/src/lib/offline-banner-state.ts — starts unknown and delays the offline banner 5s.
  • apps/mobile/src/lib/user-web-connection-lifecycle.ts — resumes only on a real offline→online recovery.
  • apps/mobile/src/components/profile-credits-card.tsx — treats "no balance data yet" as loading, not $0.
  • apps/mobile/src/components/pr-review/pr-review-connect-gate.tsx — uses isPending so a paused cold launch stays loading.
  • apps/mobile/src/lib/code-reviewer-status.ts — keeps "no data yet" loading.
  • apps/mobile/src/lib/persist/cache-persistence-mount.tsx — sets shouldDehydrateMutation: () => false.
  • apps/mobile/src/lib/query-client.ts — adds PERMANENT_CODES (no retry) and drops permission-denied queries from the cache.

The Code Reviewer overview now shows an action-required banner when it is disabled for a fixable reason, and the detail screen leads with the outcome. The action-required state and its copy move to a shared package so web and mobile read one source, and the retrigger reset becomes a status compare-and-set so a concurrent retrigger cannot dispatch twice.

Files
  • packages/app-shared/src/code-reviews/action-required.ts — new; shared state type, six reasons, and title/description/recovery copy.
  • packages/app-shared/src/code-reviews/index.ts — new; exports the module.
  • packages/app-shared/package.json — adds the ./code-reviews export.
  • apps/web/src/lib/code-reviews/action-required-shared.ts — re-exports the shared state and keeps the web-only copy fields.
  • apps/mobile/src/components/code-reviewer/platform-overview-screen.tsx — renders the action-required banner.
  • apps/mobile/src/lib/code-reviewer-config.ts — adds actionRequired to ReviewConfigData.
  • apps/mobile/src/lib/hooks/use-code-reviewer.ts — drops the stale org-only comment.
  • apps/mobile/src/components/code-reviewer/review-detail-helpers.ts — new; flattens council findings and labels decisions/votes.
  • apps/mobile/src/components/code-reviewer/review-detail-screen.tsx — renders an outcome-first detail.
  • apps/mobile/src/components/code-reviewer/review-detail-sections.tsx — new; council, gate, and finding sections.
  • apps/web/src/lib/code-reviews/db/code-reviews.ts — makes resetCodeReviewForRetry a status CAS returning a count.
  • apps/web/src/routers/code-reviews/code-reviews-router.ts — throws CONFLICT when the reset matches zero rows.

Organization invite emails now go through a durable outbox instead of being sent inline, so the invite mutation returns immediately and a failed email is resendable. The outbox table holds one row per invitation with pending/sending/delivered/failed states and claim-fenced delivery marks, and a cron route drains it every minute. The member list surfaces the email status and offers Resend for failed invites.

Files
  • packages/db/src/external-side-effect-outbox.ts — new; enqueue/claim/mark/reclaim/reset helpers, fenced on the claim token.
  • packages/db/package.json — exports the module.
  • apps/web/src/lib/organizations/dispatch-invite-email-outbox.ts — new; drains the outbox in one cron pass.
  • apps/web/src/app/api/cron/dispatch-invite-email-outbox/route.ts — new; authenticated cron route.
  • apps/web/vercel.json — schedules the cron every minute.
  • apps/web/src/routers/organizations/organization-members-router.ts — enqueues instead of sending inline, marks the row failed on deleteInvite, and adds resendInvite.
  • apps/web/src/lib/organizations/organizations.ts — joins emailStatus into the member list and accepts an optional tx on the role/remove/invite helpers.
  • apps/web/src/lib/organizations/organization-types.ts — adds emailStatus.
  • apps/web/src/lib/user/index.ts — deletes pending/sending outbox rows in softDeleteUser.
  • apps/web/src/components/organizations/OrganizationMembersCard.tsx — renders the email-status badge and a Resend button.
  • apps/web/src/components/organizations/members/InviteMemberDialog.tsx — says "Invite created".
  • apps/web/src/components/organizations/members/invited-email-status.ts — new; maps the status to a badge label.
  • apps/mobile/src/components/organization/invite-member-sheet.tsx — says "Invite created".
  • apps/mobile/src/components/organization/invited-member-row-state.ts — new; label, resend eligibility, and action options.
  • apps/mobile/src/components/organization/invited-member-row.tsx — renders the status and offers Resend.
  • apps/storybook/src/mockData/organizations.ts — records the emailStatus field.
  • ENVIRONMENT.md — records the cron path.
  • apps/mobile/.gitignore — ignores the generated sentry.options.json.

Tests: 46 files changed (19 new, 27 modified) across mobile mounted/unit tests, web db/router/component tests, and the shared action-required test.
Generated: 5 files — 2 migrations (0217_fresh_boom_boom.sql, 0218_violet_whizzer.sql), 2 snapshots, and the migration journal.


Verification

  • Automated: full mobile suite, web and db package tests, typecheck, lint, and format are green.
  • E2E (bot-e2e, iOS): the acceptance criterion (P1-B-11 S1/S2) passed — "Not found" + Back + Copy, no skeleton. Also passed: discard confirm (P0-B-11), profile row + Leave/Commit (P1-B-13a/b), home unavailable on active-list failure (P1-B-09a), and cold-launch credits (P2-B-10 online). The remaining scenarios are blocked by environment, not product defects: the GitHub token-envelope keypair is not generated in this worktree (S6), the iOS 26.5 simulator has no airplane-mode toggle (S9, S10-offline), and a stale sync record from a prior run blocks the findings scenarios (S7, S8). Every blocked scenario is covered by unit tests.

Visual Changes

The mobile screens now render honest state. After states from the iOS E2E run:

  • Session terminal error — "Not found" with Copy and Back, no endless skeleton (P1-B-11):
    s2-notfound.png
  • Session terminal error variant (P1-B-11):
    s1-notfound.png
  • New-session composer with the effective agent profile row (P1-B-13a):
    s4-composer.png
  • Home unavailable state on active-list failure (P1-B-09a):
    s5-unavailable.png

Reviewer Notes

  • The change spans mobile, web, and db. The db migrations (0217, 0218) are generated with pnpm drizzle generate; do not hand-edit them.
  • The invite-email outbox mirrors the analytics outbox claim loop (8 attempts, 5-minute reclaim).

Human steps

No human step is needed. Migrations auto-apply on deploy (NODE_ENV=production pnpm run drizzle migrate); the cron path is read from vercel.json on deploy; no new secret (reuses CRON_SECRET).

iscekic added 22 commits August 18, 2026 00:11
Add a beforeRemove discard confirm for a non-empty new-session prompt.
Skip the confirm on a successful Start or remote spawn, and re-arm it
after a failed spawn. Stay and toast when the stored draft cannot be
cleared, so a discarded prompt can never resurface.
Persist the dismiss reason, comment, last error, and retry class per
scope and finding. Restore the draft on remount and refresh the retry
card on the mounted dashboard and finding-detail screens after the
sheet closes. Clear the draft only after an authoritative accept.
Carry actionRequired through ReviewConfigData and render a distinct
banner above the enable switch. The three user-facing strings live in
@kilocode/app-shared; web re-exports them instead of keeping a copy.
Lead with conclusion, findings, council decision, and gate before
metadata. Flatten council findings and paginate in memory, appending
pages so earlier findings stay visible.
resetCodeReviewForRetry now updates only rows still in a retryable
status and returns the row count. A zero-row update surfaces as a
conflict instead of a successful retrigger.
Mount a mobile-scoped agentProfiles router and render a read-only
profile row with capability counts. Resolve personal default over org
default. Block Start until the profile query settles and keep Start
enabled on a failed query, submitting with no profile id.
setEnabled throws PRECONDITION_FAILED when the effective repo set is
empty. The mobile switch is disabled with an explanation and the repo
picker stays reachable while disabled. startAnalysis always sends
forceSandbox. A loading or failed repo query is not read as empty.
Classify connectivity as online, offline, or unknown and treat unknown
as not-online. Start the banner store unknown, pause React Query on
offline and unknown, and clear account/permission cache on change. An
unknown state with empty cache shows a skeleton, not Connect or $0.
The fulfilled-result predicate used an unassignable shape. Narrow it to
SuccessResult so the parallel-retrigger test typechecks.
Write invitation, audit, and outbox row in one transaction so the
mutation never sends mail. Drain via cron with the analytics-outbox
pattern. Resend resets the same row, fenced on the invitation still
being valid, and refuses a revoked or expired invite. Expose emailStatus
on the list contract and keep copy honest (Invite created, not sent).
Replace the hardcoded autoCommit:true with a two-way choice that
defaults to Leave changes. Leave maps to autoCommit:false and Commit
and push to autoCommit:true. The api.send payload omits autoCommit so
the worker inherits the prepare value. The choice is part of the
intent fingerprint so a changed choice is a fresh intent.
Add config_revision to agent_configs and admitted_config_revision to
security_analysis_queue. getConfig returns the revision and save takes
an expectedRevision; a stale revision yields CONFLICT. The backlog
enqueue and the CAS write share one transaction so an enqueue throw
rolls back the config. Migrate the web and mobile save consumers.
Persist safe-retry and reconcile-first mutation rows under outbox:<userId>
so a crash mid-flight reuses the stored operationKey instead of minting
a duplicate session. Only safe-retry session registration auto-replays;
reconcile-first security sync surfaces a retry card and never auto-POSTs.
The sync wire always uses the row's own key, and the dashboard filters
reconcile cards to its scope.
The S14 CAS change made securityAgent.getConfig return a discriminated
union on hasConfig/configRevision. The mobile optimistic updates and the
settings screens' dirty-state refs spread patch objects over that union,
which no longer type-checks. Add a FlattenedSecurityAgentConfig mapped
type and use it for the spreads; fix the onError key cast and the two
test files' invocationCallOrder assertions.
S13 added emailStatus to InvitedOrganizationMemberSchema; the storybook
mock data for invited members was missing it.
Fix four lint errors in the security settings overview: extract the
nested loading/error/copy ternary into a helper, use optional chaining
for the empty check, and drop the redundant data-null guards.
The continue path minted a fresh in-memory operationKey and never wrote
a safe-retry row, so a kill mid-continue could create a second session.
The sync path did not gate on the outbox load, so a crash row's stored
reconcile key could be overwritten by a fresh key. Gate both on whenLoaded
and reuse getStoredOperationKey before minting.
use-security-agent-mutations imports the outbox (P1-E-40c), which pulls
@sentr/react-native (Flow) transitively; the findings test loaded
react-native's index.js and failed the transform. Mock the outbox so the
pure-logic test never loads it.
@iscekic
iscekic requested a review from jeanduplessis August 18, 2026 06:36
@iscekic iscekic self-assigned this Aug 18, 2026
@iscekic
iscekic requested a review from pandemicsyn August 18, 2026 06:36
Comment thread apps/mobile/src/lib/query-client.ts Outdated
Comment thread apps/mobile/src/components/code-reviewer/review-detail-screen.tsx Outdated
Comment thread apps/mobile/src/lib/hooks/use-security-agent-mutations.ts Outdated
Comment thread apps/mobile/src/components/security-agent/dashboard-screen.tsx
Comment thread packages/db/src/external-side-effect-outbox.ts
Comment thread packages/db/src/external-side-effect-outbox.ts
@kilo-code-bot

kilo-code-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • apps/mobile/src/lib/persist/use-mutation-outbox.ts
  • apps/mobile/src/lib/persist/use-mutation-outbox.test.ts
Previous Review Summaries (5 snapshots, latest commit 38c1f94)

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

Previous review (commit 38c1f94)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

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

WARNING

File Line Issue
apps/mobile/src/lib/persist/use-mutation-outbox.ts 138 markLoaded() is no longer generation-fenced, so a superseded outbox load can release waiters against empty/stale rows and mint a duplicate session key
Files Reviewed (50 files)
  • apps/mobile/src/lib/persist/use-mutation-outbox.ts - 1 issue
  • apps/mobile/src/lib/persist/mutation-outbox.ts
  • apps/mobile/src/lib/persist/mutation-outbox.test.ts
  • apps/mobile/src/lib/persist/use-mutation-outbox.test.ts
  • apps/mobile/src/lib/hooks/use-security-agent-mutations.ts
  • apps/mobile/src/lib/hooks/use-security-agent-mutations.test.ts
  • apps/mobile/src/lib/hooks/use-security-findings.test.ts
  • apps/mobile/src/components/agents/use-new-session-creator.ts
  • apps/mobile/src/components/agents/use-new-session-creator.test.ts
  • apps/mobile/src/components/agents/use-continue-cloud-create.ts
  • apps/mobile/src/components/agents/use-continue-session.ts
  • apps/mobile/src/components/agents/use-continue-session.test.ts
  • apps/mobile/src/components/agents/session-detail-content.tsx
  • apps/mobile/src/components/agents/use-session-config-sync.ts
  • apps/mobile/src/components/agents/mobile-session-manager.ts
  • apps/mobile/src/components/agents/mobile-session-transport-payload.ts
  • apps/mobile/src/components/agents/chat-composer.tsx
  • apps/mobile/src/components/agents/chat-toolbar.tsx
  • apps/mobile/src/app/(app)/agent-chat/[session-id].tsx
  • apps/mobile/src/app/(app)/agent-chat/new.tsx
  • apps/mobile/src/app/(app)/agent-chat/mode-picker.tsx
  • apps/mobile/src/components/agents/mode-normalize.ts
  • apps/mobile/src/components/agents/mode-options.ts
  • apps/mobile/src/components/agents/mode-selector.tsx
  • apps/mobile/src/components/agents/model-selector.tsx
  • apps/mobile/src/components/agents/use-effective-profile-custom-modes.ts
  • apps/mobile/src/components/agents/new-session-configure-form.tsx
  • apps/mobile/src/components/agents/new-session-prompt.tsx
  • apps/mobile/src/lib/share-navigation.ts
  • apps/web/src/lib/security-agent/db/security-config.ts
  • apps/web/src/lib/security-agent/db/security-analysis.ts
  • apps/web/src/lib/ai-gateway/providers/vercel/index.ts
  • apps/web/src/routers/usage-analytics-router.ts
  • ENVIRONMENT.md
  • Remaining incremental files (tests, prefill, picker-bridge, continuation-seed, remote-spawn) — no issues found

Fix these issues in Kilo Cloud

Previous review (commit 2ace7c8)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (7 files)
  • ENVIRONMENT.md
  • apps/mobile/.gitignore
  • apps/mobile/src/components/agents/mobile-session-manager.ts
  • apps/mobile/src/components/agents/mobile-session-manager.test.ts
  • apps/mobile/src/components/agents/session-detail-content.tsx
  • apps/mobile/src/components/agents/session-composer-disabled.ts
  • apps/mobile/src/components/agents/session-composer-disabled.test.ts

Previous review (commit 2728351)

Status: 6 Issues Found | Recommendation: Address before merge

Executive Summary

An unbounded remove→refetch loop in the mobile query cache's permission-denied handler and an invite-email outbox claim that ignores invitation validity are the highest-risk issues in this otherwise well-tested honest-state change.

Overview

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

WARNING

File Line Issue
apps/mobile/src/lib/query-client.ts 104 Removing a still-observed FORBIDDEN query in queryCache.onError recreates and refetches it, causing an unbounded network loop while the error screen is mounted
apps/mobile/src/components/code-reviewer/review-detail-screen.tsx 115 manual_config?.agentConfig.gate_threshold throws TypeError for documented malformed legacy rows missing agentConfig
apps/mobile/src/lib/hooks/use-security-agent-mutations.ts 272 await whenLoaded() can hang the mutation forever when the outbox load is generation-fenced on unmount (no waiter drain/timeout)
packages/db/src/external-side-effect-outbox.ts 108 Claim loop not fenced on invitation validity: invite email still sent after acceptance or expiry

SUGGESTION

File Line Issue
apps/mobile/src/components/security-agent/dashboard-screen.tsx 199 Empty-state "Sync findings" control lacks the onSettled: refreshOutbox parity, so a retryable failure there doesn't surface the reconcile card until next focus
packages/db/src/external-side-effect-outbox.ts 265 resetInviteEmailForResend not fenced on outbox status — resend during an in-flight claim duplicates the email
Files Reviewed (128 files)
  • apps/mobile/src/lib/query-client.ts - 1 issue
  • apps/mobile/src/components/code-reviewer/review-detail-screen.tsx - 1 issue
  • apps/mobile/src/lib/hooks/use-security-agent-mutations.ts - 1 issue
  • apps/mobile/src/components/security-agent/dashboard-screen.tsx - 1 issue
  • packages/db/src/external-side-effect-outbox.ts - 2 issues
  • All other changed source files (agent-chat session flow, code-reviewer screen helpers, connectivity tri-state, offline banner, mutation outbox, dismiss drafts, security-agent screens, web security-agent CAS handlers, code-reviews router/DB, organizations and invite outbox dispatcher, cron route, db schema + migrations 0217/0218, app-shared code-review helpers, tRPC mobile routers) — no issues found. Generated migration snapshots/journals excluded from review.

Fix these issues in Kilo Cloud

Previous review (commit 7c0b995)

Status: 6 Issues Found | Recommendation: Address before merge

Executive Summary

An unbounded remove→refetch loop in the mobile query cache's permission-denied handler and an invite-email outbox claim that ignores invitation validity are the highest-risk issues in this otherwise well-tested honest-state change.

Overview

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

WARNING

File Line Issue
apps/mobile/src/lib/query-client.ts 104 Removing a still-observed FORBIDDEN query in queryCache.onError recreates and refetches it, causing an unbounded network loop while the error screen is mounted
apps/mobile/src/components/code-reviewer/review-detail-screen.tsx 115 manual_config?.agentConfig.gate_threshold throws TypeError for documented malformed legacy rows missing agentConfig
apps/mobile/src/lib/hooks/use-security-agent-mutations.ts 272 await whenLoaded() can hang the mutation forever when the outbox load is generation-fenced on unmount (no waiter drain/timeout)
packages/db/src/external-side-effect-outbox.ts 108 Claim loop not fenced on invitation validity: invite email still sent after acceptance or expiry

SUGGESTION

File Line Issue
apps/mobile/src/components/security-agent/dashboard-screen.tsx 199 Empty-state "Sync findings" control lacks the onSettled: refreshOutbox parity, so a retryable failure there doesn't surface the reconcile card until next focus
packages/db/src/external-side-effect-outbox.ts 265 resetInviteEmailForResend not fenced on outbox status — resend during an in-flight claim duplicates the email
Files Reviewed (128 files)
  • apps/mobile/src/lib/query-client.ts - 1 issue
  • apps/mobile/src/components/code-reviewer/review-detail-screen.tsx - 1 issue
  • apps/mobile/src/lib/hooks/use-security-agent-mutations.ts - 1 issue
  • apps/mobile/src/components/security-agent/dashboard-screen.tsx - 1 issue
  • packages/db/src/external-side-effect-outbox.ts - 2 issues
  • All other changed source files (agent-chat session flow, code-reviewer screen helpers, connectivity tri-state, offline banner, mutation outbox, dismiss drafts, security-agent screens, web security-agent CAS handlers, code-reviews router/DB, organizations and invite outbox dispatcher, cron route, db schema + migrations 0217/0218, app-shared code-review helpers, tRPC mobile routers) — no issues found. Generated migration snapshots/journals excluded from review.

Fix these issues in Kilo Cloud

Previous review (commit 5082220)

Status: 6 Issues Found | Recommendation: Address before merge

Executive Summary

An unbounded remove→refetch loop in the mobile query cache's permission-denied handler and an invite-email outbox claim that ignores invitation validity are the highest-risk issues in this otherwise well-tested honest-state change.

Overview

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

WARNING

File Line Issue
apps/mobile/src/lib/query-client.ts 104 Removing a still-observed FORBIDDEN query in queryCache.onError recreates and refetches it, causing an unbounded network loop while the error screen is mounted
apps/mobile/src/components/code-reviewer/review-detail-screen.tsx 115 manual_config?.agentConfig.gate_threshold throws TypeError for documented malformed legacy rows missing agentConfig
apps/mobile/src/lib/hooks/use-security-agent-mutations.ts 272 await whenLoaded() can hang the mutation forever when the outbox load is generation-fenced on unmount (no waiter drain/timeout)
packages/db/src/external-side-effect-outbox.ts 108 Claim loop not fenced on invitation validity: invite email still sent after acceptance or expiry

SUGGESTION

File Line Issue
apps/mobile/src/components/security-agent/dashboard-screen.tsx 199 Empty-state "Sync findings" control lacks the onSettled: refreshOutbox parity, so a retryable failure there doesn't surface the reconcile card until next focus
packages/db/src/external-side-effect-outbox.ts 265 resetInviteEmailForResend not fenced on outbox status — resend during an in-flight claim duplicates the email
Files Reviewed (128 files)
  • apps/mobile/src/lib/query-client.ts - 1 issue
  • apps/mobile/src/components/code-reviewer/review-detail-screen.tsx - 1 issue
  • apps/mobile/src/lib/hooks/use-security-agent-mutations.ts - 1 issue
  • apps/mobile/src/components/security-agent/dashboard-screen.tsx - 1 issue
  • packages/db/src/external-side-effect-outbox.ts - 2 issues
  • All other changed source files (agent-chat session flow, code-reviewer screen helpers, connectivity tri-state, offline banner, mutation outbox, dismiss drafts, security-agent screens, web security-agent CAS handlers, code-reviews router/DB, organizations and invite outbox dispatcher, cron route, db schema + migrations 0217/0218, app-shared code-review helpers, tRPC mobile routers) — no issues found. Generated migration snapshots/journals excluded from review.

Fix these issues in Kilo Cloud


Reviewed by grok-4.6 · Input: 111.4K · Output: 10K · Cached: 248.2K

Review guidance: REVIEW.md from base branch main

Remove the unused isProfileError parameter and the always-true copyable
field, and fix require-await lint errors in the outbox test mock.
OutboxTaxonomy is only used by OutboxRow.taxonomy inside the module. The
export tripped knip's check:unused gate in CI.
Removing a still-observed query inside queryCache.onError rebuilds it on the
next render and refetches, which fails FORBIDDEN again and loops for as long
as the error screen stays mounted. Only drop queries nothing is observing.
Legacy review rows can carry a manual_config without agentConfig. Reading
gate_threshold through the missing inner object crashed the detail screen
instead of showing the error surfaces this PR adds.
The empty-state Sync findings control fired the reconcile-first mutation with
only onSuccess, so a retryable failure left the reconcile card invisible until
the next focus refresh. Match the other sync entry points with onSettled.
The claim loop selected pending rows without checking the linked invitation, so
an accepted or expired invite was still emailed. The resend reset matched a
sending row, so a mid-send resend could email the invitee twice. Fence the
claim on invitation validity and the reset on a non-sending status.
@iscekic iscekic added the human-ready The PR is ready for human review. label Aug 18, 2026
@iscekic iscekic changed the title feat(mobile): honest state across sessions, security, and reviews feat(mobile): honest state across sessions, security, and reviews (w4a) Aug 18, 2026

@pandemicsyn pandemicsyn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, pair of minor ones inline.

Comment thread apps/web/src/lib/security-agent/db/security-config.ts Outdated
Comment thread apps/mobile/src/lib/persist/mutation-outbox.ts
… and refuse on a failed outbox read

Two review findings on PR #5324.

Security config: the CAS save committed the enabled config and the queued
backlog rows, then wrote `auto_analysis_enabled_at` after the commit. A worker
that claimed a queued row in that gap saw a null boundary and skipped the
finding, and a failed post-commit write left the backlog with no boundary at
all. The boundary write now runs inside the same transaction, so both helpers
take an optional `tx`.

Mobile outbox: a failed `listOutboxRows` read returned an empty list, which the
session creators read as "no stored row" and used to mint a fresh operation
key, so an accepted-but-unreadable row could produce a duplicate Cloud Agent
session. The read now returns null, `whenLoaded` reports the failure and
re-reads on the next attempt, and all three producers refuse the mutation
instead of minting a key.
Review finding on the previous commit: `markLoaded()` ran outside the
generation fence, so a load superseded by an identity change released the
waiters against stale rows and a submit could mint a duplicate session key.
`runLoad` now owns the marking: a superseded load applies nothing and marks
nothing, and the load that superseded it releases the waiters.
@iscekic
iscekic merged commit c799293 into main Aug 18, 2026
51 checks passed
@iscekic
iscekic deleted the audit-w4a-state-honesty-837d branch August 18, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-ready The PR is ready for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants