Skip to content

refactor(core): move app state ownership into the Rust core#320

Merged
senamakel merged 13 commits intotinyhumansai:mainfrom
senamakel:feat/redux-remove
Apr 4, 2026
Merged

refactor(core): move app state ownership into the Rust core#320
senamakel merged 13 commits intotinyhumansai:mainfrom
senamakel:feat/redux-remove

Conversation

@senamakel
Copy link
Copy Markdown
Member

@senamakel senamakel commented Apr 4, 2026

Summary

  • Move app-facing auth/user/team state ownership into a new core app_state RPC surface and poll it from React via CoreStateProvider.
  • Expand team RPC controllers so the frontend no longer needs direct backend team calls for core flows.
  • Remove auth, user, team, and intelligence from the live Redux store; keep Redux focused on UI/runtime state such as sockets, threads, accessibility, daemon health, and webhooks.
  • Rewire onboarding, routing, wallet state, settings, and team management screens to consume the core-owned snapshot instead of frontend-owned business state.

Problem

  • The frontend was still owning substantial business logic and persistent state in Redux even though the Rust core is the authoritative runtime.
  • That split made state drift likely across React, Tauri, and the sidecar, especially around auth bootstrap, onboarding flags, wallet state, and team data.
  • The app architecture should move toward a thinner shell that renders and mutates state through the core instead of duplicating business state in the web layer.

Solution

  • Add a new openhuman.app_state_* controller set in the Rust core that returns a consolidated snapshot of auth/session, current user, onboarding/analytics flags, and local app state persisted under the workspace.
  • Add missing team RPC methods in the core for listing, mutating, and switching teams so the React app can stay on the core RPC path.
  • Introduce CoreStateProvider plus a small core-state client/store in the frontend, polling the sidecar and exposing refresh/mutation helpers to components.
  • Replace Redux-backed auth/user/team/intelligence reads in key screens with the polled core snapshot and simplify Intelligence filters to local UI state.
  • Keep Redux only for presentation/runtime state that is still local to the shell or socket/UI concerns.

Submission Checklist

  • Unit testsyarn typecheck and cargo check --manifest-path Cargo.toml pass for the refactor; dedicated unit coverage was not added in this cut.
  • E2E / integration — Not run in this pass; this refactor changes UI → Tauri → sidecar state flow and should get targeted follow-up coverage.
  • N/A — Not applicable because behavior and architecture changed materially.
  • Doc comments — Added module-level Rust docs for the new app_state surface.
  • Inline comments — Existing comments were preserved where useful; no extra comment-only churn added.

Impact

  • Desktop app architecture shifts toward core-owned business state with a thinner React shell.
  • Reduces frontend state duplication for auth/user/team/onboarding/wallet flows, but this is a significant behavioral refactor and deserves manual smoke coverage plus targeted E2E follow-up.
  • No mobile impact; the touched UI path is the current desktop/Tauri app and Rust core JSON-RPC surface.

Related

  • Issue(s): none linked
  • Follow-up PR(s)/TODOs: add integration/E2E coverage for auth bootstrap, onboarding, and team management over the new core-state polling path

Summary by CodeRabbit

  • New Features

    • Added a Core App-State provider with background polling and centralized session snapshot.
    • Exposed app-state RPCs and new RPC-backed team endpoints for listing, management, invites, and joining.
  • Refactor

    • Migrated auth/session, onboarding, analytics, wallet, teams, and settings to the core state provider.
    • Simplified socket/intelligence plumbing to rely on core state snapshots and no-op fallbacks where disconnected.
  • Tests

    • Updated tests to use the provider-backed core state and adjusted mocks accordingly.

…gement

- Replaced UserProvider with CoreStateProvider in App component to centralize state management.
- Updated various components to utilize the new CoreStateProvider for accessing session tokens and user data.
- Refactored hooks and components to eliminate direct Redux store dependencies, enhancing modularity and maintainability.
- Introduced a new core state management structure to improve the handling of user authentication and onboarding states.

This refactor aims to simplify state access across the application and improve overall code clarity.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 4, 2026

Warning

Rate limit exceeded

@senamakel has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 34 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 0 minutes and 34 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a63dd9ea-2393-401a-9e59-40607aea82c4

📥 Commits

Reviewing files that changed from the base of the PR and between 0a69904 and 8fc787c.

📒 Files selected for processing (11)
  • app/src/components/settings/panels/BillingPanel.tsx
  • app/src/components/settings/panels/billingHelpers.ts
  • app/src/services/socketService.ts
  • src/api/mod.rs
  • src/api/rest.rs
  • src/openhuman/app_state/ops.rs
  • src/openhuman/credentials/mod.rs
  • src/openhuman/team/ops.rs
  • src/openhuman/team/schemas.rs
  • tests/json_rpc_e2e.rs
  • tests/skills_notion_live.rs
📝 Walkthrough

Walkthrough

This PR migrates frontend auth/user/team/local-state flows from Redux to a new CoreState system: adds a CoreStateProvider that polls a core RPC, new core RPC endpoints and Rust controllers for app_state and teams, and updates many components/hooks/services/tests to read/write the core snapshot instead of Redux.

Changes

