Skip to content

Harden runtime project switching#453

Merged
arul28 merged 1 commit into
mainfrom
codex/harden-runtime-project-switching
May 31, 2026
Merged

Harden runtime project switching#453
arul28 merged 1 commit into
mainfrom
codex/harden-runtime-project-switching

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented May 31, 2026

Summary

  • Authorize pending local project roots during desktop project switching so renderer calls and event streams do not race the main-process binding.
  • Align local runtime IPC timeouts with the longer project setup window to prevent large-project opens from tripping the old 30s timeout path.
  • Preserve Work state across same-window project tabs and prevent delayed usage-cache reads from overwriting fresher pushed snapshots.

Validation

  • Verified with Codex Computer Use: opened ADE, Versic, and perf pass in one ADE window; cold opens completed without projects.add timeout, warm switches were ~17-18ms, and ADE Work session state was preserved after switching away and back.
  • git diff --check
  • npm --prefix apps/desktop run typecheck
  • npm --prefix apps/desktop run lint
  • npm --prefix apps/desktop run build
  • Desktop Vitest shards 1/8 through 8/8 passed.
  • npm --prefix apps/ade-cli run test
  • npm --prefix apps/ade-cli run typecheck
  • npm --prefix apps/ade-cli run build
  • node scripts/validate-docs.mjs
  • iOS simulator suite passed earlier: 339/339 tests.

Summary by CodeRabbit

  • New Features

    • Enhanced project switching to safely handle in-flight project selections.
  • Bug Fixes

    • Fixed route rendering to prevent inactive project surfaces from displaying content.
    • Improved usage data caching to avoid stale data overwrites.
  • Performance

    • Increased timeout for local project initialization to improve reliability on slower systems.
    • Optimized usage snapshot fetching by relying on cache freshness instead of periodic polling.

Greptile Summary

This PR hardens the desktop app's runtime project-switching flow by closing three distinct race windows: renderer calls that fire before the main-process binding is established, local runtime IPC calls that time out during large-project cold opens, and stale usage-cache reads that overwrite fresher pushed snapshots.

  • Pending root authorization (main.ts, runtimeBridge.ts): a reference-counted windowPendingProjectRoots map pre-authorizes both the user-selected path and the resolved git root for the duration of openProject, so renderer IPC and event-stream calls that race the binding step are not rejected. Cleanup is guaranteed in a finally block.
  • IPC timeout alignment (ipcTimeouts.ts, localRuntimeConnectionPool.ts): the projects.add socket timeout grows from 3s → 120s and the renderer-side IPC deadline from 30s → 150s, keeping the outer deadline above the inner one so the socket call can always resolve before the IPC layer gives up.
  • Inactive-tab isolation (App.tsx): the Lanes surface and catch-all Routes block are now gated on the active flag, preventing background project tabs from mounting their Lanes component against the currently active runtime.
  • Usage control simplification (HeaderUsageControl.tsx): removes the 120s polling interval and focus-triggered force-refresh in favour of a single cached read on mount plus onUpdate push subscription, with a timestamp guard to prevent late-resolving stale reads from overwriting fresher data.

Confidence Score: 4/5

Safe to merge; the core project-switching hardening is well-structured and test-covered. The usage control refactor has two edge cases worth a follow-up.

The pending-root authorization, timeout alignment, and inactive-tab isolation changes are all clearly scoped, have tests, and do not touch security-sensitive paths. The usage component change removes active polling in favour of push-only updates, and its staleness guard has a subtle gap where a cached snapshot with no lastPolledAt will overwrite a fresher pushed snapshot — neither issue blocks the project-switching fix but both are worth addressing before the usage refactor causes user-visible regression in long sessions.

apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx — the shouldApplyCachedSnapshot null-timestamp branch and the removal of all periodic refresh paths deserve a second look.

Important Files Changed

