diff --git a/config/main-layout-writer-baseline.txt b/config/main-layout-writer-baseline.txt index 4166afdb39d..ea882afe8c7 100644 --- a/config/main-layout-writer-baseline.txt +++ b/config/main-layout-writer-baseline.txt @@ -3,10 +3,9 @@ # This is a RATCHET: counts may only go DOWN. Layout moves to one runtime owner; do not add # writers. Prune after removing writers: pnpm check:layout-writer-ratchets --prune 1 src/main/ipc/pty/ipc/leaf-move.ts -1 src/main/ipc/pty/ipc/spawn-commit-persist.ts 1 src/main/ipc/pty/pane/pane-owner-replacement.ts 3 src/main/ipc/pty/pane/stable-owner.ts -1 src/main/ipc/pty/runtime/spawn-commit.ts +2 src/main/ipc/pty/spawn-commit-binding.ts 1 src/main/ipc/renderer-shutdown-checkpoint.ts 1 src/main/ipc/runtime-environments/runtime-environment-removal-cleanup.ts 1 src/main/ipc/runtime-environments/runtime-environment-session-reconcile.ts diff --git a/src/main/ipc/pty/delivery/attached-pty-size.ts b/src/main/ipc/pty/delivery/attached-pty-size.ts index db236557066..0ff548a6107 100644 --- a/src/main/ipc/pty/delivery/attached-pty-size.ts +++ b/src/main/ipc/pty/delivery/attached-pty-size.ts @@ -63,8 +63,8 @@ export function reflowHeadlessTerminalToCommittedGrid(args: { ) } -/** Record the settled grid, then reflow the model onto it. Callers that seed the model between the - * two steps (ipc spawn commit) call the halves separately. */ +/** Record the settled grid, then reflow the model onto it. Spawn commits call it only after + * registration admits the incarnation; the model was seeded before their first await. */ export function commitAttachedPtySize(args: { result: Pick< PtySpawnResult, diff --git a/src/main/ipc/pty/ipc/spawn-commit-persist.ts b/src/main/ipc/pty/ipc/spawn-commit-persist.ts deleted file mode 100644 index 09c17fd391f..00000000000 --- a/src/main/ipc/pty/ipc/spawn-commit-persist.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { closeStartupQueryAuthorityForPty } from '../provider/registry' -import { createTerminalSessionStateSaveFailureMessage } from '../../../../shared/terminal-session-state-save-failure' -import { recordCodexPaneAccountForSpawn } from '../host-env/codex-home' -import { swapReplacedPaneBinding } from '../pane/pane-owner-replacement' -import { PaneOwnerContentionError } from '../pane/pane-spawn-operation' -import { persistFreshPaneBinding } from '../runtime/spawn-commit' -import { - pendingByPaneKey, - pendingPtyIdBySerializerGeneration, - rendererSerializerReadiness -} from '../pane/serializer-state' -import { ptySizes } from '../delivery/visibility-state' -import { resolveCommittedPtySize, type PtyGrid } from '../delivery/attached-pty-size' -import { discardUnpersistedPtySpawn } from '../pane/spawn-registration' -import { spawnCommitBindingOrigin } from '../../../persistence/loading-store/pty-binding-span' -import type { PtyIpcSpawnState } from './spawn-state' -import { startupCwdFallbackOutcome } from '../spawn-startup-target' -import { parseTerminalPanePlacement } from '../../../../shared/terminal-pane-placement' - -export async function persistPtyIpcSpawnCommit(ctx: PtyIpcSpawnState): Promise { - const args = ctx.args - const startupCwd = startupCwdFallbackOutcome(ctx.startupCwdFallback, ctx.result) - ? (ctx.correctStartupCwd ?? ctx.requestedCwd) - : ctx.requestedCwd - - const committedSize = resolveCommittedPtySize({ - result: ctx.result, - requested: { cols: args.cols, rows: args.rows }, - cachedBeforeAttach: ctx.sessionSizeBeforeAttach - }) - // Persist the binding before acknowledging spawn so the renderer debounce cannot orphan history. - if ( - ctx.deps.store && - typeof args.worktreeId === 'string' && - typeof args.tabId === 'string' && - ctx.validatedLeafId !== null - ) { - try { - const placement = parseTerminalPanePlacement(args.placement) - const binding = { - worktreeId: args.worktreeId, - tabId: args.tabId, - leafId: ctx.validatedLeafId, - ptyId: ctx.result.id, - ...(ctx.result.incarnationId ? { incarnationId: ctx.result.incarnationId } : {}), - ...(startupCwd ? { startupCwd } : {}), - ...(placement ? { placement } : {}), - ...(ctx.paneSpawnOperation?.mustExist ? { mayCreate: false } : {}), - origin: spawnCommitBindingOrigin(ctx.result) - } - const persisted = ctx.replacedPaneOwner - ? await ctx.deps.store.persistPtyBinding( - swapReplacedPaneBinding(ctx.deps.store, binding, ctx.replacedPaneOwner) - ) - : await persistFreshPaneBinding( - ctx.deps.store, - binding, - args.connectionId, - ctx.paneSpawnOperation?.absentOwner - ) - if (persisted === false) { - throw new Error('terminal_pane_owner_changed') - } - // The swap holds the live binding now: a later failure must not retire it under the same id. - ctx.replacedPaneOwner = null - } catch (err) { - console.error('[pty] failed to persist PTY binding after spawn:', err) - await discardUnpersistedPtySpawn(ctx.provider, ctx.result, ctx.deps.runtime) - if ( - err instanceof PaneOwnerContentionError || - (err instanceof Error && err.message === 'terminal_pane_owner_changed') - ) { - throw err - } - throw Object.assign(new Error(createTerminalSessionStateSaveFailureMessage(err)), { - agentSessionOperationOutcome: 'unknown' as const - }) - } - } - return committedSize -} - -export function publishPtyIpcSpawnCommit(ctx: PtyIpcSpawnState, committedSize: PtyGrid): void { - // Why here: every IPC spawn that survives its binding save publishes once through this point. - ctx.deps.runtime?.noteTerminalSpawnCommit?.(ctx.result) - ctx.spawnTiming.log(ctx.result.id, { - daemon: ctx.isDaemonHostSpawn, - reattach: ctx.result.isReattach ?? false - }) - recordCodexPaneAccountForSpawn({ - ptyId: ctx.result.id, - isDaemonHostSpawn: ctx.isDaemonHostSpawn, - isReattach: ctx.result.isReattach === true, - pinnedByResume: ctx.codexResumeHomeSelected, - launchCodexHomePath: ctx.selectedCodexHomePath, - target: ctx.codexSelectionTarget, - settings: ctx.deps.getSettings?.() - }) - if (ctx.initiallyHidden) { - // Refresh the pre-spawn hidden mark only after this incarnation survives its save. - ctx.deps.transitionSpawnHiddenRendererPtyDeliveryState(ctx.result.id, true) - if (ctx.preSpawnHiddenMarkId !== null && ctx.preSpawnHiddenMarkId !== ctx.result.id) { - // Defense: never strand a mark on an id the provider renamed. - ctx.deps.transitionSpawnHiddenRendererPtyDeliveryState(ctx.preSpawnHiddenMarkId, false) - } - // Why after ownership (recorded at commit start): provider lookup routes by it, and a hidden-spawned agent should be paceable from its first flood. - ctx.deps.syncPtyBackgroundedDelivery(ctx.result.id, 'spawn') - closeStartupQueryAuthorityForPty(ctx.result.id) - } - if (ctx.preAllocatedHandle && !ctx.stablePaneOwner?.handle) { - if (ctx.deps.runtime?.registerPreAllocatedHandleForPty) { - ctx.deps.runtime.registerPreAllocatedHandleForPty(ctx.result.id, ctx.preAllocatedHandle) - ctx.agentTeamsLeaderHandle = null - } - } - ptySizes.set(ctx.result.id, committedSize) - if (ctx.effectiveSessionAppId !== undefined && ctx.effectiveSessionAppId !== ctx.result.id) { - ptySizes.delete(ctx.effectiveSessionAppId) - } - const rendererPreSignaled = ctx.validatedPaneKey - ? pendingByPaneKey.has(ctx.validatedPaneKey) - : false - const rendererAlreadyRegistered = - ctx.result.isReattach === true && - !rendererPreSignaled && - rendererSerializerReadiness.has(ctx.result.id) - rendererSerializerReadiness.beginIncarnation(ctx.result.id, rendererAlreadyRegistered) - // Why: capture the pending gen at spawn time so this PTY's teardown only settles its own generation, not a remount that replaced the entry. - if (ctx.validatedPaneKey && rendererPreSignaled) { - const pending = pendingByPaneKey.get(ctx.validatedPaneKey) - if (pending) { - pendingPtyIdBySerializerGeneration.set(pending.gen, ctx.result.id) - } - } -} diff --git a/src/main/ipc/pty/ipc/spawn-commit-run-facts.test.ts b/src/main/ipc/pty/ipc/spawn-commit-run-facts.test.ts index c0117c0fc86..45cc8106618 100644 --- a/src/main/ipc/pty/ipc/spawn-commit-run-facts.test.ts +++ b/src/main/ipc/pty/ipc/spawn-commit-run-facts.test.ts @@ -7,11 +7,21 @@ import { ptyIncarnationById, ptyOwnership } from '../provider/ownership-state' import { commitPtyIpcSpawn } from './spawn-commit' import { createPtyIpcSpawnState } from './spawn-state' import type { PtySpawnIpcDeps } from './spawn-types' +import { + claimRuntimePaneCreate, + pendingRuntimePaneCreatesByOwnerKey, + reservePaneSpawn +} from '../pane/spawn-reservation' + +vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true })) const PTY_ID = 'orca-ipc-pty-run-facts' const INCARNATION_ID = 'inc-ipc-run-facts' -async function commit(persistPtyBinding: ReturnType) { +async function commit( + persistPtyBinding: ReturnType, + prepare?: (ctx: ReturnType) => void +) { const facts = new TerminalRunFactsRegister() const stops = new TerminalIntentionalStops() stops.mark(PTY_ID, 'reversible', null)(true) @@ -45,6 +55,7 @@ async function commit(persistPtyBinding: ReturnType) { ctx.validatedLeafId = 'leaf-1' ctx.provider = { ...ctx.provider, shutdown: vi.fn().mockResolvedValue(undefined) } ctx.result = { id: PTY_ID, incarnationId: INCARNATION_ID } + prepare?.(ctx) const outcome = await commitPtyIpcSpawn(ctx).then( () => 'committed', () => 'discarded' @@ -76,4 +87,25 @@ describe('renderer spawn commit: run facts', () => { expect(facts.read(PTY_ID, INCARNATION_ID).freshSpawn).toBe(false) expect(stops.claimExit(PTY_ID, INCARNATION_ID)).toEqual(['replaced']) }) + + it('gives a runtime-create waiter the window result before the reservation waiter resumes', async () => { + const key = 'window-commit-runtime-waiter' + const reservation = reservePaneSpawn(key) + const release = claimRuntimePaneCreate(key) + const pending = pendingRuntimePaneCreatesByOwnerKey.get(key)! + const joined = reservation.promise.then(() => { + release() + return pending.promise + }) + try { + const { outcome } = await commit(vi.fn().mockResolvedValue(true), (ctx) => { + ctx.paneSpawnReservationKey = key + ctx.paneSpawnReservation = reservation + }) + expect(outcome).toBe('committed') + await expect(joined).resolves.toMatchObject({ id: PTY_ID, incarnationId: INCARNATION_ID }) + } finally { + release() + } + }) }) diff --git a/src/main/ipc/pty/ipc/spawn-commit.ts b/src/main/ipc/pty/ipc/spawn-commit.ts index cf4572c6c25..10909fed91f 100644 --- a/src/main/ipc/pty/ipc/spawn-commit.ts +++ b/src/main/ipc/pty/ipc/spawn-commit.ts @@ -1,205 +1,105 @@ -import { isValidTerminalTabId } from '../../../../shared/terminal-tab-id' -import { agentHookServer } from '../../../agent-hooks/server' -import { registerPty } from '../../../memory/pty-registry' import type { PtySpawnResult } from '../../../providers/pty-provider-contract' -import { clearMigrationUnsupportedPtysForPaneKey } from '../../../agent-hooks/migration-unsupported-pty-state' -import { recordPtySpawnTelemetry } from '../pane/spawn-telemetry' -import { - shouldSkipCodexHomeEnvForWindowsShell, - codexReattachedHomeRouteField -} from '../host-env/codex-home' -import { rememberPaneKeyForPty } from '../pane/key-state' -import { resolvePaneSpawnReservation } from '../pane/spawn-reservation' -import { seedTerminalRestoreRecordsFromSpawnResult } from '../pane/agent-session-owners' -import { seedResumeLaunchIdentityFromSpawn } from '../pane/resume-launch-identity' -import { - admitProviderReattachLaunchIdentity, - admitRendererAgentLaunchAuthority -} from '../pane/launch-authority' +import { admitRendererAgentLaunchAuthority } from '../pane/launch-authority' import type { PtyIpcSpawnState } from './spawn-state' import { startupCwdFallbackOutcome } from '../spawn-startup-target' -import { persistPtyIpcSpawnCommit, publishPtyIpcSpawnCommit } from './spawn-commit-persist' -import { admitPtySpawnOwnership, registerPersistedPtySpawn } from '../pane/spawn-registration' -import { reflowHeadlessTerminalToCommittedGrid } from '../delivery/attached-pty-size' -import { seedHeadlessTerminalFromSpawnResult } from '../pane/terminal-spawn-restore' -import { markNativeWindowsConptyPty } from '../../../runtime/terminal-model-query-authority' +import { commitPtySpawn } from '../spawn-commit-assembly' +import type { PtySpawnPaneBinding } from '../spawn-commit-binding' +import { spawnCommitBindingOrigin } from '../../../persistence/loading-store/pty-binding-span' +import { parseTerminalPanePlacement } from '../../../../shared/terminal-pane-placement' import { commitPtyWithOpenCodePromptIntent } from '../../../opencode/opencode-startup-prompt-owner' export async function commitPtyIpcSpawn(ctx: PtyIpcSpawnState): Promise { - return commitPtyWithOpenCodePromptIntent(ctx, () => commitReservedPtyIpcSpawn(ctx)) -} - -async function commitReservedPtyIpcSpawn(ctx: PtyIpcSpawnState): Promise { - const args = ctx.args - admitPtySpawnOwnership(ctx.deps.runtime, ctx.result) - if (ctx.nativeWindowsConptySpawn) { - markNativeWindowsConptyPty(ctx.result.id) - } - // Seed before the first disk await so live output appends to the restored history. - seedHeadlessTerminalFromSpawnResult(ctx.deps.runtime, ctx.result, ctx.validatedPaneKey) - seedTerminalRestoreRecordsFromSpawnResult(ctx.deps.runtime, ctx.result) - const committedSize = await persistPtyIpcSpawnCommit(ctx) - if ( - typeof args.worktreeId === 'string' && - args.worktreeId.length > 0 && - args.worktreeId.length <= 512 - ) { - const agentLaunchAuthority = admitRendererAgentLaunchAuthority({ - launchToken: args.launchToken, - spawnEnv: ctx.spawnEnv, - launchAgent: args.launchAgent, - launchConfig: ctx.effectiveLaunchConfig, - isReattach: ctx.result.isReattach === true, - hasStablePaneOwner: ctx.stablePaneOwner !== null, - incarnationId: ctx.result.incarnationId - }) - const providerReattachLaunchIdentity = admitProviderReattachLaunchIdentity({ - isReattach: ctx.result.isReattach === true, - launchAgent: ctx.result.launchAgent, - incarnationId: ctx.result.incarnationId - }) - const rejectedRegistration = registerPersistedPtySpawn( - ctx.deps.runtime, - ctx.deps.store, - ctx.pendingRegistrationClaim, - ctx.result.id, - args.worktreeId, - null, - // Why: pass validated pane identity so a throttled mobile create publishes its surface main-side instead of destroying the live PTY (#7587); bound the untrusted tabId. - typeof args.tabId === 'string' && - isValidTerminalTabId(args.tabId) && - args.tabId.length <= 512 && - ctx.metadataLeafId !== null - ? { - tabId: args.tabId, - leafId: ctx.metadataLeafId, - ...(ctx.preAllocatedHandle ? { terminalHandle: ctx.preAllocatedHandle } : {}), - ...(ctx.result.incarnationId ? { incarnationId: ctx.result.incarnationId } : {}), - ...(agentLaunchAuthority ? { agentLaunchAuthority } : {}), - ...(providerReattachLaunchIdentity ? { providerReattachLaunchIdentity } : {}) - } - : undefined, - shouldSkipCodexHomeEnvForWindowsShell(ctx.effectiveShellOverride, ctx.cwd) - ) - if (rejectedRegistration) { - await rejectedRegistration.catch((error: unknown) => { - if (!(error instanceof Error) || error.message !== 'agent_session_exited_during_start') { - throw error - } - }) - ctx.deps.runtime?.cancelPendingPtyRegistration?.( - ctx.result.id, - ctx.result.incarnationId, - ctx.pendingRegistrationClaim - ) - ctx.pendingRegistrationPtyId = null - ctx.exitedBeforeRegistration = true - // The renderer drains this incarnation's buffered output and exit without publishing it live. - return resolvePaneSpawnReservation( - ctx.paneSpawnReservationKey, - ctx.paneSpawnReservation, - ctx.result - ) - } - ctx.pendingRegistrationPtyId = null - } else if (ctx.pendingRegistrationPtyId) { - ctx.deps.runtime?.cancelPendingPtyRegistration?.( - ctx.pendingRegistrationPtyId, - ctx.result.incarnationId, - ctx.pendingRegistrationClaim - ) - ctx.pendingRegistrationPtyId = null - } - publishPtyIpcSpawnCommit(ctx, committedSize) - - // Admission must precede reflow: a replaced spawn cannot resize its successor's model. - reflowHeadlessTerminalToCommittedGrid({ - result: ctx.result, - committedSize, - reflowHeadlessTerminalToPtyGrid: ctx.deps.runtime?.reflowHeadlessTerminalToPtyGrid?.bind( - ctx.deps.runtime - ) - }) - // Why: seed after registerPty binds the worktree — including on - // desktop, where the renderer-authority gate above skips the emulator - // seed but the list/read records still live main-side. - seedTerminalRestoreRecordsFromSpawnResult(ctx.deps.runtime, ctx.result) - // Why: arm main's per-PTY Command Code output detector from the launch command (startupCommand parity); banner detection covers PTYs without one. - if (!ctx.stablePaneOwner) { - ctx.deps.runtime?.noteTerminalSpawnCommand?.( - ctx.result.id, - typeof ctx.launchCommand === 'string' ? ctx.launchCommand : null - ) - } - // Why: record the paneKey mapping so clearProviderPtyState can clear the agent-hooks server's per-paneKey caches on exit. - // Why: args.env is untrusted IPC JSON (type unenforced); bound the paneKey so malformed/oversized values can't pollute ptyPaneKey or clearPaneState. - const rememberedPaneKey = ctx.validatedPaneKey - ? rememberPaneKeyForPty(ctx.result.id, ctx.validatedPaneKey) - : null - if (ctx.legacySpawnPaneKey && ctx.migrationUnsupportedPaneKey) { - agentHookServer.registerPaneKeyAlias( - ctx.legacySpawnPaneKey.paneKey, - ctx.migrationUnsupportedPaneKey, - ctx.result.id, - Date.now(), - { authorityVerified: true } - ) - clearMigrationUnsupportedPtysForPaneKey(ctx.migrationUnsupportedPaneKey) - } else if (ctx.opaqueRemintedSpawnPaneKey && ctx.validatedPaneKey) { - // Reminted $$ tokens are spawn-physical; alias them to the metadata-proven tab:leaf key. - agentHookServer.registerPaneKeyAlias( - ctx.opaqueRemintedSpawnPaneKey, - ctx.validatedPaneKey, - ctx.result.id, - Date.now(), - { authorityVerified: true } - ) - } else if (ctx.validatedPaneKey) { - if (!ctx.result.isReattach) { - clearMigrationUnsupportedPtysForPaneKey(ctx.validatedPaneKey) - } - } - // Why after commit: a spawn rejected above must not leave a published identity behind. - seedResumeLaunchIdentityFromSpawn({ - paneKey: ctx.validatedPaneKey, - launchAgent: args.launchAgent, - resumeProviderSession: args.resumeProviderSession, - connectionId: args.connectionId, - worktreeId: args.worktreeId, - launchedAt: ctx.codexHomeLaunchStartedAt, - resumeArgvDropped: ctx.codexResumeLaunch.droppedResumeArgv, - result: ctx.result - }) - // Why: record the spawn-result pid once here so the memory module needn't reach back into ipc/pty on a hot path (works for in-process and daemon-hosted PTYs). - const spawnedPid = ctx.result.pid ?? null - // Why: args.worktreeId/sessionId arrive as untrusted IPC strings (type unenforced at the boundary); bound them so malformed/oversized values can't pollute registerPty's maps. - registerPty({ - ptyId: ctx.result.id, - worktreeId: + return commitPtyWithOpenCodePromptIntent(ctx, () => { + const args = ctx.args + // Why: args.worktreeId/sessionId arrive as untrusted IPC strings (type unenforced at the boundary); bound them so malformed/oversized values can't pollute registerPty's maps. + const worktreeId = typeof args.worktreeId === 'string' && args.worktreeId.length > 0 && args.worktreeId.length <= 512 ? args.worktreeId - : null, - sessionId: - typeof args.sessionId === 'string' && - args.sessionId.length > 0 && - args.sessionId.length <= 256 - ? args.sessionId - : null, - paneKey: rememberedPaneKey, - pid: - typeof spawnedPid === 'number' && Number.isFinite(spawnedPid) && spawnedPid > 0 - ? spawnedPid : null + const response = ipcSpawnResponse(ctx) + return commitPtySpawn({ + state: ctx, + nativeWindowsConptySpawn: ctx.nativeWindowsConptySpawn, + seedPaneKey: ctx.validatedPaneKey, + paneBinding: ipcSpawnPaneBinding(ctx), + registration: { + store: ctx.deps.store, + worktreeId, + shellOverride: ctx.effectiveShellOverride, + rendererLaunchAuthority: admitRendererAgentLaunchAuthority({ + launchToken: args.launchToken, + spawnEnv: ctx.spawnEnv, + launchAgent: args.launchAgent, + launchConfig: ctx.effectiveLaunchConfig, + isReattach: ctx.result.isReattach === true, + hasStablePaneOwner: ctx.stablePaneOwner !== null, + incarnationId: ctx.result.incarnationId + }) + }, + preAllocatedHandle: ctx.preAllocatedHandle, + spawnTiming: ctx.spawnTiming, + paneKey: ctx.validatedPaneKey, + paneKeyAliases: { + legacySpawnPaneKey: ctx.legacySpawnPaneKey, + migrationUnsupportedPaneKey: ctx.migrationUnsupportedPaneKey, + opaqueRemintedSpawnPaneKey: ctx.opaqueRemintedSpawnPaneKey + }, + memorySessionId: + typeof args.sessionId === 'string' && + args.sessionId.length > 0 && + args.sessionId.length <= 256 + ? args.sessionId + : null, + resumeArgvDropped: ctx.codexResumeLaunch.droppedResumeArgv, + codexLaunchEnv: ctx.baseEnv, + initiallyHidden: ctx.initiallyHidden, + hiddenDelivery: 'renderer-scoped', + earlyExit: 'drain', + joinPayload: { reservation: response, reply: response } + }) }) - // Why: telemetry-plan.md§Agent launch semantics — fire agent_started only after spawn resolved; safeParse each field so a spoofed IPC payload can't poison the event (missing required field skips it). - if (args.telemetry && !ctx.stablePaneOwner) { - recordPtySpawnTelemetry(args.telemetry) +} + +function ipcSpawnPaneBinding(ctx: PtyIpcSpawnState): PtySpawnPaneBinding | undefined { + const args = ctx.args + // Persist the binding before acknowledging spawn so the renderer debounce cannot orphan history. + if ( + !ctx.deps.store || + typeof args.worktreeId !== 'string' || + typeof args.tabId !== 'string' || + ctx.validatedLeafId === null + ) { + return undefined } + const startupCwd = startupCwdFallbackOutcome(ctx.startupCwdFallback, ctx.result) + ? (ctx.correctStartupCwd ?? ctx.requestedCwd) + : ctx.requestedCwd + const placement = parseTerminalPanePlacement(args.placement) + return { + store: ctx.deps.store, + binding: { + worktreeId: args.worktreeId, + tabId: args.tabId, + leafId: ctx.validatedLeafId, + ptyId: ctx.result.id, + ...(ctx.result.incarnationId ? { incarnationId: ctx.result.incarnationId } : {}), + ...(startupCwd ? { startupCwd } : {}), + ...(placement ? { placement } : {}), + ...(ctx.paneSpawnOperation?.mustExist ? { mayCreate: false } : {}), + origin: spawnCommitBindingOrigin(ctx.result) + }, + absentOwner: ctx.paneSpawnOperation?.absentOwner, + refusal: 'terminal_pane_owner_changed', + failureLog: '[pty] failed to persist PTY binding after spawn:' + } +} + +function ipcSpawnResponse(ctx: PtyIpcSpawnState) { const { startupCwdSource: _startupCwdSource, ...providerResult } = ctx.result const startupCwdFallback = startupCwdFallbackOutcome(ctx.startupCwdFallback, ctx.result) - const response = { + return { ...providerResult, // Why both or neither: a pane can only adopt proven kitty flags together // with the sequence boundary they describe. @@ -218,27 +118,4 @@ async function commitReservedPtyIpcSpawn(ctx: PtyIpcSpawnState): Promise