diff --git a/src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts b/src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts index acf4f60532..fc9ba31964 100644 --- a/src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts +++ b/src/main/runtime/orca-runtime-capture-provider-terminal-buffer.ts @@ -31,7 +31,7 @@ export class OrcaRuntimeWithCaptureProviderTerminalBuffer extends OrcaRuntimeWit // Why: daemon PTYs survive an app relaunch before any renderer mounts. // Mobile still needs their retained history without navigating desktop. const snapshot = await this.ptyController?.serializeProviderBuffer?.(ptyId, opts) - if (!snapshot || this.getPtyLifecycleGeneration(ptyId) !== generation) { + if (!snapshot || this.ptyLifecycleGenerationById.get(ptyId) !== generation) { return null } const snapshotModeTracker = new TerminalKittyKeyboardModeTracker() @@ -73,7 +73,10 @@ export class OrcaRuntimeWithCaptureProviderTerminalBuffer extends OrcaRuntimeWit return null } finally { liveModeTrackers.delete(liveModeTracker) - if (liveModeTrackers.size === 0) { + if ( + liveModeTrackers.size === 0 && + this.providerModeSnapshotScansByPtyId.get(ptyId) === liveModeTrackers + ) { this.providerModeSnapshotScansByPtyId.delete(ptyId) } } @@ -163,7 +166,7 @@ export class OrcaRuntimeWithCaptureProviderTerminalBuffer extends OrcaRuntimeWit if (snapshotOptions.visibleScreenOnly) { const projection = await this.parseVisibleSnapshot(snapshot) // Live bytes ordered after the provider frame make that frame stale. - return this.getPtyLifecycleGeneration(ptyId) === generation && + return this.ptyLifecycleGenerationById.get(ptyId) === generation && this.getPtyOutputSequence(ptyId) <= snapshot.seq ? projection : { lines: [] } @@ -180,7 +183,7 @@ export class OrcaRuntimeWithCaptureProviderTerminalBuffer extends OrcaRuntimeWit try { await emulator.write(data) const projection = projectTerminalTailLines(emulator, lineLimit) - return this.getPtyLifecycleGeneration(ptyId) === generation && + return this.ptyLifecycleGenerationById.get(ptyId) === generation && this.getPtyOutputSequence(ptyId) <= snapshot.seq ? projection : { lines: [] } diff --git a/src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts b/src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts index da5d824ef6..b9f3547850 100644 --- a/src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts +++ b/src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts @@ -112,8 +112,11 @@ export class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends OrcaRuntimeWi this.headlessTerminals.set(ptyId, state) state.writeChain = state.writeChain .then(async () => { + if (this.headlessTerminals.get(ptyId) !== state) { + return + } const snapshot = await this.serializeProviderTerminalBuffer(ptyId) - if (!snapshot) { + if (this.headlessTerminals.get(ptyId) !== state || !snapshot) { return } const data = `${snapshot.scrollbackAnsi ?? ''}${snapshot.data}` @@ -123,6 +126,9 @@ export class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends OrcaRuntimeWi this.recordOsc7MetadataForPty(ptyId, data) } await state.emulator.write(data) + if (this.headlessTerminals.get(ptyId) !== state) { + return + } if (snapshot.cwd !== undefined) { state.emulator.setCwd(snapshot.cwd) if (!this.terminalCwdByPtyId.has(ptyId) && snapshot.cwd?.trim()) { @@ -141,7 +147,9 @@ export class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends OrcaRuntimeWi // Best-effort: live bytes already chain behind this replacement state. }) .finally(() => { - this.providerSnapshotPreferredPtys.delete(ptyId) + if (this.headlessTerminals.get(ptyId) === state) { + this.providerSnapshotPreferredPtys.delete(ptyId) + } }) } diff --git a/src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts b/src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts index 163ffaecdb..63135f261b 100644 --- a/src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts +++ b/src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts @@ -51,6 +51,9 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime // setting headlessTerminals, the live byte would lazy-create a separate // state and the seed-resolve would overwrite it, dropping live bytes. state.writeChain = state.writeChain.then(async () => { + if (this.headlessTerminals.get(ptyId) !== state) { + return + } try { // Why the scrollback is not suppressed mid-TUI: the seed IS the model's // normal buffer, so zeroing it while an alt-screen agent was up left the @@ -58,7 +61,11 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime const rendered = await controller.serializeBuffer!(ptyId, { scrollbackRows: MOBILE_SUBSCRIBE_SCROLLBACK_ROWS }) - if (!rendered || rendered.data.length === 0) { + if ( + this.headlessTerminals.get(ptyId) !== state || + !rendered || + rendered.data.length === 0 + ) { return } this.recordOsc7MetadataForPty(ptyId, rendered.data) @@ -70,6 +77,9 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime state.emulator.resize(rendered.cols, rendered.rows) } await state.emulator.write(rendered.data) + if (this.headlessTerminals.get(ptyId) !== state) { + return + } const ptyDims = this.getTerminalSize(ptyId) if (ptyDims && (ptyDims.cols !== rendered.cols || ptyDims.rows !== rendered.rows)) { state.emulator.resize(ptyDims.cols, ptyDims.rows) @@ -91,7 +101,9 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime // Hydration is best-effort. Live writes continue via the same // writeChain that this catch-arm leaves intact. } finally { - this.headlessHydrationState.set(ptyId, 'done') + if (this.headlessTerminals.get(ptyId) === state) { + this.headlessHydrationState.set(ptyId, 'done') + } } }) } diff --git a/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts b/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts index 6559dbfd34..5b6da61d14 100644 --- a/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts +++ b/src/main/runtime/orca-runtime-serialize-main-terminal-buffer.ts @@ -134,15 +134,24 @@ export class OrcaRuntimeWithSerializeMainTerminalBuffer extends OrcaRuntimeWithA this.recordRecentPtyOutputForPathProvenance(ptyId, data) state.writeChain = state.writeChain .then(async () => { + if (this.headlessTerminals.get(ptyId) !== state) { + return + } // Why: seed writes never set forwardQueryReplies — the main-side // replay guard. A snapshot containing old queries must answer no one. await state.emulator.write(data) + if (this.headlessTerminals.get(ptyId) !== state) { + return + } // Why AFTER the seed write: the snapshot payload cannot carry kitty // pushes (rehydrateSequences deliberately omits them), but ordering // behind it keeps the parse deterministic. Unflagged like the seed — // re-applying flags must answer no one. if (typeof metadata.kittyKeyboardFlags === 'number') { await state.emulator.applyKittyKeyboardFlags(metadata.kittyKeyboardFlags) + if (this.headlessTerminals.get(ptyId) !== state) { + return + } } if (metadata.cwd !== undefined) { state.emulator.setCwd(metadata.cwd)