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:
Jinjing
2026-08-24 13:36:17 -07:00
parent 300660a6e1
commit 412c33a875
16 changed files with 281 additions and 85 deletions
+4 -5
View File
@@ -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(
+2
View File
@@ -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)
+2 -2
View File
@@ -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
+5 -1
View File
@@ -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
}
+62 -19
View File
@@ -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,
+1 -1
View File
@@ -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,
+3 -2
View File
@@ -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,
+1 -1
View File
@@ -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,
+5 -2
View File
@@ -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)
+42 -5
View File
@@ -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
}
+62 -8
View File
@@ -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 -5
View File
@@ -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
+5 -1
View File
@@ -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)
+39 -30
View File
@@ -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
+4 -1
View File
@@ -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)
+38 -2
View File
@@ -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)
}