Skip to content

VC-831: release page-scoped sign-ins and file/dir/worktree watches when the renderer page reloads or crashes - #922

Open
hussainph wants to merge 3 commits into
mainfrom
volli/VC-831-renderer-reload-or-crash-strands-per-window-main
Open

hussainph wants to merge 3 commits into
mainfrom
volli/VC-831-renderer-reload-or-crash-strands-per-window-main

Conversation

@hussainph

@hussainph hussainph commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Defect

A reload (View → Reload / Force Reload, and the only recovery from a renderer crash) keeps the same WebContents. Three main-side owners released their per-window resources only on destroyed, so they outlived the page that asked for them:

  • Model Access sign-in (main/model-access/ipc.ts): the per-window SignInOwner was abandoned only on destroyed. If the page reloaded mid sign-in, the attempt kept the provider's single attempt slot. The reloaded page no longer knew the attempt id, so every further Sign in answered "This provider is already signing in.", and the flow's loopback callback server (fixed ports 53692 for Anthropic and 1455 for ChatGPT in pi-ai 1.0.0) stayed bound until the window closed.
  • File and folder watches (main/volli-fs-ipc.ts → host-core FileWatchManager/DirWatchManager): subscriptions are refcounted per ${client.id}:…. After a reload the new page re-watched the same key, so refcount went to 2. Its own unwatch left 1, and the fs.watch handle never closed. A watch the new page never re-opened was never released.
  • Worktree change watches (main/data-ipc.ts → WorktreeChangeWatchManager): every ticket rail opened before the reload kept its recursive watcher and git-ignore refreshes while the window lived.

Fix

  • New main/document-lifetime.ts: onDocumentGone(contents, listener) fires once when the page goes away, then detaches all three hooks. "Gone" means did-navigate (committed main-frame navigation), render-process-gone, or destroyed.
  • main/client-event-sink.ts: new pageClientEventSink(contents). It is the same id and channel as clientEventSink, but onceClosed/removeCloseListener follow the page rather than the window. isClosed still means the window is destroyed, because a reloaded page is a live client. The file, folder and worktree-change watch channels now use it. Nothing else does.
  • main/model-access/ipc.ts: the owner map entry and its attempts are abandoned through onDocumentGone. The abandoned flow aborts, which closes its loopback server, and the provider slot frees. A gone owner also stops sending. Otherwise the cancelled attempt's settled update would land on the reloaded page, which never began it and would hold it unclaimed forever.

How the regression tests prove it

These tests use fake WebContents (real EventEmitters) that emit the page events. They fail on main: 7 failures with the four production files reverted to origin/main, and the tests pass with this change.

  • model-access/ipc.test.ts: real ModelAccessSignInService with a scripted flow shaped like pi-ai's OAuth flow. It listens on a real loopback server, parks on a pasted-code prompt, and closes the server in finally. For did-navigate and render-process-gone, the test checks that the old server stops listening, that a new begin for the same provider succeeds, and that no update for the old attempt reaches the page. An in-page navigation or a started-but-refused navigation (did-start-navigation) keeps the attempt. Window destruction still frees the slot for the next window.
  • volli-fs-ipc.test.ts: dir watch → reload (did-navigate / render-process-gone) → watch again → unwatch closes every fs.watch handle. Same for a file watch. A did-navigate-in-page or did-start-navigation releases nothing.
  • data-ipc.test.ts: the worktree-change-watch door hands the manager a page-scoped client: did-navigate closes it, an in-page navigation doesn't.
  • client-event-sink.test.ts: pageClientEventSink semantics, plus the real WorktreeChangeWatchManager. A rail opened before a reload and never re-opened has its recursive watcher closed. Re-open then release after a reload closes both watchers. An in-page navigation keeps it.
  • document-lifetime.test.ts: fires once on each of the three events and detaches all hooks. A started or in-page navigation fires nothing. Dispose is idempotent.

Decisions

  • did-navigate, not did-start-navigation. The ticket proposed Session RPC's peerOf semantics. As the concurrency review on the ticket points out, did-start-navigation fires before main's will-navigate can refuse a navigation, and main refuses every non-internal main-frame navigation. Copying it would release a live page's sign-in and watches on a refused navigation. This PR uses the did-navigate + render-process-gone + destroyed trio that browser/plane-reset.ts already uses. did-navigate arrives at commit, before the new document's scripts run, so it cannot sweep a fresh page's own begin or watch.
  • A desktop-side wrapper, not a new HostClientEventSink member. Host-core and its port contract are unchanged. Only the watch channels opt in. PTY IPC still uses clientEventSink (window lifetime), per VC-425.
  • No timeout on the sign-in flow. The attempt goes away with its page, as the ticket asks.
  • isClosed unchanged. It still reports window destruction, so host-core's "closed client" short-circuits behave as before for a live, reloaded page.

Not in this PR

  • Moving Session RPC's peerOf and notifications.forgetRenderer onto the same helper, which the ticket comment suggests. That is outside this ticket's Proposed fix, and the did-start-navigation hazard there is latent and unreproduced.
  • Terminals across a reload (VC-425).
  • A watch request from the old page that is still awaiting path resolution when the page commits a reload, then registers afterwards. That narrow race would still be held until the window closes.

Tests run locally

  • vp test run src/main/model-access/ipc.test.ts src/main/client-event-sink.test.ts src/main/document-lifetime.test.ts src/main/volli-fs-ipc.test.ts src/main/data-ipc.test.ts src/main/pty.test.ts src/main/browser/plane-reset.test.ts (apps/desktop): 7 files, 548 tests passed.
  • The same new tests against origin/main's model-access/ipc.ts, volli-fs-ipc.ts and data-ipc.ts: 7 failed.
  • vp check on the 10 touched files: clean.
  • vp run --filter @volli/desktop typecheck and root vp exec tsc --noEmit -p tsconfig.json: clean.

CI history

@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 72fc8dc8-9e0e-4160-8c74-258d0be3f781
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

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