Skip to content

fix(accounts): reset transient webview status on rehydrate#1536

Open
obchain wants to merge 1 commit into
tinyhumansai:mainfrom
obchain:fix/1379-reset-account-status-on-rehydrate
Open

fix(accounts): reset transient webview status on rehydrate#1536
obchain wants to merge 1 commit into
tinyhumansai:mainfrom
obchain:fix/1379-reset-account-status-on-rehydrate

Conversation

@obchain
Copy link
Copy Markdown
Contributor

@obchain obchain commented May 12, 2026

Summary

Embedded-app tabs (Slack, Discord, WhatsApp, Telegram, ...) could open straight onto the " is taking longer than expected." overlay right after reopening the desktop app, even though every CEF browser is destroyed at shutdown so the timeout had no live session left to be about.

One verified root cause is a Redux-persist hydration leak. accountsPersistConfig whitelists the full accounts blob — including Account.status — so a transient status (timeout / loading / pending / open / error) set in the previous session was replayed on the next cold boot before any new webview spawn had started. WebviewHost reads status === 'timeout' straight from Redux, so the retry overlay rendered before frame 1.

Treat any non-closed status as session-local and reset it to closed on REHYDRATE for the accounts persist key, matching the existing pattern in notificationSlice. The persisted account directory, ordering, activeAccountId, lastActiveAccountId, and provider login session (owned by the CEF user-data-dir, not Redux) are untouched, so users still land on their accounts list with logins intact.

What changed

  • app/src/store/accountsSlice.ts — new REHYDRATE extraReducer that flips every account's transient status to closed, clears lastError, and emits a accounts:rehydrate debug log with the reset count + previous statuses for future triage.
  • app/src/store/__tests__/accountsSlice.rehydrate.test.ts — 10 vitest cases covering every transient status, the closed-stays-closed invariant, multi-account directories, the persisted MRU pointer, and REHYDRATE actions targeted at sibling persist keys.

Acceptance criteria — coverage

  • Repro gone — for the rehydration path: the stale persisted status no longer triggers the timeout overlay on cold start; webview lifecycle controls the surface again.
  • Session recovery — only Redux session state is reset. Provider login state lives in the CEF user-data-dir (not Redux) and is untouched, so users do not re-sign-in.
  • Retry works — retry path (retryWebviewAccountLoad) is unchanged. If the live load times out the user still gets the retry overlay.
  • Startup diagnosticsaccounts:rehydrate debug namespace logs the reset count + previous statuses so future Sentry/log captures can confirm the hydration path was taken.
  • Per-tab isolation — reset is per-account; no global navigation effect.
  • Regression safety — 10 new vitest cases, full accountsSlice suite 28/28 green, typecheck + prettier + eslint (0 errors) clean.
  • Diff coverage ≥ 80% — both changed files have direct test coverage of every new branch.

Scope caveat — not closing the issue

This PR addresses one verified root cause that is clearly wrong on its own (transient session state has no business surviving a process restart). I did not reproduce the original Slack screenshot end-to-end — that needs a real Slack OAuth + dirty-close + cold-reopen cycle with a slow network. Other plausible paths to the same overlay are not touched here:

  • CDP watchdog re-firing on session restore before the page settles.
  • CEF user-data-dir state after a dirty shutdown.
  • Slack web-app cold-boot genuinely exceeding IDLE_BUDGET (8s).
  • Network races on reopen.

Keeping #1379 open until the reporter confirms the repro is gone in a release build; if the overlay still appears, the remaining causes above are the next follow-up. Linking with Refs instead of Closes on purpose.

Refs #1379

Summary by CodeRabbit

  • Bug Fixes
    • Account statuses that should be temporary are now reset when the app restarts, and associated error messages are cleared to prevent transient UI overlays from reappearing.

Review Change Stack

Embedded-app tabs (Slack, Discord, WhatsApp, Telegram, ...) could open
straight onto the "<provider> is taking longer than expected" overlay
right after reopening the desktop app. One verified root cause is a
Redux-persist hydration leak: Account.status is part of the persisted
accounts blob, so a `timeout` (or `loading` / `pending` / `open` /
`error`) set in the previous session was replayed on boot before any
new webview spawn had started, even though every CEF browser is
destroyed at shutdown.