Cohort / File(s) Summary
Core State infra
app/src/lib/coreState/store.ts, app/src/providers/CoreStateProvider.tsx, app/src/services/coreStateApi.ts
Adds in-memory core snapshot store, a CoreStateProvider (polls RPC, exposes refresh/update actions), and typed RPC wrappers for snapshot/update/local-state/teams.
App bootstrap & providers
app/src/App.tsx, app/src/providers/SocketProvider.tsx, app/src/providers/IntelligenceProvider.tsx
Replaces UserProvider with CoreStateProvider, removes IntelligenceProvider, adapts SocketProvider to read token from core snapshot and adjust cleanup lifecycle.
Routing & guards
app/src/components/DefaultRedirect.tsx, app/src/components/ProtectedRoute.tsx, app/src/components/PublicRoute.tsx
All route guards now use useCoreState() (isBootstrapping, snapshot.sessionToken) for bootstrap/auth gating and redirects.
UI components (auth/token reads)
app/src/components/BottomTabBar.tsx, app/src/components/MiniSidebar.tsx, app/src/components/WalletInfoSection.tsx, app/src/components/OnboardingOverlay.tsx
Switched multiple components to read token/user/onboarding from core snapshot; onboarding now uses provider actions (setOnboardingCompletedFlag, setOnboardingTasks) not local Redux/RPC fallback.
Settings & team panels
app/src/components/settings/... (SettingsHome, BillingPanel, PrivacyPanel, RecoveryPhrasePanel, TeamInvitesPanel, TeamManagementPanel, TeamMembersPanel, TeamPanel)
Replaced Redux selectors/dispatches with useCoreState() snapshot and provider refresh functions; fetch flows use refresh/refreshTeams/refreshTeamMembers/refreshTeamInvites.
Hooks & socket manager
app/src/hooks/* (useUser.ts, useConsciousItems.ts, useWebhooks.ts, useIntelligenceSocket.ts, useIntelligenceSocketManager, useIntelligenceEvents)
Rewired hooks to source token/user from core snapshot; useUser now returns snapshot/refresh/isBootstrapping; intelligence socket APIs largely stubbed/no-op and manager reads snapshot token.
APIs & services
app/src/services/api/teamApi.ts, app/src/services/coreStateApi.ts, app/src/services/* (analytics.ts, socketService.ts, daemonHealthService.ts), app/src/lib/skills/manager.ts, app/src/main.tsx
Team API moved from REST to callCoreRpc; new coreStateApi module; services now read token/user from core snapshot; wallet persistence updated to call core RPC and patch snapshot; API client init uses snapshot token.
Store & selectors
app/src/store/index.ts, app/src/store/socketSelectors.ts
Removed persisted auth/user reducers and token-sync middleware; adjusted persistStore usage and exported RootState typing to preserve legacy type shape while runtime slices removed; socket selector now reads token from core snapshot.
Deep-linking & desktop listener
app/src/utils/desktopDeepLinkListener.ts, tests
Deep-link handling now awaits storeSession() and polls core snapshot readiness; legacy deep-link e2e tests trimmed/skipped and many tests converted to use CoreStateProvider mocks.
Rust backend: app_state & team RPCs
src/openhuman/app_state/*, src/openhuman/team/*, src/openhuman/mod.rs, src/core/all.rs
Adds app_state RPC module (snapshot & update_local_state) backed by disk JSON storage, registers new app_state controllers, and adds team RPC adapters, schemas and handlers for team list/get/create/update/delete/switch/leave/join.
Project deps & tests
Cargo.toml, app/src/.../__tests__, app/test/*
Moves tempfile to normal deps; many tests updated to mock/use CoreStateProvider and to assert provider-action calls rather than Redux state.

Sequence Diagram(s)

sequenceDiagram
    participant ReactApp as React App
    participant CoreProv as CoreStateProvider
    participant RPC as Tauri RPC Server
    participant Backend as Backend HTTP

    ReactApp->>CoreProv: mount / useCoreState()
    CoreProv->>CoreProv: init from getCoreStateSnapshot()
    loop poll every 3s
        CoreProv->>RPC: callCoreRpc("openhuman.app_state_snapshot")
        RPC->>Backend: (optional) GET /telegram/me with session token
        Backend-->>RPC: user data / empty
        RPC-->>CoreProv: AppStateSnapshot (token, user, teams, localState)
        CoreProv->>CoreProv: patch snapshot, set isBootstrapping/isReady
        CoreProv-->>ReactApp: context update (snapshot)
    end
    ReactApp->>CoreProv: update action (e.g., setWallet / setOnboarding)
    CoreProv->>RPC: callCoreRpc("openhuman.app_state_update_local_state", ...)
    RPC-->>CoreProv: updated stored state
    CoreProv->>CoreProv: merge patch into snapshot, notify
    ReactApp-->>ReactApp: re-render with new snapshot
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped from Redux fields to core,

Polling snapshots, knocking on RPC's door.
Tasks and tokens snug in one bright heap,
I guard the state while humans sleep. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: moving app state ownership from Redux/client to the Rust core via new RPC endpoints and a polling CoreStateProvider.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/openhuman/team/ops.rs (1)

35-38: ⚠️ Potential issue | 🟠 Major

Normalize api_url before joining request paths.

If effective_api_url() can return something like https://host/api without a trailing slash, base.join("teams") resolves to /teams and silently drops /api. That breaks every team RPC behind a path-prefixed deployment, and the same helper pattern should be fixed in src/openhuman/app_state/ops.rs too.

#!/bin/bash
set -euo pipefail

echo "effective_api_url implementation:"
rg -n -C4 '\beffective_api_url\b' src

echo
echo "api_url defaults/examples:"
rg -n -C2 'api_url' src app | sed -n '1,120p'

Expected result: if any normalized/default API URL ends in a path segment without a trailing /, the current Url::join(...) calls will strip that segment.

Also applies to: 133-135

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/team/ops.rs` around lines 35 - 38, The resolve_base function
uses effective_api_url and then calls Url::parse(...). Ensure the base URL is
normalized to always end with a trailing slash before any subsequent
base.join(...) calls so path segments are not dropped (e.g., if
effective_api_url returns "https://host/api" convert it to "https://host/api/");
update resolve_base to append a '/' when the parsed Url has a non-empty path and
does not already end with '/', and apply the same normalization wherever you
call base.join (including the analogous helper in app_state ops) so join
preserves the path prefix.
🧹 Nitpick comments (5)
app/src/utils/__tests__/desktopDeepLinkListener.test.ts (1)

1-5: Test coverage removed; follow-up needed.

The deep link listener tests have been skipped rather than updated for the new core-state storage. Per the PR summary, this is noted as follow-up work.

Consider opening a tracking issue to ensure these tests are rewritten to cover the new getCoreStateSnapshot() and storeSession() paths.

Do you want me to open an issue to track restoring test coverage for the deep link listener with the new core-session storage approach?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/utils/__tests__/desktopDeepLinkListener.test.ts` around lines 1 - 5,
The test file desktopDeepLinkListener.test.ts currently skips the legacy Redux
wiring; open a tracking issue and update the tests to exercise the new
core-session APIs by removing describe.skip and rewriting the test cases to call
the desktopDeepLinkListener code paths that now rely on getCoreStateSnapshot()
and storeSession() (mocking/stubbing those functions to simulate session state
and asserting the listener behavior), and add the issue ID to the test TODO so
CI fails if coverage is not restored.
app/src/hooks/useUser.ts (1)

12-12: Hardcoded error: null removes error visibility.

The previous Redux-based implementation likely surfaced errors from the user slice. Hardcoding error: null means consumers of useUser() will never see errors. If this is intentional (errors handled elsewhere), consider documenting it; otherwise, consider exposing error state from the core snapshot.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/hooks/useUser.ts` at line 12, The hook useUser currently hardcodes
"error: null", which hides any errors from the underlying user state; change the
implementation to read and return the error from the core snapshot (e.g., use
snapshot.error or snapshot?.error) instead of null so consumers receive real
error state, or if hiding errors is intentional add a comment/docs indicating
that behavior; update the returned object from useUser (the function named
useUser and any variables like snapshot or coreSnapshot) to propagate snapshot
error (e.g., error: snapshot?.error ?? null) rather than the fixed null.
app/src/pages/onboarding/Onboarding.tsx (1)

31-31: Unused useUser() hook call.

useUser() is invoked but its return value is not captured. If this is for triggering side effects (e.g., ensuring user data is loaded), consider adding a comment. Otherwise, this line can be removed.

💡 Options

If needed for side effects:

useUser(); // Ensure user context is initialized

If not needed:

-  useUser();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/pages/onboarding/Onboarding.tsx` at line 31, The call to useUser() in
Onboarding.tsx is unused; either remove the call if it's not needed, or
explicitly use its return value (e.g., const user = useUser() or destructure
needed fields) if the component depends on the user, or if the call is
intentionally only for side effects keep the call but add a comment like “//
ensure user context is initialized” so intent is clear; update the useUser()
usage accordingly in the Onboarding component.
app/src/components/settings/panels/TeamManagementPanel.tsx (1)

25-29: Consider avoiding fetch on every mount when teams array is legitimately empty.

The condition teams.length === 0 triggers a refresh on mount, but this will also fire if the user genuinely has no teams (edge case). This is a minor inefficiency rather than a bug, since refreshTeams() is idempotent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/TeamManagementPanel.tsx` around lines 25 -
29, The effect in TeamManagementPanel currently calls refreshTeams whenever
teams.length === 0 on mount, causing unnecessary fetches for users who truly
have no teams; update the component to fetch only once by tracking whether we've
already attempted the initial load (e.g., add a ref or state like
teamsFetchedRef/teamsInitialized) and change the useEffect to run refreshTeams
only if teams.length === 0 AND teamsFetchedRef.current is false, then set
teamsFetchedRef.current = true after calling refreshTeams; also ensure you
reset/mark fetched true if teams become non-empty so subsequent mounts won't
re-fetch unnecessarily.
app/src/services/coreStateApi.ts (1)

30-74: Align this new service with the repo’s TS export conventions.

The RPC helpers here are exported as function declarations, and updateCoreLocalState introduces another inline object-shape type. Converting the exports to const arrow functions and extracting the request payload into a named interface will keep this new shared service surface consistent and easier to reuse. As per coding guidelines, "Prefer arrow functions over function declarations" and "Prefer interface for defining object shapes in TypeScript".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/services/coreStateApi.ts` around lines 30 - 74, Convert the exported
function declarations to exported const arrow functions and extract the inline
param object type used by updateCoreLocalState into a named interface; e.g.,
create interface UpdateCoreLocalStateParams { encryptionKey?: string | null;
primaryWalletAddress?: string | null; onboardingTasks?: OnboardingTasks | null }
and interface OnboardingTasks { accessibilityPermissionGranted: boolean;
localModelConsentGiven: boolean; localModelDownloadStarted: boolean;
enabledTools: string[]; connectedSources: string[]; updatedAtMs?: number } then
change export function fetchCoreAppSnapshot, updateCoreLocalState, listTeams,
getTeamMembers, and getTeamInvites to exported consts using arrow syntax while
keeping their existing return types and callCoreRpc usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/App.tsx`:
- Around line 28-29: CoreStateProvider no longer triggers memory client
initialization; call the exported syncMemoryClientToken() during
CoreStateProvider's mount/initialization effect (or immediately after successful
session refresh in the same effect where storeSessionToken() is invoked) so the
memory RPC client (openhuman.memory_init) is initialized before any memory
operations; update the effect in CoreStateProvider to import and await
syncMemoryClientToken(), handle/ log errors, and ensure storeSessionToken()
still calls storeSession() while syncMemoryClientToken() runs right after or as
part of that flow.

In `@app/src/components/OnboardingOverlay.tsx`:
- Around line 43-46: Replace the synchronous state sync in the OnboardingOverlay
effect by removing the setOnboardingCompleted call and deriving the value
directly from snapshot.onboardingCompleted; specifically, delete the
setOnboardingCompleted usage inside the useEffect and instead read
onboardingCompleted from snapshot.onboardingCompleted (or compute a derived
value) wherever local onboardingCompleted state was used, eliminating the
redundant local state and the dependency on setOnboardingCompleted while keeping
the effect only for side-effects that truly require them.

In `@app/src/components/settings/panels/PrivacyPanel.tsx`:
- Around line 11-15: The handler handleToggleAnalytics flips runtime analytics
before the awaited persistent update, so if setAnalyticsEnabled(newValue) fails
the runtime state (syncAnalyticsConsent) will be wrong; change the order to
await setAnalyticsEnabled(newValue) first and only call
syncAnalyticsConsent(newValue) after the persistent write succeeds, and wrap the
await in try/catch to avoid leaving UI/runtime inconsistent (use
analyticsEnabled, setAnalyticsEnabled, and syncAnalyticsConsent references to
locate the code).

In `@app/src/components/settings/panels/TeamPanel.tsx`:
- Line 13: The hardcoded const isLoading = false in TeamPanel disables the
spinner; replace it by either reading a loading flag from useCoreState() (if
available) or implement local state (e.g., const [isLoading, setIsLoading] =
useState(false)) inside the TeamPanel component and update it around
refreshTeams() (set true before calling refreshTeams(), set false after
success/failure) so the existing spinner render (used in the TeamPanel render)
can activate during the fetch.

In `@app/src/hooks/useConsciousItems.ts`:
- Around line 144-145: The auth token is being captured once at render via
getCoreStateSnapshot() inside useConsciousItems, causing triggerAnalysis to use
a stale token; fix by making the token reactive either by replacing that
snapshot read with the useCoreState() hook (import and call useCoreState()
inside useConsciousItems and pull sessionToken) so authToken updates with state,
or if you must keep snapshot usage, defer reading the latest token inside the
triggerAnalysis callback (call getCoreStateSnapshot().snapshot.sessionToken at
invocation time) so the current sessionToken is used; update references to
authToken in triggerAnalysis accordingly (functions: useConsciousItems,
triggerAnalysis, getCoreStateSnapshot, useCoreState).

In `@app/src/hooks/useIntelligenceSocket.ts`:
- Around line 26-42: The effect must handle logout and token rotation: update
the useEffect that watches connect/isConnected/token so that when token becomes
falsy it calls disconnect (socketService.disconnect via the disconnect callback)
to tear down any authenticated socket, and when token changes while isConnected
is true it forces a reconnect (call socketService.disconnect then
socketService.connect with the new token or invoke the disconnect then connect
callbacks). Use the existing connect and disconnect callbacks (and
isConnected/token) so the effect explicitly disconnects on !token and performs a
disconnect+connect sequence when token changes to a different truthy value while
already connected.

In `@app/src/hooks/useUser.ts`:
- Line 11: The isLoading assignment in useUser uses a redundant ternary that
always yields false; replace the expression `isLoading:
!snapshot.auth.isAuthenticated && !snapshot.sessionToken ? false : false` with a
single deterministic value or correct derived logic—either set `isLoading:
false` if loading is handled elsewhere, or compute `isLoading` from
`snapshot.auth.isAuthenticated` and/or `snapshot.sessionToken` (e.g.,
`!snapshot.auth.isAuthenticated || !snapshot.sessionToken`) to reflect intended
behavior in the useUser hook.

In `@app/src/providers/CoreStateProvider.tsx`:
- Around line 77-91: The snapshot/teams commits can be overwritten by stale
async polls and don't clear identity-scoped caches when auth changes; update
refresh() and refreshTeams() to use a per-request token (e.g., capture a local
requestId or AbortController at start and compare to a shared currentRequestId)
so commits only apply if the token matches, and when applying a new snapshot in
commitState (inside refresh) detect auth identity changes (compare
snapshot.user?.id with previous snapshot.user?.id or handle null) and clear
identity-scoped fields teams, teamMembersById, and teamInvitesById when the user
became unauthenticated or switched users; likewise gate refreshTeams() commits
with the same token so stale listTeams responses are ignored. Ensure commitState
calls (in refresh and refreshTeams) only mutate the intended keys and perform
the cache clears when identity changes.

In `@app/src/utils/__tests__/authFlow.e2e.test.tsx`:
- Around line 3-4: The skipped test block describe.skip('Auth flow e2e (legacy
Redux bootstrap)') removes coverage for the auth bootstrap; port its assertions
into the core-state bootstrap E2E tests by recreating the same user interaction
assertions in the core-state bootstrap suite (rather than re-enabling the legacy
test). Locate the original assertions inside this file and replicate their
expectations in the core-state bootstrap test file/class that uses core-state
polling (use the same user interaction flows and helpers from the UI layer),
remove the skipped placeholder, and ensure the new tests run as true E2E UI
tests (trigger UI actions in src/ tests and assert resulting auth state via
core-state polling helpers like the core-state bootstrap test utilities).

In `@src/openhuman/app_state/ops.rs`:
- Around line 91-95: The current save_stored_app_state/read-modify-write in
update_local_state is vulnerable to races and partial writes; wrap all access to
the app-state file behind a process-wide mutex/shared store and perform atomic
persistence by writing to a temporary file and renaming into place.
Specifically, modify save_stored_app_state (and the code paths used by
update_local_state and the other affected block) to acquire a global mutex
before reading or writing the file obtained from app_state_path, serialize
StoredAppState to a string, write to a temp file in the same directory, fsync
the temp file and directory if possible, then rename the temp file to the target
path to ensure atomic replace; hold the mutex across the read-modify-write
sequence so two concurrent RPCs cannot clobber each other. Ensure errors from
serialization, temp-file write, fsync, and rename are propagated as before.
- Around line 79-89: The load_stored_app_state function should not return an
error on a malformed or unreadable app-state.json; instead, catch
fs::read_to_string and serde_json::from_str errors, log a warning identifying
the path and error, quarantine the bad file (e.g., rename to
app-state.json.corrupted.<timestamp> via app_state_path result) or remove it,
and then return Ok(StoredAppState::default()); apply the same resilient behavior
to the other parsing site mentioned (the similar serde_json::from_str call
around line 157) so snapshot bootstrap falls back to defaults rather than
failing.

In `@src/openhuman/app_state/schemas.rs`:
- Around line 54-67: The file src/openhuman/app_state/schemas.rs has formatting
issues in the "update_local_state" ControllerSchema arm and in the
handle_snapshot() section; run rustfmt (cargo fmt --all) to reformat the file
and ensure it passes cargo fmt --all -- --check, then re-run cargo check;
specifically reformat the "update_local_state" ControllerSchema construction and
the handle_snapshot() function so spacing, indentation, and trailing commas
match rustfmt style and project guidelines.

In `@src/openhuman/mod.rs`:
- Around line 14-17: Module declarations are out of alphabetical order: move the
line declaring pub mod app_state; to appear before pub mod agent; (so the block
reads about_app, accessibility, app_state, agent) or simply run cargo fmt --all
to automatically reorder and format the module declarations (ensure pub mod
app_state and pub mod agent are in alphabetical order).

In `@src/openhuman/team/schemas.rs`:
- Around line 162-230: The new ControllerSchema arms (e.g., "team_list_teams",
"team_get_team", "team_create_team", "team_update_team", "team_delete_team",
"team_switch_team", "team_leave_team", "team_join_team") are failing rustfmt;
run rustfmt by executing `cargo fmt --all` (or `cargo fmt --all -- --check`
locally to verify) to reformat src/openhuman/team/schemas.rs and then re-run
`cargo check` to ensure no formatting or compilation issues remain before
merging.
- Around line 475-482: The FieldSchema built by optional_string() currently sets
ty: TypeSchema::String but should represent an optional string; update
optional_string() to use ty: TypeSchema::Option(Box::new(TypeSchema::String))
(mirroring optional_u64()) while keeping required: false so the schema matches
UpdateTeamParams.name and other Option<String> fields and ensures
docs/validation reflect the optional type.

---

Outside diff comments:
In `@src/openhuman/team/ops.rs`:
- Around line 35-38: The resolve_base function uses effective_api_url and then
calls Url::parse(...). Ensure the base URL is normalized to always end with a
trailing slash before any subsequent base.join(...) calls so path segments are
not dropped (e.g., if effective_api_url returns "https://host/api" convert it to
"https://host/api/"); update resolve_base to append a '/' when the parsed Url
has a non-empty path and does not already end with '/', and apply the same
normalization wherever you call base.join (including the analogous helper in
app_state ops) so join preserves the path prefix.

---

Nitpick comments:
In `@app/src/components/settings/panels/TeamManagementPanel.tsx`:
- Around line 25-29: The effect in TeamManagementPanel currently calls
refreshTeams whenever teams.length === 0 on mount, causing unnecessary fetches
for users who truly have no teams; update the component to fetch only once by
tracking whether we've already attempted the initial load (e.g., add a ref or
state like teamsFetchedRef/teamsInitialized) and change the useEffect to run
refreshTeams only if teams.length === 0 AND teamsFetchedRef.current is false,
then set teamsFetchedRef.current = true after calling refreshTeams; also ensure
you reset/mark fetched true if teams become non-empty so subsequent mounts won't
re-fetch unnecessarily.

In `@app/src/hooks/useUser.ts`:
- Line 12: The hook useUser currently hardcodes "error: null", which hides any
errors from the underlying user state; change the implementation to read and
return the error from the core snapshot (e.g., use snapshot.error or
snapshot?.error) instead of null so consumers receive real error state, or if
hiding errors is intentional add a comment/docs indicating that behavior; update
the returned object from useUser (the function named useUser and any variables
like snapshot or coreSnapshot) to propagate snapshot error (e.g., error:
snapshot?.error ?? null) rather than the fixed null.

In `@app/src/pages/onboarding/Onboarding.tsx`:
- Line 31: The call to useUser() in Onboarding.tsx is unused; either remove the
call if it's not needed, or explicitly use its return value (e.g., const user =
useUser() or destructure needed fields) if the component depends on the user, or
if the call is intentionally only for side effects keep the call but add a
comment like “// ensure user context is initialized” so intent is clear; update
the useUser() usage accordingly in the Onboarding component.

In `@app/src/services/coreStateApi.ts`:
- Around line 30-74: Convert the exported function declarations to exported
const arrow functions and extract the inline param object type used by
updateCoreLocalState into a named interface; e.g., create interface
UpdateCoreLocalStateParams { encryptionKey?: string | null;
primaryWalletAddress?: string | null; onboardingTasks?: OnboardingTasks | null }
and interface OnboardingTasks { accessibilityPermissionGranted: boolean;
localModelConsentGiven: boolean; localModelDownloadStarted: boolean;
enabledTools: string[]; connectedSources: string[]; updatedAtMs?: number } then
change export function fetchCoreAppSnapshot, updateCoreLocalState, listTeams,
getTeamMembers, and getTeamInvites to exported consts using arrow syntax while
keeping their existing return types and callCoreRpc usage.

In `@app/src/utils/__tests__/desktopDeepLinkListener.test.ts`:
- Around line 1-5: The test file desktopDeepLinkListener.test.ts currently skips
the legacy Redux wiring; open a tracking issue and update the tests to exercise
the new core-session APIs by removing describe.skip and rewriting the test cases
to call the desktopDeepLinkListener code paths that now rely on
getCoreStateSnapshot() and storeSession() (mocking/stubbing those functions to
simulate session state and asserting the listener behavior), and add the issue
ID to the test TODO so CI fails if coverage is not restored.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eecdb781-a469-4663-bf31-e15993a6d8ef

📥 Commits

Reviewing files that changed from the base of the PR and between 4a228ee and 84cd179.

📒 Files selected for processing (47)
  • app/src/App.tsx
  • app/src/components/BottomTabBar.tsx
  • app/src/components/DefaultRedirect.tsx
  • app/src/components/MiniSidebar.tsx
  • app/src/components/OnboardingOverlay.tsx
  • app/src/components/ProtectedRoute.tsx
  • app/src/components/PublicRoute.tsx
  • app/src/components/WalletInfoSection.tsx
  • app/src/components/settings/SettingsHome.tsx
  • app/src/components/settings/panels/BillingPanel.tsx
  • app/src/components/settings/panels/PrivacyPanel.tsx
  • app/src/components/settings/panels/RecoveryPhrasePanel.tsx
  • app/src/components/settings/panels/TeamInvitesPanel.tsx
  • app/src/components/settings/panels/TeamManagementPanel.tsx
  • app/src/components/settings/panels/TeamMembersPanel.tsx
  • app/src/components/settings/panels/TeamPanel.tsx
  • app/src/hooks/useConsciousItems.ts
  • app/src/hooks/useIntelligenceSocket.ts
  • app/src/hooks/useUser.ts
  • app/src/hooks/useWebhooks.ts
  • app/src/lib/coreState/store.ts
  • app/src/lib/skills/manager.ts
  • app/src/main.tsx
  • app/src/pages/Intelligence.tsx
  • app/src/pages/Mnemonic.tsx
  • app/src/pages/onboarding/Onboarding.tsx
  • app/src/pages/onboarding/steps/MnemonicStep.tsx
  • app/src/providers/CoreStateProvider.tsx
  • app/src/providers/IntelligenceProvider.tsx
  • app/src/providers/SocketProvider.tsx
  • app/src/services/analytics.ts
  • app/src/services/api/teamApi.ts
  • app/src/services/coreStateApi.ts
  • app/src/services/daemonHealthService.ts
  • app/src/services/socketService.ts
  • app/src/store/index.ts
  • app/src/store/socketSelectors.ts
  • app/src/utils/__tests__/authFlow.e2e.test.tsx
  • app/src/utils/__tests__/desktopDeepLinkListener.test.ts
  • app/src/utils/desktopDeepLinkListener.ts
  • src/core/all.rs
  • src/openhuman/app_state/mod.rs
  • src/openhuman/app_state/ops.rs
  • src/openhuman/app_state/schemas.rs
  • src/openhuman/mod.rs
  • src/openhuman/team/ops.rs
  • src/openhuman/team/schemas.rs
💤 Files with no reviewable changes (1)
  • app/src/providers/IntelligenceProvider.tsx

Comment thread app/src/App.tsx
Comment thread app/src/components/OnboardingOverlay.tsx Outdated
Comment on lines +11 to 15
const handleToggleAnalytics = async () => {
const newValue = !analyticsEnabled;
dispatch(setAnalyticsForUser({ userId: user._id, enabled: newValue }));
syncAnalyticsConsent(newValue);

// Sync to core config so the Rust process also respects the setting
if (isTauri()) {
openhumanUpdateAnalyticsSettings({ enabled: newValue }).catch(() => {
/* best-effort */
});
}
await setAnalyticsEnabled(newValue);
};
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.

