import { MissingSessionCwdError } from "../../core/session-cwd.js"; import { SessionImportFileNotFoundError } from "../../core/session-import-errors.js"; import { SessionAlreadyActiveError } from "../../core/session-lease.js"; import type { DaemonErrorInfo, DaemonResponse } from "./daemon-protocol.js"; /** A known session (a persisted descriptor names it) that cannot be routed to yet; retryable, unlike "Unknown active session". */ export class DaemonSessionRecoveringError extends Error { readonly code = "session_recovering" as const; constructor(readonly activeSessionId: string) { super(`Active session ${activeSessionId} is recovering; retry shortly`); this.name = "DaemonSessionRecoveringError"; } } /** A session worker is starting, recovering, or stopping; retryable. */ export class DaemonSessionStartingError extends Error { readonly code = "session_starting" as const; constructor( readonly activeSessionId?: string, message = "Session worker is starting", ) { super(message); this.name = "DaemonSessionStartingError"; } } /** * The restarted daemon has no record of the attached session, so it can never come * back. Terminal, unlike the retryable session_starting/session_recovering states. */ export class DaemonSessionUnrestorableError extends Error { readonly code = "session_unrestorable" as const; constructor(message: string) { super(message); this.name = "DaemonSessionUnrestorableError"; } } export function isSessionStartingOrRecoveringError(error: unknown): boolean { if (error instanceof DaemonSessionStartingError || error instanceof DaemonSessionRecoveringError) { return true; } if ( error instanceof Error && (error.name === "DaemonSessionStartingError" || error.name === "DaemonSessionRecoveringError") ) { return true; } if (typeof error === "object" && error !== null && "code" in error) { const code = (error as { code?: unknown }).code; if (code === "session_starting" || code === "session_recovering") { return true; } } return false; } export function isSessionClosingError(error: unknown): boolean { if (!error) return false; const message = error instanceof Error ? error.message : String(error); return ( message.includes("is closing") || message.includes("is stopping") || message.includes("still initializing") || message.includes("did not finish initializing") ); } export function isSessionNotReadyError(error: unknown): boolean { if (isSessionStartingOrRecoveringError(error)) return true; if (isSessionClosingError(error)) return true; if (error instanceof Error) { const msg = error.message.toLowerCase(); if ( msg.includes("unknown active session") || msg.includes("session worker is queued") || msg.includes("cannot send daemon command") || msg.includes("not connected") || msg.includes("connection closed") || msg.includes("econnrefused") || msg.includes("enoent") ) { return true; } } if (typeof error === "object" && error !== null && "code" in error) { const code = (error as { code?: unknown }).code; if ( code === "daemon_not_connected" || code === "ECONNREFUSED" || code === "ENOENT" || code === "ECONNRESET" || code === "EPIPE" ) { return true; } } return false; } export function serializeDaemonError(error: unknown): DaemonErrorInfo | undefined { if (error instanceof MissingSessionCwdError) { return { code: "missing_session_cwd", issue: error.issue }; } if (error instanceof SessionImportFileNotFoundError) { return { code: "session_import_file_not_found", filePath: error.filePath }; } if (error instanceof SessionAlreadyActiveError) { return { code: "session_already_active", sessionPath: error.sessionPath, activeSessionId: error.activeSessionId, }; } if (error instanceof DaemonSessionRecoveringError) { return { code: "session_recovering", activeSessionId: error.activeSessionId }; } if (error instanceof DaemonSessionStartingError) { return { code: "session_starting", activeSessionId: error.activeSessionId }; } return undefined; } export class DaemonSessionCreateError extends Error { constructor(message: string) { super(message); this.name = "DaemonSessionCreateError"; } } /** Wraps untyped create failures so the CLI prints one line instead of rethrowing. */ export function deserializeDaemonCreateError(response: Extract): Error { const error = deserializeDaemonError(response); if (response.errorInfo) return error; return new DaemonSessionCreateError(error.message); } export function deserializeDaemonError(response: Extract): Error { const { errorInfo } = response; if (errorInfo?.code === "missing_session_cwd") { return new MissingSessionCwdError(errorInfo.issue); } if (errorInfo?.code === "session_import_file_not_found") { return new SessionImportFileNotFoundError(errorInfo.filePath); } if (errorInfo?.code === "session_already_active") { return new SessionAlreadyActiveError(errorInfo.sessionPath, errorInfo.activeSessionId); } if (errorInfo?.code === "session_recovering") { return new DaemonSessionRecoveringError(errorInfo.activeSessionId); } if (errorInfo?.code === "session_starting") { return new DaemonSessionStartingError(errorInfo.activeSessionId, response.error); } if ( response.error.startsWith("Session worker is starting") || response.error.startsWith("Session worker is recovering") || response.error.startsWith("Session worker is stopping") ) { return new DaemonSessionStartingError(undefined, response.error); } return new Error(response.error); }