mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
Establish PTY daemon identity before first await in spawn flow
Move identity setup, session ID minting, and hidden delivery state to the beginning of preflight, ensuring these complete synchronously before any awaited operations. Defer async operations like folder workspace validation; add liveness tracking for SSH provider failures. Refactor pane spawn reservation to prevent concurrent spawns from creating duplicate providers.
This commit is contained in:
@@ -12,20 +12,19 @@ import {
|
||||
} from '../../../project-groups/folder-workspace-path-status'
|
||||
import { getSshFilesystemProvider } from '../../../providers/ssh-filesystem-dispatch'
|
||||
|
||||
export async function assertFolderWorkspacePtyPathUsable(
|
||||
export function assertFolderWorkspacePtyPathUsable(
|
||||
store: Store | undefined,
|
||||
worktreeId: string | undefined
|
||||
): Promise<void> {
|
||||
): Promise<void> | void {
|
||||
const workspaceScope = typeof worktreeId === 'string' ? parseWorkspaceKey(worktreeId) : null
|
||||
if (!store || workspaceScope?.type !== 'folder') {
|
||||
return
|
||||
}
|
||||
const status = await getFolderWorkspacePathStatus(
|
||||
return getFolderWorkspacePathStatus(
|
||||
store,
|
||||
{ scope: 'folder-workspace', folderWorkspaceId: workspaceScope.folderWorkspaceId },
|
||||
{ getSshFilesystemProvider }
|
||||
)
|
||||
assertFolderWorkspacePathUsable(status)
|
||||
).then(assertFolderWorkspacePathUsable)
|
||||
}
|
||||
|
||||
export function resolvePtySpawnStartupCwd(
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
visitPtyProcessListingsInBatches
|
||||
} from '../../../providers/pty-process-list-admission'
|
||||
import type { PtyListedSession } from '../../../../shared/pty-listed-session'
|
||||
import { SSH_PROVIDER_UNREGISTERED_REASON } from '../../../../shared/pty-liveness-verdict'
|
||||
import { ptyOwnership } from '../provider/ownership-state'
|
||||
import {
|
||||
getProviderForPty,
|
||||
@@ -70,6 +71,7 @@ export function installPtyInspectIpcHandlers(deps: {
|
||||
runtime?.onPtyExit(args.id, -1, incarnationId)
|
||||
rememberSyntheticKillExit(args.id)
|
||||
sendPtyExitToRenderer({ id: args.id, code: -1 })
|
||||
runtime?.markPtyLivenessUnverifiable?.(args.id, SSH_PROVIDER_UNREGISTERED_REASON)
|
||||
return
|
||||
}
|
||||
const shutdownProvider = provider ?? getProviderForPty(args.id)
|
||||
|
||||
@@ -97,8 +97,8 @@ export async function beginPtyIpcSpawn(
|
||||
)
|
||||
: null
|
||||
ctx.earlyWorktreeId = args.worktreeId
|
||||
ctx.paneSpawnReservationKey =
|
||||
ctx.earlyStablePaneOwner && earlyReservationKey ? earlyReservationKey : null
|
||||
// Reserve early so renderer/runtime materialization cannot start duplicate provider spawns.
|
||||
ctx.paneSpawnReservationKey = earlyReservationKey
|
||||
ctx.paneSpawnReservation = ctx.paneSpawnReservationKey
|
||||
? reservePaneSpawn(ctx.paneSpawnReservationKey)
|
||||
: null
|
||||
|
||||
@@ -156,6 +156,10 @@ export async function buildPtyIpcSpawnOptions(
|
||||
if (ctx.preSpawnHiddenMarkId !== null) {
|
||||
ctx.deps.transitionSpawnHiddenRendererPtyDeliveryState(ctx.preSpawnHiddenMarkId, true)
|
||||
}
|
||||
ctx.releaseWorktreeSpawn = await ctx.deps.runtime?.acquireWorktreeTerminalSpawn?.(args.worktreeId)
|
||||
const runtime = ctx.deps.runtime
|
||||
const acquireWorktreeSpawn = runtime?.acquireWorktreeTerminalSpawn
|
||||
ctx.releaseWorktreeSpawn = acquireWorktreeSpawn
|
||||
? await acquireWorktreeSpawn.call(runtime, args.worktreeId)
|
||||
: undefined
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -20,10 +20,37 @@ import type { PtyIpcSpawnState } from './spawn-state'
|
||||
|
||||
export async function preparePtyIpcSpawnPreflight(ctx: PtyIpcSpawnState): Promise<void> {
|
||||
const args = ctx.args
|
||||
if (!ctx.earlyStablePaneOwner) {
|
||||
await ctx.deps.assertFolderWorkspacePtyPathUsable(args.worktreeId)
|
||||
}
|
||||
// Establish daemon identity before the first await so hidden delivery is gated before byte zero.
|
||||
ctx.provider = getProvider(args.connectionId)
|
||||
ctx.isDaemonHostSpawn =
|
||||
!args.connectionId &&
|
||||
!(ctx.provider instanceof LocalPtyProvider) &&
|
||||
!routesFreshSpawnsToLocalProvider(ctx.provider)
|
||||
ctx.isMintedSessionId = args.sessionId === undefined && ctx.isDaemonHostSpawn
|
||||
ctx.effectiveSessionId =
|
||||
args.sessionId ?? (ctx.isDaemonHostSpawn ? mintPtySessionId(args.worktreeId) : undefined)
|
||||
ctx.effectiveSessionAppId =
|
||||
ctx.effectiveSessionId !== undefined
|
||||
? getAppPtyId(args.connectionId, ctx.effectiveSessionId)
|
||||
: undefined
|
||||
ctx.effectiveSessionRelayId =
|
||||
ctx.effectiveSessionId !== undefined
|
||||
? getRelayPtyId(args.connectionId, ctx.effectiveSessionId)
|
||||
: undefined
|
||||
ctx.initiallyHidden = args.initiallyHidden === true
|
||||
ctx.preSpawnHiddenMarkId =
|
||||
ctx.initiallyHidden && ctx.isDaemonHostSpawn && ctx.effectiveSessionAppId !== undefined
|
||||
? ctx.effectiveSessionAppId
|
||||
: null
|
||||
if (ctx.preSpawnHiddenMarkId !== null) {
|
||||
ctx.deps.transitionSpawnHiddenRendererPtyDeliveryState(ctx.preSpawnHiddenMarkId, true)
|
||||
}
|
||||
if (!ctx.earlyStablePaneOwner) {
|
||||
const pathUsable = ctx.deps.assertFolderWorkspacePtyPathUsable(args.worktreeId)
|
||||
if (pathUsable) {
|
||||
await pathUsable
|
||||
}
|
||||
}
|
||||
ctx.spawnTiming.mark('stable_adoption_setup')
|
||||
ctx.preAdoptedStablePane =
|
||||
ctx.earlyStablePaneOwner && ctx.earlyWorktreeId
|
||||
@@ -40,7 +67,10 @@ export async function preparePtyIpcSpawnPreflight(ctx: PtyIpcSpawnState): Promis
|
||||
: null
|
||||
ctx.spawnTiming.mark('stable_adoption')
|
||||
if (ctx.earlyStablePaneOwner && !ctx.preAdoptedStablePane) {
|
||||
await ctx.deps.assertFolderWorkspacePtyPathUsable(args.worktreeId)
|
||||
const pathUsable = ctx.deps.assertFolderWorkspacePtyPathUsable(args.worktreeId)
|
||||
if (pathUsable) {
|
||||
await pathUsable
|
||||
}
|
||||
}
|
||||
if (!ctx.preAdoptedStablePane) {
|
||||
// Why: reattach needs exact cwd, SSH cannot probe locally, and successful stable-pane adoption needs no launch preflight.
|
||||
@@ -131,6 +161,34 @@ export async function preparePtyIpcSpawnPreflight(ctx: PtyIpcSpawnState): Promis
|
||||
: recoverFreshSpawnProviderRouting(ctx.provider, args.connectionId, args.sessionId)
|
||||
if (freshSpawnRecovery) {
|
||||
await freshSpawnRecovery
|
||||
const previousHiddenMarkId = ctx.preSpawnHiddenMarkId
|
||||
ctx.isDaemonHostSpawn =
|
||||
!args.connectionId &&
|
||||
!(ctx.provider instanceof LocalPtyProvider) &&
|
||||
!routesFreshSpawnsToLocalProvider(ctx.provider)
|
||||
ctx.isMintedSessionId = args.sessionId === undefined && ctx.isDaemonHostSpawn
|
||||
ctx.effectiveSessionId =
|
||||
args.sessionId ?? (ctx.isDaemonHostSpawn ? mintPtySessionId(args.worktreeId) : undefined)
|
||||
ctx.effectiveSessionAppId =
|
||||
ctx.effectiveSessionId !== undefined
|
||||
? getAppPtyId(args.connectionId, ctx.effectiveSessionId)
|
||||
: undefined
|
||||
ctx.effectiveSessionRelayId =
|
||||
ctx.effectiveSessionId !== undefined
|
||||
? getRelayPtyId(args.connectionId, ctx.effectiveSessionId)
|
||||
: undefined
|
||||
ctx.preSpawnHiddenMarkId =
|
||||
ctx.initiallyHidden && ctx.isDaemonHostSpawn && ctx.effectiveSessionAppId !== undefined
|
||||
? ctx.effectiveSessionAppId
|
||||
: null
|
||||
if (previousHiddenMarkId !== ctx.preSpawnHiddenMarkId) {
|
||||
if (previousHiddenMarkId !== null) {
|
||||
ctx.deps.transitionSpawnHiddenRendererPtyDeliveryState(previousHiddenMarkId, false)
|
||||
}
|
||||
if (ctx.preSpawnHiddenMarkId !== null) {
|
||||
ctx.deps.transitionSpawnHiddenRendererPtyDeliveryState(ctx.preSpawnHiddenMarkId, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.isClaudeLaunch =
|
||||
!ctx.preAdoptedStablePane && !args.connectionId && isClaudeLaunchCommand(args.command)
|
||||
@@ -147,24 +205,9 @@ export async function preparePtyIpcSpawnPreflight(ctx: PtyIpcSpawnState): Promis
|
||||
})
|
||||
: { shellOverride: args.shellOverride, terminalWindowsWslDistro: null }
|
||||
const initialShellOverride = ctx.terminalRuntimeOptions.shellOverride
|
||||
ctx.isDaemonHostSpawn =
|
||||
!args.connectionId &&
|
||||
!(ctx.provider instanceof LocalPtyProvider) &&
|
||||
!routesFreshSpawnsToLocalProvider(ctx.provider)
|
||||
// Why: daemon host-env setup needs a stable id BEFORE provider.spawn so buildPtyHostEnv hooks/Pi cleanup can run; daemon still honors opts.sessionId ?? mint().
|
||||
// Note: sessionId is STABLE across daemon restarts by design — do NOT simplify to a fresh UUID per spawn; that orphans reconnectable state.
|
||||
// Why: only clear ids minted in THIS request on failure — a caller-supplied args.sessionId may name an existing PTY we must not clobber.
|
||||
ctx.isMintedSessionId = args.sessionId === undefined && ctx.isDaemonHostSpawn
|
||||
ctx.effectiveSessionId =
|
||||
args.sessionId ?? (ctx.isDaemonHostSpawn ? mintPtySessionId(args.worktreeId) : undefined)
|
||||
ctx.effectiveSessionAppId =
|
||||
ctx.effectiveSessionId !== undefined
|
||||
? getAppPtyId(args.connectionId, ctx.effectiveSessionId)
|
||||
: undefined
|
||||
ctx.effectiveSessionRelayId =
|
||||
ctx.effectiveSessionId !== undefined
|
||||
? getRelayPtyId(args.connectionId, ctx.effectiveSessionId)
|
||||
: undefined
|
||||
ctx.expectedWslDistro = !args.connectionId
|
||||
? (resolveWslSessionContext({
|
||||
cwd: ctx.cwd,
|
||||
|
||||
@@ -88,7 +88,7 @@ export type PtySpawnIpcDeps = {
|
||||
}
|
||||
getLocalPtyStartupPromise: (connectionId?: string | null) => Promise<void> | undefined
|
||||
adoptStablePane: (args: AdoptStablePaneArgs) => Promise<AdoptStablePaneResult | null>
|
||||
assertFolderWorkspacePtyPathUsable: (worktreeId: string | undefined) => Promise<void>
|
||||
assertFolderWorkspacePtyPathUsable: (worktreeId: string | undefined) => Promise<void> | void
|
||||
resolvePtySpawnStartupCwd: (
|
||||
worktreeId: string | undefined,
|
||||
cwd: string | undefined,
|
||||
|
||||
@@ -174,8 +174,9 @@ export function registerPtyHandlers(
|
||||
mainWindow.webContents.on('did-finish-load', finishLoadHandler)
|
||||
}
|
||||
|
||||
const assertFolderWorkspacePtyPathUsable = (worktreeId: string | undefined): Promise<void> =>
|
||||
assertFolderWorkspacePtyPathUsableImpl(store, worktreeId)
|
||||
const assertFolderWorkspacePtyPathUsable = (
|
||||
worktreeId: string | undefined
|
||||
): Promise<void> | void => assertFolderWorkspacePtyPathUsableImpl(store, worktreeId)
|
||||
const resolvePtySpawnStartupCwd = (
|
||||
worktreeId: string | undefined,
|
||||
cwd: string | undefined,
|
||||
|
||||
@@ -46,7 +46,7 @@ export type PtyRuntimeControllerDeps = {
|
||||
env: T,
|
||||
launch: CodexResumeLaunch
|
||||
) => T
|
||||
assertFolderWorkspacePtyPathUsable: (worktreeId: string | undefined) => Promise<void>
|
||||
assertFolderWorkspacePtyPathUsable: (worktreeId: string | undefined) => Promise<void> | void
|
||||
resolvePtySpawnStartupCwd: (
|
||||
worktreeId: string | undefined,
|
||||
cwd: string | undefined,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
hasRendererSerializerFromRuntimeController,
|
||||
inspectProcessFromRuntimeController,
|
||||
listProcessesFromRuntimeController,
|
||||
listProcessesWithHostScopeFromRuntimeController,
|
||||
probePtyLivenessFromRuntimeController,
|
||||
resizePtyFromRuntimeController,
|
||||
serializeProviderBufferFromRuntimeController,
|
||||
@@ -47,7 +48,8 @@ export function installPtyRuntimeController(deps: PtyRuntimeControllerDeps): voi
|
||||
// attach. Attach-only and false-on-doubt: never creates or resizes.
|
||||
attach: (ptyId) => attachPtyFromRuntimeController(deps, ptyId),
|
||||
kill: (ptyId) => killPtyFromRuntimeController(deps, ptyId),
|
||||
retireRejectedPty: (ptyId) => retireRejectedPtyFromRuntimeController(deps, ptyId),
|
||||
retireRejectedPty: (ptyId, stopConfirmed) =>
|
||||
retireRejectedPtyFromRuntimeController(deps, ptyId, stopConfirmed),
|
||||
markReversibleStops: (ptyIds) => markReversibleStopsFromRuntimeController(deps, ptyIds),
|
||||
stopAndWait: (ptyId, opts) => stopAndWaitPtyFromRuntimeController(deps, ptyId, opts),
|
||||
getForegroundProcess: (ptyId) => getForegroundProcessFromRuntimeController(ptyId),
|
||||
@@ -57,7 +59,8 @@ export function installPtyRuntimeController(deps: PtyRuntimeControllerDeps): voi
|
||||
hasChildProcesses: (ptyId) => hasChildProcessesFromRuntimeController(ptyId),
|
||||
clearBuffer: (ptyId) => clearBufferFromRuntimeController(deps, ptyId),
|
||||
hasPty: (ptyId) => hasPtyFromRuntimeController(ptyId),
|
||||
listProcesses: (connectionId) => listProcessesFromRuntimeController(connectionId),
|
||||
listProcesses: (connectionId) => listProcessesFromRuntimeController(deps, connectionId),
|
||||
listProcessesWithHostScope: () => listProcessesWithHostScopeFromRuntimeController(deps),
|
||||
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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { IPtyProvider } from '../../../providers/types'
|
||||
import { SSH_PROVIDER_UNREGISTERED_REASON } from '../../../../shared/pty-liveness-verdict'
|
||||
import { parseAppSshPtyId } from '../../../providers/ssh-pty-id'
|
||||
import { ptyOwnership, ptyIncarnationById } from '../provider/ownership-state'
|
||||
import { getProvider, getProviderForPty } from '../provider/registry'
|
||||
@@ -35,7 +36,8 @@ export function killPtyFromRuntimeController(
|
||||
runtime?.onPtyExit(ptyId, -1, incarnationId)
|
||||
rememberSyntheticKillExit(ptyId)
|
||||
sendPtyExitToRenderer({ id: ptyId, code: -1 })
|
||||
return true
|
||||
runtime?.markPtyLivenessUnverifiable?.(ptyId, SSH_PROVIDER_UNREGISTERED_REASON)
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -67,6 +69,12 @@ export function killPtyFromRuntimeController(
|
||||
// Why: close runtime tails without clearing provider ownership, so
|
||||
// a retry can still target a PTY that survived the failed shutdown.
|
||||
if (!retired) {
|
||||
if (connectionId) {
|
||||
runtime?.markPtyLivenessUnverifiable?.(
|
||||
ptyId,
|
||||
err instanceof Error ? err.message : String(err)
|
||||
)
|
||||
}
|
||||
runtime?.onPtyExit(ptyId, -1, ptyIncarnationById.get(ptyId))
|
||||
}
|
||||
})
|
||||
@@ -90,7 +98,8 @@ export function killPtyFromRuntimeController(
|
||||
|
||||
export function retireRejectedPtyFromRuntimeController(
|
||||
deps: PtyRuntimeControllerDeps,
|
||||
ptyId: string
|
||||
ptyId: string,
|
||||
stopConfirmed: boolean
|
||||
): void {
|
||||
const {
|
||||
runtime,
|
||||
@@ -101,6 +110,19 @@ export function retireRejectedPtyFromRuntimeController(
|
||||
finishPtyShutdown
|
||||
} = deps
|
||||
rememberRetiredRejectedPty(ptyId)
|
||||
if (!stopConfirmed) {
|
||||
runtime?.markPtyLivenessUnverifiable?.(
|
||||
ptyId,
|
||||
'a follow-up stop was issued but its outcome could not be verified'
|
||||
)
|
||||
if (!ptyOwnership.has(ptyId)) {
|
||||
return
|
||||
}
|
||||
runtime?.onPtyExit(ptyId, -1, ptyIncarnationById.get(ptyId))
|
||||
rememberSyntheticKillExit(ptyId)
|
||||
sendPtyExitToRenderer({ id: ptyId, code: -1 })
|
||||
return
|
||||
}
|
||||
// Why: a completed stop already cleared provider state, tombstoned the lease and told the
|
||||
// renderer; repeating that double-fires the exit IPC. The runtime still needs code 0 so an
|
||||
// SSH pane retires for good instead of staying preserved by the stop's negative exit.
|
||||
@@ -197,7 +219,7 @@ export async function stopAndWaitPtyFromRuntimeController(
|
||||
runtime?.onPtyExit(ptyId, -1, incarnationId)
|
||||
rememberSyntheticKillExit(ptyId)
|
||||
sendPtyExitToRenderer({ id: ptyId, code: -1 })
|
||||
return true
|
||||
runtime?.markPtyLivenessUnverifiable?.(ptyId, SSH_PROVIDER_UNREGISTERED_REASON)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -210,6 +232,12 @@ export async function stopAndWaitPtyFromRuntimeController(
|
||||
})
|
||||
} catch (err) {
|
||||
if (!isPtyAlreadyGoneError(err)) {
|
||||
if (connectionId) {
|
||||
runtime?.markPtyLivenessUnverifiable?.(
|
||||
ptyId,
|
||||
err instanceof Error ? err.message : String(err)
|
||||
)
|
||||
}
|
||||
console.warn(
|
||||
`[pty] Failed to stop PTY ${ptyId}: ${err instanceof Error ? err.message : String(err)}`
|
||||
)
|
||||
@@ -218,9 +246,16 @@ export async function stopAndWaitPtyFromRuntimeController(
|
||||
}
|
||||
try {
|
||||
if (!(await verifyPtyStopped(provider, ptyId, opts))) {
|
||||
runtime?.markPtyLivenessLive?.(ptyId)
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
if (connectionId) {
|
||||
runtime?.markPtyLivenessUnverifiable?.(
|
||||
ptyId,
|
||||
err instanceof Error ? err.message : String(err)
|
||||
)
|
||||
}
|
||||
console.warn(
|
||||
`[pty] Failed to verify PTY ${ptyId} stopped: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
@@ -230,9 +265,11 @@ export async function stopAndWaitPtyFromRuntimeController(
|
||||
}
|
||||
const incarnationId = finishPtyShutdown(ptyId, connectionId, store)
|
||||
if (!providerExitObserved) {
|
||||
runtime?.onPtyExit(ptyId, -1, incarnationId)
|
||||
// The owning provider's fresh inventory observed absence, so this is a
|
||||
// death certificate even when its exit event was missed.
|
||||
runtime?.onPtyExit(ptyId, 0, incarnationId)
|
||||
rememberSyntheticKillExit(ptyId)
|
||||
sendPtyExitToRenderer({ id: ptyId, code: -1 })
|
||||
sendPtyExitToRenderer({ id: ptyId, code: 0 })
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import type { IPtyProvider } from '../../../providers/types'
|
||||
import { LocalPtyProvider } from '../../../providers/local-pty-provider'
|
||||
import type { PtyProcessInfo } from '../../../providers/pty-process-info'
|
||||
import { parseAppSshPtyId } from '../../../providers/ssh-pty-id'
|
||||
import {
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
toSshExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import { ptyOwnership } from '../provider/ownership-state'
|
||||
import { ptySizes } from '../delivery/visibility-state'
|
||||
import { rendererSerializerReadiness } from '../pane/serializer-state'
|
||||
import { getProvider, getProviderForPty, localProvider, sshProviders } from '../provider/registry'
|
||||
import {
|
||||
getProvider,
|
||||
getProviderForPty,
|
||||
localProvider,
|
||||
registeredPtyProviders
|
||||
} from '../provider/registry'
|
||||
import { inspectPtyProviderProcess } from '../../../providers/pty-process-inspection'
|
||||
import type { PtyRuntimeControllerDeps } from './controller-deps'
|
||||
|
||||
@@ -141,18 +152,61 @@ export function hasPtyFromRuntimeController(ptyId: string): boolean | null {
|
||||
}
|
||||
}
|
||||
|
||||
export async function listProcessesFromRuntimeController(connectionId?: string | null) {
|
||||
function markSshInventoryUnverifiable(
|
||||
runtime: PtyRuntimeControllerDeps['runtime'],
|
||||
connectionId: string,
|
||||
error: unknown
|
||||
): void {
|
||||
const reason = error instanceof Error ? error.message : String(error)
|
||||
for (const [ptyId, ownerConnectionId] of ptyOwnership) {
|
||||
if (ownerConnectionId === connectionId) {
|
||||
runtime?.markPtyLivenessUnverifiable?.(ptyId, reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function listProcessesWithHostScopeFromRuntimeController(
|
||||
deps: PtyRuntimeControllerDeps
|
||||
): Promise<{ processes: PtyProcessInfo[]; hostIds: ExecutionHostId[] }> {
|
||||
const providerSessions = await Promise.all(
|
||||
registeredPtyProviders().map(async ({ provider, connectionId }) => {
|
||||
const hostId: ExecutionHostId = connectionId
|
||||
? toSshExecutionHostId(connectionId)
|
||||
: LOCAL_EXECUTION_HOST_ID
|
||||
try {
|
||||
return { processes: await provider.listProcesses(), hostId }
|
||||
} catch (error) {
|
||||
if (!connectionId) {
|
||||
throw error
|
||||
}
|
||||
markSshInventoryUnverifiable(deps.runtime, connectionId, error)
|
||||
return null
|
||||
}
|
||||
})
|
||||
)
|
||||
const respondingSessions = providerSessions.filter((session) => session !== null)
|
||||
return {
|
||||
processes: respondingSessions.flatMap((session) => session.processes),
|
||||
hostIds: respondingSessions.map((session) => session.hostId)
|
||||
}
|
||||
}
|
||||
|
||||
export async function listProcessesFromRuntimeController(
|
||||
deps: PtyRuntimeControllerDeps,
|
||||
connectionId?: string | null
|
||||
) {
|
||||
if (connectionId === null) {
|
||||
return localProvider.listProcesses()
|
||||
}
|
||||
if (connectionId !== undefined) {
|
||||
return getProvider(connectionId).listProcesses()
|
||||
try {
|
||||
return await getProvider(connectionId).listProcesses()
|
||||
} catch (error) {
|
||||
markSshInventoryUnverifiable(deps.runtime, connectionId, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
const providerSessions = await Promise.all([
|
||||
localProvider.listProcesses(),
|
||||
...Array.from(sshProviders.values(), (provider) => provider.listProcesses())
|
||||
])
|
||||
return providerSessions.flat()
|
||||
return (await listProcessesWithHostScopeFromRuntimeController(deps)).processes
|
||||
}
|
||||
|
||||
export function resizePtyFromRuntimeController(ptyId: string, cols: number, rows: number): boolean {
|
||||
|
||||
@@ -6,9 +6,10 @@ import { ensureWslHookRelayForReattach } from '../../../agent-hooks/wsl-hook-rel
|
||||
import type { CodexPaneHomeRoute } from '../../../codex/codex-pane-account-registry'
|
||||
import type { RuntimePtySpawnState } from './spawn-state'
|
||||
|
||||
export async function adoptMaterializedRuntimePtySpawn(
|
||||
ctx: RuntimePtySpawnState
|
||||
): Promise<PtySpawnResult | null> {
|
||||
export function adoptMaterializedRuntimePtySpawn(
|
||||
ctx: RuntimePtySpawnState,
|
||||
startupAlreadyAwaited = false
|
||||
): Promise<PtySpawnResult | null> | PtySpawnResult | null {
|
||||
const args = ctx.args
|
||||
ctx.codexHomeLaunchStartedAt = !args.connectionId ? new Date() : undefined
|
||||
ctx.codexHomeLaunchStartedSequence = !args.connectionId
|
||||
@@ -24,8 +25,8 @@ export async function adoptMaterializedRuntimePtySpawn(
|
||||
)
|
||||
: new Map<string, CodexPaneHomeRoute | null>()
|
||||
const startupPromise = ctx.deps.getLocalPtyStartupPromise(args.connectionId)
|
||||
if (startupPromise) {
|
||||
await startupPromise
|
||||
if (startupPromise && !startupAlreadyAwaited) {
|
||||
return startupPromise.then(() => adoptMaterializedRuntimePtySpawn(ctx, true))
|
||||
}
|
||||
if (!ctx.preAdoptedStablePane?.materialized) {
|
||||
return null
|
||||
|
||||
@@ -19,7 +19,11 @@ import type { RuntimePtySpawnState } from './spawn-state'
|
||||
|
||||
export async function executeRuntimePtySpawn(ctx: RuntimePtySpawnState): Promise<void> {
|
||||
const args = ctx.args
|
||||
ctx.releaseWorktreeSpawn = await ctx.deps.runtime?.acquireWorktreeTerminalSpawn?.(args.worktreeId)
|
||||
const runtime = ctx.deps.runtime
|
||||
const acquireWorktreeSpawn = runtime?.acquireWorktreeTerminalSpawn
|
||||
ctx.releaseWorktreeSpawn = acquireWorktreeSpawn
|
||||
? await acquireWorktreeSpawn.call(runtime, args.worktreeId)
|
||||
: undefined
|
||||
try {
|
||||
if (args.preAllocatedHandle) {
|
||||
ctx.deps.trustedTerminalHandleEnv.add(args.preAllocatedHandle)
|
||||
|
||||
@@ -170,44 +170,53 @@ export async function buildRuntimePtySpawnOptions(
|
||||
ctx.spawnOptions.onPtySpawnCommitted = ctx.reportPtySpawnCommitted
|
||||
}
|
||||
|
||||
ctx.paneSpawnReservationKey = makePaneSpawnReservationKey(
|
||||
const resolvedPaneSpawnReservationKey = makePaneSpawnReservationKey(
|
||||
args.worktreeId,
|
||||
args.connectionId,
|
||||
ctx.spawnIdentityPaneKey
|
||||
)
|
||||
const existingPaneSpawn = ctx.paneSpawnReservationKey
|
||||
? paneSpawnReservationsByOwnerKey.get(ctx.paneSpawnReservationKey)
|
||||
: undefined
|
||||
if (existingPaneSpawn) {
|
||||
const concurrentResult = await existingPaneSpawn.promise
|
||||
const concurrentOwner = resolveStablePaneOwner(
|
||||
ctx.deps.runtime,
|
||||
ctx.deps.store,
|
||||
ctx.spawnIdentityPaneKey,
|
||||
args.worktreeId,
|
||||
args.connectionId
|
||||
)
|
||||
if (
|
||||
!concurrentOwner?.handle ||
|
||||
concurrentOwner.ptyId !== concurrentResult.id ||
|
||||
(concurrentOwner.incarnationId !== undefined &&
|
||||
concurrentResult.incarnationId !== undefined &&
|
||||
concurrentOwner.incarnationId !== concurrentResult.incarnationId)
|
||||
) {
|
||||
throw new Error('terminal_pane_owner_unknown')
|
||||
}
|
||||
return {
|
||||
id: concurrentOwner.ptyId,
|
||||
...(concurrentOwner.incarnationId ? { incarnationId: concurrentOwner.incarnationId } : {}),
|
||||
stablePaneOwner: {
|
||||
handle: concurrentOwner.handle,
|
||||
tabId: concurrentOwner.tabId,
|
||||
leafId: concurrentOwner.leafId
|
||||
if (
|
||||
ctx.paneSpawnReservationKey &&
|
||||
ctx.paneSpawnReservationKey !== resolvedPaneSpawnReservationKey
|
||||
) {
|
||||
throw new Error('terminal_pane_identity_changed')
|
||||
}
|
||||
if (!ctx.paneSpawnReservationKey) {
|
||||
ctx.paneSpawnReservationKey = resolvedPaneSpawnReservationKey
|
||||
const existingPaneSpawn = ctx.spawnIdentityPaneKey
|
||||
? paneSpawnReservationsByOwnerKey.get(resolvedPaneSpawnReservationKey!)
|
||||
: undefined
|
||||
if (existingPaneSpawn) {
|
||||
const concurrentResult = await existingPaneSpawn.promise
|
||||
const concurrentOwner = resolveStablePaneOwner(
|
||||
ctx.deps.runtime,
|
||||
ctx.deps.store,
|
||||
ctx.spawnIdentityPaneKey,
|
||||
args.worktreeId,
|
||||
args.connectionId
|
||||
)
|
||||
if (
|
||||
!concurrentOwner?.handle ||
|
||||
concurrentOwner.ptyId !== concurrentResult.id ||
|
||||
(concurrentOwner.incarnationId !== undefined &&
|
||||
concurrentResult.incarnationId !== undefined &&
|
||||
concurrentOwner.incarnationId !== concurrentResult.incarnationId)
|
||||
) {
|
||||
throw new Error('terminal_pane_owner_unknown')
|
||||
}
|
||||
return {
|
||||
id: concurrentOwner.ptyId,
|
||||
...(concurrentOwner.incarnationId ? { incarnationId: concurrentOwner.incarnationId } : {}),
|
||||
stablePaneOwner: {
|
||||
handle: concurrentOwner.handle,
|
||||
tabId: concurrentOwner.tabId,
|
||||
leafId: concurrentOwner.leafId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.finishTerminalInstall = beginPtySpawnForWorktree(args.worktreeId, ctx.cwd, args.connectionId)
|
||||
ctx.paneSpawnReservation = ctx.paneSpawnReservationKey
|
||||
ctx.paneSpawnReservation ??= ctx.paneSpawnReservationKey
|
||||
? reservePaneSpawn(ctx.paneSpawnReservationKey)
|
||||
: null
|
||||
ctx.stablePaneOwner = null
|
||||
|
||||
@@ -43,7 +43,10 @@ export async function prepareRuntimePtySpawn(
|
||||
): Promise<PtySpawnResult | null> {
|
||||
const args = ctx.args
|
||||
if (!ctx.preAdoptedStablePane) {
|
||||
await ctx.deps.assertFolderWorkspacePtyPathUsable(args.worktreeId)
|
||||
const pathUsable = ctx.deps.assertFolderWorkspacePtyPathUsable(args.worktreeId)
|
||||
if (pathUsable) {
|
||||
await pathUsable
|
||||
}
|
||||
}
|
||||
ctx.cwd = ctx.deps.resolvePtySpawnStartupCwd(args.worktreeId, args.cwd)
|
||||
ctx.provider = getProvider(args.connectionId)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import type { AgentSessionClaimedSpawnResult } from '../../../../shared/agent-session-host-authority'
|
||||
import { rejectPaneSpawnReservation } from '../pane/spawn-reservation'
|
||||
import { isTerminalLeafId, makePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import { isValidTerminalTabId } from '../../../../shared/terminal-tab-id'
|
||||
import {
|
||||
makePaneSpawnReservationKey,
|
||||
paneSpawnReservationsByOwnerKey,
|
||||
rejectPaneSpawnReservation,
|
||||
reservePaneSpawn
|
||||
} from '../pane/spawn-reservation'
|
||||
import { resolveStablePaneOwner } from '../pane/stable-owner'
|
||||
import type { PtyRuntimeControllerDeps } from './controller-deps'
|
||||
import { adoptMaterializedRuntimePtySpawn } from './spawn-early'
|
||||
import { prepareRuntimePtySpawn } from './spawn-preflight'
|
||||
@@ -29,7 +37,35 @@ export async function spawnPtyFromRuntimeController(
|
||||
args: RuntimePtySpawnArgs
|
||||
) {
|
||||
const ctx = createRuntimePtySpawnState(deps, args)
|
||||
const materialized = await adoptMaterializedRuntimePtySpawn(ctx)
|
||||
if (!args.adoptedStablePane) {
|
||||
const leafId =
|
||||
typeof args.leafId === 'string' && isTerminalLeafId(args.leafId) ? args.leafId : null
|
||||
const paneKey =
|
||||
typeof args.worktreeId === 'string' &&
|
||||
typeof args.tabId === 'string' &&
|
||||
isValidTerminalTabId(args.tabId) &&
|
||||
args.tabId.length <= 512 &&
|
||||
leafId
|
||||
? makePaneKey(args.tabId, leafId)
|
||||
: null
|
||||
const ownerKey = makePaneSpawnReservationKey(args.worktreeId, args.connectionId, paneKey)
|
||||
const existingOwner = paneKey
|
||||
? resolveStablePaneOwner(
|
||||
deps.runtime,
|
||||
deps.store,
|
||||
paneKey,
|
||||
args.worktreeId,
|
||||
args.connectionId
|
||||
)
|
||||
: null
|
||||
if (ownerKey && !existingOwner && !paneSpawnReservationsByOwnerKey.has(ownerKey)) {
|
||||
ctx.paneSpawnReservationKey = ownerKey
|
||||
ctx.paneSpawnReservation = reservePaneSpawn(ownerKey)
|
||||
}
|
||||
}
|
||||
const materializedOrPromise = adoptMaterializedRuntimePtySpawn(ctx)
|
||||
const materialized =
|
||||
materializedOrPromise instanceof Promise ? await materializedOrPromise : materializedOrPromise
|
||||
if (materialized) {
|
||||
return toRuntimeSpawnReply(materialized)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user