⚠️ Potential issue | 🟠 Major

Persist the consent change before flipping runtime analytics.

syncAnalyticsConsent(newValue) runs before the awaited core write. If setAnalyticsEnabled(newValue) fails, runtime collection is already changed while the saved setting and UI stay on the old value.

🔒 Persist first, then sync runtime state
   const handleToggleAnalytics = async () => {
     const newValue = !analyticsEnabled;
-    syncAnalyticsConsent(newValue);
-    await setAnalyticsEnabled(newValue);
+    await setAnalyticsEnabled(newValue);
+    syncAnalyticsConsent(newValue);
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleToggleAnalytics = async () => {
const newValue = !analyticsEnabled;
dispatch(setAnalyticsForUser({ userId: user._id, enabled: newValue }));
syncAnalyticsConsent(newValue);
// Sync to core config so the Rust process also respects the setting
if (isTauri()) {
openhumanUpdateAnalyticsSettings({ enabled: newValue }).catch(() => {
/* best-effort */
});
}
await setAnalyticsEnabled(newValue);
};
const handleToggleAnalytics = async () => {
const newValue = !analyticsEnabled;
await setAnalyticsEnabled(newValue);
syncAnalyticsConsent(newValue);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/PrivacyPanel.tsx` around lines 11 - 15,
The handler handleToggleAnalytics flips runtime analytics before the awaited
persistent update, so if setAnalyticsEnabled(newValue) fails the runtime state
(syncAnalyticsConsent) will be wrong; change the order to await
setAnalyticsEnabled(newValue) first and only call syncAnalyticsConsent(newValue)
after the persistent write succeeds, and wrap the await in try/catch to avoid
leaving UI/runtime inconsistent (use analyticsEnabled, setAnalyticsEnabled, and
syncAnalyticsConsent references to locate the code).

Comment thread app/src/components/settings/panels/TeamPanel.tsx Outdated
Comment thread app/src/hooks/useConsciousItems.ts Outdated
Comment thread src/openhuman/app_state/ops.rs Outdated
Comment thread src/openhuman/app_state/schemas.rs
Comment thread src/openhuman/mod.rs
Comment on lines 14 to 17
pub mod about_app;
pub mod accessibility;
pub mod app_state;
pub mod agent;
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.

⚠️ Potential issue | 🟡 Minor

Fix module declaration order to pass cargo fmt.

The pipeline failed because app_state is declared after agent but should come before it alphabetically. Run cargo fmt --all to fix the ordering.

🔧 Proposed fix
 pub mod about_app;
 pub mod accessibility;
-pub mod app_state;
 pub mod agent;
+pub mod app_state;
 pub mod approval;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub mod about_app;
pub mod accessibility;
pub mod app_state;
pub mod agent;
pub mod about_app;
pub mod accessibility;
pub mod agent;
pub mod app_state;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/mod.rs` around lines 14 - 17, Module declarations are out of
alphabetical order: move the line declaring pub mod app_state; to appear before
pub mod agent; (so the block reads about_app, accessibility, app_state, agent)
or simply run cargo fmt --all to automatically reorder and format the module
declarations (ensure pub mod app_state and pub mod agent are in alphabetical
order).

Comment thread src/openhuman/team/schemas.rs Outdated
Comment thread src/openhuman/team/schemas.rs
- Reintroduced the import of polyfills in main.tsx to ensure compatibility.
- Adjusted import statements in PrivacyPanel and SocketProvider for consistency.
- Simplified conditional expressions in TeamInvitesPanel and TeamMembersPanel for better readability.
- Refactored CoreStateProvider and store.ts to streamline state initialization.
- Enhanced formatting in various files for improved code clarity and maintainability.
- Updated OnboardingOverlay tests to utilize mock state management and simplify assertions.
- Refactored ProtectedRoute tests to improve readability and ensure consistent use of mock state.
- Enhanced teamApi tests to replace direct API calls with core RPC method calls, improving test isolation and clarity.
- Adjusted socketSelectors tests to utilize a centralized core state snapshot for better state management during tests.
…e loading logic

- Removed unnecessary local state for onboarding completion in OnboardingOverlay, directly utilizing snapshot data.
- Enhanced loading logic to prevent unnecessary renders and improve user experience during onboarding.
- Updated PrivacyPanel to handle analytics consent persistence with error handling.
- Refactored TeamManagementPanel and TeamPanel to improve team data fetching logic and loading states.
- Streamlined CoreStateProvider to manage session token synchronization and state updates more effectively.
…ment

- Added `tempfile` dependency to Cargo.toml for improved temporary file handling.
- Refactored app state loading and saving functions to enhance error handling and ensure data integrity.
- Introduced quarantine mechanism for corrupted app state files to prevent application crashes.
- Updated URL parsing logic to ensure proper formatting of API URLs.
- Adjusted type definitions in schemas for better clarity and consistency.
…d route tests

- Consolidated mock state management in OnboardingOverlay and ProtectedRoute tests for improved readability.
- Streamlined assertions by reducing unnecessary lines in test setups.
- Enhanced consistency in mock return values across tests to ensure clarity and maintainability.
- Updated PublicRoute tests to utilize MemoryRouter for improved routing simulation.
- Simplified mock state management by integrating `useCoreState` for user authentication scenarios.
- Streamlined test assertions and removed redundant preloaded state configurations for clarity and maintainability.
…nic tests

- Simplified mock state management in PublicRoute and Mnemonic tests for improved readability.
- Consolidated mock return values to reduce redundancy and enhance clarity in test setups.
- Improved consistency in the usage of `useCoreState` across test files.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/store/index.ts (1)

59-70: ⚠️ Potential issue | 🔴 Critical

Don't remove these runtime slices until the remaining consumers are migrated.

RootState = RuntimeRootState & LegacyRootState only hides the breakage from TypeScript; configureStore still stops mounting auth, user, team, and intelligence at runtime. Consumers that still read those paths — e.g. app/src/providers/UserProvider.tsx:32-33 and app/src/store/authSelectors.ts:3-7 in the provided context — will now receive undefined and can throw as soon as they dereference the missing slice. Keep the legacy reducers mounted (or stub them with the old shape) until those readers are removed, then delete the shim in the same PR.

Also applies to: 88-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/store/index.ts` around lines 59 - 70, The configureStore call removed
legacy runtime slices causing consumers to get undefined for
auth/user/team/intelligence; restore the legacy reducers (or add stubs with the
original state shape) to the reducer map used by configureStore so slices like
auth, user, team, and intelligence remain mounted until consumers are migrated;
update persistedAiReducer/persistedThreadReducer usage only after ensuring
RootState (RuntimeRootState & LegacyRootState) matches the actual mounted
reducers and references to authSelectors and UserProvider keep receiving the
expected shape.
🧹 Nitpick comments (7)
app/src/services/coreStateApi.ts (1)

20-36: Consider narrowing the auth.user type.

auth.user is typed as unknown | null while currentUser is properly typed as User | null. If they share the same shape, consolidate to User | null for consistency and better type safety downstream.

♻️ Suggested type refinement
 interface AppStateSnapshotResult {
   auth: {
     isAuthenticated: boolean;
     userId: string | null;
-    user: unknown | null;
+    user: User | null;
     profileId: string | null;
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/services/coreStateApi.ts` around lines 20 - 36, Update the
AppStateSnapshotResult type to tighten auth.user from unknown | null to User |
null so it matches currentUser; locate the AppStateSnapshotResult interface and
change the auth.user property type to User | null (ensuring any imports or type
references for User are available) so downstream code benefits from consistent,
stronger typing between auth.user and currentUser.
src/openhuman/team/schemas.rs (1)

68-84: Add contract tests for the new team controller IDs.

These namespace/function pairs are now consumed directly by app/src/services/api/teamApi.ts, but the Rust tests in this module only pin team_list_members and overall list lengths. A typo in any new controller name or input field would still compile and only fail at runtime as an unknown method. A small table-driven test over the new schemas would catch that drift on the Rust side first.

Based on learnings: Keep functionality validation in Rust tests for rust-core.

Also applies to: 148-246

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/team/schemas.rs` around lines 68 - 84, Add a table-driven
contract test that verifies each controller schema returned by
all_team_controller_schemas() contains the expected namespace/function IDs used
by the JS client; iterate the vector produced by all_team_controller_schemas()
(and/or call team_schemas() for each expected ID like "team_list_members",
"team_list_teams", "team_get_team", "team_create_team", "team_update_team",
"team_delete_team", "team_switch_team", "team_leave_team", "team_join_team",
"team_create_invite", "team_list_invites", "team_revoke_invite",
"team_remove_member", "team_change_member_role") and assert the schema.namespace
and schema.function (or equivalent fields on ControllerSchema) match the
expected strings so a typo or missing schema will fail at test time; add this
test to the same test module that contains all_team_controller_schemas().
app/src/providers/CoreStateProvider.tsx (1)

198-219: RPC success/failure responses are not validated before committing state.

Both setAnalyticsEnabled and setOnboardingCompletedFlag await the RPC call but don't check the response. Per context snippet 4, setOnboardingCompleted returns Promise<boolean> indicating success/failure, and openhumanUpdateAnalyticsSettings returns CommandResponse<ConfigSnapshot>.

If the core returns false or an error response (but doesn't throw), the local state is still optimistically updated and may drift from the actual persisted state until the next poll.

Consider validating responses:

 const setOnboardingCompletedFlag = useCallback(
   async (value: boolean) => {
-    await setOnboardingCompleted(value);
+    const success = await setOnboardingCompleted(value);
+    if (!success) {
+      console.warn('[core-state] onboarding flag update rejected by core');
+      return;
+    }
     commitState(previous => ({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/providers/CoreStateProvider.tsx` around lines 198 - 219, Both
callbacks optimistically commit local state without validating RPC results;
update setAnalyticsEnabled and setOnboardingCompletedFlag to inspect the RPC
responses from openhumanUpdateAnalyticsSettings
(CommandResponse<ConfigSnapshot>) and setOnboardingCompleted (Promise<boolean>)
before calling commitState or syncAnalyticsConsent: if the RPC indicates success
use commitState as now, if it indicates failure or returns false do not mutate
snapshot (or revert) and surface/log the error so UI state stays consistent with
core. Reference the functions setAnalyticsEnabled, setOnboardingCompletedFlag,
openhumanUpdateAnalyticsSettings, setOnboardingCompleted, commitState and
syncAnalyticsConsent when implementing the checks and error handling.
src/openhuman/team/ops.rs (3)

263-276: update_team allows sending an empty update body.

If name is None or whitespace-only, the function sends PUT /teams/:teamId with {}. This may be intentional for future extensibility, but currently the only field is name, so an empty update is a no-op.

Consider either returning early if no fields to update, or documenting this as intentional:

 pub async fn update_team(
     config: &Config,
     team_id: &str,
     name: Option<&str>,
 ) -> Result<RpcOutcome<Value>, String> {
     let team_id = normalize_id(team_id, "teamId")?;
     let path = build_api_path(&["teams", &team_id])?;
     let mut body = serde_json::Map::new();
     if let Some(name) = name.map(str::trim).filter(|value| !value.is_empty()) {
         body.insert("name".to_string(), Value::String(name.to_string()));
     }
+    // Note: Empty body is allowed for forward compatibility with additional fields.
     let data = get_authed_value(config, Method::PUT, &path, Some(Value::Object(body))).await?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/team/ops.rs` around lines 263 - 276, The update_team function
currently sends an empty PUT when no updatable fields are provided; modify
update_team so that after building the JSON body (the serde_json::Map used
before calling get_authed_value) you check if body.is_empty() and return an Err
with a clear message (e.g. "no fields to update") instead of calling
get_authed_value; reference the update_team function and its use of
normalize_id, build_api_path, body (serde_json::Map), get_authed_value and
RpcOutcome to locate where to add this early-return check.

243-261: Simplify create_team serialization.

The TeamNameBody struct is only used in create_team and serialized via json!() macro. This is redundant - you can either use the struct directly with serde_json::to_value() or skip the struct entirely:

-#[derive(Debug, Serialize)]
-struct TeamNameBody<'a> {
-    name: &'a str,
-}
-
 pub async fn create_team(config: &Config, name: &str) -> Result<RpcOutcome<Value>, String> {
     let trimmed = name.trim();
     if trimmed.is_empty() {
         return Err("name is required".to_string());
     }
     let data = get_authed_value(
         config,
         Method::POST,
         "/teams",
-        Some(json!(TeamNameBody { name: trimmed })),
+        Some(json!({ "name": trimmed })),
     )
     .await?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/team/ops.rs` around lines 243 - 261, Remove the unused
TeamNameBody struct and simplify create_team by passing a plain JSON object for
the request body; replace json!(TeamNameBody { name: trimmed }) with
json!({"name": trimmed}) (or alternatively use serde_json::to_value if you
prefer a typed value) in the get_authed_value call so create_team,
get_authed_value, and RpcOutcome::single_log remain unchanged.

35-44: Extract duplicate resolve_base function to shared utility module.

The resolve_base function is duplicated identically in both src/openhuman/team/ops.rs (lines 35-44) and src/openhuman/app_state/ops.rs (lines 210-219). This DRY violation risks divergent behavior if one is updated without the other.

Additionally, src/openhuman/billing/ops.rs implements a variant without trailing slash normalization, indicating this function has multiple valid forms across the codebase.

Consider creating a shared utility module (e.g., crate::openhuman::api_utils or similar) with both versions, then import and use appropriately in each ops.rs file. Run cargo fmt and cargo check before merging any refactoring.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/team/ops.rs` around lines 35 - 44, The duplicated resolve_base
function should be extracted to a shared utility module (e.g.,
crate::openhuman::api_utils) and replaced in both callers; create two exported
helpers to cover both behaviors (e.g., resolve_base_with_trailing_slash and
resolve_base_without_trailing_normalization) implement the parsing and error
mapping once, move the trailing-slash normalization into the
_with_trailing_slash variant, update usages in the Team ops and AppState ops to
import and call the appropriate helper (replace local resolve_base references),
and run cargo fmt and cargo check to verify the refactor.
app/src/hooks/useIntelligenceSocket.ts (1)

8-18: Consider adding JSDoc or removing unused no-op stubs.

useIntelligenceSocket returns no-op functions (sendMessage, sendChatInit, sendTyping) that are never called by consumers (per context, Intelligence.tsx only uses isConnected). This could confuse future maintainers who might expect these to work.

Either:

  1. Remove the unused stubs if the functionality is permanently deprecated
  2. Add a comment explaining these are intentional no-ops pending future implementation
 export const useIntelligenceSocket = () => {
   const socketStatus = useAppSelector(selectSocketStatus);

+  // Note: send* methods are no-ops after Redux intelligence removal.
+  // Socket messaging is now handled directly by the core.
   return {
     isConnected: socketStatus === 'connected',
     isReady: socketStatus === 'connected',
     sendMessage: async () => {},
     sendChatInit: async () => {},
     sendTyping: () => {},
   };
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/hooks/useIntelligenceSocket.ts` around lines 8 - 18, The hook
useIntelligenceSocket currently returns no-op functions (sendMessage,
sendChatInit, sendTyping) that are unused; either remove these unused properties
from the returned object so callers only receive isConnected/isReady, or add a
concise JSDoc above useIntelligenceSocket stating that sendMessage,
sendChatInit, and sendTyping are intentionally no-ops/pending implementation;
update references inside the hook (sendMessage/sendChatInit/sendTyping) and
ensure isConnected/isReady remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/components/__tests__/ProtectedRoute.test.tsx`:
- Around line 22-40: The test currently only verifies ProtectedRoute children
are absent when mockUseCoreState returns isBootstrapping: true; change it to
assert the actual loading UI is rendered by checking for RouteLoadingScreen
(e.g., its stable text or test hook) rather than just absence of "Protected
Content". Update the case in ProtectedRoute.test.tsx to mockUseCoreState({
isBootstrapping: true, snapshot: { sessionToken: null }}) then use renderRoute
and assert screen.getByText or getByTestId for the RouteLoadingScreen output (or
the existing test utility from app/src/test/test-utils.tsx) so the test fails if
the component returns null.

In `@app/src/components/settings/panels/TeamInvitesPanel.tsx`:
- Around line 21-36: Change the invites handling to distinguish "not loaded"
from "loaded empty" by making invites undefined on cache-miss (const invites =
currentTeamId ? teamInvitesById[currentTeamId] : undefined) instead of
defaulting to [], remove the local isLoadingInvites state and its
setIsLoadingInvites calls in the useEffect, and simply call void
refreshTeamInvites(currentTeamId) when currentTeamId changes; this avoids
synchronous setState in the useEffect and eliminates the shared loading race—if
you must show a loading UI, derive it from invites === undefined (or expose a
loading flag from useCoreState) and, if refreshTeamInvites can race, add a
request token/abort handling inside refreshTeamInvites or use a ref-based
requestId check so stale .finally handlers don’t clear state for a newer team.

In `@app/src/lib/coreState/store.ts`:
- Around line 60-85: patchCoreStateSnapshot causes optimistic local changes to
be lost by a stale poll because setCoreStateSnapshot blindly replaces
currentState; fix by adding an optimistic-update timestamp and merging logic:
have patchCoreStateSnapshot (and any caller like manager.ts) write a
lastLocalUpdate timestamp inside currentState.snapshot.localState metadata, then
change setCoreStateSnapshot to compare incoming snapshot timestamps and
merge/preserve any localState fields whose lastLocalUpdate is newer than the
incoming snapshot (or skip applying the poll entirely if its snapshot timestamp
is older than lastLocalUpdate). Update getCoreStateSnapshot usage accordingly so
callers read the merged state; this keeps optimistic updates (e.g.,
localState.primaryWalletAddress) from being overwritten by stale poll responses
while keeping poll-driven full replacements when newer.

---

Outside diff comments:
In `@app/src/store/index.ts`:
- Around line 59-70: The configureStore call removed legacy runtime slices
causing consumers to get undefined for auth/user/team/intelligence; restore the
legacy reducers (or add stubs with the original state shape) to the reducer map
used by configureStore so slices like auth, user, team, and intelligence remain
mounted until consumers are migrated; update
persistedAiReducer/persistedThreadReducer usage only after ensuring RootState
(RuntimeRootState & LegacyRootState) matches the actual mounted reducers and
references to authSelectors and UserProvider keep receiving the expected shape.

---

Nitpick comments:
In `@app/src/hooks/useIntelligenceSocket.ts`:
- Around line 8-18: The hook useIntelligenceSocket currently returns no-op
functions (sendMessage, sendChatInit, sendTyping) that are unused; either remove
these unused properties from the returned object so callers only receive
isConnected/isReady, or add a concise JSDoc above useIntelligenceSocket stating
that sendMessage, sendChatInit, and sendTyping are intentionally no-ops/pending
implementation; update references inside the hook
(sendMessage/sendChatInit/sendTyping) and ensure isConnected/isReady remain
unchanged.

In `@app/src/providers/CoreStateProvider.tsx`:
- Around line 198-219: Both callbacks optimistically commit local state without
validating RPC results; update setAnalyticsEnabled and
setOnboardingCompletedFlag to inspect the RPC responses from
openhumanUpdateAnalyticsSettings (CommandResponse<ConfigSnapshot>) and
setOnboardingCompleted (Promise<boolean>) before calling commitState or
syncAnalyticsConsent: if the RPC indicates success use commitState as now, if it
indicates failure or returns false do not mutate snapshot (or revert) and
surface/log the error so UI state stays consistent with core. Reference the
functions setAnalyticsEnabled, setOnboardingCompletedFlag,
openhumanUpdateAnalyticsSettings, setOnboardingCompleted, commitState and
syncAnalyticsConsent when implementing the checks and error handling.

In `@app/src/services/coreStateApi.ts`:
- Around line 20-36: Update the AppStateSnapshotResult type to tighten auth.user
from unknown | null to User | null so it matches currentUser; locate the
AppStateSnapshotResult interface and change the auth.user property type to User
| null (ensuring any imports or type references for User are available) so
downstream code benefits from consistent, stronger typing between auth.user and
currentUser.

In `@src/openhuman/team/ops.rs`:
- Around line 263-276: The update_team function currently sends an empty PUT
when no updatable fields are provided; modify update_team so that after building
the JSON body (the serde_json::Map used before calling get_authed_value) you
check if body.is_empty() and return an Err with a clear message (e.g. "no fields
to update") instead of calling get_authed_value; reference the update_team
function and its use of normalize_id, build_api_path, body (serde_json::Map),
get_authed_value and RpcOutcome to locate where to add this early-return check.
- Around line 243-261: Remove the unused TeamNameBody struct and simplify
create_team by passing a plain JSON object for the request body; replace
json!(TeamNameBody { name: trimmed }) with json!({"name": trimmed}) (or
alternatively use serde_json::to_value if you prefer a typed value) in the
get_authed_value call so create_team, get_authed_value, and
RpcOutcome::single_log remain unchanged.
- Around line 35-44: The duplicated resolve_base function should be extracted to
a shared utility module (e.g., crate::openhuman::api_utils) and replaced in both
callers; create two exported helpers to cover both behaviors (e.g.,
resolve_base_with_trailing_slash and
resolve_base_without_trailing_normalization) implement the parsing and error
mapping once, move the trailing-slash normalization into the
_with_trailing_slash variant, update usages in the Team ops and AppState ops to
import and call the appropriate helper (replace local resolve_base references),
and run cargo fmt and cargo check to verify the refactor.

In `@src/openhuman/team/schemas.rs`:
- Around line 68-84: Add a table-driven contract test that verifies each
controller schema returned by all_team_controller_schemas() contains the
expected namespace/function IDs used by the JS client; iterate the vector
produced by all_team_controller_schemas() (and/or call team_schemas() for each
expected ID like "team_list_members", "team_list_teams", "team_get_team",
"team_create_team", "team_update_team", "team_delete_team", "team_switch_team",
"team_leave_team", "team_join_team", "team_create_invite", "team_list_invites",
"team_revoke_invite", "team_remove_member", "team_change_member_role") and
assert the schema.namespace and schema.function (or equivalent fields on
ControllerSchema) match the expected strings so a typo or missing schema will
fail at test time; add this test to the same test module that contains
all_team_controller_schemas().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1fe97db8-3d8e-4e67-b128-1a71848db443

📥 Commits

Reviewing files that changed from the base of the PR and between 84cd179 and 903e6b0.

📒 Files selected for processing (31)
  • Cargo.toml
  • app/src/components/MiniSidebar.tsx
  • app/src/components/OnboardingOverlay.tsx
  • app/src/components/__tests__/OnboardingOverlay.test.tsx
  • app/src/components/__tests__/ProtectedRoute.test.tsx
  • app/src/components/settings/panels/PrivacyPanel.tsx
  • app/src/components/settings/panels/TeamInvitesPanel.tsx
  • app/src/components/settings/panels/TeamManagementPanel.tsx
  • app/src/components/settings/panels/TeamMembersPanel.tsx
  • app/src/components/settings/panels/TeamPanel.tsx
  • app/src/hooks/useConsciousItems.ts
  • app/src/hooks/useIntelligenceSocket.ts
  • app/src/hooks/useUser.ts
  • app/src/lib/coreState/store.ts
  • app/src/lib/skills/manager.ts
  • app/src/main.tsx
  • app/src/pages/Intelligence.tsx
  • app/src/pages/onboarding/Onboarding.tsx
  • app/src/providers/CoreStateProvider.tsx
  • app/src/providers/SocketProvider.tsx
  • app/src/services/api/__tests__/teamApi.test.ts
  • app/src/services/api/teamApi.ts
  • app/src/services/coreStateApi.ts
  • app/src/services/socketService.ts
  • app/src/store/__tests__/socketSelectors.test.ts
  • app/src/store/index.ts
  • src/openhuman/app_state/ops.rs
  • src/openhuman/app_state/schemas.rs
  • src/openhuman/mod.rs
  • src/openhuman/team/ops.rs
  • src/openhuman/team/schemas.rs
✅ Files skipped from review due to trivial changes (3)
  • app/src/hooks/useConsciousItems.ts
  • src/openhuman/app_state/schemas.rs
  • app/src/components/settings/panels/TeamMembersPanel.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • app/src/components/MiniSidebar.tsx
  • app/src/main.tsx
  • app/src/components/settings/panels/TeamPanel.tsx
  • app/src/components/settings/panels/TeamManagementPanel.tsx
  • app/src/pages/Intelligence.tsx
  • app/src/providers/SocketProvider.tsx

Comment on lines +22 to +40
it('renders a loading screen while bootstrapping', () => {
mockUseCoreState.mockReturnValue({
isBootstrapping: true,
snapshot: { sessionToken: null },
});

renderRoute(
<Route
path="/"
element={
<ProtectedRoute>
<div>Protected Content</div>
</ProtectedRoute>
}
/>
);

expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
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.

⚠️ Potential issue | 🟡 Minor

Assert the loading UI itself here.

This test still passes if ProtectedRoute returns null. The production component renders RouteLoadingScreen while isBootstrapping is true, so please assert that screen (or its stable test hook) instead of only checking that children are absent.

Based on learnings: Prefer testing behavior over implementation details; use existing helpers from app/src/test/ (test-utils.tsx, shared mock backend) before adding new harness code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/__tests__/ProtectedRoute.test.tsx` around lines 22 - 40,
The test currently only verifies ProtectedRoute children are absent when
mockUseCoreState returns isBootstrapping: true; change it to assert the actual
loading UI is rendered by checking for RouteLoadingScreen (e.g., its stable text
or test hook) rather than just absence of "Protected Content". Update the case
in ProtectedRoute.test.tsx to mockUseCoreState({ isBootstrapping: true,
snapshot: { sessionToken: null }}) then use renderRoute and assert
screen.getByText or getByTestId for the RouteLoadingScreen output (or the
existing test utility from app/src/test/test-utils.tsx) so the test fails if the
component returns null.

Comment on lines +21 to +36
const invites = currentTeamId ? (teamInvitesById[currentTeamId] ?? []) : [];

const [isGenerating, setIsGenerating] = useState(false);
const [copiedId, setCopiedId] = useState<string | null>(null);
const [revokingId, setRevokingId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [isLoadingInvites, setIsLoadingInvites] = useState(false);

// Confirmation modal state
const [inviteToRevoke, setInviteToRevoke] = useState<{ id: string; code: string } | null>(null);

useEffect(() => {
if (currentTeamId) dispatch(fetchInvites(currentTeamId));
}, [currentTeamId, dispatch]);
if (!currentTeamId) return;
setIsLoadingInvites(true);
void refreshTeamInvites(currentTeamId).finally(() => setIsLoadingInvites(false));
}, [currentTeamId, refreshTeamInvites]);
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.

⚠️ Potential issue | 🟠 Major

Keep "not loaded yet" distinct from "loaded empty" for invites.

const invites = ... ?? [] plus isLoadingInvites starting at false means each team change can render the "No invites yet" branch once before the effect runs. The shared flag also races: an older refreshTeamInvites() can still hit .finally() and clear loading for a newer currentTeamId. Treat the cache miss as its own state (undefined vs []) or expose loading from useCoreState instead of toggling local state in the effect.

Based on learnings: In React components, do not perform synchronous setState (or other state-updating calls) directly inside useEffect bodies. The lint rule react-hooks/set-state-in-effect (enforced in this codebase) disallows patterns such as resetting state flags at the top of an effect before returning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/TeamInvitesPanel.tsx` around lines 21 -
36, Change the invites handling to distinguish "not loaded" from "loaded empty"
by making invites undefined on cache-miss (const invites = currentTeamId ?
teamInvitesById[currentTeamId] : undefined) instead of defaulting to [], remove
the local isLoadingInvites state and its setIsLoadingInvites calls in the
useEffect, and simply call void refreshTeamInvites(currentTeamId) when
currentTeamId changes; this avoids synchronous setState in the useEffect and
eliminates the shared loading race—if you must show a loading UI, derive it from
invites === undefined (or expose a loading flag from useCoreState) and, if
refreshTeamInvites can race, add a request token/abort handling inside
refreshTeamInvites or use a ref-based requestId check so stale .finally handlers
don’t clear state for a newer team.

Comment on lines +60 to +85
export function getCoreStateSnapshot(): CoreState {
return currentState;
}

export function setCoreStateSnapshot(next: CoreState): void {
currentState = next;
}

export function patchCoreStateSnapshot(patch: {
snapshot?: Record<string, unknown> & { localState?: Partial<CoreLocalState> };
[key: string]: unknown;
}): void {
currentState = {
...currentState,
...patch,
snapshot: patch.snapshot
? {
...currentState.snapshot,
...patch.snapshot,
localState: patch.snapshot.localState
? { ...currentState.snapshot.localState, ...patch.snapshot.localState }
: currentState.snapshot.localState,
}
: currentState.snapshot,
};
}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all usages of patchCoreStateSnapshot to understand the race window
rg -n 'patchCoreStateSnapshot' --type=ts -C3

Repository: tinyhumansai/openhuman

Length of output: 1465


🏁 Script executed:

# Find all usages of setCoreStateSnapshot
rg -n 'setCoreStateSnapshot' --type=ts -B2 -A2

Repository: tinyhumansai/openhuman

Length of output: 4147


🏁 Script executed:

# Look for CoreStateProvider and polling mechanism
rg -n 'CoreStateProvider\|setInterval\|polling' --type=tsx --type=ts -B2 -A2

Repository: tinyhumansai/openhuman

Length of output: 93


🏁 Script executed:

# Check the full manager.ts context around line 528 to understand the RPC flow
head -n 540 app/src/lib/skills/manager.ts | tail -n 30

Repository: tinyhumansai/openhuman

Length of output: 990


🏁 Script executed:

# Get the full CoreStateProvider.tsx to understand polling
cat -n app/src/providers/CoreStateProvider.tsx

Repository: tinyhumansai/openhuman

Length of output: 11030


🏁 Script executed:

# Search for setInterval or useEffect with polling
rg -n 'useEffect|setInterval|clearInterval|refreshCoreState|3000|3 ?s' --type=tsx --type=ts app/src/providers/CoreStateProvider.tsx -A5

Repository: tinyhumansai/openhuman

Length of output: 93


🏁 Script executed:

# Get complete context of patchCoreStateSnapshot usage in manager.ts
sed -n '515,540p' app/src/lib/skills/manager.ts

Repository: tinyhumansai/openhuman

Length of output: 865


🏁 Script executed:

# Check if there are any other usages of patchCoreStateSnapshot
rg 'patchCoreStateSnapshot' --type=ts -l

Repository: tinyhumansai/openhuman

Length of output: 126


🏁 Script executed:

# Check the store.ts file fully to understand all functions
cat -n app/src/lib/coreState/store.ts | head -n 90

Repository: tinyhumansai/openhuman

Length of output: 2922


Race condition: patchCoreStateSnapshot updates can be lost to polling.

The patchCoreStateSnapshot function applies partial updates to the singleton, but polling in CoreStateProvider runs every 3 seconds, calling fetchCoreAppSnapshot() and then setCoreStateSnapshot() with a complete replacement of currentState. This creates a race condition where:

  1. manager.ts calls patchCoreStateSnapshot to optimistically update localState.primaryWalletAddress after an RPC write
  2. A polling cycle begins before the RPC persists to the server
  3. The poll fetches stale data and calls setCoreStateSnapshot, overwriting the optimistic patch with stale values

The wallet address (and any other patched state) is silently lost until the next poll after the RPC has propagated.

The requestId guard in CoreStateProvider.refresh() only protects against stale POLL results overwriting newer POLL results; it does not prevent local patches from being overwritten by stale poll responses.

Consider either:

  • Removing patchCoreStateSnapshot and calling refresh() after mutations instead
  • Having setCoreStateSnapshot preserve locally patched fields (requires tracking mutations)
  • Using optimistic update timestamps to skip stale poll responses
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/lib/coreState/store.ts` around lines 60 - 85, patchCoreStateSnapshot
causes optimistic local changes to be lost by a stale poll because
setCoreStateSnapshot blindly replaces currentState; fix by adding an
optimistic-update timestamp and merging logic: have patchCoreStateSnapshot (and
any caller like manager.ts) write a lastLocalUpdate timestamp inside
currentState.snapshot.localState metadata, then change setCoreStateSnapshot to
compare incoming snapshot timestamps and merge/preserve any localState fields
whose lastLocalUpdate is newer than the incoming snapshot (or skip applying the
poll entirely if its snapshot timestamp is older than lastLocalUpdate). Update
getCoreStateSnapshot usage accordingly so callers read the merged state; this
keeps optimistic updates (e.g., localState.primaryWalletAddress) from being
overwritten by stale poll responses while keeping poll-driven full replacements
when newer.

- Cleaned up import statements in BillingPanel.tsx for better organization.
- Enhanced formatting of billing plan descriptions in billingHelpers.ts for consistency.
- Improved readability of conditional checks in BillingPanel component.
- Updated references from `/settings` to `/auth/me` in various modules to standardize user authentication flows.
- Renamed `parse_settings_response_json` to `parse_api_response_json` for improved clarity in response handling.
- Adjusted user ID extraction functions to reflect the new endpoint structure, enhancing maintainability across the codebase.
- Updated test cases to align with the new endpoint naming conventions, ensuring consistency in API interactions.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/components/__tests__/PublicRoute.test.tsx (1)

19-83: ⚠️ Potential issue | 🟡 Minor

Add the bootstrapping case here.

These cases only cover authenticated vs unauthenticated behavior. PublicRoute also renders RouteLoadingScreen while isBootstrapping is true, and this new core-state harness is exactly what needs to exercise that path.

Based on learnings: Prefer testing behavior over implementation details; use existing helpers from app/src/test/ (test-utils.tsx, shared mock backend) before adding new harness code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/__tests__/PublicRoute.test.tsx` around lines 19 - 83, Add
a new test in PublicRoute.test.tsx that covers the bootstrapping path by using
mockUseCoreState to return { isBootstrapping: true, snapshot: { sessionToken:
null } } and calling the existing renderRoute helper with a Route that renders
<PublicRoute> children; assert that RouteLoadingScreen (or its visible
text/role) is shown instead of the children. Reuse the existing test
setup/helpers (renderRoute, mockUseCoreState) and assert visible behavior rather
than internal implementation details.
♻️ Duplicate comments (2)
app/src/providers/CoreStateProvider.tsx (1)

198-218: ⚠️ Potential issue | 🔴 Critical

Invalidate in-flight snapshot polls before local snapshot mutations.

These setters update snapshot locally, but they never advance snapshotRequestIdRef.current. If a poll started earlier is still in flight, its response still passes the stale-check in refresh() and can overwrite the newer local state. The dangerous case is clearSession(): a slow pre-logout snapshot can restore sessionToken/currentUser right after logout.

🔧 Suggested fix
+  const invalidateSnapshotRefreshes = useCallback(() => {
+    snapshotRequestIdRef.current += 1;
+  }, []);
+
   const setAnalyticsEnabled = useCallback(
     async (enabled: boolean) => {
+      invalidateSnapshotRefreshes();
       await openhumanUpdateAnalyticsSettings({ enabled });
       commitState(previous => ({
         ...previous,
         snapshot: { ...previous.snapshot, analyticsEnabled: enabled },
       }));
       syncAnalyticsConsent(enabled);
     },
-    [commitState]
+    [commitState, invalidateSnapshotRefreshes]
   );

   const setOnboardingCompletedFlag = useCallback(
     async (value: boolean) => {
+      invalidateSnapshotRefreshes();
       await setOnboardingCompleted(value);
       commitState(previous => ({
         ...previous,
         snapshot: { ...previous.snapshot, onboardingCompleted: value },
       }));
     },
-    [commitState]
+    [commitState, invalidateSnapshotRefreshes]
   );
@@
-  const clearSession = useCallback(async () => {
+  const clearSession = useCallback(async () => {
+    invalidateSnapshotRefreshes();
     await tauriLogout();
     commitState(previous => ({
       ...previous,
       teams: [],
       teamMembersById: {},
@@
     }));
     memoryTokenRef.current = null;
-  }, [commitState]);
+  }, [commitState, invalidateSnapshotRefreshes]);

Also applies to: 244-260

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/providers/CoreStateProvider.tsx` around lines 198 - 218, When
mutating the local snapshot in setters like setAnalyticsEnabled and
setOnboardingCompletedFlag (and other snapshot-updating setters),
increment/invalidate snapshotRequestIdRef.current before calling commitState so
any earlier in-flight refresh() responses fail the stale-check; i.e., bump
snapshotRequestIdRef.current (or call a helper like advanceSnapshotRequestId())
immediately prior to the local commit and then proceed to call commitState and
related side-effects (syncAnalyticsConsent, setOnboardingCompleted) to prevent
older poll results from overwriting the new local snapshot (apply same change
for clearSession and the other snapshot mutators).
app/src/components/__tests__/ProtectedRoute.test.tsx (1)

20-35: ⚠️ Potential issue | 🟡 Minor

Assert the loading UI itself here.

This still passes if ProtectedRoute returns null. Since the production branch renders RouteLoadingScreen while bootstrapping, please check that screen's stable text or test hook instead of only asserting that Protected Content is absent.

Based on learnings: Prefer testing behavior over implementation details; use existing helpers from app/src/test/ (test-utils.tsx, shared mock backend) before adding new harness code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/__tests__/ProtectedRoute.test.tsx` around lines 20 - 35,
The test only asserts absence of "Protected Content" which would pass if
ProtectedRoute returned null; update the test to assert the actual loading UI is
rendered while bootstrapping by checking for RouteLoadingScreen's stable text or
test hook instead of just absence; specifically, in ProtectedRoute.test (where
mockUseCoreState is set to { isBootstrapping: true }), replace or add an
assertion that looks for the loading indicator produced by RouteLoadingScreen
(stable text or data-testid) using screen.getByText or getByTestId after
renderRoute, and prefer using existing test helpers from
app/src/test/test-utils.tsx and the shared mock backend rather than adding new
harness code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/providers/CoreStateProvider.tsx`:
- Around line 88-126: The refresh function applies a snapshot only if requestId
matches snapshotRequestIdRef.current, but side effects (syncAnalyticsConsent and
syncMemoryClientToken) still run for discarded snapshots; modify refresh to
check the same requestId after commitState and bail out early if it no longer
matches snapshotRequestIdRef.current so you skip calling syncAnalyticsConsent
and any memoryTokenRef/syncMemoryClientToken logic for stale snapshots;
reference the refresh function, snapshotRequestIdRef, commitState,
syncAnalyticsConsent, syncMemoryClientToken, and memoryTokenRef when making this
change.
- Around line 146-166: Guard team member/invite refreshes against auth/session
changes: in refreshTeamMembers and refreshTeamInvites (which call
getTeamMembers/getTeamInvites and then commitState), capture a stable
auth/session identifier from the current state (e.g., current user id or account
id) before awaiting the network call, and after the await verify the identifier
is unchanged; only call commitState to merge results when the auth/session id
still matches. This prevents a late response from getTeamMembers/getTeamInvites
from overwriting data after a logout or account switch (similar to the
protection used in refreshTeams).

---

Outside diff comments:
In `@app/src/components/__tests__/PublicRoute.test.tsx`:
- Around line 19-83: Add a new test in PublicRoute.test.tsx that covers the
bootstrapping path by using mockUseCoreState to return { isBootstrapping: true,
snapshot: { sessionToken: null } } and calling the existing renderRoute helper
with a Route that renders <PublicRoute> children; assert that RouteLoadingScreen
(or its visible text/role) is shown instead of the children. Reuse the existing
test setup/helpers (renderRoute, mockUseCoreState) and assert visible behavior
rather than internal implementation details.

---

Duplicate comments:
In `@app/src/components/__tests__/ProtectedRoute.test.tsx`:
- Around line 20-35: The test only asserts absence of "Protected Content" which
would pass if ProtectedRoute returned null; update the test to assert the actual
loading UI is rendered while bootstrapping by checking for RouteLoadingScreen's
stable text or test hook instead of just absence; specifically, in
ProtectedRoute.test (where mockUseCoreState is set to { isBootstrapping: true
}), replace or add an assertion that looks for the loading indicator produced by
RouteLoadingScreen (stable text or data-testid) using screen.getByText or
getByTestId after renderRoute, and prefer using existing test helpers from
app/src/test/test-utils.tsx and the shared mock backend rather than adding new
harness code.

In `@app/src/providers/CoreStateProvider.tsx`:
- Around line 198-218: When mutating the local snapshot in setters like
setAnalyticsEnabled and setOnboardingCompletedFlag (and other snapshot-updating
setters), increment/invalidate snapshotRequestIdRef.current before calling
commitState so any earlier in-flight refresh() responses fail the stale-check;
i.e., bump snapshotRequestIdRef.current (or call a helper like
advanceSnapshotRequestId()) immediately prior to the local commit and then
proceed to call commitState and related side-effects (syncAnalyticsConsent,
setOnboardingCompleted) to prevent older poll results from overwriting the new
local snapshot (apply same change for clearSession and the other snapshot
mutators).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 76cbaf46-716b-4dac-9bb1-a8b35fd0d1a5

📥 Commits

Reviewing files that changed from the base of the PR and between 903e6b0 and 0a69904.

📒 Files selected for processing (7)
  • app/src/components/__tests__/OnboardingOverlay.test.tsx
  • app/src/components/__tests__/ProtectedRoute.test.tsx
  • app/src/components/__tests__/PublicRoute.test.tsx
  • app/src/hooks/useUser.ts
  • app/src/providers/CoreStateProvider.tsx
  • app/src/store/__tests__/socketSelectors.test.ts
  • app/test/Mnemonic.test.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/store/tests/socketSelectors.test.ts
  • app/src/hooks/useUser.ts
  • app/src/components/tests/OnboardingOverlay.test.tsx

Comment on lines +88 to +126
const refresh = useCallback(async () => {
const requestId = ++snapshotRequestIdRef.current;
const snapshot = normalizeSnapshot(await fetchCoreAppSnapshot());
commitState(previous => {
if (requestId !== snapshotRequestIdRef.current) {
return previous;
}

const previousIdentity = snapshotIdentity(previous.snapshot);
const nextIdentity = snapshotIdentity(snapshot);
const shouldClearScopedCaches =
previousIdentity !== nextIdentity ||
(previous.snapshot.auth.isAuthenticated && !snapshot.auth.isAuthenticated);

return {
...previous,
isBootstrapping: false,
isReady: true,
snapshot,
teams: shouldClearScopedCaches ? [] : previous.teams,
teamMembersById: shouldClearScopedCaches ? {} : previous.teamMembersById,
teamInvitesById: shouldClearScopedCaches ? {} : previous.teamInvitesById,
};
});
syncAnalyticsConsent(snapshot.analyticsEnabled);

if (!snapshot.sessionToken) {
memoryTokenRef.current = null;
return;
}

if (memoryTokenRef.current !== snapshot.sessionToken) {
try {
await syncMemoryClientToken(snapshot.sessionToken);
memoryTokenRef.current = snapshot.sessionToken;
} catch (error) {
console.warn('[core-state] memory client sync failed during refresh:', error);
}
}
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.

⚠️ Potential issue | 🟠 Major

Skip analytics/token side effects when a snapshot was discarded.

commitState() correctly drops stale fetchCoreAppSnapshot() results, but the code below still runs syncAnalyticsConsent() and syncMemoryClientToken() for that stale snapshot. A slow pre-logout/pre-login poll can therefore flip consent or re-sync an old session token even though the snapshot was never applied.

🔧 Suggested fix
   const refresh = useCallback(async () => {
     const requestId = ++snapshotRequestIdRef.current;
     const snapshot = normalizeSnapshot(await fetchCoreAppSnapshot());
+    let applied = false;
     commitState(previous => {
       if (requestId !== snapshotRequestIdRef.current) {
         return previous;
       }
+      applied = true;

       const previousIdentity = snapshotIdentity(previous.snapshot);
       const nextIdentity = snapshotIdentity(snapshot);
       const shouldClearScopedCaches =
         previousIdentity !== nextIdentity ||
@@
         teamInvitesById: shouldClearScopedCaches ? {} : previous.teamInvitesById,
       };
     });
+    if (!applied) {
+      return;
+    }
+
     syncAnalyticsConsent(snapshot.analyticsEnabled);

     if (!snapshot.sessionToken) {
       memoryTokenRef.current = null;
       return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/providers/CoreStateProvider.tsx` around lines 88 - 126, The refresh
function applies a snapshot only if requestId matches
snapshotRequestIdRef.current, but side effects (syncAnalyticsConsent and
syncMemoryClientToken) still run for discarded snapshots; modify refresh to
check the same requestId after commitState and bail out early if it no longer
matches snapshotRequestIdRef.current so you skip calling syncAnalyticsConsent
and any memoryTokenRef/syncMemoryClientToken logic for stale snapshots;
reference the refresh function, snapshotRequestIdRef, commitState,
syncAnalyticsConsent, syncMemoryClientToken, and memoryTokenRef when making this
change.

Comment on lines +146 to +166
const refreshTeamMembers = useCallback(
async (teamId: string) => {
const members = await getTeamMembers(teamId);
commitState(previous => ({
...previous,
teamMembersById: { ...previous.teamMembersById, [teamId]: members },
}));
},
[commitState]
);

const refreshTeamInvites = useCallback(
async (teamId: string) => {
const invites = await getTeamInvites(teamId);
commitState(previous => ({
...previous,
teamInvitesById: { ...previous.teamInvitesById, [teamId]: invites },
}));
},
[commitState]
);
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.

⚠️ Potential issue | 🟠 Major

Guard team member/invite refreshes against auth changes.

Unlike refreshTeams(), these loaders always merge the response. If logout or an account switch happens while either request is in flight, the late response repopulates previous-user team data into the new session.

🔒 Suggested fix
   const refreshTeamMembers = useCallback(
     async (teamId: string) => {
+      const identityAtStart = snapshotIdentity(getCoreStateSnapshot().snapshot);
       const members = await getTeamMembers(teamId);
-      commitState(previous => ({
-        ...previous,
-        teamMembersById: { ...previous.teamMembersById, [teamId]: members },
-      }));
+      commitState(previous => {
+        if (snapshotIdentity(previous.snapshot) !== identityAtStart) {
+          return previous;
+        }
+        return {
+          ...previous,
+          teamMembersById: { ...previous.teamMembersById, [teamId]: members },
+        };
+      });
     },
     [commitState]
   );

   const refreshTeamInvites = useCallback(
     async (teamId: string) => {
+      const identityAtStart = snapshotIdentity(getCoreStateSnapshot().snapshot);
       const invites = await getTeamInvites(teamId);
-      commitState(previous => ({
-        ...previous,
-        teamInvitesById: { ...previous.teamInvitesById, [teamId]: invites },
-      }));
+      commitState(previous => {
+        if (snapshotIdentity(previous.snapshot) !== identityAtStart) {
+          return previous;
+        }
+        return {
+          ...previous,
+          teamInvitesById: { ...previous.teamInvitesById, [teamId]: invites },
+        };
+      });
     },
     [commitState]
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/providers/CoreStateProvider.tsx` around lines 146 - 166, Guard team
member/invite refreshes against auth/session changes: in refreshTeamMembers and
refreshTeamInvites (which call getTeamMembers/getTeamInvites and then
commitState), capture a stable auth/session identifier from the current state
(e.g., current user id or account id) before awaiting the network call, and
after the await verify the identifier is unchanged; only call commitState to
merge results when the auth/session id still matches. This prevents a late
response from getTeamMembers/getTeamInvites from overwriting data after a logout
or account switch (similar to the protection used in refreshTeams).

@senamakel senamakel merged commit ef708bd into tinyhumansai:main Apr 4, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant