diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index b1a258e504b..a86beadbea2 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -10471,6 +10471,37 @@ describe('OrcaRuntimeService', () => { expect(batches.flatMap((batch) => batch.facts)).not.toContainEqual({ kind: 'agent-exited' }) }) + it('confirms a host-certified SSH exit that races the foreground read', async () => { + const { runtime, batches } = createSideEffectRuntime() + runtime.registerPty('pty-1', TEST_WORKTREE_ID, 'conn-1') + syncSinglePty(runtime) + runtime.onPtySpawned('pty-1', 'inc-ssh-exit', { awaitsRegistration: false }) + runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;Codex ready\x07') + let resolveForeground!: (process: string) => void + const foregroundRead = new Promise((resolve) => { + resolveForeground = resolve + }) + const getForegroundProcess = vi.fn(() => foregroundRead) + runtime.setPtyController({ + write: () => true, + kill: () => true, + getForegroundProcess + }) + + runtime.onPtyData('pty-1', '\x1b]0;bichir\x07', 100) + await vi.waitFor(() => expect(getForegroundProcess).toHaveBeenCalledOnce()) + runtime.onPtyExit('pty-1', -1, 'inc-ssh-exit', { hostExitConfirmed: true }) + resolveForeground('zsh') + + await vi.waitFor(() => + expect(batches.flatMap((batch) => batch.facts)).toContainEqual({ + kind: 'agent-exited', + executionHostConfirmed: true, + incarnationId: 'inc-ssh-exit' + }) + ) + }) + it('still confirms a local agent exit if the PTY disconnects during the foreground read', async () => { const { runtime, batches } = createSideEffectRuntime() runtime.registerPty('pty-1', TEST_WORKTREE_ID) @@ -15549,7 +15580,7 @@ describe('OrcaRuntimeService', () => { }) it('adopts renderer pane identity for remote runtime terminal creates', async () => { - const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) + const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg', incarnationId: 'inc-remote-create' }) const runtime = new OrcaRuntimeService(store) const tabId = 'tab-remote-runtime' const leafId = '11111111-1111-4111-8111-111111111111' @@ -15560,7 +15591,7 @@ describe('OrcaRuntimeService', () => { getForegroundProcess: async () => null }) - await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { + const terminal = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { focus: false, tabId, leafId, @@ -15574,6 +15605,7 @@ describe('OrcaRuntimeService', () => { (spawn.mock.calls[0]?.[0] as { env?: Record } | undefined)?.env ?? {} expect(spawnedEnv.ORCA_TAB_ID).toBe(tabId) expect(spawnedEnv.ORCA_PANE_KEY).toBe(`${tabId}:${leafId}`) + expect(terminal.incarnationId).toBe('inc-remote-create') }) it('does not adopt web mirror ids as host terminal ids', async () => { diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 9b2c7d8536e..93dd647cfef 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -1560,6 +1560,8 @@ type RuntimePtyWorktreeRecord = { launchAgent: TuiAgent | null foregroundAgent: TuiAgent | null connected: boolean + /** Host-certified exit survives connected=false until pending title reads settle. */ + hostExitConfirmed: boolean disconnectedAt: number | null lastExitCode: number | null lastExitCause: TerminalExitCause | null @@ -11259,6 +11261,7 @@ export class OrcaRuntimeService { this.spawnPublishedPtys.add(ptyId) const pty = this.getOrCreatePtyWorktreeRecord(ptyId) if (pty) { + pty.hostExitConfirmed = false if (incarnationId) { pty.incarnationId = incarnationId } @@ -11303,6 +11306,7 @@ export class OrcaRuntimeService { ...(binding && paneKey ? { tabId: binding.tabId, paneKey } : {}), ...(binding?.incarnationId ? { incarnationId: binding.incarnationId } : {}) }) + pty.hostExitConfirmed = false const agentLaunchAuthority = binding?.agentLaunchAuthority if ( agentLaunchAuthority && @@ -15917,6 +15921,9 @@ export class OrcaRuntimeService { this.remoteDesktopViewerRevisions.delete(ptyId) this.disposeHeadlessTerminal(ptyId) if (pty) { + if (options?.hostExitConfirmed === true) { + pty.hostExitConfirmed = true + } pty.connected = false pty.runtimeSessionOwned = false this.setPairedRendererSessionOwnership(pty.ptyId, false) @@ -18953,6 +18960,7 @@ export class OrcaRuntimeService { tabId: parsed?.tabId ?? record?.tabId ?? '', leafId: parsed?.leafId ?? record?.leafId ?? '', ptyId: record?.ptyId ?? null, + ...(pty?.incarnationId ? { incarnationId: pty.incarnationId } : {}), connected: pty?.connected === true, ...(worktreeId ? { worktreeId } : {}), ...this.getPtyExecutionHostMetadata(record?.ptyId ?? pty?.ptyId ?? null) @@ -19003,6 +19011,7 @@ export class OrcaRuntimeService { tabId: parsed.tabId, leafId: parsed.leafId, ptyId: terminal.ptyId ?? null, + ...(terminal.incarnationId ? { incarnationId: terminal.incarnationId } : {}), worktreeId: expectedWorktreeId })) this.terminalPaneRecoveryByIdentity.set(recoveryKey, recovery) @@ -19667,7 +19676,7 @@ export class OrcaRuntimeService { if (this.getPtyLivenessVerdict(ptyId)?.status === 'unverifiable') { return true } - return Boolean(pty?.connectionId && !pty.connected) + return Boolean(pty?.connectionId && !pty.connected && !pty.hostExitConfirmed) } private confirmPtyAgentExit(ptyId: string): void { @@ -29131,6 +29140,7 @@ export class OrcaRuntimeService { tabId, paneKey, ptyId: result.id, + ...(result.incarnationId ? { incarnationId: result.incarnationId } : {}), worktreeId: workspace.id, title: pty?.title ?? launchOpts.title ?? null, ...this.getPtyExecutionHostMetadata(result.id), @@ -29212,12 +29222,15 @@ export class OrcaRuntimeService { // populates this.leaves may not have arrived yet. Wait for the leaf to // appear so we can return a valid handle the caller can use right away. const handle = await this.waitForTerminalHandle(reply.tabId) + const ptyId = this.handles.get(handle)?.ptyId ?? null + const incarnationId = ptyId ? (this.ptysById.get(ptyId)?.incarnationId ?? null) : null return { handle, tabId: reply.tabId, worktreeId: worktreeId ?? '', title: reply.title, - ...this.getPtyExecutionHostMetadata(this.handles.get(handle)?.ptyId ?? null), + ...(incarnationId ? { incarnationId } : {}), + ...this.getPtyExecutionHostMetadata(ptyId), surface: 'visible' } } @@ -29301,7 +29314,8 @@ export class OrcaRuntimeService { this.adoptControllerTerminalHandle(session.id, terminalHandle) const pty = this.recordPtyWorktree(session.id, worktreeId, { connected: true, - title: session.title + title: session.title, + ...(session.incarnationId ? { incarnationId: session.incarnationId } : {}) }) const adoptedHandle = this.issuePtyHandle(pty) if (adoptedHandle !== terminalHandle) { @@ -29310,6 +29324,7 @@ export class OrcaRuntimeService { return { handle: adoptedHandle, ptyId: session.id, + ...(session.incarnationId ? { incarnationId: session.incarnationId } : {}), worktreeId, title: session.title || null, surface: 'background' @@ -33182,6 +33197,7 @@ export class OrcaRuntimeService { launchAgent: null, foregroundAgent: null, connected: state.connected ?? true, + hostExitConfirmed: false, disconnectedAt: state.connected === false ? Date.now() : null, lastExitCode: null, lastExitCause: null, diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pane-pty-binding.ts b/src/renderer/src/components/terminal-pane/pty-connection/pane-pty-binding.ts index 86f9523324c..f21ac36be4d 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pane-pty-binding.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pane-pty-binding.ts @@ -1,5 +1,6 @@ import type { IDisposable } from '@xterm/xterm' import type { HasPty } from '../terminal-dead-session-reconcile' +import type { PtyIncarnationId } from '../../../../../shared/pty-incarnation' export type PanePtyBinding = IDisposable & { syncProcessTracking: () => void @@ -27,4 +28,6 @@ export type PanePtyBinding = IDisposable & { * preserves on exit. Read at unmount so the parked sidecar can carry the * fact as a plain value instead of a session reference. */ isUntouchedFreshSpawnPty: (ptyId: string) => boolean + /** Captured by parked watchers so confirmed exit facts retain incarnation authority. */ + getPtyIncarnationId?: (ptyId: string) => PtyIncarnationId | null } diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.ts b/src/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.ts index 4bcf7f81838..97058843745 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.ts @@ -130,12 +130,17 @@ export function installPanePtyVisibilityBind(session: ConnectPanePtySession): vo // Do not strand a successful spawn because a delivery callback failed. } } - session.onPtyRebind = (ptyId: string, replacedPtyId: string): void => { + session.onPtyRebind = ( + ptyId: string, + replacedPtyId: string, + incarnationId?: PtyIncarnationId + ): void => { if (!session.canAdoptCapturedDirectSshRetryPty(ptyId)) { return } // Why: provider handle rotation keeps the existing pane/session generation; // replace its stale store identity without fresh-spawn exit semantics. + session.ptyIncarnationId = incarnationId ?? null session.bindActivePanePty(ptyId, { replacePtyId: replacedPtyId }) } // ─── Attention signal: BEL ──────────────────────────────────────────── diff --git a/src/renderer/src/components/terminal-pane/pty-connection/session-reconcile-dispose.ts b/src/renderer/src/components/terminal-pane/pty-connection/session-reconcile-dispose.ts index 16541b4e72d..769d2cbf430 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/session-reconcile-dispose.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/session-reconcile-dispose.ts @@ -174,6 +174,8 @@ export function installSessionReconcileDispose(session: ConnectPanePtySession): reconcileIfSessionMissing: session.reconcileIfSessionMissing, isUntouchedFreshSpawnPty: (ptyId) => session.spawnedFreshPtyId === ptyId && !Number.isFinite(session.lastTerminalInputAt), + getPtyIncarnationId: (ptyId) => + session.transport.getPtyId() === ptyId ? session.ptyIncarnationId : null, dispose() { session.disposed = true // Why: a detached client stops observing the pane's bytes, so it must cede diff --git a/src/renderer/src/components/terminal-pane/pty-transport-types.ts b/src/renderer/src/components/terminal-pane/pty-transport-types.ts index abc8c5c711b..24ab0cb63f1 100644 --- a/src/renderer/src/components/terminal-pane/pty-transport-types.ts +++ b/src/renderer/src/components/terminal-pane/pty-transport-types.ts @@ -258,7 +258,7 @@ export type IpcPtyTransportOptions = { onTitleChange?: (title: string, rawTitle: string) => void onPtySpawn?: (ptyId: string, incarnationId?: PtyIncarnationId) => void /** Rebind an existing pane after its provider replaces the PTY identity. */ - onPtyRebind?: (ptyId: string, replacedPtyId: string) => void + onPtyRebind?: (ptyId: string, replacedPtyId: string, incarnationId?: PtyIncarnationId) => void onBell?: () => void onAgentBecameIdle?: (title: string) => void onAgentBecameWorking?: () => void diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-expired-pane-recovery.test.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-expired-pane-recovery.test.ts index 9e529ccb795..a36331ee20c 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-expired-pane-recovery.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-expired-pane-recovery.test.ts @@ -93,6 +93,7 @@ describe('createRemoteRuntimePtyTransport', () => { tabId: 'host-tab-1', leafId: 'pane:1', ptyId: 'ssh-private-pty', + incarnationId: 'replacement-incarnation', worktreeId: 'wt-1' } } @@ -126,7 +127,8 @@ describe('createRemoteRuntimePtyTransport', () => { expect(latestSubscribePayload()).toMatchObject({ terminal: 'terminal-replacement' }) expect(onPtyRebind).toHaveBeenCalledWith( 'remote:hub-env@@terminal-replacement', - 'remote:hub-env@@terminal-expired' + 'remote:hub-env@@terminal-expired', + 'replacement-incarnation' ) expect(onPtyExit).not.toHaveBeenCalled() expect(onError).not.toHaveBeenCalled() diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-handle-resolution.test.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-handle-resolution.test.ts index 283abb2cec4..651825ffe2b 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-handle-resolution.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-handle-resolution.test.ts @@ -47,6 +47,7 @@ describe('createRemoteRuntimePtyTransport', () => { tabId: 'tab-1', leafId, ptyId: 'ssh:hub-private@@pty-2', + incarnationId: 'hub-incarnation-1', worktreeId: 'wt-1', executionHostId: 'ssh:hub-private', hostPlatform: 'win32' @@ -57,10 +58,12 @@ describe('createRemoteRuntimePtyTransport', () => { throw new Error(`Unexpected method ${request.method}`) }) const { createRemoteRuntimePtyTransport } = await import('./remote-runtime-pty-transport') + const onPtySpawn = vi.fn() const transport = createRemoteRuntimePtyTransport('hub-env', { worktreeId: 'wt-1', tabId: 'tab-1', - leafId + leafId, + onPtySpawn }) const result = await transport.connect({ @@ -73,9 +76,11 @@ describe('createRemoteRuntimePtyTransport', () => { expect(result).toEqual({ id: 'remote:hub-env@@hub-terminal-1', + incarnationId: 'hub-incarnation-1', replay: '', isReattach: true }) + expect(onPtySpawn).toHaveBeenCalledWith('remote:hub-env@@hub-terminal-1', 'hub-incarnation-1') expect(transport.getPtyId()).toBe('remote:hub-env@@hub-terminal-1') expect(transport.getExecutionHostId?.()).toBe('ssh:hub-private') expect(transport.getRemotePlatform?.()).toBe('win32') diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts index 16c6b235eac..a9be9c37ce9 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts @@ -202,6 +202,29 @@ export function createRemoteRuntimePtyTransport( let attachGeneration = 0 let subscriptionGeneration = 0 + function notifyPtySpawnForRemote( + id: string, + incarnationId?: RuntimeTerminalCreate['incarnationId'] + ): void { + if (incarnationId) { + onPtySpawn?.(id, incarnationId) + } else { + onPtySpawn?.(id) + } + } + + function notifyPtyRebindForRemote( + id: string, + replacedId: string, + incarnationId?: RuntimeTerminalResolvePane['incarnationId'] + ): void { + if (incarnationId) { + onPtyRebind?.(id, replacedId, incarnationId) + } else { + onPtyRebind?.(id, replacedId) + } + } + function setAttachmentReady(ready: boolean): void { attachmentReady = ready if (!ready) { @@ -832,6 +855,7 @@ export function createRemoteRuntimePtyTransport( return undefined } + let resolvedIncarnationId: RuntimeTerminalResolvePane['incarnationId'] if (leafId && worktreeId && !resolvePaneUnavailable) { try { const resolved = await callRuntime<{ terminal: RuntimeTerminalResolvePane }>( @@ -846,6 +870,7 @@ export function createRemoteRuntimePtyTransport( (!terminal.worktreeId || terminal.worktreeId === worktreeId) ) { adoptExecutionMetadata(terminal) + resolvedIncarnationId = terminal.incarnationId } } catch (error) { if (error instanceof RuntimeRpcCallError && error.code === 'method_not_found') { @@ -866,7 +891,7 @@ export function createRemoteRuntimePtyTransport( rows: options.rows ?? 24 } if (notifySpawn) { - onPtySpawn?.(remotePtyId) + notifyPtySpawnForRemote(remotePtyId, resolvedIncarnationId) } try { @@ -882,6 +907,7 @@ export function createRemoteRuntimePtyTransport( return { id: remotePtyId, + ...(resolvedIncarnationId ? { incarnationId: resolvedIncarnationId } : {}), replay: '', isReattach: true } satisfies PtyConnectResult @@ -1149,7 +1175,7 @@ export function createRemoteRuntimePtyTransport( rows: options.rows ?? 24 } if (notifySpawn) { - onPtySpawn?.(remotePtyId) + notifyPtySpawnForRemote(remotePtyId, terminal.incarnationId) } emitRecoveryState() try { @@ -1167,7 +1193,12 @@ export function createRemoteRuntimePtyTransport( ) { return undefined } - return { id: remotePtyId, replay: '', isReattach: true } + return { + id: remotePtyId, + ...(terminal.incarnationId ? { incarnationId: terminal.incarnationId } : {}), + replay: '', + isReattach: true + } } function recoverExpiredHostPane(): void { @@ -1199,7 +1230,7 @@ export function createRemoteRuntimePtyTransport( if (replacedPtyId && replacedPtyId !== remotePtyId) { replaceFitOverridePtyId(replacedPtyId, remotePtyId) replaceDriverPtyId(replacedPtyId, remotePtyId) - onPtyRebind?.(remotePtyId, replacedPtyId) + notifyPtyRebindForRemote(remotePtyId, replacedPtyId, terminal.incarnationId) } await subscribeToHandle() }) @@ -2179,7 +2210,7 @@ export function createRemoteRuntimePtyTransport( rows: options.rows ?? 24 } if (createdTerminal.isReattach !== true) { - onPtySpawn?.(remotePtyId) + notifyPtySpawnForRemote(remotePtyId, createdTerminal.incarnationId) } emitRecoveryState() @@ -2196,6 +2227,9 @@ export function createRemoteRuntimePtyTransport( return { id: remotePtyId, + ...(createdTerminal.incarnationId + ? { incarnationId: createdTerminal.incarnationId } + : {}), replay: '', ...(createdTerminal.isReattach === true ? { isReattach: true } : {}) } satisfies PtyConnectResult diff --git a/src/renderer/src/components/terminal-pane/terminal-parked-pty-watcher.ts b/src/renderer/src/components/terminal-pane/terminal-parked-pty-watcher.ts index 1cd049a3878..8811e4ab831 100644 --- a/src/renderer/src/components/terminal-pane/terminal-parked-pty-watcher.ts +++ b/src/renderer/src/components/terminal-pane/terminal-parked-pty-watcher.ts @@ -119,6 +119,7 @@ export function startParkedPtyWatcher(args: { const initialTitle = state.runtimePaneTitlesByTabId[tab.id]?.[pane.paneId] const disposeWatcher = startParkedTerminalByteWatcher({ ptyId, + ...(pane.incarnationId ? { incarnationId: pane.incarnationId } : {}), tabId: tab.id, worktreeId, leafId: pane.leafId, diff --git a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.ts b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.ts index 148c71a174d..7d972c4a2c1 100644 --- a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.ts +++ b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.ts @@ -70,6 +70,9 @@ export function resolveParkedTerminalPaneCandidates( // Why: the fact belongs to the captured PTY; a re-minted id on this leaf is a different shell. ...(prior.ptyId === pane.ptyId && prior.untouchedFreshSpawn ? { untouchedFreshSpawn: true } + : {}), + ...(prior.ptyId === pane.ptyId && prior.incarnationId + ? { incarnationId: prior.incarnationId } : {}) } : pane diff --git a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts index 7e305ac7fe3..5dbb6c0589f 100644 --- a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts +++ b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.ts @@ -10,9 +10,12 @@ import { discardPreHandlerPtyState, hasPreHandlerPtyExit } from './pty-pre-handler-buffer' import { parseRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' +import type { PtyIncarnationId } from '../../../../shared/pty-incarnation' export type ParkedTerminalPaneCapture = { ptyId: string | null + /** Process identity carried across the mounted-to-parked handoff. */ + incarnationId?: PtyIncarnationId | null /** PaneManager numeric pane id the live pane used for runtime titles. */ paneId: number /** Stable terminal-layout leaf UUID (paneKey attribution). */ diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts index 52a348b627d..cf859332f48 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts @@ -1963,10 +1963,17 @@ export function useTerminalPaneLifecycle({ manager.getPanes().map((capturedPane) => { const capturedPtyId = paneTransports.get(capturedPane.id)?.getPtyId() ?? null const capturedBinding = panePtyBindings.get(capturedPane.id) as - | (IDisposable & { isUntouchedFreshSpawnPty?: (ptyId: string) => boolean }) + | (IDisposable & { + isUntouchedFreshSpawnPty?: (ptyId: string) => boolean + getPtyIncarnationId?: (ptyId: string) => string | null + }) | undefined return { ptyId: capturedPtyId, + incarnationId: + capturedPtyId !== null + ? (capturedBinding?.getPtyIncarnationId?.(capturedPtyId) ?? null) + : null, paneId: capturedPane.id, leafId: capturedPane.leafId, drivesTabTitle: manager.getActivePane()?.id === capturedPane.id, diff --git a/src/shared/runtime-terminal-contracts.ts b/src/shared/runtime-terminal-contracts.ts index a1692e5f754..1c0f347dcac 100644 --- a/src/shared/runtime-terminal-contracts.ts +++ b/src/shared/runtime-terminal-contracts.ts @@ -243,6 +243,8 @@ export type RuntimeTerminalCreate = { tabId?: string paneKey?: string | null ptyId?: string | null + /** Opaque process identity behind a reusable PTY id; absence is unknown. */ + incarnationId?: PtyIncarnationId worktreeId: string title: string | null executionHostId?: ExecutionHostId @@ -264,6 +266,8 @@ export type RuntimeTerminalResolvePane = { tabId: string leafId: string ptyId: string | null + /** Opaque process identity behind a reusable PTY id; absence is unknown. */ + incarnationId?: PtyIncarnationId connected?: boolean worktreeId?: string executionHostId?: ExecutionHostId