Filename Overview
apps/desktop/src/main/main.ts Adds reference-counted windowPendingProjectRoots map; authorizes both selectedPath and the resolved repoRoot as pending before bindWindowToProject is called, cleaning up in a finally block. Logic is correct and thread-safe for the single-threaded main process.
apps/desktop/src/main/services/ipc/ipcTimeouts.ts Splits local and remote runtime timeout paths; local runtime channels now use 150s to cover cold project setup, while remote keeps the previous 30s. The IPC timeout (150s) correctly exceeds the socket-level projects.add timeout (120s).
apps/desktop/src/main/services/ipc/runtimeBridge.ts Adds pendingLocalProjectRoots to the authorized roots set in collectAuthorizedLocalRuntimeRoots; change is minimal and uses the same normalization path as the rest of the function.
apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.ts Raises LOCAL_RUNTIME_PROJECT_TIMEOUT_MS from 3s to 120s for the projects.add call; the previous 3s was too low for large-project cold opens.
apps/desktop/src/preload/preload.ts In switchToPath, the local runtime binding is updated to the target path before the async IPC call; on error the previous binding is restored.
apps/desktop/src/renderer/components/app/App.tsx Gates shouldRenderLanes and the catch-all Routes block on active, preventing inactive project tabs from mounting their Lanes surface against the currently active runtime.
apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx Removes periodic interval and focus-triggered refresh; replaces with cached read + onUpdate push subscription. The nextTimestamp == null branch in shouldApplyCachedSnapshot is overly permissive.
apps/desktop/src/main/services/ipc/registerIpc.ts Extends the getWindowSession callback type to expose pendingLocalProjectRoots as an optional field; purely additive.

Sequence Diagram

sequenceDiagram
    participant R as Renderer
    participant P as Preload
    participant M as Main (openProject)
    participant B as runtimeBridge
    participant L as LocalRuntimePool

    R->>P: switchToPath("/new-repo")
    P->>P: rememberProjectBinding(nextBinding "/new-repo")
    P->>M: IPC projectSwitchToPath
    activate M
    M->>M: authorizePendingWindowProjectRoot(windowId, "/new-repo")
    M->>M: resolveRepoRoot → "/new-repo"
    M->>M: authorizePendingWindowProjectRoot(windowId, repoRoot) [if different]
    R->>B: "localRuntimeCallAction { rootPath: "/new-repo" }"
    B->>B: collectAuthorizedLocalRuntimeRoots → includes pendingLocalProjectRoots
    B->>L: callActionForRoot("/new-repo") [authorized via pending]
    L-->>B: result
    B-->>R: result
    M->>L: "projects.add { rootPath } [timeout: 120s]"
    L-->>M: ok
    M->>M: bindWindowToProject(windowId, repoRoot)
    deactivate M
    M->>M: finally: pendingRepoRootCleanup() + pendingSelectedRootCleanup()
    M-->>P: ProjectInfo
    P->>P: rememberProjectBinding(nextBinding or resolved)
Loading

Comments Outside Diff (1)

  1. apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx, line 152-183 (link)

    P2 Usage data becomes permanently stale if push updates stop

    The periodic 120-second interval and focus-triggered refresh have been removed. The component now relies entirely on onUpdate push notifications for ongoing freshness. In a long-running session where the push mechanism stops delivering (backend process restarts, IPC hiccup, etc.) the usage meter will show stale data indefinitely — there is no automatic recovery path, only the manual refresh button inside the drawer that users must discover. If onUpdate is guaranteed to be driven by a background polling loop that is independent of this component, the risk is low; but if the subscriber count reaching zero could pause that loop, the component is left dark. Consider keeping at least a coarse background refresh (e.g., every 10 minutes) as a fallback to bound the maximum staleness.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx
    Line: 152-183
    
    Comment:
    **Usage data becomes permanently stale if push updates stop**
    
    The periodic 120-second interval and focus-triggered refresh have been removed. The component now relies entirely on `onUpdate` push notifications for ongoing freshness. In a long-running session where the push mechanism stops delivering (backend process restarts, IPC hiccup, etc.) the usage meter will show stale data indefinitely — there is no automatic recovery path, only the manual refresh button inside the drawer that users must discover. If `onUpdate` is guaranteed to be driven by a background polling loop that is independent of this component, the risk is low; but if the subscriber count reaching zero could pause that loop, the component is left dark. Consider keeping at least a coarse background refresh (e.g., every 10 minutes) as a fallback to bound the maximum staleness.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx:86-95
**Null timestamp treated as "apply" rather than "skip"**

When `nextTimestamp == null` (i.e., the cached snapshot has a missing or unparseable `lastPolledAt`), the guard returns `true` and applies the cached snapshot even if `currentSnapshot` is a fresher, valid push update. A scenario where this bites: a push notification arrives with a valid timestamp, `snapshotRef.current` is set to that data, then `readCachedSnapshot` resolves with a stale or stripped snapshot that has no `lastPolledAt` — the guard would overwrite the fresher state with the stale one. Flipping that branch to `return false` (treat an undatable cached snapshot as unsafe to apply) would prevent stale overwrites while still letting valid older-timestamped snapshots be filtered by the `>=` comparison.

