mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 00:27:39 +00:00
A settled orchestration worker's pane (automaticResumeBlockedBy: 'legacy-orchestration-worker') attached renderer-only: transport.attach registered IPC listeners without ever asking main to attach the daemon session. Main is the daemon's only client, so after an app restart its adapter never learned the id, pty:hasPty answered a fabricated false, and the visibility reconciler tore the tab down while the process still ran. The fence's intent is right, the mechanism was wrong. A fenced pane now takes the SAME deferred reattach as every other restored pane and carries the fence as attachOnly on the connect: main attaches the live session or refuses to mint one. A session its owner proves absent comes back as the existing exitedBeforeAttach terminal state, so the pane neither respawns nor retires a binding it may never replace. Deletes the renderer-only attach path (retained-legacy-pty-attach.ts, the legacyAttachOnlyPtyId short-circuit, and the SSH gate that skipped the deferred flow for these panes). Pre-commit hook skipped: it runs `pnpm install`, which fails on this worktree's symlinked node_modules. Its checks (oxlint, react-doctor oxlint, oxfmt) were run manually on every staged file.
96 lines
3.4 KiB
TypeScript
96 lines
3.4 KiB
TypeScript
// Error classes shared across the daemon protocol boundary (client, server,
|
|
// host). Split from types.ts, which is capped for wire-shape declarations.
|
|
import { SESSION_NOT_FOUND_MESSAGE_PREFIX } from '../../shared/pty-attach-absence-evidence'
|
|
|
|
const ATTACH_CANCELED_PREFIX = 'Attach canceled for session '
|
|
|
|
export class TerminalAttachCanceledError extends Error {
|
|
constructor(sessionId: string) {
|
|
super(`${ATTACH_CANCELED_PREFIX}${sessionId}`)
|
|
this.name = 'TerminalAttachCanceledError'
|
|
}
|
|
}
|
|
|
|
/** Recognizes the daemon's attach-cancellation across the wire, where only the message survives. */
|
|
export function isTerminalAttachCanceledMessage(message: string): boolean {
|
|
return message.startsWith(ATTACH_CANCELED_PREFIX)
|
|
}
|
|
|
|
export class DaemonProtocolError extends Error {
|
|
constructor(message: string) {
|
|
super(message)
|
|
this.name = 'DaemonProtocolError'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The control connection itself failed: nothing was delivered and no reply is
|
|
* coming. Distinct from a DaemonProtocolError the daemon actually answered with
|
|
* (or one we raised on our own timeout) — only this class proves the socket, and
|
|
* therefore every session sharing it, is already lost. Extends
|
|
* DaemonProtocolError and keeps the same messages so existing instanceof and
|
|
* message checks (isDaemonGoneError) still match.
|
|
*/
|
|
export class DaemonConnectionLostError extends DaemonProtocolError {
|
|
constructor(message: string) {
|
|
super(message)
|
|
this.name = 'DaemonConnectionLostError'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Nothing came back before our own deadline. The daemon may still be alive and merely
|
|
* slow, so this is not a connection loss — but a request AND the cancel sent to clean
|
|
* it up both hitting this means the daemon is wedged with its socket still open.
|
|
*/
|
|
export class DaemonRequestTimeoutError extends DaemonProtocolError {
|
|
constructor(message: string) {
|
|
super(message)
|
|
this.name = 'DaemonRequestTimeoutError'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The one message `isDaemonGoneError` matches that a still-open connection can produce,
|
|
* so it is the only way a wedged-but-connected daemon reaches the respawn path.
|
|
*/
|
|
export const DAEMON_UNAVAILABLE_RECONNECT_MESSAGE = 'Daemon temporarily unavailable; reconnect'
|
|
|
|
export class SessionNotFoundError extends Error {
|
|
constructor(sessionId: string) {
|
|
super(`${SESSION_NOT_FOUND_MESSAGE_PREFIX}${sessionId}`)
|
|
this.name = 'SessionNotFoundError'
|
|
}
|
|
}
|
|
|
|
export class TerminalSessionOwnerUnverifiedError extends Error {
|
|
constructor(sessionId: string) {
|
|
super(`Terminal session owner could not be verified: ${sessionId}`)
|
|
this.name = 'TerminalSessionOwnerUnverifiedError'
|
|
}
|
|
}
|
|
|
|
export class TerminalHostGoneError extends Error {
|
|
constructor() {
|
|
super('terminal_host_gone')
|
|
this.name = 'TerminalHostGoneError'
|
|
}
|
|
}
|
|
|
|
// Connect ENOENT/ECONNREFUSED proves the endpoint is absent; open ENOENT can be a missing token file.
|
|
export function isDaemonEndpointGoneError(err: unknown): boolean {
|
|
const candidate = err as { code?: unknown; syscall?: unknown } | null
|
|
return (
|
|
typeof candidate === 'object' &&
|
|
candidate !== null &&
|
|
candidate.syscall === 'connect' &&
|
|
(candidate.code === 'ENOENT' || candidate.code === 'ECONNREFUSED')
|
|
)
|
|
}
|
|
|
|
export function decodeDaemonResponseError(message: string): Error {
|
|
return message.startsWith(SESSION_NOT_FOUND_MESSAGE_PREFIX)
|
|
? new SessionNotFoundError(message.slice(SESSION_NOT_FOUND_MESSAGE_PREFIX.length))
|
|
: new DaemonProtocolError(message)
|
|
}
|