Files
4d82149fe5 fix(runtime): reject stale inventory after PTY lifecycle changes (#21014)
* fix(runtime): reject provider inventory across PTY lifecycle changes

* fix(runtime): canonicalize SSH inventory generation keys

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
2026-09-19 17:24:29 -07:00

89 lines
4.8 KiB
Diff

diff --git a/src/main/runtime/orca-runtime-invalidate-all-handles-for-pty.ts b/src/main/runtime/orca-runtime-invalidate-all-handles-for-pty.ts
index 448fb2a00fb..9d6e5d56ca4 100644
--- a/src/main/runtime/orca-runtime-invalidate-all-handles-for-pty.ts
+++ b/src/main/runtime/orca-runtime-invalidate-all-handles-for-pty.ts
@@ -1,6 +1,12 @@
// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests.
import { OrcaRuntimeWithResolveKnownWorkspaceFileTarget } from './orca-runtime-resolve-known-workspace-file-target'
import type { PtyIncarnationId } from '../../shared/pty-incarnation'
+import { getPtyExecutionHost } from '../../shared/terminal-execution-host'
+import {
+ LOCAL_EXECUTION_HOST_ID,
+ parseExecutionHostId,
+ toSshExecutionHostId
+} from '../../shared/execution-host'
export class OrcaRuntimeWithInvalidateAllHandlesForPty extends OrcaRuntimeWithResolveKnownWorkspaceFileTarget {
protected invalidateAllHandlesForPty(ptyId: string, preserveHandle?: string): Set<string> {
@@ -110,11 +116,30 @@ export class OrcaRuntimeWithInvalidateAllHandlesForPty extends OrcaRuntimeWithRe
return false
}
+ protected invalidatePtyControllerInventoryForLifecycle(
+ ptyId: string,
+ connectionId?: string | null
+ ): void {
+ const generation = ++this.ptyControllerInventorySequence
+ const hostId = getPtyExecutionHost(ptyId)
+ if (hostId === 'foreign' || (hostId && parseExecutionHostId(hostId)?.kind === 'runtime')) {
+ this.ptyControllerAggregateInventoryGeneration = generation
+ return
+ }
+ const connection =
+ connectionId === undefined ? this.ptysById.get(ptyId)?.connectionId : connectionId
+ const providerKey =
+ hostId ?? (connection ? toSshExecutionHostId(connection) : LOCAL_EXECUTION_HOST_ID)
+ // A pending census predates this admission or exit, including legacy IDs without an incarnation.
+ this.ptyControllerInventoryGenerationByProvider.set(providerKey, generation)
+ }
+
onPtySpawned(
ptyId: string,
incarnationId?: PtyIncarnationId,
options: { awaitsRegistration?: boolean } = {}
): void {
+ this.invalidatePtyControllerInventoryForLifecycle(ptyId)
const existingPty = this.ptysById.get(ptyId)
if (
existingPty &&
diff --git a/src/main/runtime/orca-runtime-on-pty-exit.ts b/src/main/runtime/orca-runtime-on-pty-exit.ts
index 6ddf877f87c..ce24ed65d98 100644
--- a/src/main/runtime/orca-runtime-on-pty-exit.ts
+++ b/src/main/runtime/orca-runtime-on-pty-exit.ts
@@ -30,6 +30,7 @@ export class OrcaRuntimeWithOnPtyExit extends OrcaRuntimeWithOnClientDisconnecte
if (exitIncarnationId && pty?.incarnationId && exitIncarnationId !== pty.incarnationId) {
return
}
+ this.invalidatePtyControllerInventoryForLifecycle(ptyId, pty?.connectionId)
// A bare exit code is not enough to establish why a process ended: older
// daemons and SSH relays can report 0 for crashes and wrapper exits.
const observedCause = options.cause ?? resolveUnreportedExitCause(exitCode)
diff --git a/src/main/runtime/orca-runtime-refresh-pty-worktree-records-with-controller-inventory.ts b/src/main/runtime/orca-runtime-refresh-pty-worktree-records-with-controller-inventory.ts
index aca67f67ba2..930907da120 100644
--- a/src/main/runtime/orca-runtime-refresh-pty-worktree-records-with-controller-inventory.ts
+++ b/src/main/runtime/orca-runtime-refresh-pty-worktree-records-with-controller-inventory.ts
@@ -55,7 +55,10 @@ export class OrcaRuntimeWithRefreshPtyWorktreeRecordsWithControllerInventory ext
}
const inventoryGeneration = this.ptyControllerInventorySequence + 1
this.ptyControllerInventorySequence = inventoryGeneration
- const providerKey = typeof connectionId === 'string' ? `ssh:${connectionId}` : 'local'
+ const providerKey =
+ typeof connectionId === 'string'
+ ? toSshExecutionHostId(connectionId)
+ : LOCAL_EXECUTION_HOST_ID
const livenessObservationAtStart = this.ptyLivenessObservationSequence
if (connectionId === undefined) {
this.ptyControllerAggregateInventoryGeneration = inventoryGeneration
diff --git a/src/main/runtime/orca-runtime-register-pty.ts b/src/main/runtime/orca-runtime-register-pty.ts
index dae2519ca9f..2d36a18a1e0 100644
--- a/src/main/runtime/orca-runtime-register-pty.ts
+++ b/src/main/runtime/orca-runtime-register-pty.ts
@@ -26,6 +26,7 @@ export class OrcaRuntimeWithRegisterPty extends OrcaRuntimeWithInvalidateAllHand
isWsl?: boolean
): void {
this.assertPtyDidNotExitBeforeRegistration(ptyId, binding?.incarnationId)
+ this.invalidatePtyControllerInventoryForLifecycle(ptyId, connectionId)
const existingPty = this.ptysById.get(ptyId)
const replacementHandle = binding?.terminalHandle?.trim()
const pendingReplacement = this.pendingPtyHandleReplacementFences.get(ptyId)