Files
orca/src/main/providers/ssh-pty-errors.ts
T
Neil 7c6740fb2d fix(ssh): stop a live-PTY restore requirement from authorising a respawn
A relay answer of sourceRecovery.status === 'restoreRequired' is a SUCCESSFUL
RPC that proves the PTY is alive - only its output delivery was retired. The
spawn reattach path raised SSH_SESSION_EXPIRED for it, which is the one token
that authorises replacing the pane's PTY: both spawn-execute sites then call
markSshRemotePtyLease(..., 'expired') and deletePtyOwnership(), and the
renderer cold-starts a fresh agent over the same worktree. A live remote agent
was orphaned by a request that had just observed it running.

Split the vocabulary so the two verdicts cannot be confused
(docs/reference/ssh-execution-boundary.md), retry the attach first since a
retired delivery is repairable, and give the token its own user-facing copy so
it never inherits the "open a new terminal" advice.

Second root cause, same symptom: the relay's delivery record was keyed on
client id alone, so a re-bound primary channel matched a delivery bound to the
dead sink and got 'existing' back - the pane repaints and never receives
frames. Key it on the transport generation too, as the dispatcher's own
publication ledger already does, and scope every release/retire path in
activate() to a record the caller still owns.
2026-09-01 14:15:57 -07:00

40 lines
1.7 KiB
TypeScript

import { SSH_PTY_IDENTITY_MISMATCH_ERROR } from '../../shared/ssh-pty-failure-tokens'
export {
SSH_PTY_IDENTITY_MISMATCH_ERROR,
SSH_SESSION_EXPIRED_ERROR,
SSH_SOURCE_RESTORE_REQUIRED_ERROR
} from '../../shared/ssh-pty-failure-tokens'
export function isSshPtyNotFoundError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error)
return /PTY ".+" not found/i.test(message)
}
export function isSshPtyIdentityMismatchError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error)
return message.includes(SSH_PTY_IDENTITY_MISMATCH_ERROR) || /identity mismatch/i.test(message)
}
/**
* A reachable relay answered for this exact PTY id and reported it absent — positive evidence of
* absence from the execution host, so `exited` rather than `unverifiable`
* (docs/reference/ssh-execution-boundary.md). Deliberately NOT raised for a transport failure, a
* request timeout, a disposed multiplexer, an identity mismatch (the id names a live PTY belonging
* to another pane), or `restoreRequired` (the PTY is live, only its source stream is not) — none of
* those observe the process, and treating them as absence orphans live remote work.
*
* Carries the same `SSH_SESSION_EXPIRED` message so message-based consumers are unaffected; only
* callers that can act on the stronger verdict test the class.
*/
export class SshPtyAbsentFromRelayError extends Error {
constructor(message: string) {
super(message)
this.name = 'SshPtyAbsentFromRelayError'
}
}
export function isSshPtyAbsentFromRelayError(error: unknown): boolean {
return error instanceof SshPtyAbsentFromRelayError
}