mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
286 lines
12 KiB
TypeScript
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
|
|
}
|