From 2e97d9aa84d83ef31d861603bcd6926c5adba013 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Fri, 16 Oct 2026 02:10:23 -0700 Subject: [PATCH] fix: resolve terminal authority from workspace owner evidence (#29565) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- config/focus-setting-read-baseline.txt | 7 - ...agent-session-execution-host-scope.test.ts | 139 +++++++++++++- .../sleeping-record-execution-host-scope.ts | 76 +++++++- .../workspace-terminal-host-authority.test.ts | 176 +++++++++++++++++- .../lib/workspace-terminal-host-authority.ts | 65 ++++++- 5 files changed, 439 insertions(+), 24 deletions(-) diff --git a/config/focus-setting-read-baseline.txt b/config/focus-setting-read-baseline.txt index 8099794849d..e610f6cb499 100644 --- a/config/focus-setting-read-baseline.txt +++ b/config/focus-setting-read-baseline.txt @@ -225,7 +225,6 @@ 1 src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx 1 src/renderer/src/components/terminal-pane/pending-split-close-test-fixture.ts 1 src/renderer/src/components/terminal-pane/pty-connection/agent-idle-working-handlers.ts -1 src/renderer/src/components/terminal-pane/pty-connection/cold-restore-resume-startup.ts 1 src/renderer/src/components/terminal-pane/pty-connection/interrupt-input-intent.ts 1 src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts 1 src/renderer/src/components/terminal-pane/pty-connection/pane-host-capability-flags.ts @@ -264,9 +263,7 @@ 2 src/renderer/src/components/use-task-page-store-bindings.ts 2 src/renderer/src/components/use-terminal-activation-actions.ts 2 src/renderer/src/components/use-terminal-bulk-close-actions.ts -1 src/renderer/src/components/use-terminal-controller.ts 8 src/renderer/src/components/use-terminal-create-actions.ts -1 src/renderer/src/components/use-terminal-watcher-effects.ts 2 src/renderer/src/components/use-worktree-jump-palette-controller.ts 1 src/renderer/src/components/use-worktree-jump-palette-create-action.ts 2 src/renderer/src/components/use-worktree-jump-palette-filter.ts @@ -339,13 +336,11 @@ 2 src/renderer/src/lib/remap-open-editor-tabs-for-path-change.ts 1 src/renderer/src/lib/remote-workspace-drop-test-fixtures.ts 2 src/renderer/src/lib/repo-runtime-owner.ts -2 src/renderer/src/lib/resume-sleeping-agent-session.ts 1 src/renderer/src/lib/runtime-session-mirror-owners.ts 1 src/renderer/src/lib/runtime-session-mirror-targets.ts 1 src/renderer/src/lib/runtime-workspace-file-route.ts 1 src/renderer/src/lib/single-runtime-legacy-owner.ts 1 src/renderer/src/lib/sleeping-agent-session-launch.ts -1 src/renderer/src/lib/sleeping-record-execution-host-scope.ts 1 src/renderer/src/lib/structured-agent-session-launch.ts 1 src/renderer/src/lib/structured-agent-session-provisional-tab.ts 4 src/renderer/src/lib/structured-agent-session-tab-activation.ts @@ -360,11 +355,9 @@ 1 src/renderer/src/lib/workspace-file-drag-source.ts 1 src/renderer/src/lib/workspace-tab-commands.ts 1 src/renderer/src/lib/workspace-tab-palette-activation.ts -3 src/renderer/src/lib/workspace-terminal-host-authority.ts 1 src/renderer/src/lib/worktree-activation-pty-inventory.ts 3 src/renderer/src/lib/worktree-activation.ts 1 src/renderer/src/lib/worktree-creation-agent-seeds.ts -1 src/renderer/src/lib/worktree-initial-terminal-seeding.ts 1 src/renderer/src/lib/worktree-jump-navigation.ts 2 src/renderer/src/lib/worktree-operation-route.ts 1 src/renderer/src/lib/worktree-row-open-in-owner.ts diff --git a/src/renderer/src/lib/resume-sleeping-agent-session-execution-host-scope.test.ts b/src/renderer/src/lib/resume-sleeping-agent-session-execution-host-scope.test.ts index 7e372ad8eea..fb954f5941d 100644 --- a/src/renderer/src/lib/resume-sleeping-agent-session-execution-host-scope.test.ts +++ b/src/renderer/src/lib/resume-sleeping-agent-session-execution-host-scope.test.ts @@ -119,7 +119,12 @@ describe('sleepingRecordNamesAnotherExecutionHost', () => { // yours is the inverse of the defect and worse. const record = makeRecord({ connectionId }) expect( - sleepingRecordNamesAnotherExecutionHost(record, { repos: [], worktreesByRepo: {} }) + sleepingRecordNamesAnotherExecutionHost(record, { + repos: [], + worktreesByRepo: {}, + settings: { activeRuntimeEnvironmentId: 'unrelated' }, + runtimeEnvironments: [{ id: 'unrelated' }] + }) ).toBe(false) } ) @@ -129,7 +134,9 @@ describe('sleepingRecordNamesAnotherExecutionHost', () => { expect( sleepingRecordNamesAnotherExecutionHost(record, { repos: [{ id: 'repo-1' }], - worktreesByRepo: {} + worktreesByRepo: {}, + settings: { activeRuntimeEnvironmentId: 'unrelated' }, + runtimeEnvironments: [{ id: 'unrelated' }] }) ).toBe(true) }) @@ -242,3 +249,131 @@ describe('the resume sweep under execution-host scope', () => { expect(state.sleepingAgentSessionsByPaneKey[native.paneKey]).toBeUndefined() }) }) + +const ownerEvidenceCases: { + name: string + state: WorktreeRuntimeOwnerState + connectionId: string | null + worktreeId?: string + foreign: boolean +}[] = [ + { + name: 'the active local host over an SSH catalog', + state: { + ...catalogOwnedBy('ssh:openclaw'), + activeWorktreeId: WORKTREE_ID, + activeWorkspaceExecutionHostId: 'local' + }, + connectionId: TARGET_ID, + foreign: true + }, + { + name: 'the matching active SSH host over a local catalog', + state: { + ...catalogOwnedBy('local'), + activeWorktreeId: WORKTREE_ID, + activeWorkspaceExecutionHostId: 'ssh:openclaw' + }, + connectionId: TARGET_ID, + foreign: false + }, + { + name: 'a foreign detected-only SSH owner', + state: { + detectedWorktreesByRepo: { + remote: { worktrees: [{ id: WORKTREE_ID, repoId: 'remote', hostId: 'ssh:other' }] } + } + }, + connectionId: TARGET_ID, + foreign: true + }, + { + name: 'conflicting detected owners', + state: { + detectedWorktreesByRepo: { + a: { worktrees: [{ id: WORKTREE_ID, repoId: 'a', hostId: 'ssh:other' }] }, + b: { worktrees: [{ id: WORKTREE_ID, repoId: 'b', hostId: 'local' }] } + } + }, + connectionId: TARGET_ID, + foreign: false + }, + ...[TARGET_ID, 'other'].map((connectionId) => ({ + name: `physical SSH ownership through a runtime for ${connectionId}`, + state: { + repos: [{ id: 'repo-1', connectionId: TARGET_ID, executionHostId: 'runtime:hub' as const }], + worktreesByRepo: { + 'repo-1': [{ id: WORKTREE_ID, repoId: 'repo-1', hostId: 'ssh:openclaw' as const }] + } + }, + connectionId, + foreign: connectionId !== TARGET_ID + })), + { + name: 'a direct SSH folder for a locally captured session', + worktreeId: 'folder:f', + state: { folderWorkspaces: [{ id: 'f', projectGroupId: 'g', connectionId: TARGET_ID }] }, + connectionId: null, + foreign: true + }, + { + name: 'a known unstamped local folder', + worktreeId: 'folder:f', + state: { folderWorkspaces: [{ id: 'f', projectGroupId: 'g' }] }, + connectionId: TARGET_ID, + foreign: true + }, + { + name: 'a restored runtime folder before its catalog', + worktreeId: 'folder:f', + state: { restoredRuntimeHostIdByWorkspaceSessionKey: { 'folder:f': 'runtime:restored' } }, + connectionId: TARGET_ID, + foreign: false + }, + { + name: 'a missing folder', + worktreeId: 'folder:f', + state: {}, + connectionId: TARGET_ID, + foreign: false + }, + { + name: 'conflicting folder owners', + worktreeId: 'folder:f', + state: { + folderWorkspaces: [ + { id: 'f', projectGroupId: 'g', executionHostId: 'local' }, + { id: 'f', projectGroupId: 'g', executionHostId: 'ssh:other' } + ] + }, + connectionId: TARGET_ID, + foreign: false + }, + { + name: 'conflicting inherited folder group owners', + worktreeId: 'folder:f', + state: { + folderWorkspaces: [{ id: 'f', projectGroupId: 'g' }], + projectGroups: [ + { id: 'g', executionHostId: 'local' }, + { id: 'g', executionHostId: 'ssh:other' } + ] + }, + connectionId: TARGET_ID, + foreign: false + } +] + +it.each(ownerEvidenceCases)( + 'compares capture against $name', + ({ state, connectionId, worktreeId, foreign }) => { + const record = makeRecord({ connectionId, ...(worktreeId ? { worktreeId } : {}) }) + expect( + sleepingRecordNamesAnotherExecutionHost(record, { + settings: { activeRuntimeEnvironmentId: 'unrelated' }, + runtimeEnvironments: [{ id: 'unrelated' }], + ...state + }) + ).toBe(foreign) + } +) diff --git a/src/renderer/src/lib/sleeping-record-execution-host-scope.ts b/src/renderer/src/lib/sleeping-record-execution-host-scope.ts index 891c88158b3..97ac63b31f8 100644 --- a/src/renderer/src/lib/sleeping-record-execution-host-scope.ts +++ b/src/renderer/src/lib/sleeping-record-execution-host-scope.ts @@ -3,16 +3,24 @@ import { getRepoExecutionHostId, parseExecutionHostId, toSshExecutionHostId, + toRuntimeExecutionHostId, type ExecutionHostId } from '../../../shared/execution-host' +import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' import { getRepoIdFromWorktreeId } from '@/store/slices/worktree-helpers' import { parseWorkspaceKey } from '../../../shared/workspace-scope' import { findFolderWorkspaceOwner, + getExplicitRuntimeEnvironmentIdForFolderWorkspace, type FolderWorkspaceRuntimeOwnerState } from './folder-workspace-runtime-owner' -import { findIndexedRepoOwner, findIndexedWorktreeOwner } from './worktree-runtime-owner-index' -import { getKnownExecutionHostIdForWorktree } from './worktree-runtime-owner' +import { + findIndexedRepoOwner, + findIndexedWorktreeOwner, + findIndexedProjectGroupOwner, + hasIndexedDetectedWorktree +} from './worktree-runtime-owner-index' +import { resolveExplicitWorktreeOperationRouteResult } from './worktree-operation-catalog-route' import type { WorktreeRuntimeOwnerState } from './worktree-runtime-owner-state' /** @@ -79,12 +87,74 @@ export function sleepingRecordNamesAnotherExecutionHost( ): boolean { return agentResumeOriginNamesAnotherExecutionHost( record.connectionId, - getKnownExecutionHostIdForWorktree(state, record.worktreeId) + currentExecutionHostIdForRecord(record, state) ) } type SleepingRecordOwnerState = WorktreeRuntimeOwnerState & FolderWorkspaceRuntimeOwnerState +// Current routing selection is identity evidence; a focused runtime is not. +function currentExecutionHostIdForRecord( + record: SleepingAgentSessionRecord, + state: SleepingRecordOwnerState +): ExecutionHostId | null { + const worktreeId = record.worktreeId + if (worktreeId === FLOATING_TERMINAL_WORKTREE_ID) { + return 'local' + } + const activeHost = + state.activeWorktreeId === worktreeId + ? parseExecutionHostId(state.activeWorkspaceExecutionHostId) + : null + if (activeHost) { + return activeHost.id + } + const scope = parseWorkspaceKey(worktreeId) + if (scope?.type === 'folder') { + const environmentId = getExplicitRuntimeEnvironmentIdForFolderWorkspace( + state, + scope.folderWorkspaceId + ) + if (environmentId) { + return toRuntimeExecutionHostId(environmentId) + } + const folder = findFolderWorkspaceOwner(state, scope.folderWorkspaceId) + if (!folder) { + return null + } + if ( + !folder.executionHostId && + !folder.connectionId && + state.projectGroups?.some((group) => group.id === folder.projectGroupId) && + !findIndexedProjectGroupOwner(state.projectGroups, folder.projectGroupId) + ) { + return null + } + return ownerExecutionHostIdForRecord(record, state) ?? 'local' + } + const resolution = resolveExplicitWorktreeOperationRouteResult(state, worktreeId) + if (resolution.kind === 'resolved') { + return ( + resolution.route.executionHostId ?? + (resolution.route.runtimeEnvironmentId + ? toRuntimeExecutionHostId(resolution.route.runtimeEnvironmentId) + : null) + ) + } + if ( + resolution.kind === 'ambiguous' || + hasIndexedDetectedWorktree(state.detectedWorktreesByRepo, worktreeId) + ) { + return null + } + const worktree = findIndexedWorktreeOwner(state.worktreesByRepo, worktreeId) + const repo = findIndexedRepoOwner( + state.repos, + worktree?.repoId ?? getRepoIdFromWorktreeId(worktreeId) + ) + return repo ? getRepoExecutionHostId(repo) : null +} + // Why owner rows only: two records' machines are a property of their own workspaces, never of the // focused server, so this reads no routing default and an unknown owner stays unknown. function ownerExecutionHostIdForRecord( diff --git a/src/renderer/src/lib/workspace-terminal-host-authority.test.ts b/src/renderer/src/lib/workspace-terminal-host-authority.test.ts index 71ca6b3473e..89979637bfb 100644 --- a/src/renderer/src/lib/workspace-terminal-host-authority.test.ts +++ b/src/renderer/src/lib/workspace-terminal-host-authority.test.ts @@ -2,11 +2,15 @@ * The seeding predicate answers one question for both remote flavors: does some other party own * terminal creation for this workspace right now? Three answers, and only `none` seeds. */ -import { describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import type * as AgentStatusModule from '@/lib/agent-status' import { createTestStore, makeWorktree } from '@/store/slices/store-test-helpers' import { ensureWorktreeHasInitialTerminal } from '@/lib/worktree-initial-terminal-seeding' -import { resolveWorkspaceTerminalHostAuthority } from '@/lib/workspace-terminal-host-authority' +import { + resolveWorkspaceTerminalHostAuthority, + type WorkspaceTerminalHostAuthorityState +} from '@/lib/workspace-terminal-host-authority' +import { makeFolderWorkspace } from '@/store/slices/worktrees-slice-test-fixtures' vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } })) vi.mock('@/lib/agent-status', async (importOriginal) => { @@ -95,6 +99,38 @@ describe('workspace terminal seeding authority', () => { expect(terminalTabCount(store, LOCAL_WORKTREE_ID)).toBe(1) }) + it.each(['local', 'unstamped', 'missing', 'folder'] as const)( + 'seeds a %s workspace while an unrelated runtime is selected', + (ownership) => { + const store = createTestStore() + seedLocal(store) + if (ownership === 'unstamped') { + store.setState({ + worktreesByRepo: { + repoLocal: [makeWorktree({ id: LOCAL_WORKTREE_ID, repoId: 'repoLocal' })] + } + }) + } else if (ownership === 'missing') { + store.setState({ repos: [], worktreesByRepo: {} }) + } + if (ownership === 'folder') { + store.setState({ folderWorkspaces: [makeFolderWorkspace()] }) + } + const worktreeId = ownership === 'folder' ? 'folder:folder-1' : LOCAL_WORKTREE_ID + const state = { + ...store.getState(), + settings: { + ...store.getState().settings, + activeRuntimeEnvironmentId: ENVIRONMENT_ID + }, + runtimeEnvironments: [{ id: ENVIRONMENT_ID }] + } + + expect(ensureWorktreeHasInitialTerminal(state, worktreeId)).toBeTruthy() + expect(terminalTabCount(store, worktreeId)).toBe(1) + } + ) + it('seeds once the SSH target connects, with no workspace snapshot to wait for', () => { // The relay that served host workspace snapshots is gone (#27214), so nothing will ever answer // one; waiting for it left a reconnected target unable to seed or resume for good. @@ -220,3 +256,139 @@ describe('workspace terminal seeding authority', () => { expect(terminalTabCount(store, PAIRED_WORKTREE_ID)).toBe(0) }) }) + +const WT = 'repo::/workspace' +const FOLDER = 'folder:folder-1' +const TARGET = 'ssh:target' as const +const base = { + settings: { activeRuntimeEnvironmentId: 'unrelated' }, + runtimeEnvironments: [{ id: 'unrelated' }], + runtimeEnvironmentCatalogHydrated: false +} satisfies WorkspaceTerminalHostAuthorityState +const sshRow = { id: WT, repoId: 'repo', hostId: TARGET } +const hubA = { id: 'repo', connectionId: 'target', executionHostId: 'runtime:hub-a' as const } +const hubB = { id: 'repo', connectionId: 'target', executionHostId: 'runtime:hub-b' as const } + +const cases: { + name: string + id: string + state: WorkspaceTerminalHostAuthorityState + expected: 'live' | 'unverifiable' | 'none' +}[] = [ + { + name: 'repo-owned runtime before worktree hydration', + id: WT, + expected: 'live', + state: { repos: [{ id: 'repo', executionHostId: 'runtime:owner' }] } + }, + { + name: 'runtime HUB behind an SSH worktree', + id: WT, + expected: 'live', + state: { repos: [hubA], worktreesByRepo: { repo: [sshRow] } } + }, + { + name: 'runtime project group owns a folder', + id: FOLDER, + expected: 'live', + state: { + folderWorkspaces: [{ id: 'folder-1', projectGroupId: 'group' }], + projectGroups: [{ id: 'group', executionHostId: 'runtime:owner' }] + } + }, + { + name: 'restored runtime folder before catalog hydration', + id: FOLDER, + expected: 'live', + state: { + folderWorkspaces: [], + projectGroups: [], + restoredRuntimeHostIdByWorkspaceSessionKey: { [FOLDER]: 'runtime:restored' } + } + }, + { + name: 'disconnected direct SSH folder is outside replacement scope', + id: FOLDER, + expected: 'none', + state: { + folderWorkspaces: [{ id: 'folder-1', projectGroupId: 'group', connectionId: 'target' }] + } + }, + { + name: 'conflicting folder owners stay unverifiable', + id: FOLDER, + expected: 'unverifiable', + state: { + folderWorkspaces: [ + { id: 'folder-1', projectGroupId: 'group', executionHostId: 'local' }, + { id: 'folder-1', projectGroupId: 'group', executionHostId: 'runtime:owner' } + ] + } + }, + { + name: 'conflicting folder project-group owners stay unverifiable', + id: FOLDER, + expected: 'unverifiable', + state: { + folderWorkspaces: [{ id: 'folder-1', projectGroupId: 'group' }], + projectGroups: [ + { id: 'group', executionHostId: 'local' }, + { id: 'group', executionHostId: 'runtime:owner' } + ] + } + }, + { + name: 'unroutable detected publication stays unverifiable', + id: WT, + expected: 'unverifiable', + state: { detectedWorktreesByRepo: { repo: { worktrees: [{ id: WT, repoId: 'repo' }] } } } + } +] +it.each(cases)('$name', ({ id, state, expected }) => { + expect(resolveWorkspaceTerminalHostAuthority({ ...base, ...state }, id)).toBe(expected) +}) + +it('lets the explicit same-workspace host disambiguate rival catalog owners', () => { + const state: WorkspaceTerminalHostAuthorityState = { + ...base, + activeWorktreeId: WT, + repos: [ + { id: 'repo', executionHostId: 'local' }, + { id: 'repo', executionHostId: 'runtime:owner' } + ] + } + expect( + resolveWorkspaceTerminalHostAuthority({ ...state, activeWorkspaceExecutionHostId: 'local' }, WT) + ).toBe('none') + expect( + resolveWorkspaceTerminalHostAuthority( + { ...state, activeWorkspaceExecutionHostId: 'runtime:owner' }, + WT + ) + ).toBe('live') +}) + +it('waits for rival SSH HUB owners, then accepts the surviving runtime owner', () => { + const state: WorkspaceTerminalHostAuthorityState = { + ...base, + activeWorktreeId: WT, + activeWorkspaceExecutionHostId: TARGET, + worktreesByRepo: { repo: [sshRow] } + } + expect(resolveWorkspaceTerminalHostAuthority({ ...state, repos: [hubA, hubB] }, WT)).toBe( + 'unverifiable' + ) + expect(resolveWorkspaceTerminalHostAuthority({ ...state, repos: [hubA] }, WT)).toBe('live') +}) + +afterEach(() => vi.unstubAllGlobals()) +it.each([WT, FOLDER])('leaves local terminal ownership with the real paired web host: %s', (id) => { + vi.stubGlobal('__ORCA_WEB_CLIENT__', true) + const state: WorkspaceTerminalHostAuthorityState = { + settings: { activeRuntimeEnvironmentId: null }, + runtimeEnvironments: [{ id: 'paired-host' }], + worktreesByRepo: { repo: [{ id: WT, repoId: 'repo', hostId: 'local' }] }, + folderWorkspaces: [{ id: 'folder-1', projectGroupId: 'group', executionHostId: 'local' }] + } + expect(resolveWorkspaceTerminalHostAuthority(state, id)).toBe('live') +}) diff --git a/src/renderer/src/lib/workspace-terminal-host-authority.ts b/src/renderer/src/lib/workspace-terminal-host-authority.ts index 8533da44e2b..5ae0ac011d2 100644 --- a/src/renderer/src/lib/workspace-terminal-host-authority.ts +++ b/src/renderer/src/lib/workspace-terminal-host-authority.ts @@ -4,10 +4,17 @@ import { parseWorkspaceKey } from '../../../shared/workspace-scope' import type { HostLiveTerminalProbeVerdict } from '@/runtime/host-live-terminal-probe' import type { SshConnectionState } from '../../../shared/ssh-types' import { isWebRuntimeSessionActive } from '@/runtime/web-runtime-session-environment' +import { resolveExplicitWorktreeOperationRouteResult } from '@/lib/worktree-operation-catalog-route' +import { resolveStrictWorktreeOperationRouteResultForHost } from '@/lib/worktree-operation-route' +import { getPairedWebClientEnvironmentId } from '@/lib/paired-web-client-host' import { - getExecutionHostIdForWorktree, - getRuntimeEnvironmentIdForWorktree -} from '@/lib/worktree-runtime-owner' + findFolderWorkspaceOwner, + getExplicitRuntimeEnvironmentIdForFolderWorkspace +} from '@/lib/folder-workspace-runtime-owner' +import { + findIndexedProjectGroupOwner, + hasIndexedDetectedWorktree +} from '@/lib/worktree-runtime-owner-index' import type { WorktreeRuntimeOwnerState } from '@/lib/worktree-runtime-owner-state' /** @@ -64,16 +71,54 @@ export function resolveWorkspaceTerminalHostAuthority( if (!worktreeId || worktreeId === FLOATING_TERMINAL_WORKTREE_ID) { return 'none' } - if (isWebRuntimeSessionActive(getRuntimeEnvironmentIdForWorktree(state, worktreeId))) { - return 'live' + const selectedHost = + state.activeWorktreeId === worktreeId + ? parseExecutionHostId(state.activeWorkspaceExecutionHostId) + : null + const scope = parseWorkspaceKey(worktreeId) + if (!selectedHost && scope?.type === 'folder') { + const folderId = scope.folderWorkspaceId + const owner = findFolderWorkspaceOwner(state, folderId) + if (!owner && state.folderWorkspaces?.some((folder) => folder.id === folderId)) { + return 'unverifiable' + } + if ( + owner && + !owner.executionHostId && + !owner.connectionId && + state.projectGroups?.some((group) => group.id === owner.projectGroupId) && + !findIndexedProjectGroupOwner(state.projectGroups, owner.projectGroupId) + ) { + return 'unverifiable' + } + const environmentId = + getExplicitRuntimeEnvironmentIdForFolderWorkspace(state, folderId) ?? + getPairedWebClientEnvironmentId(state) + // Folder workspaces were never host-replaced, so direct SSH keeps `none`. + return isWebRuntimeSessionActive(environmentId) ? 'live' : 'none' } - const host = parseExecutionHostId(getExecutionHostIdForWorktree(state, worktreeId)) - if (host?.kind === 'runtime') { - // A runtime host we could not name (rival detected publications, unhydrated catalog) is unasked. + const resolution = selectedHost + ? resolveStrictWorktreeOperationRouteResultForHost(state, worktreeId, selectedHost.id) + : resolveExplicitWorktreeOperationRouteResult(state, worktreeId) + if ( + resolution.kind === 'ambiguous' || + (resolution.kind === 'missing' && + hasIndexedDetectedWorktree(state.detectedWorktreesByRepo, worktreeId)) + ) { return 'unverifiable' } - if (host?.kind === 'ssh' && parseWorkspaceKey(worktreeId)?.type !== 'folder') { - // Folder workspaces were never host-replaced, so they keep `none`. + const route = resolution.kind === 'resolved' ? resolution.route : null + // A focused runtime routes legacy operations; only owner evidence grants terminal authority. + if ( + isWebRuntimeSessionActive(route?.runtimeEnvironmentId ?? getPairedWebClientEnvironmentId(state)) + ) { + return 'live' + } + const host = parseExecutionHostId(route?.executionHostId) + if (host?.kind === 'runtime') { + return 'unverifiable' + } + if (host?.kind === 'ssh' && scope?.type !== 'folder') { return resolveDirectSshAuthority(state, host.targetId) } // Local, or outside the host's replace scope: this client is the execution host and its own rows are