diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index c9bf94a1731..d9789cb0ab3 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -17,23 +17,27 @@ "protection": "partial", "owner": "terminal-session-readiness", "layer": "posix-pty-and-shell-startup", - "surfaces": ["queued terminal startup commands", "zsh and bash startup files"], + "surfaces": ["queued terminal startup commands", "zsh, bash, and Fish startup output"], "platforms": ["macos", "linux"], "providers": ["local", "local-daemon", "ssh-daemon", "ssh-relay", "paired-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": ["local", "local-daemon", "ssh-relay"], - "coverageNotes": "Real macOS node-pty integration covers zsh 5.9 .zshenv/.zprofile and bash 3.2 profile replacement shells, including exec -a names, same-shell and child-process silent reads, a blocking zle-line-init hook, non-shell readline replacements, and a non-shell image renamed zsh. An Ubuntu 20.04 container confirms compatible zsh/bash termios. Deterministic owner contracts cover local, daemon, and SSH relay identity stripping plus provider- and renderer-owned delivery. Renderer-owned relay fallback publishes the existing ready marker, so mixed client/host versions need no new wire capability. Live SSH, paired-runtime, and WSL remain gaps.", - "motivatingLinks": ["https://github.com/stablyai/orca/issues/13767"], - "invariant": "Before user startup files run, the wrapper publishes its shell PID. If a later exec discards the ready marker, prompt fallback releases queued startup input only after the launched shell's actual executable image with that exact PID owns the PTY foreground, emits its post-hook line-editor enable sequence, and exposes zle/readline termios; startup reads, line-init hooks, child foreground processes, and non-shell readline replacements cannot trigger early fallback release.", - "oracle": "Run the real generated wrappers under node-pty with disposable zsh and bash startup files. Plain and exec -a replacement shells must receive one queued command at their first completed line editor without waiting for the 15-second timeout. The identical oracle must time out without the startup identity and prompt fallback. Same-shell reads, child-process reads, a blocking zle-line-init hook, sqlite3, and sqlite3 renamed zsh must not trigger prompt fallback. Relay owner coverage must strip the identity and release through the same prompt contract.", + "coverageNotes": "Real macOS node-pty integration covers zsh 5.9 .zshenv/.zprofile and bash 3.2 profile replacement shells, including exec -a names, same-shell and child-process silent reads, a blocking zle-line-init hook, non-shell readline replacements, and a non-shell image renamed zsh. A real Fish PTY proves marker stripping is byte-exact and does not render bracketed-paste control text. An Ubuntu 20.04 container confirms compatible zsh/bash termios. Deterministic owner contracts cover local, daemon, and SSH relay identity/readiness composition plus provider- and renderer-owned delivery. Renderer-owned relay fallback publishes the existing ready marker, so mixed client/host versions need no new wire capability. Live SSH, paired-runtime, and WSL remain gaps.", + "motivatingLinks": [ + "https://github.com/stablyai/orca/issues/13767", + "https://linear.app/stably/issue/STA-4068" + ], + "invariant": "Shell startup scanning preserves every non-Orca output byte in order across arbitrary PTY chunk boundaries. Before user startup files run, the wrapper publishes its shell PID. If a later exec discards the ready marker, prompt fallback releases queued startup input only after the launched shell's actual executable image with that exact PID owns the PTY foreground, emits its post-hook line-editor enable sequence, and exposes zle/readline termios; startup reads, line-init hooks, child foreground processes, and non-shell readline replacements cannot trigger early fallback release.", + "oracle": "Split Fish output immediately after the ready marker and after the following ESC introducer; local and daemon owners must emit the exact ESC [ ? 2004 h bytes and headless xterm must not render literal control text. Run the real generated wrappers under node-pty with disposable zsh, bash, and Fish startup files. Plain and exec -a replacement shells must receive one queued command at their first completed line editor without waiting for the 15-second timeout. The identical oracle must time out without the startup identity and prompt fallback. Same-shell reads, child-process reads, a blocking zle-line-init hook, sqlite3, and sqlite3 renamed zsh must not trigger prompt fallback. Relay owner coverage must use the same lossless scanner composition and release through the same prompt contract.", "commands": [ - "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready.test.ts src/main/providers/local-pty-provider.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/line-editor-ready-output-scanner.test.ts src/main/shell-startup-identity-scanner.test.ts src/main/shell-startup-output-scanner.test.ts src/main/shell-prompt-readiness-probe.test.ts src/shared/pty-slave-line-discipline-echo.test.ts src/shared/shell-process-readiness.test.ts src/main/daemon/session.test.ts src/main/daemon/shell-ready.test.ts src/main/providers/local-pty-shell-ready.test.ts src/main/providers/local-pty-provider.test.ts src/relay/pty-shell-launch.test.ts src/relay/pty-handler.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/relay/pty-handler.test.ts" ], "testFiles": [ "src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts", "src/main/line-editor-ready-output-scanner.test.ts", "src/main/shell-startup-identity-scanner.test.ts", + "src/main/shell-startup-output-scanner.test.ts", "src/main/shell-prompt-readiness-probe.test.ts", "src/shared/pty-slave-line-discipline-echo.test.ts", "src/shared/shell-process-readiness.test.ts", @@ -45,6 +49,25 @@ "src/relay/pty-handler.test.ts" ], "assertionRefs": [ + { + "file": "src/main/shell-startup-output-scanner.test.ts", + "assertions": [ + "Fish bracketed-paste output stays byte-exact across marker and ESC chunk splits", + "identity and readiness markers strip together without reordering prompt output", + "incomplete scanner prefixes drain in byte order" + ] + }, + { + "file": "src/main/daemon/session.test.ts", + "assertions": [ + "daemon output is byte-exact for both Fish chunk splits", + "headless xterm never renders literal bracketed-paste control text" + ] + }, + { + "file": "src/main/providers/local-pty-provider.test.ts", + "assertions": ["local PTY output is byte-exact for both Fish chunk splits"] + }, { "file": "src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts", "assertions": [ @@ -90,7 +113,7 @@ }, "performanceBudget": { "required": true, - "evidence": "The normal ready-marker path cancels before external probing. A missing marker triggers a debounced stty read only after a line-editor enable sequence, followed by narrow PID status and executable-image inspection only when termios matches. Slow ordinary startup output triggers zero probes; rejected protocol emissions are capped at four probes per startup. There is no per-command hook or steady-state polling." + "evidence": "One bounded composition pass replaces two sequential scanner passes at each owner and retains only the existing bounded prefixes. The normal ready-marker path cancels before external probing. A missing marker triggers a debounced stty read only after a line-editor enable sequence, followed by narrow PID status and executable-image inspection only when termios matches. Slow ordinary startup output triggers zero probes; rejected protocol emissions are capped at four probes per startup. There is no per-command hook or steady-state polling." }, "promotionCriteria": [ "Collect Linux, SSH-daemon, and paired-runtime live evidence.", diff --git a/src/main/daemon/session.ts b/src/main/daemon/session.ts index 81343b39744..3771b3194b5 100644 --- a/src/main/daemon/session.ts +++ b/src/main/daemon/session.ts @@ -8,17 +8,11 @@ import { import { isValidPtySize, normalizePtySize } from './daemon-pty-size' import { PostReadyFlushGate } from './post-ready-flush-gate' import { - createShellReadyScanState, - drainShellReadyHeldBytes, - scanForShellReady, - type ShellReadyScanState -} from '../shell-ready-marker-scanner' -import { - createShellStartupIdentityScanState, - drainShellStartupIdentityHeldBytes, - scanForShellStartupIdentity, - type ShellStartupIdentityScanState -} from '../shell-startup-identity-scanner' + createShellStartupOutputScanState, + drainShellStartupOutputScanState, + scanShellStartupOutput, + type ShellStartupOutputScanState +} from '../shell-startup-output-scanner' import { createShellPromptReadinessProbe, type ShellPromptReadinessProbe @@ -136,8 +130,7 @@ export class Session { private preReadyStdinQueue: string[] = [] private releaseStartupDeviceAttributesResponder: (() => void) | null = null private startupDeviceAttributesQueryFilter: StartupDeviceAttributesQueryFilter | null = null - private shellReadyScanState: ShellReadyScanState | null = null - private shellStartupIdentityScanState: ShellStartupIdentityScanState | null = null + private shellStartupOutputScanState: ShellStartupOutputScanState | null = null private shellStartupPid: number | null = null private shellPromptReadinessProbe: ShellPromptReadinessProbe | null = null private shellReadyTimer: ReturnType | null = null @@ -184,8 +177,7 @@ export class Session { if (opts.shellReadySupported) { this._shellState = 'pending' - this.shellReadyScanState = createShellReadyScanState() - this.shellStartupIdentityScanState = createShellStartupIdentityScanState() + this.shellStartupOutputScanState = createShellStartupOutputScanState() // Why: `write` queues everything until the ready marker, including the renderer's DA1 // reply — and a shell that withholds its first prompt until DA1 is answered (fish) then // never emits the marker that would release it. Answer from the daemon, past the queue. @@ -636,7 +628,7 @@ export class Session { } this.shellPromptReadinessProbe?.dispose() this.shellPromptReadinessProbe = null - this.shellReadyScanState = null + this.shellStartupOutputScanState = null this.preReadyStdinQueue = [] this.postReadyFlushGate.clear() this.disposeSubprocessHandle() @@ -682,18 +674,13 @@ export class Session { } let releaseStartupDeviceAttributes = false - if (this._shellState === 'pending' && this.shellStartupIdentityScanState) { - const scanned = scanForShellStartupIdentity(this.shellStartupIdentityScanState, data) + if (this._shellState === 'pending' && this.shellStartupOutputScanState) { + const scanned = scanShellStartupOutput(this.shellStartupOutputScanState, data) data = scanned.output if (scanned.shellPid) { this.shellStartupPid = scanned.shellPid - this.shellStartupIdentityScanState = null } - } - if (this._shellState === 'pending' && this.shellReadyScanState) { - const scanned = scanForShellReady(this.shellReadyScanState, data) - data = scanned.output - if (scanned.matched) { + if (scanned.ready) { this.transitionToReady(scanned.postMarkerBytesObserved) releaseStartupDeviceAttributes = true } @@ -773,20 +760,11 @@ export class Session { } private releaseHeldShellReadyBytes(): string { - if (!this.shellReadyScanState && !this.shellStartupIdentityScanState) { + if (!this.shellStartupOutputScanState) { return '' } - let heldBytes = this.shellStartupIdentityScanState - ? drainShellStartupIdentityHeldBytes(this.shellStartupIdentityScanState) - : '' - this.shellStartupIdentityScanState = null - if (this.shellReadyScanState && heldBytes) { - heldBytes = scanForShellReady(this.shellReadyScanState, heldBytes).output - } - if (this.shellReadyScanState) { - heldBytes += drainShellReadyHeldBytes(this.shellReadyScanState) - } - this.shellReadyScanState = null + const heldBytes = drainShellStartupOutputScanState(this.shellStartupOutputScanState) + this.shellStartupOutputScanState = null // Why: scanning strips marker bytes before fan-out; if readiness never completes, release any held prefix before timeout/exit discards it. this.startupIngress.accept(heldBytes) return heldBytes @@ -813,8 +791,7 @@ export class Session { private transitionToReady(postMarkerBytesObserved = false): void { this._shellState = 'ready' - this.shellReadyScanState = null - this.shellStartupIdentityScanState = null + this.shellStartupOutputScanState = null this.shellPromptReadinessProbe?.dispose() this.shellPromptReadinessProbe = null if (this.shellReadyTimer) { diff --git a/src/main/daemon/shell-ready.test.ts b/src/main/daemon/shell-ready.test.ts index b7299343579..2d931e7206e 100644 --- a/src/main/daemon/shell-ready.test.ts +++ b/src/main/daemon/shell-ready.test.ts @@ -6,6 +6,12 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync import type * as ShellReadyModule from './shell-ready' import { getZshShellReadyMarkerRegistrationBlock } from '../shell-templates' import { fishRequirementViolation, resolveFishBinary } from '../../shared/fish-binary-requirement' +import { + createShellStartupOutputScanState, + drainShellStartupOutputScanState, + scanShellStartupOutput +} from '../shell-startup-output-scanner' +import { HeadlessEmulator } from './headless-emulator' async function importFreshShellReady(): Promise { vi.resetModules() @@ -296,6 +302,8 @@ describePosix('daemon shell-ready launch config', () => { } }) let output = '' + let scannedOutput = '' + const startupScanState = createShellStartupOutputScanState() let commandWritten = false let erasureProbeWritten = false let queryCarry = '' @@ -321,6 +329,7 @@ describePosix('daemon shell-ready launch config', () => { }, 50) proc.onData((chunk) => { output += chunk + scannedOutput += scanShellStartupOutput(startupScanState, chunk).output // Why: fish stalls its first prompt 10s waiting on these and re-queries // each prompt, so answer every occurrence — an unanswered query makes // fish swallow the post-marker command as its reply. @@ -350,9 +359,15 @@ describePosix('daemon shell-ready launch config', () => { clearTimeout(deadline) clearInterval(sentinelPoll) proc.kill() + scannedOutput += drainShellStartupOutputScanState(startupScanState) expect(output).toContain(SHELL_READY_MARKER_OUTPUT) expect(output.split(SHELL_READY_MARKER_OUTPUT)).toHaveLength(2) + expect(scannedOutput).toBe(output.replace(SHELL_READY_MARKER_OUTPUT, '')) + const rendered = new HeadlessEmulator({ cols: 80, rows: 24 }) + expect(rendered.writeSync(scannedOutput)).toBe(true) + expect(rendered.getVisibleLines().join('\n')).not.toContain('[?2004h') + rendered.dispose() expect(existsSync(sentinel)).toBe(true) // Why: asserts the erase directly rather than inferring it from the marker // count, which only holds once enough prompts have been drawn to expose it. diff --git a/src/main/providers/local-pty-provider.ts b/src/main/providers/local-pty-provider.ts index 6601a56ac31..3026ded42a3 100644 --- a/src/main/providers/local-pty-provider.ts +++ b/src/main/providers/local-pty-provider.ts @@ -29,9 +29,6 @@ import { prepareMacosTccLoginShell } from './macos-tcc-login-shell' import { getMarkerlessShellLaunchConfig, getShellReadyLaunchConfig, - createShellReadyScanState, - drainShellReadyHeldBytes, - scanForShellReady, writeStartupCommandWhenShellReady, STARTUP_COMMAND_READY_MAX_WAIT_MS } from './local-pty-shell-ready' @@ -76,10 +73,10 @@ import { readPtySlavePath } from '../../shared/pty-slave-line-discipline-echo' import { - createShellStartupIdentityScanState, - drainShellStartupIdentityHeldBytes, - scanForShellStartupIdentity -} from '../shell-startup-identity-scanner' + createShellStartupOutputScanState, + drainShellStartupOutputScanState, + scanShellStartupOutput +} from '../shell-startup-output-scanner' import { createShellPromptReadinessProbe, type ShellPromptReadinessProbe @@ -961,11 +958,8 @@ export class LocalPtyProvider implements IPtyProvider { let shellReadyTimeout: ReturnType | null = null let shellStartupPid: number | null = null let shellPromptReadinessProbe: ShellPromptReadinessProbe | null = null - let shellStartupIdentityScanState = shellReadyLaunch?.supportsReadyMarker - ? createShellStartupIdentityScanState() - : null - const shellReadyScanState = shellReadyLaunch?.supportsReadyMarker - ? createShellReadyScanState() + let shellStartupOutputScanState = shellReadyLaunch?.supportsReadyMarker + ? createShellStartupOutputScanState() : null const shellReadyPromise = args.command ? new Promise((resolve) => { @@ -987,23 +981,17 @@ export class LocalPtyProvider implements IPtyProvider { resolve(signal) } const releaseHeldShellReadyBytes = (): void => { - if (!shellReadyScanState) { + if (!shellStartupOutputScanState) { return } - let heldBytes = shellStartupIdentityScanState - ? drainShellStartupIdentityHeldBytes(shellStartupIdentityScanState) - : '' - shellStartupIdentityScanState = null - if (heldBytes) { - heldBytes = scanForShellReady(shellReadyScanState, heldBytes).output - } - heldBytes += drainShellReadyHeldBytes(shellReadyScanState) + const heldBytes = drainShellStartupOutputScanState(shellStartupOutputScanState) + shellStartupOutputScanState = null if (heldBytes.length === 0) { return } startupIngress.accept(heldBytes) } - if (shellReadyScanState) { + if (shellStartupOutputScanState) { shellPromptReadinessProbe = createShellPromptReadinessProbe({ slavePath: readPtySlavePath(proc), shellPath, @@ -1048,18 +1036,13 @@ export class LocalPtyProvider implements IPtyProvider { const disposables: { dispose: () => void }[] = [] const onDataDisposable = proc.onData((rawData) => { let data = rawData - if (shellStartupIdentityScanState && resolveShellReady) { - const scanned = scanForShellStartupIdentity(shellStartupIdentityScanState, data) + if (shellStartupOutputScanState && resolveShellReady) { + const scanned = scanShellStartupOutput(shellStartupOutputScanState, data) data = scanned.output if (scanned.shellPid) { shellStartupPid = scanned.shellPid - shellStartupIdentityScanState = null } - } - if (shellReadyScanState && resolveShellReady) { - const scanned = scanForShellReady(shellReadyScanState, data) - data = scanned.output - if (scanned.matched) { + if (scanned.ready) { finishShellReady({ postMarkerBytesObserved: scanned.postMarkerBytesObserved }) } } diff --git a/src/main/shell-startup-output-scanner.test.ts b/src/main/shell-startup-output-scanner.test.ts new file mode 100644 index 00000000000..ee3530ec386 --- /dev/null +++ b/src/main/shell-startup-output-scanner.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest' +import { + createShellStartupOutputScanState, + drainShellStartupOutputScanState, + scanShellStartupOutput +} from './shell-startup-output-scanner' + +const READY_MARKER = '\x1b]777;orca-shell-ready\x07' + +describe('shell startup output scanner', () => { + it.each([ + ['after the ready marker', [READY_MARKER, '\x1b[?2004hfish> ']], + ['after the ESC introducer', [`${READY_MARKER}\x1b`, '[?2004hfish> ']] + ])('preserves Fish output split %s', (_boundary, chunks) => { + const state = createShellStartupOutputScanState() + let output = '' + + for (const chunk of chunks) { + output += scanShellStartupOutput(state, chunk).output + } + + expect(output).toBe('\x1b[?2004hfish> ') + }) + + it('strips identity and readiness markers from one chunk', () => { + const state = createShellStartupOutputScanState() + const scanned = scanShellStartupOutput( + state, + `\x1b]777;orca-shell-start:12345\x07${READY_MARKER}prompt` + ) + + expect(scanned).toEqual({ + output: 'prompt', + shellPid: 12345, + ready: true, + postMarkerBytesObserved: true + }) + }) + + it('drains every incomplete scanner prefix in byte order', () => { + const state = createShellStartupOutputScanState() + expect(scanShellStartupOutput(state, '\x1b]777;orca-shell-st').output).toBe('') + + expect(drainShellStartupOutputScanState(state)).toBe('\x1b]777;orca-shell-st') + }) +}) diff --git a/src/main/shell-startup-output-scanner.ts b/src/main/shell-startup-output-scanner.ts new file mode 100644 index 00000000000..4a9d5f90e87 --- /dev/null +++ b/src/main/shell-startup-output-scanner.ts @@ -0,0 +1,68 @@ +import { + createShellReadyScanState, + drainShellReadyHeldBytes, + scanForShellReady, + type ShellReadyScanState +} from './shell-ready-marker-scanner' +import { + createShellStartupIdentityScanState, + drainShellStartupIdentityHeldBytes, + scanForShellStartupIdentity, + type ShellStartupIdentityScanState +} from './shell-startup-identity-scanner' + +export type ShellStartupOutputScanState = { + ready: ShellReadyScanState + identity: ShellStartupIdentityScanState | null +} + +export type ShellStartupOutputScanResult = { + output: string + shellPid: number | null + ready: boolean + postMarkerBytesObserved: boolean +} + +export function createShellStartupOutputScanState(): ShellStartupOutputScanState { + return { + ready: createShellReadyScanState(), + identity: createShellStartupIdentityScanState() + } +} + +export function scanShellStartupOutput( + state: ShellStartupOutputScanState, + data: string +): ShellStartupOutputScanResult { + let shellPid: number | null = null + if (state.identity) { + const identity = scanForShellStartupIdentity(state.identity, data) + data = identity.output + shellPid = identity.shellPid + if (shellPid) { + state.identity = null + } + } + + const readiness = scanForShellReady(state.ready, data) + let output = readiness.output + if (readiness.matched && state.identity) { + output += drainShellStartupIdentityHeldBytes(state.identity) + state.identity = null + } + return { + output, + shellPid, + ready: readiness.matched, + postMarkerBytesObserved: readiness.postMarkerBytesObserved + } +} + +export function drainShellStartupOutputScanState(state: ShellStartupOutputScanState): string { + let output = state.identity ? drainShellStartupIdentityHeldBytes(state.identity) : '' + state.identity = null + if (output) { + output = scanForShellReady(state.ready, output).output + } + return output + drainShellReadyHeldBytes(state.ready) +} diff --git a/src/relay/pty-handler.ts b/src/relay/pty-handler.ts index 6dd3cd8a7fc..e7c09df95de 100644 --- a/src/relay/pty-handler.ts +++ b/src/relay/pty-handler.ts @@ -27,19 +27,13 @@ import { } from '../shared/cross-platform-path' import { splitWorktreeId } from '../shared/worktree-id' import { PhysicalExitTracker } from '../shared/physical-exit-tracker' +import { SHELL_READY_MARKER_PREFIX } from '../main/shell-ready-marker-scanner' import { - createShellReadyScanState, - drainShellReadyHeldBytes, - scanForShellReady, - SHELL_READY_MARKER_PREFIX, - type ShellReadyScanState -} from '../main/shell-ready-marker-scanner' -import { - createShellStartupIdentityScanState, - drainShellStartupIdentityHeldBytes, - scanForShellStartupIdentity, - type ShellStartupIdentityScanState -} from '../main/shell-startup-identity-scanner' + createShellStartupOutputScanState, + drainShellStartupOutputScanState, + scanShellStartupOutput, + type ShellStartupOutputScanState +} from '../main/shell-startup-output-scanner' import { createShellPromptReadinessProbe, type ShellPromptReadinessProbe @@ -196,8 +190,7 @@ type ManagedStartupCommand = { providerDelivery: boolean delivered: boolean waitForShellReady: boolean - scanState: ShellReadyScanState | null - identityScanState: ShellStartupIdentityScanState | null + outputScanState: ShellStartupOutputScanState | null shellPid: number | null promptProbe: ShellPromptReadinessProbe | null timer: ReturnType | null @@ -658,17 +651,11 @@ export class PtyHandler { } private drainStartupScanBytes(startup: ManagedStartupCommand): string { - let heldBytes = startup.identityScanState - ? drainShellStartupIdentityHeldBytes(startup.identityScanState) - : '' - startup.identityScanState = null - if (startup.scanState && heldBytes) { - heldBytes = scanForShellReady(startup.scanState, heldBytes).output - } - if (startup.scanState) { - heldBytes += drainShellReadyHeldBytes(startup.scanState) - startup.scanState = null + if (!startup.outputScanState) { + return '' } + const heldBytes = drainShellStartupOutputScanState(startup.outputScanState) + startup.outputScanState = null return heldBytes } @@ -767,18 +754,13 @@ export class PtyHandler { } managed.pty.onData((data: string) => { const startup = managed.startupCommand - if (startup?.identityScanState && !startup.delivered) { - const scanned = scanForShellStartupIdentity(startup.identityScanState, data) + if (startup?.waitForShellReady && startup.outputScanState && !startup.delivered) { + const scanned = scanShellStartupOutput(startup.outputScanState, data) data = scanned.output if (scanned.shellPid) { startup.shellPid = scanned.shellPid - startup.identityScanState = null } - } - if (startup?.waitForShellReady && startup.scanState && !startup.delivered) { - const scanned = scanForShellReady(startup.scanState, data) - data = scanned.output - if (scanned.matched) { + if (scanned.ready) { if (startup.providerDelivery) { this.scheduleStartupCommandResolution(managed, STARTUP_COMMAND_WRITE_DELAY_MS) } else { @@ -1644,13 +1626,9 @@ export class PtyHandler { providerDelivery: shouldProviderDeliverCommand, delivered: false, waitForShellReady: shellLaunch.env.ORCA_SHELL_READY_MARKER === '1', - scanState: + outputScanState: shellLaunch.env.ORCA_SHELL_READY_MARKER === '1' - ? createShellReadyScanState() - : null, - identityScanState: - shellLaunch.env.ORCA_SHELL_READY_MARKER === '1' - ? createShellStartupIdentityScanState() + ? createShellStartupOutputScanState() : null, shellPid: null, promptProbe: null,