From 87ca5dad433fd63e6f5faa27cec3ab4faabbb9c9 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:31:46 -0700 Subject: [PATCH] refactor(repo-host): one helper for the hosts registering a repo id (#31634) Four twin checks (sort stamps, sidebar unread, sort-order notify, worktree-meta owner) each rebuilt new Set(repos.filter(id).map(getRepoExecutionHostId)). Name it once as getRepoRegistrationHostIds in shared/repo-host-identity. --- .../host-qualified-worktree-meta.ts | 3 ++- .../runtime-managed-worktree-metadata.ts | 11 +++-------- .../orca-runtime-sidebar-temporal.ts | 9 ++------- .../sidebar-inputs/sort-stamp-records.ts | 15 +++------------ src/shared/repo-host-identity.test.ts | 19 ++++++++++++++++++- src/shared/repo-host-identity.ts | 9 +++++++++ 6 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/main/persistence/host-qualified-worktree-meta.ts b/src/main/persistence/host-qualified-worktree-meta.ts index 598995f93fb..ecf4fa51d51 100644 --- a/src/main/persistence/host-qualified-worktree-meta.ts +++ b/src/main/persistence/host-qualified-worktree-meta.ts @@ -1,3 +1,4 @@ +import { getRepoRegistrationHostIds } from '../../shared/repo-host-identity' import { getRepoExecutionHostId, type ExecutionHostId } from '../../shared/execution-host' import type { Repo } from '../../shared/repo-types' import type { WorktreeMeta } from '../../shared/worktree/meta-types' @@ -86,7 +87,7 @@ export function resolveWorktreeMetaOwner( const repoId = getRepoIdFromWorktreeId(worktreeId) const owners = store.getRepos().filter((repo) => repo.id === repoId) // Why: the raw-id row's stamp names whichever twin wrote first, not the twin the caller means. - if (new Set(owners.map(getRepoExecutionHostId)).size > 1) { + if (getRepoRegistrationHostIds(owners, repoId).size > 1) { return undefined } const stamped = store.getWorktreeMeta(worktreeId)?.hostId diff --git a/src/main/runtime/runtime-managed-worktree-metadata.ts b/src/main/runtime/runtime-managed-worktree-metadata.ts index f50dc94447d..73e58b5b1fc 100644 --- a/src/main/runtime/runtime-managed-worktree-metadata.ts +++ b/src/main/runtime/runtime-managed-worktree-metadata.ts @@ -1,8 +1,8 @@ +import { getRepoRegistrationHostIds } from '../../shared/repo-host-identity' import type { GitPushTarget, Worktree } from '../../shared/worktree/types' import type { WorktreeMeta } from '../../shared/worktree/meta-types' import { worktreeWorkspaceKey } from '../../shared/workspace-scope' import { splitWorktreeId } from '../../shared/worktree/id' -import { getRepoExecutionHostId } from '../../shared/execution-host' import type { WorktreeSortOrderUpdate } from '../../shared/worktree/sort-order-update' import { stripOrcaProvenanceMetaUpdates } from '../worktree-removal-safety' import { isRemovedFolderWorkspaceInstance } from '../folder-workspace-meta-presence' @@ -174,13 +174,8 @@ export function persistRuntimeManagedWorktreeSortOrder(args: { } } for (const [repoId, sortOrders] of updatesByRepo) { - const hosts = new Set( - args.store - .getRepos() - .filter((repo) => repo.id === repoId) - .map(getRepoExecutionHostId) - ) - args.notifyChanged(repoId, hosts.size > 1 ? undefined : sortOrders) + const isTwin = getRepoRegistrationHostIds(args.store.getRepos(), repoId).size > 1 + args.notifyChanged(repoId, isTwin ? undefined : sortOrders) } return { updated: updates.length } } diff --git a/src/main/runtime/sidebar-inputs/orca-runtime-sidebar-temporal.ts b/src/main/runtime/sidebar-inputs/orca-runtime-sidebar-temporal.ts index b2ac9a31efb..e1e941cefab 100644 --- a/src/main/runtime/sidebar-inputs/orca-runtime-sidebar-temporal.ts +++ b/src/main/runtime/sidebar-inputs/orca-runtime-sidebar-temporal.ts @@ -1,3 +1,4 @@ +import { getRepoRegistrationHostIds } from '../../../shared/repo-host-identity' import { isRemoteRuntimePtyId } from '../../../shared/remote-runtime-pty-id' import { OrcaRuntimeWithDetectedCheckoutsAndLiveContent } from './orca-runtime-detected-checkouts-live-content' import type { OrcaRuntimeWithAgentTransitions } from '../orca-runtime-agent-transitions' @@ -5,7 +6,6 @@ import { ORCA_SIDEBAR_INPUTS_IS_MAIN } from '../../../shared/sidebar-inputs-swit import { DEFAULT_HIDE_SLEEPING_WORKSPACES } from '../../../shared/constants' import { getConnectionExecutionHostId, - getRepoExecutionHostId, isLocalExecutionHost, LOCAL_EXECUTION_HOST_ID, parseExecutionHostId, @@ -202,12 +202,7 @@ export abstract class OrcaRuntimeWithSidebarTemporal extends OrcaRuntimeWithDete return false } const repoId = getRepoIdFromWorktreeId(worktreeId) - const hosts = new Set( - (this.store?.getRepos() ?? []) - .filter((repo) => repo.id === repoId) - .map(getRepoExecutionHostId) - ) - return hosts.size > 1 + return getRepoRegistrationHostIds(this.store?.getRepos() ?? [], repoId).size > 1 } private workspaceHostId(worktreeId: string): ExecutionHostId | null { diff --git a/src/main/runtime/sidebar-inputs/sort-stamp-records.ts b/src/main/runtime/sidebar-inputs/sort-stamp-records.ts index b63885874d4..dee4df34585 100644 --- a/src/main/runtime/sidebar-inputs/sort-stamp-records.ts +++ b/src/main/runtime/sidebar-inputs/sort-stamp-records.ts @@ -1,9 +1,6 @@ +import { getRepoRegistrationHostIds } from '../../../shared/repo-host-identity' import type { AgentPaneWorkspace } from '../../agent-hooks/agent-transition-detector' -import { - getRepoExecutionHostId, - LOCAL_EXECUTION_HOST_ID, - type ExecutionHostId -} from '../../../shared/execution-host' +import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../shared/execution-host' import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' import type { SmartStampFields, @@ -70,13 +67,7 @@ function localTwinHostId( store: Pick, 'getRepos'>, worktreeId: string ): ExecutionHostId | null { - const repoId = getRepoIdFromWorktreeId(worktreeId) - const hosts = new Set( - store - .getRepos() - .filter((repo) => repo.id === repoId) - .map(getRepoExecutionHostId) - ) + const hosts = getRepoRegistrationHostIds(store.getRepos(), getRepoIdFromWorktreeId(worktreeId)) return hosts.size > 1 && hosts.has(LOCAL_EXECUTION_HOST_ID) ? LOCAL_EXECUTION_HOST_ID : null } diff --git a/src/shared/repo-host-identity.test.ts b/src/shared/repo-host-identity.test.ts index 0b61e82f6ab..2326b94951d 100644 --- a/src/shared/repo-host-identity.test.ts +++ b/src/shared/repo-host-identity.test.ts @@ -1,6 +1,10 @@ import { describe, expect, expectTypeOf, it } from 'vitest' import { toRuntimeExecutionHostId, toSshExecutionHostId } from './execution-host' -import { getRepoHostIdentityForParts, type HostScopedRepoKey } from './repo-host-identity' +import { + getRepoHostIdentityForParts, + getRepoRegistrationHostIds, + type HostScopedRepoKey +} from './repo-host-identity' // Why: these keys index persisted and cross-module repo state, so the bytes are frozen (D25). describe('repo host identity golden bytes', () => { @@ -29,3 +33,16 @@ describe('repo host identity golden bytes', () => { expect(raw).toBe(forged) }) }) + +describe('getRepoRegistrationHostIds', () => { + it('collects each host registering the id once, across both SSH spellings', () => { + const repos = [ + { id: 'repo-1' }, + { id: 'repo-1', connectionId: 'box' }, + { id: 'repo-1', executionHostId: 'ssh:box' as const }, + { id: 'repo-2', executionHostId: 'runtime:env' as const } + ] + expect(getRepoRegistrationHostIds(repos, 'repo-1')).toEqual(new Set(['local', 'ssh:box'])) + expect(getRepoRegistrationHostIds(repos, 'missing').size).toBe(0) + }) +}) diff --git a/src/shared/repo-host-identity.ts b/src/shared/repo-host-identity.ts index f4c148b31ef..f835687a691 100644 --- a/src/shared/repo-host-identity.ts +++ b/src/shared/repo-host-identity.ts @@ -1,3 +1,4 @@ +import { getRepoExecutionHostId, type ExecutionHostId } from './execution-host' import type { Repo } from './repo-types' declare const hostScopedRepoKeyBrand: unique symbol @@ -14,3 +15,11 @@ export function getRepoHostIdentityForParts(repoId: string, hostId: string): Hos // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this is the only constructor of the brand; the bytes are the unbranded composite, frozen by golden tests (D25). return `${hostId}\0${repoId}` as HostScopedRepoKey } + +/** The hosts registering `repoId`; two or more means same-id twins on different hosts. */ +export function getRepoRegistrationHostIds( + repos: readonly RepoHostTarget[], + repoId: string +): Set { + return new Set(repos.filter((repo) => repo.id === repoId).map(getRepoExecutionHostId)) +}