Treat any non-`closed` status as session-local and reset it to
`closed` on REHYDRATE for the `accounts` persist key, matching the
existing pattern in notificationSlice. Account directory, order,
activeAccountId, lastActiveAccountId, and provider login sessions
(owned by CEF user-data-dir, not Redux) are untouched. Adds an
`accounts:rehydrate` debug log capturing the reset count + previous
statuses so future Sentry/log captures can confirm the hydration path
was taken.

Covered by a new rehydrate suite that exercises every transient
status, multi-account directories, the persisted MRU pointer, and
REHYDRATE actions targeted at sibling persist keys.

Refs tinyhumansai#1379
@obchain obchain requested a review from a team May 12, 2026 11:20
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6326178-cfb5-4fa1-81d6-3970386977af

📥 Commits

Reviewing files that changed from the base of the PR and between 78d1f3d and 4d27019.

📒 Files selected for processing (2)
  • app/src/store/__tests__/accountsSlice.rehydrate.test.ts
  • app/src/store/accountsSlice.ts

📝 Walkthrough

Walkthrough

Adds redux-persist REHYDRATE handling to accountsSlice that resets transient account statuses to 'closed' and clears lastError on app rehydration. Includes a comprehensive test suite verifying status reset, field preservation, and edge cases.

Changes

Account status rehydration

Layer / File(s) Summary
Transient status definition and REHYDRATE handler
app/src/store/accountsSlice.ts
Introduces debug logger, defines TRANSIENT_ACCOUNT_STATUSES constant, and adds REHYDRATE extra-reducer handler that resets transient statuses to 'closed' and clears lastError for each account on redux-persist rehydration.
REHYDRATE test suite
app/src/store/__tests__/accountsSlice.rehydrate.test.ts
Provides makeAccount, seedState, and rehydrate test helpers, then validates that transient statuses are reset to 'closed', lastError is cleared, persisted fields are preserved (order, activeAccountId, lastActiveAccountId, account labels), non-accounts persist keys are ignored, and rehydration handles empty state.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • tinyhumansai/openhuman#1285: Modifies accountsSlice.ts and AccountsState shape with lastActiveAccountId and MRU reducers, both PRs change the accounts slice and related state management.

Poem

A rabbit hops through state so grand,
Clearing dust from every land,
Transient whispers fade away—
Fresh, reborn with each new day! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: resetting transient account status values to 'closed' on Redux rehydration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Clean, well-scoped fix — the REHYDRATE handler follows the notificationSlice pattern exactly, TRANSIENT_ACCOUNT_STATUSES is exhaustive against AccountStatus, Immer mutations are correct, debug logging uses the right namespace with no PII, and the 10 test cases cover every stated invariant. Two minor polish suggestions inline — neither is a blocker.

builder.addCase(REHYDRATE, (state, action) => {
const rehydrateAction = action as {
type: typeof REHYDRATE;
key: string;
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.

[minor] The rehydrateAction cast declares payload?: Partial<AccountsState> but the implementation never reads it — it works entirely off state.accounts (the Immer draft), which is correct because persistReducer merges the payload into state before this inner reducer fires.

A one-line comment would prevent the next reader from reaching for payload by mistake:

Suggested change
key: string;
if (rehydrateAction.key !== 'accounts') return;
// `persistReducer` merges `action.payload` into `state` before this
// inner reducer fires, so `state.accounts` already holds the hydrated
// data — we reset in-place rather than reading from payload.
const reset: Array<{ id: string; previous: AccountStatus }> = [];

return reducer(state, { type: REHYDRATE, key, payload: state } as unknown as {
type: typeof REHYDRATE;
});
}
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.

[minor] This helper passes state as both the reducer's first argument and payload. It works because they're the same object in isolation, but in the real app persistReducer merges the payload into initialState first — so state and payload would normally differ on a fresh reducer instance.

Not a correctness issue (the reducer reads state.accounts, not payload), but a comment here would clarify the assumption:

// Bypasses persistReducer — state doubles as payload. This is fine
// because the slice reads from the Immer draft, not action.payload.
function rehydrate(state: AccountsState, key = 'accounts') {

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.

2 participants