mirror of
https://github.com/stablyai/orca.git
synced 2026-10-14 00:02:42 +00:00
* fix(host-routing): resolve the execution host before reading a connection Three issues in one defect class: a resolver reads one spelling of one arbitrarily chosen row instead of resolving the worktree's execution host, so something local answers a question about a remote. returned that row's connectionId. With duplicate repo rows for one repo id it could pair a runtime owner with a client-owned SSH connection. It now resolves through the same ambiguity-aware index getRuntimeEnvironmentIdForWorktree uses, prefers the repo row for the host the worktree names, and derives the connection from the resolved host. Conflicting rows return `undefined` (this module's documented "cannot determine the host"), never `null`. `store.getRepo(worktree.repoId)?.connectionId ?? null`. `getRepo` is host-blind and the same repo id can exist on local, SSH and runtime hosts, so a remote worktree could spawn its PTY on the client with the remote cwd. resolveWorktreeLaunchHost picks the row for the worktree's host and reads the connection off that host; conflicting rows are unresolved, not local. session-partition owner maps that contradict each other. Both now compute through one shared function whose argument records the divergence. No behaviour change on either side: converging needs a read-both migration, since both partitions hold real data written by shipping builds. * fix(host-routing): keep nested SSH connections resolvable under a runtime host getRepoSshConnectionId read only the resolved execution host, so a repo row owned by a runtime that reaches a nested SSH target (connectionId: ssh-*, executionHostId: runtime:*) resolved to no connection — answering 'local' for a remote worktree, the same defect #17909 fixed in the other direction. * fix(host-routing): resolve both sides of the execution host through one rule The renderer resolver leaked between two different SSH hosts: a worktree on `ssh:m4air` whose only indexed repo row belonged to `openclaw` answered 'openclaw', because the host-scoped lookup missing fell through to an id-only one. Main's resolver, in the same change, answered 'm4air' — two resolvers, one right and one wrong, on identical input. Both sides now adapt one shared rule (`worktree-execution-host-resolution.ts`): the worktree's own host outranks every repo row, and a row on a different host is never evidence about this one. The renderer's WeakMap index becomes the memoizing adapter it always was; `resolveWorktreeLaunchHost` becomes main's mapping of unresolved onto its throw. Settles the rule the change previously answered two ways. `getRepoSshConnectionId` and `getSshTargetIdForExecutionHost` disagreed for a runtime host carrying a nested `connectionId`; they now compose, so the execution host is the single authority. On a `runtime:*` row that field is a paired HUB's private SSH target, spread through by `repoWithFetchedOwner` and unaddressable from this client — the project-first successor of the row nulls it for exactly that reason. That also fixes the `kind !== 'ssh'` fallback, which fired for `local`: a row declaring itself local handed out an SSH connection.
146 lines
5.6 KiB
TypeScript
146 lines
5.6 KiB
TypeScript
import {
|
|
LOCAL_EXECUTION_HOST_ID,
|
|
getRepoExecutionHostId,
|
|
parseExecutionHostId,
|
|
toSshExecutionHostId,
|
|
type ExecutionHostId
|
|
} from '../../shared/execution-host'
|
|
import type { FolderWorkspace } from '../../shared/folder-workspace-types'
|
|
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
|
|
import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
|
|
import { workspaceSessionPartitionHostId } from '../../shared/workspace-session-partition-owner'
|
|
import { parseWorkspaceKey } from '../../shared/workspace-scope'
|
|
import type { RuntimeStore } from './runtime-store-contract'
|
|
|
|
type RuntimeWorkspaceSessionDependencies = {
|
|
getStore: () => RuntimeStore | null
|
|
resolveFolderConnectionId: (workspace: FolderWorkspace) => string | null
|
|
hasRuntimeOwnedPtyCandidate: (
|
|
session: WorkspaceSessionState,
|
|
worktreeId: string,
|
|
tabs: WorkspaceSessionState['tabsByWorktree'][string]
|
|
) => boolean
|
|
}
|
|
|
|
export class RuntimeWorkspaceSessionController {
|
|
constructor(private readonly deps: RuntimeWorkspaceSessionDependencies) {}
|
|
|
|
tryGetHostId(worktreeId: string): ExecutionHostId | null {
|
|
const store = this.deps.getStore()
|
|
const scope = parseWorkspaceKey(worktreeId)
|
|
if (scope?.type === 'folder') {
|
|
const workspace = store
|
|
?.getFolderWorkspaces?.()
|
|
.find((entry) => entry.id === scope.folderWorkspaceId)
|
|
if (!workspace) {
|
|
return null
|
|
}
|
|
// An explicit host is authoritative for folder workspaces. The connection
|
|
// id is only a legacy fallback for records written before host ids existed.
|
|
if (workspace.executionHostId != null) {
|
|
return parseExecutionHostId(workspace.executionHostId)?.id ?? null
|
|
}
|
|
const connectionId = this.deps.resolveFolderConnectionId(workspace)
|
|
return connectionId ? toSshExecutionHostId(connectionId) : LOCAL_EXECUTION_HOST_ID
|
|
}
|
|
const resolvedWorktreeId = scope?.type === 'worktree' ? scope.worktreeId : worktreeId
|
|
const repo = store?.getRepo?.(getRepoIdFromWorktreeId(resolvedWorktreeId))
|
|
// Why: SSH worktrees keep their own `ssh:<targetId>` partition here while the renderer writes
|
|
// them to 'local'; the shared owner map records that divergence (#12723).
|
|
return repo
|
|
? workspaceSessionPartitionHostId(getRepoExecutionHostId(repo), 'host-partition')
|
|
: LOCAL_EXECUTION_HOST_ID
|
|
}
|
|
|
|
getHostId(worktreeId: string): ExecutionHostId {
|
|
const hostId = this.tryGetHostId(worktreeId)
|
|
if (!hostId) {
|
|
throw new Error('folder_workspace_not_found')
|
|
}
|
|
return hostId
|
|
}
|
|
|
|
get(worktreeId: string): WorkspaceSessionState | null {
|
|
const hostId = this.tryGetHostId(worktreeId)
|
|
return hostId ? (this.deps.getStore()?.getWorkspaceSession?.(hostId) ?? null) : null
|
|
}
|
|
|
|
set(worktreeId: string, session: WorkspaceSessionState): void {
|
|
this.deps.getStore()?.setWorkspaceSession?.(session, this.getHostId(worktreeId))
|
|
}
|
|
|
|
getKnownWorktreeIds(): Set<string> {
|
|
const store = this.deps.getStore()
|
|
const repos = store?.getRepos?.() ?? []
|
|
const repoIds = new Set(repos.map((repo) => repo.id))
|
|
const hostIds = new Set<ExecutionHostId>(['local'])
|
|
for (const repo of repos) {
|
|
hostIds.add(getRepoExecutionHostId(repo))
|
|
}
|
|
const worktreeIds = new Set<string>()
|
|
for (const hostId of hostIds) {
|
|
const session = store?.getWorkspaceSession?.(hostId)
|
|
for (const worktreeId of Object.keys(session?.tabsByWorktree ?? {})) {
|
|
if (repoIds.has(getRepoIdFromWorktreeId(worktreeId))) {
|
|
worktreeIds.add(worktreeId)
|
|
}
|
|
}
|
|
}
|
|
return worktreeIds
|
|
}
|
|
|
|
getHydrationTargets(includeAllPersistedWorktrees: boolean): Map<string, WorkspaceSessionState> {
|
|
const store = this.deps.getStore()
|
|
const repos = store?.getRepos?.() ?? []
|
|
const repoHostIdByRepoId = new Map(
|
|
repos.map((repo) => [repo.id, getRepoExecutionHostId(repo)] as const)
|
|
)
|
|
const folderHostIdByWorkspaceId = new Map(
|
|
(store?.getFolderWorkspaces?.() ?? []).map((workspace) => {
|
|
const explicitHostId =
|
|
workspace.executionHostId != null
|
|
? (parseExecutionHostId(workspace.executionHostId)?.id ?? null)
|
|
: null
|
|
const connectionId = explicitHostId ? null : this.deps.resolveFolderConnectionId(workspace)
|
|
return [
|
|
workspace.id,
|
|
explicitHostId ??
|
|
(connectionId ? toSshExecutionHostId(connectionId) : LOCAL_EXECUTION_HOST_ID)
|
|
] as const
|
|
})
|
|
)
|
|
const hostIds = new Set<ExecutionHostId>(['local'])
|
|
for (const repo of repos) {
|
|
hostIds.add(getRepoExecutionHostId(repo))
|
|
}
|
|
for (const hostId of store?.getWorkspaceSessionHostIds?.() ?? []) {
|
|
hostIds.add(hostId)
|
|
}
|
|
|
|
const targets = new Map<string, WorkspaceSessionState>()
|
|
for (const hostId of hostIds) {
|
|
const session = store?.getWorkspaceSession?.(hostId)
|
|
if (!session) {
|
|
continue
|
|
}
|
|
for (const [worktreeId, tabs] of Object.entries(session.tabsByWorktree ?? {})) {
|
|
const scope = parseWorkspaceKey(worktreeId)
|
|
const ownerHostId =
|
|
scope?.type === 'folder'
|
|
? (folderHostIdByWorkspaceId.get(scope.folderWorkspaceId) ?? null)
|
|
: (repoHostIdByRepoId.get(
|
|
getRepoIdFromWorktreeId(scope?.type === 'worktree' ? scope.worktreeId : worktreeId)
|
|
) ?? LOCAL_EXECUTION_HOST_ID)
|
|
if (
|
|
ownerHostId === hostId &&
|
|
(includeAllPersistedWorktrees ||
|
|
this.deps.hasRuntimeOwnedPtyCandidate(session, worktreeId, tabs))
|
|
) {
|
|
targets.set(worktreeId, session)
|
|
}
|
|
}
|
|
}
|
|
return targets
|
|
}
|
|
}
|