### Issue 2 of 2
apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx:152-183
**Usage data becomes permanently stale if push updates stop**

The periodic 120-second interval and focus-triggered refresh have been removed. The component now relies entirely on `onUpdate` push notifications for ongoing freshness. In a long-running session where the push mechanism stops delivering (backend process restarts, IPC hiccup, etc.) the usage meter will show stale data indefinitely — there is no automatic recovery path, only the manual refresh button inside the drawer that users must discover. If `onUpdate` is guaranteed to be driven by a background polling loop that is independent of this component, the risk is low; but if the subscriber count reaching zero could pause that loop, the component is left dark. Consider keeping at least a coarse background refresh (e.g., every 10 minutes) as a fallback to bound the maximum staleness.

Reviews (1): Last reviewed commit: "Harden runtime project switching" | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored May 31, 2026 7:44am

@arul28 arul28 merged commit 1940368 into main May 31, 2026
11 of 12 checks passed
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bbc09780-c86d-4a3d-a7d1-e471331c7d0a

📥 Commits

Reviewing files that changed from the base of the PR and between 3f363b9 and 1ad7b8b.

📒 Files selected for processing (14)
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/ipc/ipcTimeouts.test.ts
  • apps/desktop/src/main/services/ipc/ipcTimeouts.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/ipc/runtimeBridge.test.ts
  • apps/desktop/src/main/services/ipc/runtimeBridge.ts
  • apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.test.ts
  • apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.ts
  • apps/desktop/src/preload/preload.test.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/components/app/App.tsx
  • apps/desktop/src/renderer/components/app/App.workKeepAlive.test.tsx
  • apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx
  • apps/desktop/src/renderer/components/usage/usage.test.tsx

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Walkthrough

This PR improves desktop application resilience during project operations. It tracks in-flight project bindings per window, enables pending roots to authorize runtime requests during switches, increases provisioning timeouts for cold projects, prevents inactive surfaces from rendering stale content, and refactors usage snapshot fetching to rely on cached freshness instead of forced polling.

Changes

Project Switch Safety & Timeout Configuration

Layer / File(s) Summary
Pending project root tracking infrastructure
apps/desktop/src/main/main.ts
Introduces windowPendingProjectRoots map to track in-flight roots per window, with helpers to list pending roots and authorize them using reference counting with cleanup callbacks. Window teardown removes tracked roots on close.
SwitchProjectFromDialog with pending authorization
apps/desktop/src/main/main.ts
Captures window ID upfront, authorizes both selected and resolved repo roots as pending for that window, replaces dynamic IPC window context calls with the captured ID, and ensures pending authorizations are cleaned up in a finally block.
Expose pending roots via getWindowSession IPC
apps/desktop/src/main/main.ts, apps/desktop/src/main/services/ipc/registerIpc.ts
Extends getWindowSession return type with pendingLocalProjectRoots list and updates the dependency injection type to match.
Runtime bridge authorization of pending roots
apps/desktop/src/main/services/ipc/runtimeBridge.ts, apps/desktop/src/main/services/ipc/runtimeBridge.test.ts
Updates runtime bridge to authorize pending roots from session alongside existing roots when building allowed local runtime project roots. Test verifies IPC requests targeting pending roots are accepted and routed to the connection pool.
Preload early binding for project switches
apps/desktop/src/preload/preload.ts, apps/desktop/src/preload/preload.test.ts
Computes and publishes target binding before initiating switch IPC to ensure renderer-side state is available before main-process transition. Test updated to show local runtime reads now route to target project during in-flight switch.
IPC timeout configuration for local project setup
apps/desktop/src/main/services/ipc/ipcTimeouts.ts, apps/desktop/src/main/services/ipc/ipcTimeouts.test.ts, apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.ts, apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.test.ts
Introduces LOCAL_RUNTIME_PROJECT_SETUP_TIMEOUT_MS constant (150s) for local runtime IPC channels. Increases LOCAL_RUNTIME_PROJECT_TIMEOUT_MS from 3s to 120s for projects.add calls. Tests updated to assert new timeout values.
App rendering gates for inactive project surfaces
apps/desktop/src/renderer/components/app/App.tsx, apps/desktop/src/renderer/components/app/App.workKeepAlive.test.tsx
Gates lanes-tab and fallback route rendering behind active prop to prevent inactive surfaces from rendering stale content. Test verifies inactive Lanes surface from localStorage is not rendered when Work page is active.

