Repository navigation
fix(worktrees): resolve a two-host project by the worktree's own host (#10634) - #10986
Conversation
…#10634) A project registered on both a local host and an SSH host permanently poisoned every one of its workspaces with "Workspace identity is ambiguous across hosts. Refresh projects and try again." Refresh could never help: nothing was stale, both host setups were valid and intentional. The error survived restarts. The ambiguity was manufactured. `resolveExactWorktreeRoute` starts from a worktree that already carries exactly one `hostId`, then throws that away and asks `resolveIndexedRepoOperationRoute` which host owns the *repo* — a question with two right answers once a project spans hosts. Only the project spans hosts; each worktree never does. Route resolution now filters repo setups to the ones matching the worktree's own host before looking for a transport, so a two-host project resolves as cleanly as a one-host project. Genuine ambiguity still returns `ambiguous`. Second half: the error escaped as an *uncaught renderer error* because passive background paths — unread marking, activity bumps — called a helper that threw. Those callers now degrade: `trySettingsForWorktreeOwner` returns null, the passive update is skipped with a warning, and local state stays consistent. Explicit user actions still surface the error.
…owners Adversarial review found the routing fix sound but its coverage thin: only markWorktreeUnread had an ambiguous-owner test, so restoring the throw in clearWorktreeUnread or bumpWorktreeActivity would have reproduced the uncaught renderer error with the suite still green. Both are now covered, verified by mutation. bumpWorktreeActivity also skipped silently where the other paths warned. It now warns — but once per workspace, not per event: activity bumps fire on every PTY event, so an unbounded warn would flood the console for exactly the users already hitting this bug.
📝 WalkthroughWalkthroughSSH worktree route resolution now matches repository execution or connection hosts against the owner’s SSH host and reports resolved, ambiguous, or missing routes. Passive metadata operations use centralized best-effort persistence that handles ambiguous ownership, deduplicates warnings, and refetches worktrees after persistence failures. Tests cover SSH routing, multi-host unread persistence, ambiguous ownership, passive unread operations, and warning deduplication. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d3046ffa-e4af-4b61-89b4-da19a42dca85
📒 Files selected for processing (4)
src/renderer/src/lib/worktree-operation-route.test.tssrc/renderer/src/lib/worktree-operation-route.tssrc/renderer/src/store/slices/worktrees.test.tssrc/renderer/src/store/slices/worktrees.ts
| const ownerSettings = trySettingsForWorktreeOwner(get(), worktreeId) | ||
| if (!ownerSettings) { | ||
| warnAmbiguousOwnerOnce(worktreeId, 'persist worktree activity timestamp') | ||
| return | ||
| } | ||
| void persistWorktreeMeta(ownerSettings, worktreeId, { |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Use the passive persistence helper for activity updates.
This path returns on selector_not_found without refetching, unlike persistPassiveWorktreeMetaForOwner; activity metadata can remain stale after the runtime no longer recognizes the worktree.
Proposed fix
- const ownerSettings = trySettingsForWorktreeOwner(get(), worktreeId)
- if (!ownerSettings) {
- warnAmbiguousOwnerOnce(worktreeId, 'persist worktree activity timestamp')
- return
- }
- void persistWorktreeMeta(ownerSettings, worktreeId, {
- lastActivityAt: now
- }).catch((err) => {
- if (isRuntimeSelectorNotFoundError(err)) {
- return
- }
- console.error('Failed to persist worktree activity timestamp:', err)
- void get().fetchWorktrees(getRepoIdFromWorktreeId(worktreeId))
- })
+ persistPassiveWorktreeMetaForOwner(
+ get,
+ worktreeId,
+ { lastActivityAt: now },
+ 'persist worktree activity timestamp'
+ )📝 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.
| const ownerSettings = trySettingsForWorktreeOwner(get(), worktreeId) | |
| if (!ownerSettings) { | |
| warnAmbiguousOwnerOnce(worktreeId, 'persist worktree activity timestamp') | |
| return | |
| } | |
| void persistWorktreeMeta(ownerSettings, worktreeId, { | |
| persistPassiveWorktreeMetaForOwner( | |
| get, | |
| worktreeId, | |
| { lastActivityAt: now }, | |
| 'persist worktree activity timestamp' | |
| ) |
…stablyai#10634) (stablyai#10986) * fix(worktrees): resolve a two-host project by the worktree's own host (stablyai#10634) A project registered on both a local host and an SSH host permanently poisoned every one of its workspaces with "Workspace identity is ambiguous across hosts. Refresh projects and try again." Refresh could never help: nothing was stale, both host setups were valid and intentional. The error survived restarts. The ambiguity was manufactured. `resolveExactWorktreeRoute` starts from a worktree that already carries exactly one `hostId`, then throws that away and asks `resolveIndexedRepoOperationRoute` which host owns the *repo* — a question with two right answers once a project spans hosts. Only the project spans hosts; each worktree never does. Route resolution now filters repo setups to the ones matching the worktree's own host before looking for a transport, so a two-host project resolves as cleanly as a one-host project. Genuine ambiguity still returns `ambiguous`. Second half: the error escaped as an *uncaught renderer error* because passive background paths — unread marking, activity bumps — called a helper that threw. Those callers now degrade: `trySettingsForWorktreeOwner` returns null, the passive update is skipped with a warning, and local state stays consistent. Explicit user actions still surface the error. * fix(worktrees): cover every passive path and warn once for ambiguous owners Adversarial review found the routing fix sound but its coverage thin: only markWorktreeUnread had an ambiguous-owner test, so restoring the throw in clearWorktreeUnread or bumpWorktreeActivity would have reproduced the uncaught renderer error with the suite still green. Both are now covered, verified by mutation. bumpWorktreeActivity also skipped silently where the other paths warned. It now warns — but once per workspace, not per event: activity bumps fire on every PTY event, so an unbounded warn would flood the console for exactly the users already hitting this bug. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Fixes #10634.
What users hit
Register the same project on two hosts — a local folder and an SSH checkout of the same repo — and every workspace in that project breaks permanently:
"Refresh projects" can never fix it. Nothing is stale; both host setups are valid and intentional. It survives app restarts. And because it was thrown from a background notification path, it surfaced as an uncaught renderer error rather than a handled state.
The ambiguity was manufactured
resolveExactWorktreeRoutestarts from a worktree that already carries exactly onehostId— then discards it and asksresolveIndexedRepoOperationRoutewhich host owns the repo. That question genuinely has two answers once a project spans hosts.But only the project spans hosts. Every individual worktree names exactly one. The information needed to answer was in hand and thrown away.
Route resolution now filters repo setups to those matching the worktree's own host (checking both
getRepoExecutionHostIdand the repo'sconnectionId) before looking for a transport. A two-host project resolves as cleanly as a one-host project. Genuine ambiguity — two setups on the same host — still returnsambiguous.Second half: it should never have been uncaught
Passive background paths — unread marking, cleared-unread, activity bumps — called a helper that threw.
trySettingsForWorktreeOwnernow returnsnull; those paths skip the persist and keep local state consistent. Explicit user actions (remove, force-delete) still throw and surface the error, which is correct — the user asked for something Orca can't safely do.bumpWorktreeActivitywarns once per workspace, not per event: activity bumps fire on every PTY event, so an unbounded warn would flood the console for exactly the users already hitting this bug.Verification
Adversarial review (4 lenses, 3 independent refuters per finding) confirmed the routing logic sound and found 3 in-scope gaps, all fixed in the second commit:
markWorktreeUnreadhad an ambiguous-owner test, so restoring the throw inclearWorktreeUnreadorbumpWorktreeActivitywould have reproduced the uncaught error with the suite greenbumpWorktreeActivityskipped silently where its siblings warnedEvery guard mutation-tested — reverted individually, mutation confirmed applied, specific tests confirmed failing:
resolveIndexedRepoOperationRouteclearWorktreeUnreadonlypnpm typecheckclean across all three projects.oxlintexits 0. 5,077 renderer store + lib tests pass. max-lines ratchet and reliability gates pass.Scope
Renderer-only; no main-process or IPC changes. No new user-facing strings, so no locale work. No UI or layout changes — the visible difference is that a two-host project stops erroring. Folder workspaces keep their existing early path. Provider-agnostic: nothing here assumes GitHub.
Not a duplicate of #10809 — that fixes linking a folder to a project (#10620, main process, identity probing). This fixes operating on a worktree once two valid setups already exist. Zero file overlap.
Not verified
The reporter's exact environment (a real SSH host plus a local clone of the same repo) wasn't reproduced end to end; the two-host state is driven directly in tests. The root cause was traced in current source rather than inferred.