Skip to content

Stop the Cleanup dirty sweep from flooding remote hosts - #356

Merged
frenchie4111 merged 2 commits into
mainfrom
fix-remote-tab-switching-slow
Nov 2, 2026
Merged

frenchie4111 merged 2 commits into
mainfrom
fix-remote-tab-switching-slow

Conversation

@frenchie4111

Copy link
Copy Markdown
Collaborator

Hotfix candidate for 2.18.1.

Symptom

In 2.18, switching tabs or creating a tab on a remote takes minutes or looks broken. It shows up in both "All" (Fleet) mode and single-host mode with a remote active.

Root cause

Logs from an affected remote (agentjesus):

  • harness-server was at 335% CPU and 5.4 GB of memory.
  • About 2,000 worktree:isDirty requests were sent in one burst. Each took 7+ minutes to answer.
  • The server's event loop was freezing for 8–20 s at a time.
  • The client renderer sat at 2–3% CPU the whole time, so it was just waiting on the server.

Fleet view (#344) feeds App from getMergedWorktrees. Its cache is keyed on each backend's whole AppState, which is a new object after every event, so the merge reruns on every streamed token. Every output (flatWorktrees, the owner maps, and so on) came back as a new array or map each time, even when nothing in it had changed.

Cleanup.tsx's dirty-scan effect depended on that array. While the modal was open, every token re-sent a git status for every worktree, and every deletion made from the modal re-sent the whole sweep again. On the remote, each of those git calls stalls the event loop while it starts, so tab switches and new tabs queued behind thousands of reads. Cleanup also sent every check to the active backend rather than the one hosting each worktree.

Fix

  • merge-backend-states.ts: each merged output keeps its previous reference unless its contents changed, and the whole result does too when nothing did. Every other consumer of these lists also stops re-firing on every token.
  • Cleanup.tsx: each worktree is checked once per open, isDirty and getBranchCommits go through backendFor(path), and the activity log loads once instead of on every update.
  • Server (main/index.ts): concurrent isDirty requests for the same path and priority share one git call, so a client still on 2.18 can't overwhelm an updated server.

Testing

  • npm run typecheck, npx electron-vite build and npx vitest run (2238 tests) all pass.
  • Three new tests in merge-backend-states.test.ts cover reference reuse.
  • Not reproduced end to end in the app. The diagnosis comes from the remote's perf.log and the code.

A remote that is already stuck has to have harness-server restarted to clear its backlog.

🤖 Generated with Claude Code

frenchie4111 and others added 2 commits November 2, 2026 10:46
Fleet view (2.18) feeds App from the merged-backend pipeline, which is
keyed on each backend's whole AppState and so reruns on every event —
and every output was a fresh array/map each time. The Cleanup modal's
dirty-scan effect keyed on that worktree array, so it re-sent a
`git status` for every worktree on every streamed token. On a remote
host that queued thousands of reads (observed: ~2,000 isDirty requests
each taking 7+ minutes, 20s event-loop stalls, 5.4GB RSS), and tab
switching and tab creation sat behind the backlog.

- merge-backend-states: each output keeps its previous reference unless
  its contents changed; the whole result does when nothing did.
- Cleanup: ask about each path once per open, route each request to the
  backend that owns the worktree, and load the activity log once.
- main: concurrent isDirty requests for the same path share one git call,
  so an un-updated client can't wedge a server again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Extract the isDirty in-flight dedupe into createSingleFlight with tests
  for sharing, per-key separation, and clearing on resolve and reject.
- Cleanup fetches the activity log once from each backend that owns an
  eligible worktree, so remote rows age by their own host's history
  instead of falling back to lastActive.
- Pin single-mode merge stability: a hidden backend streaming must leave
  every output field, and the result itself, unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frenchie4111
frenchie4111 merged commit 27479fc into main Nov 2, 2026
1 check 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