mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 00:02:38 +00:00
* Robustify SSH terminal reconnect and session restore recovery - Release the replay guard after a fallback timeout to prevent permanent keyboard input lockouts when an unmounted terminal never parses. - Verify backing process liveness on PTY attach and reap stale entries so dead shells cleanly trigger a fresh pane spawn. - Normalize connection IDs during restore to prevent spurious mismatch errors, and treat true mismatches as expired sessions instead of crashing. * Route disconnected SSH terminal panes through deferred connection gate Avoid spawning SSH terminal processes against disconnected targets, which otherwise throws "No PTY provider" and leaves panes stranded. - Intercept spawning via a new connect gate that triggers the deferred connection flow when the SSH target is disconnected. - Fall back to composite worktree IDs during cold-start hydration to ensure deferred SSH session IDs are properly stashed. - Retry spawning and remounting terminal panes upon SSH reconnect if they are stranded or failed to spawn.
68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
import { parseExecutionHostId } from './execution-host'
|
|
|
|
const SSH_PTY_ID_PREFIX = 'ssh:'
|
|
const SSH_PTY_ID_SEPARATOR = '@@'
|
|
|
|
// Why: reconnect/restore paths sometimes hand these routers the execution-host
|
|
// id form ("ssh:<targetId>", from a workspace `hostId`) instead of the bare SSH
|
|
// target id that app PTY ids embed. Both name the same connection, so collapse
|
|
// to the bare id before comparing/encoding — otherwise a valid reattach throws a
|
|
// spurious "belongs to SSH connection" error at the user.
|
|
function normalizeConnectionId(connectionId: string): string {
|
|
const parsed = parseExecutionHostId(connectionId)
|
|
return parsed?.kind === 'ssh' ? parsed.targetId : connectionId
|
|
}
|
|
|
|
// Why: SSH relays allocate target-local ids like "pty-1"; app-wide routing
|
|
// needs the target id embedded so two relays cannot collide after restore.
|
|
export type ParsedSshPtyId = {
|
|
connectionId: string
|
|
relayPtyId: string
|
|
}
|
|
|
|
export function parseAppSshPtyId(ptyId: string): ParsedSshPtyId | null {
|
|
if (!ptyId.startsWith(SSH_PTY_ID_PREFIX)) {
|
|
return null
|
|
}
|
|
const separatorIndex = ptyId.indexOf(SSH_PTY_ID_SEPARATOR, SSH_PTY_ID_PREFIX.length)
|
|
if (separatorIndex === -1) {
|
|
return null
|
|
}
|
|
const encodedConnectionId = ptyId.slice(SSH_PTY_ID_PREFIX.length, separatorIndex)
|
|
const relayPtyId = ptyId.slice(separatorIndex + SSH_PTY_ID_SEPARATOR.length)
|
|
if (!encodedConnectionId || !relayPtyId) {
|
|
return null
|
|
}
|
|
try {
|
|
return {
|
|
connectionId: decodeURIComponent(encodedConnectionId),
|
|
relayPtyId
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function toAppSshPtyId(connectionId: string, relayPtyId: string): string {
|
|
const normalizedConnectionId = normalizeConnectionId(connectionId)
|
|
const parsed = parseAppSshPtyId(relayPtyId)
|
|
if (parsed) {
|
|
if (parsed.connectionId !== normalizedConnectionId) {
|
|
throw new Error(`PTY ${relayPtyId} belongs to SSH connection "${parsed.connectionId}"`)
|
|
}
|
|
return relayPtyId
|
|
}
|
|
return `${SSH_PTY_ID_PREFIX}${encodeURIComponent(normalizedConnectionId)}${SSH_PTY_ID_SEPARATOR}${relayPtyId}`
|
|
}
|
|
|
|
export function toRelaySshPtyId(connectionId: string, ptyId: string): string {
|
|
const parsed = parseAppSshPtyId(ptyId)
|
|
if (!parsed) {
|
|
return ptyId
|
|
}
|
|
if (parsed.connectionId !== normalizeConnectionId(connectionId)) {
|
|
throw new Error(`PTY ${ptyId} belongs to SSH connection "${parsed.connectionId}"`)
|
|
}
|
|
return parsed.relayPtyId
|
|
}
|