Usage Control Snapshot Caching

Layer / File(s) Summary
Refactored snapshot caching logic
apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx
Removes polling constants, adds helpers to parse snapshot timestamps and check freshness before applying cached data. Simplifies readSnapshot to call getSnapshot() without forced refresh, introduces readCachedSnapshot wrapper for freshness-gated application, and updates cleanup to only cancel pending work and unsubscribe from updates.
Test coverage updates for snapshot caching
apps/desktop/src/renderer/components/usage/usage.test.tsx
Updates deferred<T>() helper to return both resolve and reject. Reworks tests to remove force-refresh and polling assertions, adds coverage for cached snapshot rendering, push-update application without refresh, and late cache resolution not overwriting pushed data.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • arul28/ADE#327: Both PRs directly modify HeaderUsageControl snapshot fetching logic—this PR refactors to use cached freshness instead of polling/refresh cycles.

Suggested labels

desktop

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/harden-runtime-project-switching

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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.

@capy-ai
Copy link
Copy Markdown

capy-ai Bot commented May 31, 2026

Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

PR Review

Scope: 14 file(s), +244 / −68
Verdict: Looks good

This PR hardens same-window project switching by temporarily authorizing the target local runtime root on the main process, extending local-runtime IPC/setup timeouts, keeping Work mounted across inactive project tabs while skipping Lanes/other routes until a tab is active, and preventing stale usage cache reads from clobbering fresher onUpdate snapshots. The race and authorization paths are covered by targeted tests; I did not find blocking bugs in the diff.


Notes

  • Pending-root authorization (main.ts + runtimeBridge.ts) is scoped per window with refcounted cleanup in finally, and only paths entering switchProjectFromDialog are added—renderer IPC still must match collectAuthorizedLocalRuntimeRoots, so this closes the switch race without opening arbitrary cross-project access.
  • Preload eager binding on switchToPath aligns renderer rootPath with main pending auth; failure restores the previous binding.
  • Header usage correctly drops duplicate refresh() on mount/focus/interval (the source of the stale-read race) while still subscribing to onUpdate and reading cache once via getSnapshot(); background polling remains in usageTrackingService (2 min, immediate start() poll).
  • App.tsx inactive-tab behavior is intentional: Work stays warm; Lanes and other routes no longer mount against the wrong runtime (see App.workKeepAlive.test.tsx).
  • I did not re-run the PR’s full validation matrix in this automation environment; findings are from diff review and tracing control flow only.
Open in Web View Automation 

Sent by Cursor Automation: BUGBOT in Versic

Comment on lines +86 to +95
function shouldApplyCachedSnapshot(
nextSnapshot: UsageSnapshot | null,
currentSnapshot: UsageSnapshot | null,
): boolean {
if (!currentSnapshot) return true;
if (!nextSnapshot) return false;
const nextTimestamp = snapshotLastPolledMs(nextSnapshot);
const currentTimestamp = snapshotLastPolledMs(currentSnapshot);
return nextTimestamp == null || currentTimestamp == null || nextTimestamp >= currentTimestamp;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Null timestamp treated as "apply" rather than "skip"

When nextTimestamp == null (i.e., the cached snapshot has a missing or unparseable lastPolledAt), the guard returns true and applies the cached snapshot even if currentSnapshot is a fresher, valid push update. A scenario where this bites: a push notification arrives with a valid timestamp, snapshotRef.current is set to that data, then readCachedSnapshot resolves with a stale or stripped snapshot that has no lastPolledAt — the guard would overwrite the fresher state with the stale one. Flipping that branch to return false (treat an undatable cached snapshot as unsafe to apply) would prevent stale overwrites while still letting valid older-timestamped snapshots be filtered by the >= comparison.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/components/usage/HeaderUsageControl.tsx
Line: 86-95

Comment:
**Null timestamp treated as "apply" rather than "skip"**

When `nextTimestamp == null` (i.e., the cached snapshot has a missing or unparseable `lastPolledAt`), the guard returns `true` and applies the cached snapshot even if `currentSnapshot` is a fresher, valid push update. A scenario where this bites: a push notification arrives with a valid timestamp, `snapshotRef.current` is set to that data, then `readCachedSnapshot` resolves with a stale or stripped snapshot that has no `lastPolledAt` — the guard would overwrite the fresher state with the stale one. Flipping that branch to `return false` (treat an undatable cached snapshot as unsafe to apply) would prevent stale overwrites while still letting valid older-timestamped snapshots be filtered by the `>=` comparison.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

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