Repository navigation
fix(worktrees): keep each same-path copy on its own host branch - #33428
Conversation
…s review clear on the same copy
…freshes refuse an unplaced twin
…ar instead of reaching its sibling
…ontrol refresh refuses an unplaced twin
… review link on its host
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review.
|
There was a problem hiding this comment.
Actionable comments posted: 1

Review info
-
Run configuration-
- Source mode: Auto
- Repository: Unavailable
- Branch: Unavailable
- File: No configuration file from this repository applied
- Commit: Unavailable
- Layers: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
99b24510-b39d-4d78-9dfc-491e3d09ba3e
Files selected for processing: 23-
config/workspace-record-read-baseline.txtsrc/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-git-status-effects.tsxsrc/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-manual-refresh.tsxsrc/renderer/src/components/right-sidebar/git-status-refresh-branch-line-total.test.tssrc/renderer/src/components/right-sidebar/git-status-refresh.test.tssrc/renderer/src/components/right-sidebar/git-status-refresh.tssrc/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.test.tssrc/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-probes.tssrc/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.tssrc/renderer/src/components/right-sidebar/useGitStatusPolling.test.tssrc/renderer/src/components/right-sidebar/useGitStatusPolling.tssrc/renderer/src/components/right-sidebar/worktree-git-owner-target.tssrc/renderer/src/hooks/worktree-head-identity-apply.test.tssrc/renderer/src/hooks/worktree-head-identity-apply.tssrc/renderer/src/store/slices/worktree-helpers.tssrc/renderer/src/store/slices/worktree-listing-branch-switch.test.tssrc/renderer/src/store/slices/worktree-listing-branch-switch.tssrc/renderer/src/store/slices/worktrees-fetch-listing-merge.test.tssrc/renderer/src/store/slices/worktrees-git-identity-branch-title.test.tssrc/renderer/src/store/slices/worktrees-git-identity-review-clear-persistence.test.tssrc/renderer/src/store/slices/worktrees-git-identity-twin-host.test.tssrc/renderer/src/store/slices/worktrees-identity-migration.test.tssrc/renderer/src/store/slices/worktrees/metadata/worktree-git-identity-update.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.
-
…nd relink check, scoped to its own copy
…ows are stamped before sanitizing
There was a problem hiding this comment.
Actionable comments posted: 3

