Files
orca/src/main/ipc/pty/runtime/spawn-commit.ts
T

286 lines
12 KiB
TypeScript

import { commitRuntimeStartupEffects } from './spawn-startup-effects'
import { isValidTerminalTabId } from '../../../../shared/terminal-tab-id'
import { ptyOwnership, ptyIncarnationById, deletePtyOwnership } from '../provider/ownership-state'
import { ptySizes } from '../delivery/visibility-state'
import { commitRuntimePtySize } from './spawn-commit-pty-size'
import {
shouldSkipCodexHomeEnvForWindowsShell,
recordCodexPaneAccountForSpawn,
codexReattachedHomeRouteField
} from '../host-env/codex-home'
import { registerPty } from '../../../memory/pty-registry'
import { rememberPaneKeyForPty } from '../pane/key-state'
import {
pendingByPaneKey,
pendingPtyIdBySerializerGeneration,
rendererSerializerReadiness
} from '../pane/serializer-state'
import { seedTerminalRestoreRecordsFromSpawnResult } from '../pane/agent-session-owners'
import { persistAdmittedStablePaneBinding } from '../pane/stable-owner'
import { claimSshPaneLease } from '../pane/ssh-pane-lease-claim'
import {
isNativeWindowsLocalPtySpawn,
markNativeWindowsConptyPty
} from '../../../runtime/terminal-model-query-authority'
import { toSshExecutionHostId } from '../../../../shared/execution-host'
import { createTerminalSessionStateSaveFailureMessage } from '../../../../shared/terminal-session-state-save-failure'
import { clearProviderPtyState } from '../provider/state-cleanup'
import { resolvePaneSpawnReservation } from '../pane/spawn-reservation'
import { admitProviderReattachLaunchIdentity } from '../pane/launch-authority'
import type { RuntimePtySpawnState } from './spawn-state'
export async function commitRuntimePtySpawn(ctx: RuntimePtySpawnState) {
const args = ctx.args
const providerReattachLaunchIdentity = admitProviderReattachLaunchIdentity(ctx.result)
try {
ctx.stablePaneBindingPersisted = persistAdmittedStablePaneBinding({
store: ctx.hostSessionBinding?.store,
owner: ctx.stablePaneOwner,
result: ctx.result,
worktreeId: ctx.hostSessionBinding?.worktreeId,
startupCwd: ctx.cwd,
connectionId: args.connectionId
})
} catch (error) {
if (error instanceof Error && error.message === 'terminal_pane_owner_changed') {
throw error
}
console.error('[pty] failed to persist runtime PTY binding after attach:', error)
throw Object.assign(new Error(createTerminalSessionStateSaveFailureMessage()), {
agentSessionOperationOutcome: 'unknown' as const
})
}
if (ctx.result.agentSessionEnsure?.disposition === 'adopted') {
// Why: an adoption is an attach to a live owner by definition, but the SSH relay's adopted
// reply omits isReattach; derive it once so the size commit and the reservation agree.
const adoptedResult = { ...ctx.result, isReattach: true }
const owner = ctx.result.agentSessionEnsure.owner
ptyOwnership.set(ctx.result.id, args.connectionId ?? ptyOwnership.get(ctx.result.id) ?? null)
ctx.deps.runtime?.registerPreAllocatedHandleForPty(ctx.result.id, owner.surface.terminalHandle)
if (ctx.result.incarnationId) {
ptyIncarnationById.set(ctx.result.id, ctx.result.incarnationId)
}
ctx.deps.runtime?.registerPty(
ctx.result.id,
owner.surface.worktreeId,
args.connectionId ?? null,
{
tabId: owner.surface.tabId,
leafId: owner.surface.leafId,
terminalHandle: owner.surface.terminalHandle,
...(ctx.result.incarnationId ? { incarnationId: ctx.result.incarnationId } : {}),
...(providerReattachLaunchIdentity ? { providerReattachLaunchIdentity } : {})
}
)
if (!args.connectionId) {
ctx.deps.options?.onCodexHomePtySpawned?.({
id: ctx.result.id,
codexHomePath: ctx.selectedCodexHomePath,
reattached: true,
startedAt: ctx.codexHomeLaunchStartedAt,
startedSequence: ctx.codexHomeLaunchStartedSequence,
...codexReattachedHomeRouteField(ctx.reattachedCodexHomeRoutes, ctx.result.id, true),
...(ctx.env ? { launchEnv: ctx.env } : {})
})
}
// Why: this branch returns before the normal commit site; without this the cache keeps
// whatever the caller requested.
commitRuntimePtySize(ctx, adoptedResult)
// Why: the adopted branch returns before the normal settle site, so the
// reservation must be resolved here or every later spawn for this pane
// awaits a promise that never settles.
resolvePaneSpawnReservation(
ctx.paneSpawnReservationKey,
ctx.paneSpawnReservation,
adoptedResult
)
return {
id: ctx.result.id,
...(ctx.result.incarnationId ? { incarnationId: ctx.result.incarnationId } : {}),
agentSessionEnsure: ctx.result.agentSessionEnsure
}
}
ptyOwnership.set(ctx.result.id, args.connectionId ?? null)
if (ctx.result.incarnationId) {
ptyIncarnationById.set(ctx.result.id, ctx.result.incarnationId)
}
// Why: record the native-Windows-local-PTY determination before any byte reaches the emulator, so its ConPTY DA1 override exists from byte zero.
if (
isNativeWindowsLocalPtySpawn({
connectionId: args.connectionId,
cwd: args.cwd,
shellOverride: ctx.daemonShellOverride
})
) {
markNativeWindowsConptyPty(ctx.result.id)
}
const persistSshLease = (): void =>
claimSshPaneLease({
store: ctx.deps.store,
connectionId: args.connectionId,
ptyId: ctx.result.id,
worktreeId: args.worktreeId,
tabId: args.tabId,
leafId: args.leafId
})
if (!ctx.hostSessionBinding) {
persistSshLease()
}
commitRuntimePtySize(ctx, ctx.result)
if (ctx.effectiveSessionAppId !== undefined && ctx.effectiveSessionAppId !== ctx.result.id) {
ptySizes.delete(ctx.effectiveSessionAppId)
}
recordCodexPaneAccountForSpawn({
ptyId: ctx.result.id,
isDaemonHostSpawn: ctx.isDaemonHostSpawn,
isReattach: ctx.result.isReattach === true,
pinnedByResume: ctx.codexResumeHomeSelected,
launchCodexHomePath: ctx.selectedCodexHomePath,
launchEnv: args.env,
target: ctx.codexSelectionTarget,
settings: ctx.deps.getSettings?.()
})
if (ctx.hostSessionBinding && !ctx.stablePaneBindingPersisted) {
try {
const binding = {
worktreeId: ctx.hostSessionBinding.worktreeId,
tabId: ctx.hostSessionBinding.tabId,
leafId: ctx.hostSessionBinding.leafId,
ptyId: ctx.result.id,
hostAdmittedMembership: true,
...(ctx.result.incarnationId ? { incarnationId: ctx.result.incarnationId } : {}),
...(ctx.cwd ? { startupCwd: ctx.cwd } : {}),
...(ctx.hostSessionBinding.expectedSourceBinding
? { expectedSourceBinding: ctx.hostSessionBinding.expectedSourceBinding }
: {})
}
const persisted = args.connectionId
? ctx.hostSessionBinding.store.persistPtyBinding(
binding,
toSshExecutionHostId(args.connectionId)
)
: ctx.hostSessionBinding.store.persistPtyBinding(binding)
if (persisted === false) {
throw new Error('terminal_split_source_not_found')
}
} catch (err) {
console.error('[pty] failed to persist runtime PTY binding after spawn:', err)
if (!ctx.result.isReattach) {
deletePtyOwnership(ctx.result.id)
try {
await ctx.provider.shutdown(ctx.result.id, { immediate: true })
} catch (shutdownErr) {
console.warn('[pty] failed to clean up PTY after persistence failure:', shutdownErr)
}
clearProviderPtyState(ctx.result.id)
}
if (err instanceof Error && err.message === 'terminal_split_source_not_found') {
throw err
}
throw Object.assign(new Error(createTerminalSessionStateSaveFailureMessage()), {
agentSessionOperationOutcome: 'unknown' as const
})
}
persistSshLease()
}
if (args.preAllocatedHandle && !ctx.stablePaneOwner?.handle) {
ctx.deps.runtime?.registerPreAllocatedHandleForPty(ctx.result.id, args.preAllocatedHandle)
}
if (args.worktreeId) {
ctx.deps.runtime?.registerPty(
ctx.result.id,
args.worktreeId,
args.connectionId ?? null,
// Why: thread validated pane identity so main can back a pending mobile create even if graph-sync stalls (#7587).
typeof args.tabId === 'string' &&
isValidTerminalTabId(args.tabId) &&
args.tabId.length <= 512 &&
ctx.metadataLeafId !== null
? {
tabId: args.tabId,
leafId: ctx.metadataLeafId,
...(args.preAllocatedHandle ? { terminalHandle: args.preAllocatedHandle } : {}),
...(ctx.result.incarnationId ? { incarnationId: ctx.result.incarnationId } : {}),
...(providerReattachLaunchIdentity ? { providerReattachLaunchIdentity } : {})
}
: undefined,
!args.connectionId
? shouldSkipCodexHomeEnvForWindowsShell(ctx.daemonShellOverride, ctx.cwd)
: undefined
)
} else {
// Why: non-worktree PTYs have no later surface-registration phase to clear admission intent.
ctx.deps.runtime?.cancelPendingPtyRegistration?.(ctx.result.id, ctx.result.incarnationId)
}
// Why: runtime-controller creates (headless serve, CLI, splits) adopt surviving daemon sessions too; without this seed their records stay blank.
seedTerminalRestoreRecordsFromSpawnResult(ctx.deps.runtime, ctx.result)
commitRuntimeStartupEffects(ctx)
// Why: runtime-owned CLI PTYs bypass the renderer pty:spawn handler; record paneKey here too since hook titles and cache cleanup need this reverse lookup.
const paneKey = rememberPaneKeyForPty(ctx.result.id, ctx.env?.ORCA_PANE_KEY)
const pendingSerializer = paneKey ? pendingByPaneKey.get(paneKey) : undefined
const inheritRendererReadiness =
ctx.result.isReattach === true &&
!pendingSerializer &&
rendererSerializerReadiness.has(ctx.result.id)
rendererSerializerReadiness.beginIncarnation(ctx.result.id, inheritRendererReadiness)
if (paneKey && pendingSerializer) {
pendingPtyIdBySerializerGeneration.set(pendingSerializer.gen, ctx.result.id)
}
if (!args.connectionId) {
registerPty({
ptyId: ctx.result.id,
worktreeId: args.worktreeId ?? null,
sessionId: ctx.sessionId ?? null,
paneKey,
pid:
typeof ctx.result.pid === 'number' && Number.isFinite(ctx.result.pid) && ctx.result.pid > 0
? ctx.result.pid
: null
})
}
// Why: runtime-owned/background spawns bypass mounted-pane state, so inventory consumers need an explicit signal.
ctx.deps.sendPtySpawnedToRenderer(ctx.result.id)
if (!args.connectionId) {
ctx.deps.options?.onCodexHomePtySpawned?.({
id: ctx.result.id,
codexHomePath: ctx.selectedCodexHomePath,
startedAt: ctx.codexHomeLaunchStartedAt,
startedSequence: ctx.codexHomeLaunchStartedSequence,
...codexReattachedHomeRouteField(
ctx.reattachedCodexHomeRoutes,
ctx.result.id,
ctx.result.isReattach === true
),
...(ctx.result.isReattach === true
? { reattached: true }
: ctx.env
? { launchEnv: ctx.env }
: {})
})
}
const response = {
id: ctx.result.id,
...(ctx.result.incarnationId ? { incarnationId: ctx.result.incarnationId } : {}),
...(ctx.stablePaneOwner && (ctx.stablePaneOwner.handle || args.preAllocatedHandle)
? {
stablePaneOwner: {
handle: ctx.stablePaneOwner.handle ?? args.preAllocatedHandle!,
tabId: ctx.stablePaneOwner.tabId,
leafId: ctx.stablePaneOwner.leafId
}
}
: {}),
...(ctx.result.agentSessionEnsure ? { agentSessionEnsure: ctx.result.agentSessionEnsure } : {})
}
resolvePaneSpawnReservation(ctx.paneSpawnReservationKey, ctx.paneSpawnReservation, {
...ctx.result,
...(typeof ctx.result.snapshotKittyKeyboardFlags === 'number' &&
ctx.reconciledSnapshotSeq !== null &&
ctx.snapshotKittyFlagsCoverReconciledSeq
? { snapshotSeq: ctx.reconciledSnapshotSeq }
: { snapshotKittyKeyboardFlags: undefined }),
isReattach: true
})
return response
}