diff --git a/src/main/ipc/pty-controller-process-inventory.test.ts b/src/main/ipc/pty-controller-process-inventory.test.ts index 50b67963ab9..a1a352eb9e4 100644 --- a/src/main/ipc/pty-controller-process-inventory.test.ts +++ b/src/main/ipc/pty-controller-process-inventory.test.ts @@ -99,10 +99,7 @@ const mainWindow = { } function captureController(): { - listProcesses: ( - connectionId?: string | null, - opts?: { deadlineMs?: number } - ) => Promise + listProcesses: (hostId?: string, opts?: { deadlineMs?: number }) => Promise } { handleMock.mockReset() onMock.mockReset() @@ -177,7 +174,7 @@ describe('aggregate PTY process inventory', () => { const controller = captureController() const deadlineMs = Date.now() + 1200 - await controller.listProcesses('conn-a', { deadlineMs }) + await controller.listProcesses('ssh:conn-a', { deadlineMs }) expect(remote.calls).toEqual([{ opts: { deadlineMs } }]) }) diff --git a/src/main/ipc/pty-runtime-kill-and-exit.test.ts b/src/main/ipc/pty-runtime-kill-and-exit.test.ts index 52390bd2bf6..f67c7756a01 100644 --- a/src/main/ipc/pty-runtime-kill-and-exit.test.ts +++ b/src/main/ipc/pty-runtime-kill-and-exit.test.ts @@ -3,6 +3,7 @@ import { spawnMock } from './pty-ipc-mock-registry' import { makeDeferred } from './pty-ipc-test-constants' import { setupPtyIpcSuite } from './pty-ipc-test-harness' import { LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId } from '../../shared/execution-host' +import type { RuntimePtyController } from '../runtime/runtime-pty-controller-contract' import { registerPtyHandlers, registerSshPtyProvider, @@ -105,31 +106,28 @@ describe('registerPtyHandlers', () => { registerSshPtyProvider('ssh-b', { listProcesses: sshBList } as never) setPtyOwnership('ssh-b-pty', 'ssh:ssh-b') const runtime = { - setPtyController: vi.fn(), + setPtyController: vi.fn((_controller: RuntimePtyController) => {}), markPtyLivenessUnverifiable: vi.fn() } handlers.clear() registerPtyHandlers(mainWindow as never, runtime as never) - const controller = runtime.setPtyController.mock.calls[0]?.[0] as { - listProcesses(connectionId?: string | null): Promise<{ id: string }[]> - listProcessesWithHostScope(): Promise<{ processes: { id: string }[]; hostIds: string[] }> - } + const controller = runtime.setPtyController.mock.calls[0]?.[0] - await expect(controller.listProcesses(null)).resolves.toEqual([ + await expect(controller?.listProcesses?.('local')).resolves.toEqual([ { id: 'local-pty', title: 'Local', cwd: '/local' } ]) expect(localList).toHaveBeenCalledOnce() expect(sshAList).not.toHaveBeenCalled() expect(sshBList).not.toHaveBeenCalled() - await expect(controller.listProcesses('ssh-a')).resolves.toEqual([{ id: 'ssh-a-pty' }]) + await expect(controller?.listProcesses?.('ssh:ssh-a')).resolves.toEqual([{ id: 'ssh-a-pty' }]) expect(sshAList).toHaveBeenCalledOnce() expect(sshBList).not.toHaveBeenCalled() // STA-517: the aggregate used to propagate ssh-b's failure, which cost the runtime the // whole liveness inventory — so no PTY was ever proven dead and mobile kept every // retained pane "active". One unreachable relay now drops out of the answer instead. - await expect(controller.listProcesses()).resolves.toEqual([ + await expect(controller?.listProcesses?.()).resolves.toEqual([ { id: 'local-pty', title: 'Local', cwd: '/local' }, { id: 'ssh-a-pty' } ]) @@ -139,7 +137,7 @@ describe('registerPtyHandlers', () => { 'ssh-b unavailable' ) - await expect(controller.listProcessesWithHostScope()).resolves.toEqual({ + await expect(controller?.listProcessesWithHostScope?.()).resolves.toEqual({ processes: [{ id: 'local-pty', title: 'Local', cwd: '/local' }, { id: 'ssh-a-pty' }], hostIds: [LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId('ssh-a')] }) diff --git a/src/main/ipc/pty-ssh-stop-verdict.test.ts b/src/main/ipc/pty-ssh-stop-verdict.test.ts index f4730a9377f..fbc00f2257e 100644 --- a/src/main/ipc/pty-ssh-stop-verdict.test.ts +++ b/src/main/ipc/pty-ssh-stop-verdict.test.ts @@ -63,7 +63,7 @@ describe('stopping a PTY whose SSH provider is unregistered', () => { function installController(): { controller: { kill: (ptyId: string) => boolean - listProcesses: (connectionId?: string | null) => Promise<{ id: string }[]> + listProcesses: (hostId?: string) => Promise<{ id: string }[]> retireRejectedPty: (ptyId: string, stopConfirmed: boolean) => void stopAndWait: (ptyId: string, opts?: { deadlineMs?: number }) => Promise } diff --git a/src/main/ipc/pty/ipc/inspect.ts b/src/main/ipc/pty/ipc/inspect.ts index 8d634de2407..af814a4a6aa 100644 --- a/src/main/ipc/pty/ipc/inspect.ts +++ b/src/main/ipc/pty/ipc/inspect.ts @@ -11,12 +11,12 @@ import { ptyOwnership } from '../provider/ownership-state' import { getProviderForPty, getProvider, - getPtySshConnectionId, hasPtyProviderForInspection, isDispatchablePtyHost, registeredPtyProviders, resolvePtyExecutionHost, - tryGetProviderForPty + tryGetProviderForPty, + type ResolvedPtyHost } from '../provider/registry' import { getConnectionExecutionHostId, @@ -32,7 +32,7 @@ import { } from '../pane/serializer-state' export function installPtyInspectIpcHandlers(deps: { - getLocalPtyProviderStartupPromise: (connectionId?: string | null) => Promise | undefined + getLocalPtyProviderStartupPromise: (hostId?: ResolvedPtyHost) => Promise | undefined }): void { const ipcMain = getPtyIpc() const { getLocalPtyProviderStartupPromise } = deps @@ -42,7 +42,7 @@ export function installPtyInspectIpcHandlers(deps: { // renderer-kill.ts inlines this — pty:kill's listener teardown is // ordering-sensitive and must not gain even a no-barrier microtask. const awaitSwapWindow = async (id: string): Promise => { - await getLocalPtyProviderStartupPromise(getPtySshConnectionId(id)) + await getLocalPtyProviderStartupPromise(resolvePtyExecutionHost(id)) } ipcMain.handle( diff --git a/src/main/ipc/pty/ipc/renderer-kill.ts b/src/main/ipc/pty/ipc/renderer-kill.ts index 6dbdf023360..503efaaeeee 100644 --- a/src/main/ipc/pty/ipc/renderer-kill.ts +++ b/src/main/ipc/pty/ipc/renderer-kill.ts @@ -9,7 +9,9 @@ import { ptyIncarnationById } from '../provider/ownership-state' import { getProviderForPty, getPtySshConnectionId, - tryGetProviderForPty + resolvePtyExecutionHost, + tryGetProviderForPty, + type ResolvedPtyHost } from '../provider/registry' import { finishPtyShutdown, isPtyAlreadyGoneError } from '../provider/liveness' import { recordUndeliveredSshPtyKill } from '../runtime/undelivered-ssh-kill' @@ -17,7 +19,7 @@ import { recordUndeliveredSshPtyKill } from '../runtime/undelivered-ssh-kill' export type PtyKillIpcDeps = { store?: Store runtime?: OrcaRuntimeService - getLocalPtyProviderStartupPromise: (connectionId?: string | null) => Promise | undefined + getLocalPtyProviderStartupPromise: (hostId?: ResolvedPtyHost) => Promise | undefined shutdownProviderAndDetectExit: ( provider: IPtyProvider, id: string, @@ -91,7 +93,7 @@ async function stopRendererOwnedPtyProcess( runtime?.markPtyStopRequested?.(args.id) const connectionId = getPtySshConnectionId(args.id) // Why: wait for daemon startup before selecting the local provider, else a fallback shutdown falsely succeeds and orphans a restored daemon PTY (#7742). - const startupPromise = getLocalPtyProviderStartupPromise(connectionId) + const startupPromise = getLocalPtyProviderStartupPromise(resolvePtyExecutionHost(args.id)) if (startupPromise) { await startupPromise } diff --git a/src/main/ipc/pty/ipc/spawn-types.ts b/src/main/ipc/pty/ipc/spawn-types.ts index b0ac1b4c2a0..a80a7dbf770 100644 --- a/src/main/ipc/pty/ipc/spawn-types.ts +++ b/src/main/ipc/pty/ipc/spawn-types.ts @@ -20,6 +20,7 @@ import type { } from '../host-env/types' import type { CodexResumeLaunch, PreparedCodexResumeHome } from '../host-env/codex-resume' import type { StablePaneOwner } from '../pane/stable-owner' +import type { ResolvedPtyHost } from '../provider/registry' export type PtySpawnIpcArgs = { cols: number @@ -91,7 +92,7 @@ export type PtySpawnIpcDeps = { prepareCodexSessionResume?: PrepareCodexSessionResume onCodexHomePtySpawned?: (args: CodexHomePtySpawnedLifecycleArgs) => void } - getLocalPtyStartupPromise: (connectionId?: string | null) => Promise | undefined + getLocalPtyStartupPromise: (hostId?: ResolvedPtyHost) => Promise | undefined adoptStablePane: (args: AdoptStablePaneArgs) => Promise assertFolderWorkspacePtyPathUsable: (worktreeId: string | undefined) => Promise | void resolvePtySpawnStartupCwd: ( diff --git a/src/main/ipc/pty/ipc/spawn.ts b/src/main/ipc/pty/ipc/spawn.ts index 6fd5183a36d..299094c4bef 100644 --- a/src/main/ipc/pty/ipc/spawn.ts +++ b/src/main/ipc/pty/ipc/spawn.ts @@ -1,13 +1,16 @@ import { getPtyIpc } from '../../pty-host-bindings' import { runPtyIpcSpawn } from './spawn-run' import type { PtySpawnIpcArgs, PtySpawnIpcDeps } from './spawn-types' +import { getConnectionExecutionHostId } from '../../../../shared/execution-host' export function installPtySpawnIpcHandler(deps: PtySpawnIpcDeps): void { const ipcMain = getPtyIpc() const { getLocalPtyStartupPromise } = deps ipcMain.handle('pty:spawn', async (_event, args: PtySpawnIpcArgs) => { - const startupPromise = getLocalPtyStartupPromise(args.connectionId) + const startupPromise = getLocalPtyStartupPromise( + getConnectionExecutionHostId(args.connectionId) + ) if (startupPromise) { await startupPromise } diff --git a/src/main/ipc/pty/provider/registry.ts b/src/main/ipc/pty/provider/registry.ts index a35dcfd322f..98de7a69620 100644 --- a/src/main/ipc/pty/provider/registry.ts +++ b/src/main/ipc/pty/provider/registry.ts @@ -66,7 +66,7 @@ export function registeredPtyProviders(): RegisteredPtyProvider[] { } export class PtyHostNotDispatchableError extends Error { - constructor(readonly hostId: ExecutionHostId | 'foreign') { + constructor(readonly hostId: ResolvedPtyHost) { // Why: a paired runtime drives its own PTYs over runtime RPC; this process has no provider for them. super(`PTY host "${hostId}" is not dispatchable from this process`) this.name = 'PtyHostNotDispatchableError' @@ -93,9 +93,11 @@ export function getProvider(hostId: ExecutionHostId): IPtyProvider { return provider } -/** The host that runs a PTY: its recorded owner, else the host its id names. 'foreign' = off this - * machine but unnameable. */ -export function resolvePtyExecutionHost(ptyId: string): ExecutionHostId | 'foreign' { +/** 'foreign' = a PTY that runs off this machine on a host its id cannot name. */ +export type ResolvedPtyHost = ExecutionHostId | 'foreign' + +/** The host that runs a PTY: its recorded owner, else the host its id names. */ +export function resolvePtyExecutionHost(ptyId: string): ResolvedPtyHost { const owner = ptyOwnership.get(ptyId) if (owner !== undefined) { return owner @@ -117,7 +119,7 @@ export function getPtySshConnectionId(ptyId: string): string | null { } /** Whether this process can hold a provider for the host at all (this machine or an SSH relay). */ -export function isDispatchablePtyHost(hostId: ExecutionHostId | 'foreign'): boolean { +export function isDispatchablePtyHost(hostId: ResolvedPtyHost): boolean { return ( hostId === LOCAL_EXECUTION_HOST_ID || (hostId !== 'foreign' && getSshTargetIdForExecutionHost(hostId) !== null) diff --git a/src/main/ipc/pty/register-handlers.ts b/src/main/ipc/pty/register-handlers.ts index 5e823ac535c..74419ae25f7 100644 --- a/src/main/ipc/pty/register-handlers.ts +++ b/src/main/ipc/pty/register-handlers.ts @@ -8,7 +8,7 @@ import { getHiddenRendererPtyDeliveryDebug, resetRendererScopedHiddenPtyDeliveryState } from '../pty-hidden-delivery-gate' -import { localProvider } from './provider/registry' +import { localProvider, type ResolvedPtyHost } from './provider/registry' import { finishPtyShutdown } from './provider/liveness' import type { GetSelectedCodexHomePath, PrepareClaudeAuth } from './host-env/types' import { installPtyInspectIpcHandlers } from './ipc/inspect' @@ -66,6 +66,7 @@ import { stripSequencedStartupResumeArgv } from './host-env/codex-resume' import { ensureLinuxTerminalOrcaCliShimDir } from '../../cli/linux-terminal-orca-cli-shim' +import { LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host' export function registerPtyHandlers( mainWindow?: PtyRendererDelivery, @@ -95,8 +96,11 @@ export function registerPtyHandlers( setRebindProviderListeners(() => {}) registerRendererLifecycleResetHandlers(mainWindow?.webContents) - const getLocalPtyStartupPromise = (connectionId?: string | null): Promise | undefined => { - if (connectionId) { + // Why only `local`: the daemon swap re-owns this machine's PTYs; no other host waits on it. + const getLocalPtyStartupPromise = ( + hostId: ResolvedPtyHost = LOCAL_EXECUTION_HOST_ID + ): Promise | undefined => { + if (hostId !== LOCAL_EXECUTION_HOST_ID) { return undefined } // Why: during cold start the daemon provider swap overlaps first paint, so local spawns must wait; SSH/headless don't use the desktop daemon. @@ -104,9 +108,9 @@ export function registerPtyHandlers( } const getLocalPtyProviderStartupPromise = ( - connectionId?: string | null + hostId: ResolvedPtyHost = LOCAL_EXECUTION_HOST_ID ): Promise | undefined => { - if (connectionId) { + if (hostId !== LOCAL_EXECUTION_HOST_ID) { return undefined } return options?.awaitLocalPtyProviderStartup?.() ?? options?.awaitLocalPtyStartup?.() diff --git a/src/main/ipc/pty/runtime/controller-deps.ts b/src/main/ipc/pty/runtime/controller-deps.ts index a2dcc21bada..5c2ea1bf82c 100644 --- a/src/main/ipc/pty/runtime/controller-deps.ts +++ b/src/main/ipc/pty/runtime/controller-deps.ts @@ -18,13 +18,14 @@ import type { CodexResumeLaunch, PreparedCodexResumeHome } from '../host-env/cod import type { StablePaneOwner } from '../pane/stable-owner' import type { AdoptStablePaneArgs, AdoptStablePaneResult } from '../ipc/spawn-types' import type { finishPtyShutdown } from '../provider/liveness' +import type { ResolvedPtyHost } from '../provider/registry' export type PtyRuntimeControllerDeps = { runtime?: OrcaRuntimeService store?: Store adoptStablePane: (args: AdoptStablePaneArgs) => Promise - getLocalPtyStartupPromise: (connectionId?: string | null) => Promise | undefined - getLocalPtyProviderStartupPromise: (connectionId?: string | null) => Promise | undefined + getLocalPtyStartupPromise: (hostId?: ResolvedPtyHost) => Promise | undefined + getLocalPtyProviderStartupPromise: (hostId?: ResolvedPtyHost) => Promise | undefined prepareCodexResumeHome: (args: { connectionId?: string | null launchAgent?: TuiAgent diff --git a/src/main/ipc/pty/runtime/controller.ts b/src/main/ipc/pty/runtime/controller.ts index 4013e06e37f..fad4ac5bc26 100644 --- a/src/main/ipc/pty/runtime/controller.ts +++ b/src/main/ipc/pty/runtime/controller.ts @@ -73,12 +73,11 @@ export function installPtyRuntimeController(deps: PtyRuntimeControllerDeps): voi clearBuffer: (ptyId) => clearBufferFromRuntimeController(deps, ptyId), resetInputModes: (ptyId) => resetInputModesFromRuntimeController(deps, ptyId), hasPty: (ptyId) => hasPtyFromRuntimeController(deps, ptyId), - listProcesses: (connectionId, opts) => - listProcessesFromRuntimeController(deps, connectionId, opts), + listProcesses: (hostId, opts) => listProcessesFromRuntimeController(deps, hostId, opts), listProcessesWithHostScope: (opts) => listProcessesWithHostScopeFromRuntimeController(deps, opts), - supportsForegroundProcessEvidence: (connectionId) => - supportsForegroundProcessEvidenceFromRuntimeController(connectionId), + supportsForegroundProcessEvidence: (hostId) => + supportsForegroundProcessEvidenceFromRuntimeController(hostId), serializeBuffer: (ptyId, opts) => { // Why: mobile xterm must start from the desktop's exact screen state/dimensions before live TUI chunks render correctly. return requestSerializedBuffer(ptyId, opts) diff --git a/src/main/ipc/pty/runtime/foreground-process-evidence-capability.ts b/src/main/ipc/pty/runtime/foreground-process-evidence-capability.ts index 34eedf6aa46..49874afe6db 100644 --- a/src/main/ipc/pty/runtime/foreground-process-evidence-capability.ts +++ b/src/main/ipc/pty/runtime/foreground-process-evidence-capability.ts @@ -1,21 +1,17 @@ import { getProvider, registeredPtyProviders } from '../provider/registry' -import { - getConnectionExecutionHostId, - LOCAL_EXECUTION_HOST_ID -} from '../../../../shared/execution-host' +import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../shared/execution-host' /** Probe the owning provider before opting into the no-process-table inventory projection. */ export async function supportsForegroundProcessEvidenceFromRuntimeController( - connectionId?: string | null + hostId?: ExecutionHostId ): Promise { - if (connectionId === null) { + if (hostId === LOCAL_EXECUTION_HOST_ID) { return true } - if (connectionId === undefined) { - const providers = registeredPtyProviders() + if (hostId === undefined) { const supported = await Promise.all( - providers.map(async ({ provider, hostId }) => - hostId === LOCAL_EXECUTION_HOST_ID + registeredPtyProviders().map(async ({ provider, hostId: providerHostId }) => + providerHostId === LOCAL_EXECUTION_HOST_ID ? true : ((await provider.supportsForegroundProcessEvidence?.()) ?? false) ) @@ -23,11 +19,7 @@ export async function supportsForegroundProcessEvidenceFromRuntimeController( return supported.every(Boolean) } try { - return ( - (await getProvider( - getConnectionExecutionHostId(connectionId) - ).supportsForegroundProcessEvidence?.()) ?? false - ) + return (await getProvider(hostId).supportsForegroundProcessEvidence?.()) ?? false } catch { return false } diff --git a/src/main/ipc/pty/runtime/inventory-operations.ts b/src/main/ipc/pty/runtime/inventory-operations.ts index 88574985009..7fadb456d8c 100644 --- a/src/main/ipc/pty/runtime/inventory-operations.ts +++ b/src/main/ipc/pty/runtime/inventory-operations.ts @@ -1,10 +1,6 @@ import { getPtyIdsForHost } from '../provider/ownership-state' import { getProvider, localProvider, registeredPtyProviders } from '../provider/registry' -import { - LOCAL_EXECUTION_HOST_ID, - getConnectionExecutionHostId, - type ExecutionHostId -} from '../../../../shared/execution-host' +import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../shared/execution-host' import type { PtyProcessInfo } from '../../../providers/pty-process-info' import type { PtyRuntimeControllerDeps } from './controller-deps' @@ -49,14 +45,13 @@ export async function listProcessesWithHostScopeFromRuntimeController( export async function listProcessesFromRuntimeController( deps: PtyRuntimeControllerDeps, - connectionId?: string | null, + hostId?: ExecutionHostId, opts?: { deadlineMs?: number; includeForegroundProcessEvidence?: boolean } ) { - if (connectionId === null) { + if (hostId === LOCAL_EXECUTION_HOST_ID) { return localProvider.listProcesses() } - if (connectionId !== undefined) { - const hostId = getConnectionExecutionHostId(connectionId) + if (hostId !== undefined) { try { return await getProvider(hostId).listProcesses(opts) } catch (error) { diff --git a/src/main/ipc/pty/runtime/kill.ts b/src/main/ipc/pty/runtime/kill.ts index 4e3befe6081..f35de0e774d 100644 --- a/src/main/ipc/pty/runtime/kill.ts +++ b/src/main/ipc/pty/runtime/kill.ts @@ -1,7 +1,11 @@ import type { IPtyProvider } from '../../../providers/types' import { SSH_PROVIDER_UNREGISTERED_REASON } from '../../../../shared/pty-liveness-verdict' import { ptyOwnership, ptyIncarnationById } from '../provider/ownership-state' -import { getProviderForPty, getPtySshConnectionId } from '../provider/registry' +import { + getProviderForPty, + getPtySshConnectionId, + resolvePtyExecutionHost +} from '../provider/registry' import { isPtyAlreadyGoneError, delay, verifyPtyStopped } from '../provider/liveness' import { recordUndeliveredSshPtyKill } from './undelivered-ssh-kill' import type { PtyRuntimeControllerDeps } from './controller-deps' @@ -100,7 +104,7 @@ export function killPtyFromRuntimeController( }) return true } - const startupPromise = getLocalPtyProviderStartupPromise(connectionId) + const startupPromise = getLocalPtyProviderStartupPromise(resolvePtyExecutionHost(ptyId)) if (startupPromise) { // Why: select the provider after the daemon swap; the fallback first can report success while orphaning a daemon PTY. void startupPromise.then(killWithCurrentProvider).catch((err) => { @@ -167,7 +171,7 @@ export async function stopAndWaitPtyFromRuntimeController( // below; each RPC leaf converts it to the remaining time when it issues, so // sequential RPCs share the budget and cannot overrun the sweep deadline. const deadlineMs = opts?.deadlineMs - const startupPromise = getLocalPtyProviderStartupPromise(connectionId) + const startupPromise = getLocalPtyProviderStartupPromise(resolvePtyExecutionHost(ptyId)) if (startupPromise) { // Why: exact-stop must resolve the provider after daemon startup just // like renderer kills, or the fallback can falsely confirm teardown. diff --git a/src/main/ipc/pty/runtime/operations.ts b/src/main/ipc/pty/runtime/operations.ts index ab52a8a7103..a5881b3dcb2 100644 --- a/src/main/ipc/pty/runtime/operations.ts +++ b/src/main/ipc/pty/runtime/operations.ts @@ -5,12 +5,7 @@ import { LocalPtyProvider } from '../../../providers/local-pty-provider' import { ptyOwnership } from '../provider/ownership-state' import { ptySizes } from '../delivery/visibility-state' import { rendererSerializerReadiness } from '../pane/serializer-state' -import { - getProviderForPty, - getPtySshConnectionId, - localProvider, - resolvePtyExecutionHost -} from '../provider/registry' +import { getProviderForPty, localProvider, resolvePtyExecutionHost } from '../provider/registry' import { inspectPtyProviderProcess } from '../../../providers/pty-process-inspection' import type { PtyRuntimeControllerDeps } from './controller-deps' import { @@ -97,10 +92,9 @@ export async function probePtyLivenessFromRuntimeController( if (isRemoteRuntimePtyId(ptyId)) { return null } - const connectionId = getPtySshConnectionId(ptyId) // Why: during cold start the daemon swap is in flight; the pre-swap // fallback would answer absent for every daemon-owned id. - const startupPromise = deps.getLocalPtyProviderStartupPromise(connectionId) + const startupPromise = deps.getLocalPtyProviderStartupPromise(resolvePtyExecutionHost(ptyId)) if (startupPromise) { await startupPromise } @@ -244,8 +238,7 @@ export function hasPtyFromRuntimeController( if (isRemoteRuntimePtyId(ptyId)) { return null } - const connectionId = getPtySshConnectionId(ptyId) - const startupPromise = deps.getLocalPtyProviderStartupPromise(connectionId) + const startupPromise = deps.getLocalPtyProviderStartupPromise(resolvePtyExecutionHost(ptyId)) if (startupPromise && !settledLocalPtyProviderStartups.has(startupPromise)) { // Why: a sync probe cannot wait out the cold-start daemon swap the way // probePtyLiveness does, and the pre-swap provider's "no PTY" for a diff --git a/src/main/ipc/pty/runtime/spawn-early.ts b/src/main/ipc/pty/runtime/spawn-early.ts index 575f89b22e7..a93795d183e 100644 --- a/src/main/ipc/pty/runtime/spawn-early.ts +++ b/src/main/ipc/pty/runtime/spawn-early.ts @@ -5,6 +5,7 @@ import { snapshotCodexPaneHomeRoutes, codexReattachedHomeRouteField } from '../h import { ensureWslHookRelayForReattach } from '../../../agent-hooks/wsl-hook-relay-reattach' import type { CodexPaneHomeRoute } from '../../../codex/codex-pane-account-registry' import type { RuntimePtySpawnState } from './spawn-state' +import { getConnectionExecutionHostId } from '../../../../shared/execution-host' export function adoptMaterializedRuntimePtySpawn( ctx: RuntimePtySpawnState, @@ -28,7 +29,9 @@ export function adoptMaterializedRuntimePtySpawn( ) : new Map() } - const startupPromise = ctx.deps.getLocalPtyStartupPromise(args.connectionId) + const startupPromise = ctx.deps.getLocalPtyStartupPromise( + getConnectionExecutionHostId(args.connectionId) + ) if (startupPromise && !startupAlreadyAwaited) { return startupPromise.then(() => adoptMaterializedRuntimePtySpawn(ctx, true)) } diff --git a/src/main/runtime/launched-agent-foreground.ts b/src/main/runtime/launched-agent-foreground.ts index b7e300b797a..f84f0f0305b 100644 --- a/src/main/runtime/launched-agent-foreground.ts +++ b/src/main/runtime/launched-agent-foreground.ts @@ -8,6 +8,7 @@ import type { TuiAgent } from '../../shared/tui-agent' import { TUI_AGENT_CONFIG } from '../../shared/tui-agent-config' import type { RuntimePtyController } from './runtime-pty-controller-contract' import { judgeTerminalForeground, readTerminalProcessRows } from './terminal-foreground-group' +import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' /** * What holds a launched agent's terminal: the agent (anything but the pane's shell), the shell @@ -61,7 +62,7 @@ export async function readLaunchedAgentForeground( return (await controller.confirmShellForeground?.(ptyId)) ? 'shell' : 'unknown' } if (!host.remote) { - const rootPid = (await controller.listProcesses?.(null))?.find( + const rootPid = (await controller.listProcesses?.(LOCAL_EXECUTION_HOST_ID))?.find( (pane) => pane.id === ptyId )?.rootProcessId const rows = rootPid ? await readTerminalProcessRows(rootPid) : null diff --git a/src/main/runtime/orca-runtime-agent-session-operation.test.ts b/src/main/runtime/orca-runtime-agent-session-operation.test.ts index e20ef08cc58..8ab5b5580af 100644 --- a/src/main/runtime/orca-runtime-agent-session-operation.test.ts +++ b/src/main/runtime/orca-runtime-agent-session-operation.test.ts @@ -441,7 +441,7 @@ describe('agent-session create operation ledger', () => { disposition: 'replayed', terminal: { handle: orphanHandle, ptyId: 'ssh-1:pty2:e:1', worktreeId: 'worktree-1' } }) - expect(listProcesses).toHaveBeenCalledWith('ssh-1') + expect(listProcesses).toHaveBeenCalledWith('ssh:ssh-1') expect(createTerminal).toHaveBeenCalledOnce() expect(failure.message).toBe('execution_owner_unavailable') }) @@ -455,7 +455,7 @@ describe('agent-session create operation ledger', () => { await expect(runtime.createAgentSession(request(id), { clientId: 'device-a' })).rejects.toThrow( failure.message ) - expect(listProcesses).toHaveBeenCalledWith('ssh-1') + expect(listProcesses).toHaveBeenCalledWith('ssh:ssh-1') expect(createTerminal).toHaveBeenCalledOnce() }) diff --git a/src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts b/src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts index 3218ddfbf31..5af3fb288c3 100644 --- a/src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts +++ b/src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts @@ -52,7 +52,9 @@ export class OrcaRuntimeWithPerformMobileSessionPtyRecordsRefresh extends OrcaRu if ( targetConnectionId !== null && this.ptyController.supportsForegroundProcessEvidence && - !(await this.ptyController.supportsForegroundProcessEvidence(targetConnectionId)) + !(await this.ptyController.supportsForegroundProcessEvidence( + parsedTargetHost?.kind === 'ssh' ? parsedTargetHost.id : undefined + )) ) { // A legacy relay ignores the optional projection and would still run its // expensive process-table inventory on every mobile cadence tick. diff --git a/src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts b/src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts index 9eb72c03a6b..24206ade484 100644 --- a/src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts +++ b/src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts @@ -8,7 +8,7 @@ const SSH_SCOPE = JSON.stringify({ kind: 'ssh', targetId: 'ssh-1' }) const PROCESS_INCARNATION = 'remote:ssh-1:pty-1:inc-1' function runtimeWithInventory( - listProcesses: (connectionId?: string | null) => Promise + listProcesses: (hostId?: string) => Promise ): OrcaRuntimeService { const runtime = new OrcaRuntimeService() runtime.setPtyController({ @@ -38,8 +38,8 @@ describe('terminal process incarnation liveness', () => { await expect( runtime.inspectTerminalProcessIncarnationLiveness('remote:ssh-1:pty-1:inc-old', SSH_SCOPE) ).resolves.toBe('exited') - expect(listProcesses).toHaveBeenNthCalledWith(1, 'ssh-1') - expect(listProcesses).toHaveBeenNthCalledWith(2, 'ssh-1') + expect(listProcesses).toHaveBeenNthCalledWith(1, 'ssh:ssh-1') + expect(listProcesses).toHaveBeenNthCalledWith(2, 'ssh:ssh-1') }) it('keeps missing or malformed identity and unavailable inventory unverifiable', async () => { @@ -77,16 +77,16 @@ describe('terminal process incarnation liveness', () => { }) it.each([ - [{ kind: 'local', hostId: 'local' }, null], - [{ kind: 'wsl', hostId: 'local', distro: 'Ubuntu' }, null] - ] as const)('uses the local provider inventory for %s scope', async (scope, connectionId) => { + [{ kind: 'local', hostId: 'local' }, 'local'], + [{ kind: 'wsl', hostId: 'local', distro: 'Ubuntu' }, 'local'] + ] as const)('uses the local provider inventory for %s scope', async (scope, hostId) => { const listProcesses = vi.fn().mockResolvedValue([]) const runtime = runtimeWithInventory(listProcesses) await expect( runtime.inspectTerminalProcessIncarnationLiveness('local-pty:inc-1', JSON.stringify(scope)) ).resolves.toBe('exited') - expect(listProcesses).toHaveBeenCalledWith(connectionId) + expect(listProcesses).toHaveBeenCalledWith(hostId) }) }) diff --git a/src/main/runtime/orca-runtime-refresh-pty-worktree-records-with-controller-inventory.ts b/src/main/runtime/orca-runtime-refresh-pty-worktree-records-with-controller-inventory.ts index 59247841a7a..f5bd3e4b7ae 100644 --- a/src/main/runtime/orca-runtime-refresh-pty-worktree-records-with-controller-inventory.ts +++ b/src/main/runtime/orca-runtime-refresh-pty-worktree-records-with-controller-inventory.ts @@ -6,8 +6,7 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants' import { getConnectionExecutionHostId, LOCAL_EXECUTION_HOST_ID, - parseExecutionHostId, - toSshExecutionHostId + parseExecutionHostId } from '../../shared/execution-host' import { PTY_CONTROLLER_LIST_PROVIDER_MARGIN_MS, @@ -77,13 +76,9 @@ export class OrcaRuntimeWithRefreshPtyWorktreeRecordsWithControllerInventory ext const processInventory = connectionId === undefined && this.ptyController.listProcessesWithHostScope ? this.ptyController.listProcessesWithHostScope(providerListOpts) - : this.ptyController.listProcesses(connectionId, providerListOpts).then((processes) => { - const hostId: ExecutionHostId = - connectionId === undefined || connectionId === null - ? LOCAL_EXECUTION_HOST_ID - : toSshExecutionHostId(connectionId) - return { processes, hostIds: [hostId] } - }) + : this.ptyController + .listProcesses(connectionId === undefined ? undefined : providerKey, providerListOpts) + .then((processes) => ({ processes, hostIds: [providerKey] })) const sessionsResult = await withTimeoutResult(processInventory, listBudgetMs) if (!sessionsResult.ok) { return null diff --git a/src/main/runtime/orca-runtime-subscribe-to-terminal-resize.ts b/src/main/runtime/orca-runtime-subscribe-to-terminal-resize.ts index 99667d91bd7..5b0f5242f69 100644 --- a/src/main/runtime/orca-runtime-subscribe-to-terminal-resize.ts +++ b/src/main/runtime/orca-runtime-subscribe-to-terminal-resize.ts @@ -24,6 +24,7 @@ import { } from './orchestration/worker-terminal-process-liveness' import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import { buildOrchestrationTaskDisplayMetadata } from '../../shared/orchestration-task-display' +import { LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId } from '../../shared/execution-host' export class OrcaRuntimeWithSubscribeToTerminalResize extends OrcaRuntimeWithApplyMobileDisplayMode { subscribeToTerminalResize( @@ -176,7 +177,11 @@ export class OrcaRuntimeWithSubscribeToTerminalResize extends OrcaRuntimeWithApp return 'unverifiable' } const listed = await withTimeoutResult( - this.ptyController.listProcesses(hostScope.kind === 'ssh' ? hostScope.targetId : null), + this.ptyController.listProcesses( + hostScope.kind === 'ssh' + ? toSshExecutionHostId(hostScope.targetId) + : LOCAL_EXECUTION_HOST_ID + ), PTY_CONTROLLER_LIST_TIMEOUT_MS ) if (!listed.ok) { diff --git a/src/main/runtime/orca-runtime-terminal-create-deduplication.ts b/src/main/runtime/orca-runtime-terminal-create-deduplication.ts index d5c8bf0498e..c80a67578ec 100644 --- a/src/main/runtime/orca-runtime-terminal-create-deduplication.ts +++ b/src/main/runtime/orca-runtime-terminal-create-deduplication.ts @@ -7,7 +7,11 @@ import { withTimeoutResult } from './runtime-async-boundaries' import { PTY_CONTROLLER_LIST_TIMEOUT_MS } from './orca-runtime-postlude' import { inferWorktreeIdFromPtyId } from './runtime-worktree-path-identity' import { getRegisteredSshState } from '../ssh/ssh-target-registry' -import { LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId } from '../../shared/execution-host' +import { + getConnectionExecutionHostId, + LOCAL_EXECUTION_HOST_ID, + toSshExecutionHostId +} from '../../shared/execution-host' import { resolveWorktreeLaunchHost } from './worktree-launch-host-repo' import type { TuiAgent } from '../../shared/tui-agent' @@ -69,7 +73,9 @@ export class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRuntimeWithC throw new Error('runtime_unavailable') } const listed = await withTimeoutResult( - this.ptyController.listProcesses(connectionId), + this.ptyController.listProcesses( + connectionId === undefined ? undefined : getConnectionExecutionHostId(connectionId) + ), PTY_CONTROLLER_LIST_TIMEOUT_MS ) if (!listed.ok) { diff --git a/src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts b/src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts index f8f8600dc1f..fc61c51b2ea 100644 --- a/src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts +++ b/src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts @@ -259,7 +259,7 @@ describe('terminal create idempotency', () => { }) // Mirrors listProcessesFromRuntimeController: `undefined` aggregates every provider and -// silently drops a non-answering SSH host, `null` is local-only, a string is host-scoped +// silently drops a non-answering SSH host, `local` is local-only, an `ssh:` host is host-scoped // and rethrows the host's failure. function createHostScopedInventory(hosts: { local?: PtyProcessInfo[] @@ -267,12 +267,12 @@ function createHostScopedInventory(hosts: { }) { const local = hosts.local ?? [] const ssh = hosts.ssh ?? {} - return vi.fn(async (connectionId?: string | null): Promise => { - if (connectionId === null) { + return vi.fn(async (hostId?: string): Promise => { + if (hostId === 'local') { return local } - if (typeof connectionId === 'string') { - const host = ssh[connectionId] + if (typeof hostId === 'string') { + const host = ssh[hostId.slice('ssh:'.length)] if (host === undefined || host === 'unreachable') { throw new Error('ssh relay did not answer') } @@ -310,7 +310,7 @@ describe('terminal create reconciliation scopes inventory to the owning executio runtime.dedupeTerminalCreate('device-a', 'id:worktree-1', 'mutation-1', true, create) ).rejects.toThrow('runtime_unavailable') expect(create).not.toHaveBeenCalled() - expect(listProcesses).toHaveBeenCalledWith('ssh-1') + expect(listProcesses).toHaveBeenCalledWith('ssh:ssh-1') }) it('adopts the original PTY from the owning host listing', async () => { @@ -343,7 +343,7 @@ describe('terminal create reconciliation scopes inventory to the owning executio ) expect(create).toHaveBeenCalledWith('id:worktree-1', result.handle) - expect(listProcesses).toHaveBeenCalledWith('ssh-1') + expect(listProcesses).toHaveBeenCalledWith('ssh:ssh-1') }) it('scopes the listing to the local host for a workspace with no connection', async () => { @@ -357,7 +357,7 @@ describe('terminal create reconciliation scopes inventory to the owning executio await expect( runtime.dedupeTerminalCreate('device-a', 'id:worktree-1', 'mutation-1', true, create) ).resolves.toMatchObject({ handle, ptyId: 'worktree-1@@session-a' }) - expect(listProcesses).toHaveBeenCalledWith(null) + expect(listProcesses).toHaveBeenCalledWith('local') expect(create).not.toHaveBeenCalled() }) @@ -377,6 +377,6 @@ describe('terminal create reconciliation scopes inventory to the owning executio ) expect(create).toHaveBeenCalledWith('id:folder:folder-1', result.handle) - expect(listProcesses).toHaveBeenCalledWith(null) + expect(listProcesses).toHaveBeenCalledWith('local') }) }) diff --git a/src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts b/src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts index 8c5ca64bcf2..dd80f786653 100644 --- a/src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts +++ b/src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts @@ -198,7 +198,7 @@ describe('OrcaRuntimeService', () => { expect(listWorktrees).not.toHaveBeenCalled() expect(listProcesses).toHaveBeenCalledOnce() expect(listProcesses).toHaveBeenCalledWith( - 'ssh-target', + 'ssh:ssh-target', expect.objectContaining({ deadlineMs: expect.any(Number) }) ) expect(result).toMatchObject({ diff --git a/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-02.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-02.spec.ts index dffd6e971eb..4c0561db992 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-02.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-02.spec.ts @@ -212,7 +212,7 @@ describe('OrcaRuntimeService', () => { paneKey: makePaneKey('tab-agent', HEADLESS_LEAF_ID) }) expect(listProcesses).toHaveBeenCalledTimes(inventoryCount + 1) - expect(listProcesses).toHaveBeenLastCalledWith(null, LIST_PROVIDER_DEADLINE) + expect(listProcesses).toHaveBeenLastCalledWith('local', LIST_PROVIDER_DEADLINE) expect( (await runtime.listTerminals()).terminals.find((terminal) => terminal.ptyId === 'pty-agent') ).toMatchObject({ diff --git a/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-04.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-04.spec.ts index 1d9b78e2e61..ca20cbb0ac2 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-04.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-04.spec.ts @@ -307,8 +307,8 @@ describe('OrcaRuntimeService', () => { } ] } as unknown as OrchestrationDb) - const listProcesses = vi.fn(async (connectionId?: string | null) => { - if (connectionId !== null) { + const listProcesses = vi.fn(async (hostId?: string) => { + if (hostId !== 'local') { throw new Error('unrelated SSH inventory must not run') } return [ @@ -347,7 +347,7 @@ describe('OrcaRuntimeService', () => { expect(getSession().sleepingAgentSessionsByPaneKey?.[workerPaneKey]).toBeUndefined() expect(getSession().sleepingAgentSessionsByPaneKey?.[secondWorkerPaneKey]).toBeUndefined() expect(listProcesses).toHaveBeenCalledTimes(3) - expect(listProcesses).toHaveBeenCalledWith(null, LIST_PROVIDER_DEADLINE) + expect(listProcesses).toHaveBeenCalledWith('local', LIST_PROVIDER_DEADLINE) expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([ expect.objectContaining({ id: 'legacy-worker-two', ptyId: 'pty-exited-two' }) ]) @@ -460,7 +460,12 @@ describe('OrcaRuntimeService', () => { await vi.advanceTimersByTimeAsync(1_000) expect(listProcesses).toHaveBeenCalledTimes(4) - expect(listProcesses.mock.calls.map((call) => call[0])).toEqual([null, null, null, null]) + expect(listProcesses.mock.calls.map((call) => call[0])).toEqual([ + 'local', + 'local', + 'local', + 'local' + ]) expect(hasPty).not.toHaveBeenCalled() expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([ expect.objectContaining({ id: 'legacy-worker', ptyId: 'pty-inventory-unavailable' }) diff --git a/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-05.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-05.spec.ts index a025b046e36..01e6e8f5826 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-05.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-05.spec.ts @@ -151,7 +151,7 @@ describe('OrcaRuntimeService', () => { deferredDispatchIds: ['dispatch-missing', 'dispatch-ambiguous'] }) expect(listProcesses).toHaveBeenCalledOnce() - expect(listProcesses).toHaveBeenCalledWith(null, LIST_PROVIDER_DEADLINE) + expect(listProcesses).toHaveBeenCalledWith('local', LIST_PROVIDER_DEADLINE) expect(getForegroundProcess).not.toHaveBeenCalled() await runtime.refreshPtyForegroundAgentFromController('pty-ambiguous') expect(getForegroundProcess).toHaveBeenCalledExactlyOnceWith('pty-ambiguous') @@ -263,7 +263,7 @@ describe('OrcaRuntimeService', () => { ) expect(getSession().sleepingAgentSessionsByPaneKey?.[workerPaneKey]).toBeUndefined() expect(listProcesses).toHaveBeenCalledTimes(3) - expect(listProcesses).toHaveBeenCalledWith(null, LIST_PROVIDER_DEADLINE) + expect(listProcesses).toHaveBeenCalledWith('local', LIST_PROVIDER_DEADLINE) expect(revealTerminalSession).toHaveBeenCalledWith(TEST_FOLDER_WORKSPACE_KEY, { ptyId: 'pty-folder-legacy', title: 'Folder worker', @@ -400,7 +400,7 @@ describe('OrcaRuntimeService', () => { expect(getWorkspaceSession).toHaveBeenCalledWith(`ssh:${connectionId}`) expect(setWorkspaceSession).toHaveBeenCalledWith(expect.any(Object), `ssh:${connectionId}`) expect(listProcesses).toHaveBeenCalledTimes(3) - expect(listProcesses).toHaveBeenCalledWith(connectionId, LIST_PROVIDER_DEADLINE) + expect(listProcesses).toHaveBeenCalledWith(`ssh:${connectionId}`, LIST_PROVIDER_DEADLINE) expect(sshSession.tabsByWorktree[TEST_FOLDER_WORKSPACE_KEY]).toContainEqual( expect.objectContaining({ id: 'legacy-ssh-folder-worker', diff --git a/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-06.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-06.spec.ts index 2100c708043..8559bbe0d74 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-06.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-output-and-worker-recovery-part-06.spec.ts @@ -167,7 +167,7 @@ describe('OrcaRuntimeService', () => { exitedDispatchIds: [], deferredDispatchIds: [] }) - expect(listProcesses).toHaveBeenLastCalledWith(connectionId, LIST_PROVIDER_DEADLINE) + expect(listProcesses).toHaveBeenLastCalledWith(`ssh:${connectionId}`, LIST_PROVIDER_DEADLINE) } finally { unregisterSshGitProvider(connectionId) } @@ -310,7 +310,13 @@ describe('OrcaRuntimeService', () => { expect(getSession().sleepingAgentSessionsByPaneKey?.[workerPaneKey]).toBeUndefined() expect(revealTerminalSession).toHaveBeenCalledOnce() expect(listProcesses).toHaveBeenCalledTimes(5) - expect(listProcesses.mock.calls.map((call) => call[0])).toEqual([null, null, null, null, null]) + expect(listProcesses.mock.calls.map((call) => call[0])).toEqual([ + 'local', + 'local', + 'local', + 'local', + 'local' + ]) }) it('restores orphan pane and group topology without replacing a newer host-owned tab', async () => { diff --git a/src/main/runtime/pty-inventory-lifecycle-scope.test.ts b/src/main/runtime/pty-inventory-lifecycle-scope.test.ts index 0309ac7631d..2591a16b427 100644 --- a/src/main/runtime/pty-inventory-lifecycle-scope.test.ts +++ b/src/main/runtime/pty-inventory-lifecycle-scope.test.ts @@ -25,7 +25,7 @@ describe('lifecycle invalidation respects inventory scope and retry bounds', () reply.resolve([processRow('ssh:host-b@@child')]) expect(await pending).toBeNull() expect(list).toHaveBeenCalledTimes(1) - expect(list.mock.calls[0][0]).toBe('host-b') + expect(list.mock.calls[0][0]).toBe('ssh:host-b') expect(hasPty).not.toHaveBeenCalled() expect(runtime.capture(id).connected).toBe(true) } @@ -88,7 +88,7 @@ describe('lifecycle invalidation respects inventory scope and retry bounds', () const a = deferred() const b = deferred() const { runtime } = createInventoryRuntime((connection) => - connection === 'a' ? a.promise : b.promise + connection === 'ssh:a' ? a.promise : b.promise ) const pa = runtime.read('a') const pb = runtime.read('b') diff --git a/src/main/runtime/run-terminal-client-use.ts b/src/main/runtime/run-terminal-client-use.ts index 912dbaca177..eb997235832 100644 --- a/src/main/runtime/run-terminal-client-use.ts +++ b/src/main/runtime/run-terminal-client-use.ts @@ -8,6 +8,7 @@ import { confirmRootShellAloneFromProcessTable, inspectionShowsShellAlone } from './run-terminal-shell-alone' +import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' /** * Whether a client drove or is viewing a PTY's current process, for closing finished run @@ -47,7 +48,7 @@ export async function confirmRunTerminalShellAlone( (await controller?.inspectProcess?.(ptyId, { scanChildProcesses: true })) ?? null ) } - const processes = (await controller?.listProcesses?.(null)) ?? [] + const processes = (await controller?.listProcesses?.(LOCAL_EXECUTION_HOST_ID)) ?? [] const rootPid = processes.find((entry) => entry.id === ptyId)?.rootProcessId return rootPid ? await confirmRootShellAloneFromProcessTable(rootPid) : false } catch { diff --git a/src/main/runtime/runtime-pty-controller-contract.ts b/src/main/runtime/runtime-pty-controller-contract.ts index 6cbd270a3ea..6b6dafcf899 100644 --- a/src/main/runtime/runtime-pty-controller-contract.ts +++ b/src/main/runtime/runtime-pty-controller-contract.ts @@ -135,8 +135,9 @@ export type RuntimePtyController = { resize?(ptyId: string, cols: number, rows: number): boolean // Why: exact-id mobile polls should not enumerate every local and SSH PTY. hasPty?(ptyId: string): boolean | null + /** Omitting the host lists every registered host. */ listProcesses?( - connectionId?: string | null, + hostId?: ExecutionHostId, opts?: { deadlineMs?: number; includeForegroundProcessEvidence?: boolean } ): Promise listProcessesWithHostScope?(opts?: { @@ -146,7 +147,7 @@ export type RuntimePtyController = { processes: PtyProcessInfo[] hostIds: ExecutionHostId[] }> - supportsForegroundProcessEvidence?(connectionId?: string | null): Promise + supportsForegroundProcessEvidence?(hostId?: ExecutionHostId): Promise serializeBuffer?( ptyId: string, opts?: { scrollbackRows?: number }