Review info
-
Run configuration-
- Source mode: Auto
- Repository: Unavailable
- Branch: Unavailable
- File: No configuration file from this repository applied
- Commit: Unavailable
- Layers: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
641dfd86-79be-462b-b7f9-e15de2ec9096
Files selected for processing: 28-
config/workspace-record-read-baseline.txtsrc/renderer/src/components/right-sidebar/checks-panel/manual-refresh-dependencies.tssrc/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-controller-state.tsxsrc/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-generation.tsxsrc/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-git-status-effects.test.tsxsrc/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-git-status-effects.tsxsrc/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-manual-refresh.test.tsxsrc/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-manual-refresh.tsxsrc/renderer/src/components/right-sidebar/git-status-twin-copy.test.tssrc/renderer/src/components/right-sidebar/source-control/listing/operation-target.tssrc/renderer/src/components/right-sidebar/source-control/review/create-pr-intent-flow.tssrc/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-probes.tssrc/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-run.tssrc/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-target.tssrc/renderer/src/components/right-sidebar/source-control/review/use-pull-request-generation.tssrc/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.tssrc/renderer/src/components/right-sidebar/useGitStatusPolling.test.tssrc/renderer/src/components/right-sidebar/useGitStatusPolling.tssrc/renderer/src/lib/connection-owner-resolution.tssrc/renderer/src/lib/worktree-git-copy.tssrc/renderer/src/store/slices/pull-request-generation-state.tssrc/renderer/src/store/slices/worktree-listing-branch-switch.tssrc/renderer/src/store/slices/worktrees-git-identity-twin-host.test.tssrc/renderer/src/store/slices/worktrees/listing/detected-worktree-host-merge.tssrc/renderer/src/store/slices/worktrees/metadata/hosted-review-link-mutation.tssrc/renderer/src/store/slices/worktrees/metadata/update-worktree-meta.tssrc/renderer/src/store/slices/worktrees/metadata/worktree-copy-review-clear.tssrc/renderer/src/store/slices/worktrees/metadata/worktree-git-identity-update.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.
-
| for (const worktreeId of worktreeIds) { | ||
| hostedReviewLinkMutationGenerationByWorktreeId.delete(worktreeId) | ||
| hostedReviewLinkClearTombstonesByWorktreeId.delete(worktreeId) | ||
| deleteCopyTombstones(hostedReviewLinkClearTombstonesByCopy, worktreeId) |
There was a problem hiding this comment.
🔎 Supported by static analysis
-
Script executed:
#!/bin/bash rg -n -C 6 'pruneHostedReviewLinkMutationGenerations\(' src/renderer/src --glob '!**/*.test.ts'Repository: stablyai/orca
Length of output: 4764
Script executed:
set -u echo "== deleteCopyTombstones definition ==" rg -n -F -e 'function deleteCopyTombstones' -e 'deleteCopyTombstones(' -- src/renderer/src/store/slices/worktrees/metadata/hosted-review-link-mutation.ts || test "$?" -eq 1 echo "== remove-worktree.ts 230-290 ==" sed -n '230,290p' src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts | cat -n echo "== worktree-purge-state.ts 55-96 ==" sed -n '55,96p' src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts | cat -nRepository: stablyai/orca
Length of output: 5030
Script executed:
set -u rg -n -F -e 'deleteCopyTombstones' -- src/renderer/src || test "$?" -eq 1 echo "== hosted-review-link-mutation.ts imports and 60-100 ==" sed -n '1,30p' src/renderer/src/store/slices/worktrees/metadata/hosted-review-link-mutation.ts | cat -n sed -n '60,100p' src/renderer/src/store/slices/worktrees/metadata/hosted-review-link-mutation.ts | cat -nRepository: stablyai/orca
Length of output: 4280
Script executed:
set -u echo "== worktree-copy-review-clear.ts 1-70 ==" sed -n '1,70p' src/renderer/src/store/slices/worktrees/metadata/worktree-copy-review-clear.ts | cat -n echo "== consumers of the copy tombstone map ==" rg -n -F -e 'hostedReviewLinkClearTombstonesByCopy' -e 'reviewClearTombstoneKey(' -- src/renderer/src --glob '!**/*.test.ts' || test "$?" -eq 1Repository: stablyai/orca
Length of output: 5720
Scope the removal prune to the removed copy.
remove-worktree.ts calls pruneHostedReviewLinkMutationGenerations([worktreeId]) after it removes one host's copy. That call drops the review-clear tombstones and the shared generation for every copy with the same worktree ID. A same-path twin on another host can lose its clear tombstone, and with it the stale-refetch guard that the copy-keyed design adds. The removal path should prune only the removed copy's tombstone. The ID-wide cleanup should run only when no sibling copy remains.
| worktreeId: string, | ||
| copy: { owner: string; hostId: ExecutionHostId | undefined } | ||
| ): void { | ||
| set((s) => { | ||
| const nextWorktrees = applyWorktreeUpdates( | ||
| s.worktreesByRepo, | ||
| worktreeId, | ||
| CLEARED_HOSTED_REVIEW_LINK_UPDATES | ||
| ) | ||
| const byRepo = s.worktreesByRepo | ||
| const nextWorktrees = clearCopyReviewLinks(byRepo, worktreeId, copy.owner) | ||
| const nextDetectedWorktrees = applyDetectedWorktreeUpdates( | ||
| s.detectedWorktreesByRepo, | ||
| worktreeId, | ||
| CLEARED_HOSTED_REVIEW_LINK_UPDATES | ||
| CLEARED_HOSTED_REVIEW_LINK_UPDATES, | ||
| copy.hostId | ||
| ) | ||
| if ( | ||
| nextWorktrees === s.worktreesByRepo && | ||
| nextDetectedWorktrees === s.detectedWorktreesByRepo | ||
| ) { | ||
| if (nextWorktrees === byRepo && nextDetectedWorktrees === s.detectedWorktreesByRepo) { | ||
| return s | ||
| } | ||
| return { | ||
| ...(nextWorktrees !== s.worktreesByRepo | ||
| ...(nextWorktrees !== byRepo |
There was a problem hiding this comment.
Match detected rows by copy owner.
When the branch-switch identity update rereads a same-ID copy, applyHostedReviewLinkClear can receive the selected copy while applyDetectedWorktreeUpdates matches only hostId. Two copies with the same host and different runtimeOwnerEnvironmentId can therefore cause the sibling's detected review link to be cleared. Host-scoping the async reread and save does not fix this detected-row mutation.
Pass the captured owner to detected-row matching while preserving host-only behavior for existing callers.
Suggested fix
-
diff --git a/src/renderer/src/store/slices/worktrees/listing/worktree-meta-host-match.ts b/src/renderer/src/store/slices/worktrees/listing/worktree-meta-host-match.ts @@ import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../../../shared/execution-host' +import { worktreeCopyOwnerKey } from '../metadata/worktree-copy-review-clear' @@ export function worktreeRowMatchesMetaHost( - worktree: { hostId?: ExecutionHostId }, - executionHostId: ExecutionHostId | undefined + worktree: { hostId?: ExecutionHostId; runtimeOwnerEnvironmentId?: string }, + executionHostId: ExecutionHostId | undefined, + copyOwner?: string ): boolean { + if (copyOwner !== undefined) { + return worktreeCopyOwnerKey(worktree) === copyOwner + } return ( executionHostId === undefined || worktree.hostId === executionHostId || diff --git a/src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.ts b/src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.ts @@ worktreeId: string, rawUpdates: Partial<WorktreeMeta>, - executionHostId?: ExecutionHostId + executionHostId?: ExecutionHostId, + copyOwner?: string ): AppState['detectedWorktreesByRepo'] { @@ - if (worktree.id !== worktreeId || !worktreeRowMatchesMetaHost(worktree, executionHostId)) { + if ( + worktree.id !== worktreeId || + !worktreeRowMatchesMetaHost(worktree, executionHostId, copyOwner) + ) { return worktree } diff --git a/src/renderer/src/store/slices/worktrees/metadata/hosted-review-link-mutation.ts b/src/renderer/src/store/slices/worktrees/metadata/hosted-review-link-mutation.ts @@ worktreeId, CLEARED_HOSTED_REVIEW_LINK_UPDATES, - copy.hostId + copy.hostId, + copy.owner )
…ves; local null stays local
…rd survives its first stamp
…legacy row's relink drops its stamped guard
…ry stamps it local
There was a problem hiding this comment.
Actionable comments posted: 2

Review info
-
Run configuration-
- Source mode: Auto
- Repository: Unavailable
- Branch: Unavailable
- File: No configuration file from this repository applied
- Commit: Unavailable
- Layers: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
5084da53-c677-4b01-9f11-df81d2655e5d
Files selected for processing: 12-
src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-generation.tsxsrc/renderer/src/components/right-sidebar/git-status-twin-copy.test.tssrc/renderer/src/components/right-sidebar/source-control/review/create-pr-intent-flow.tssrc/renderer/src/components/right-sidebar/source-control/review/create-pr-intent-operation-target.test.tssrc/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-run.tssrc/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-target.tssrc/renderer/src/components/right-sidebar/source-control/review/use-pull-request-generation.tssrc/renderer/src/lib/worktree-git-copy.tssrc/renderer/src/store/slices/worktrees-git-identity-twin-host.test.tssrc/renderer/src/store/slices/worktrees/metadata/hosted-review-link-mutation.tssrc/renderer/src/store/slices/worktrees/metadata/worktree-copy-review-clear.tssrc/renderer/src/store/slices/worktrees/metadata/worktree-git-identity-update.ts
Files skipped from review as they are similar to previous changes: 2-
- src/renderer/src/store/slices/worktrees-git-identity-twin-host.test.ts
- src/renderer/src/store/slices/worktrees/metadata/worktree-git-identity-update.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.
-
| repoId: activeRepo.id, | ||
| branch: branchName, | ||
| runtimeTarget: activeRepoTarget | ||
| runtimeTarget: copy?.transport ?? activeRepoTarget |
There was a problem hiding this comment.
Refuse generation when no copy resolves. Do not fall back to activeRepoTarget.
resolveWorktreeGitCopyNow returns null for an unselected same-ID twin. The git-status code and the Create PR path both refuse in that case. This code instead falls back to activeRepoTarget and getConnectionId(...).
For an ambiguous twin, getConnectionId returns undefined. The generation then runs with the repo target and no connection ID, so it can run against the local twin's checkout. The comment on Line 108 says the result must not land on a twin.
Return undefined early when activeWorktreeId is set and copy is null. Then use copy.transport and copy.connectionId without a fallback.
… copy's push target
…ne copy for a named host
…r review-clear keys
…me-path-hosts # Conflicts: # src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-git-status-effects.tsx # src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-manual-refresh.tsx # src/renderer/src/components/right-sidebar/git-status-refresh.ts # src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-run.ts # src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-target.ts # src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.ts
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream stablyai#33428 makes refreshGitStatusForWorktree require ownerHostId so a same-path twin on another host never gets this copy's transport. Resolve the copy once, as useGitStatusPolling does, and pass its host, connection and transport; the test pins that all three reach the refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ELI5
Orca names a workspace by its project and folder path. That name has no machine in it. When the same project sits at the same path on two machines, both copies can end up with the same name (a "same-id twin"). Orca then mixed the two up:
That's why both rows showed
master. Now every branch read and write names the exact copy it's for.What Changed
Mechanism, one resolver and one rule:
resolveWorktreeGitCopy(new,src/renderer/src/lib/worktree-git-copy.ts) resolves the transport, the SSH connection and the host of the same copy in one step.git statusof the SSH copy's path. The resolver looks up the connection for the named copy instead (getConnectionIdForWorktreeCopy).worktreeCopyPushTarget). It no longer comes from the id-keyed worktree map, which could hold the other copy's remote and branch.updateWorktreeGitIdentityrequiresownerHostId, so the compiler rejects a call without one.selectWorkspaceByHostTarget. With no host named, neither copy is patched.persistWorktreeMetawith the host), falling back to the row's stamp.Why
A worktree id is not unique across hosts. Fixing each reported path on its own kept missing others. Review found the transport, the connection, the tombstones and the save each still keyed by bare id. So the design changed:
Same-id twins are an existing state. Listings, sidebar activity, GitHub (#33257) and Linear (#33270) were already made host-aware the same way.
Known follow-up: the review-clear tombstone rules now carry stamp normalization for legacy and partially stamped rows. A later PR should simplify them, for example by keying guards with
getWorktreeHostIdentityonce every listing path stamps rows the same way.Not changed here: the active-workspace git status maps (
gitStatusByWorktree) are still keyed by bare id. They drive the active panels only, not the sidebar.Linked Issue
Part of #32918. This fixes the "both projects show master" symptom for same-id twins. It does not address the profile-state revision conflict or the pairing error in that report.
Thanks to @shainisinha-wtg for the detailed report, screenshot and log bundle.
Visual Proof
N/A: no new UI. Each row shows its own host's branch. Tests and the live lane below cover it.
Testing
I manually tested these changes locally
Automated tests added/updated, or explained why not below
git-status-twin-copy.test.ts(poll level, real app store): a local copy and an SSH copy at the same path, with the SSH copy selected.git:statusmust go out withconnectionId: 'box-b'.worktrees-git-identity-twin-host.test.tscovers twin patching:The same file covers review clears and tombstones:
The slice-level twin tests fail on origin/main.
src/renderer/src/store/slices,src/renderer/src/lib,src/renderer/src/hooksandsrc/renderer/src/components/right-sidebarpass single-worker (1690 files).The full local static job (every step of pr.yml "static analysis and typecheck") passes.
Docker managed-orcad lane: dispatched on CI for this head (two Docker SSH hosts with a shared home, explorer host selection, terminal journeys). The harness can't produce a same-id twin. Two independent hosts, and two hosts sharing a home (each gets its own server under
hosts/<machine>), mint different repo ids. So that lane checks the same-path, two-host paths for regressions but can't show this bug.Review
Focus on:
resolveWorktreeGitCopy: the connection must come from the same copy as the transport;gitIdentityTargetsinworktree-git-identity-update.ts;hosted-review-link-mutation.tsandworktree-copy-review-clear.ts.Agent skill upstream boundary
Notes
Behaviour changes for non-twin worktrees:
runtimeTargetForWorkspaceOwnerwithhostId);executionHostIdwhen a host is known. Main already uses it to pick the matching metadata row.Review-clear guards are keyed by the stamp the row's next listing gives it, filling in only missing parts:
localand unset share one key;runtime:host is its own server, so backfilled provenance never changes its key;This way legacy and partially stamped rows keep their guard across their first stamped listing, and a relink of the still-unstamped row matches it too.
Remote and mixed versions: renderer-only. No wire, RPC or persisted-shape changes.
Folder workspaces: git identity is written only for git worktrees. Folder paths keep their existing per-folder connection lookup.
Mobile: phones read the host's sidebar feed, which already keys rows by host. Unaffected.
Cross-platform: no path or process changes.
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)