errors.ts
1 /** 2 * Browser connection error helpers. 3 * 4 * Simplified — no more token/extension/CDP classification. 5 * The daemon architecture has a single failure mode: daemon not reachable or extension not connected. 6 */ 7 8 import { BrowserConnectError, type BrowserConnectKind } from '../errors.js'; 9 import { DEFAULT_DAEMON_PORT } from '../constants.js'; 10 11 /** 12 * Unified browser error classification. 13 * 14 * All transient error detection lives here — daemon-client, pipeline executor, 15 * and page retry logic all use this single system instead of maintaining 16 * separate pattern lists. 17 */ 18 19 /** Error category — determines which layer should retry. */ 20 export type BrowserErrorKind = 21 | 'extension-transient' // daemon/extension hiccup — daemon-client retries 22 | 'target-navigation' // CDP target invalidated by SPA nav — page-level settle retry 23 | 'non-retryable'; // permanent error — no retry 24 25 /** How the caller should handle the error. */ 26 export interface RetryAdvice { 27 /** Error category — callers use this to decide whether *they* should retry. */ 28 kind: BrowserErrorKind; 29 /** Whether the error is transient and worth retrying. */ 30 retryable: boolean; 31 /** Suggested delay before retry (ms). */ 32 delayMs: number; 33 } 34 35 /** 36 * Extension/daemon transient patterns — service worker restarts, attach races, 37 * tab closure, daemon hiccups. These warrant a longer retry delay (~1500ms) 38 * because the extension needs time to recover. 39 */ 40 const EXTENSION_TRANSIENT_PATTERNS = [ 41 'Extension disconnected', 42 'Extension not connected', 43 'attach failed', 44 'no longer exists', 45 'CDP connection', 46 'Daemon command failed', 47 'No window with id', 48 ] as const; 49 50 /** 51 * CDP target navigation patterns — SPA client-side redirects can invalidate the 52 * CDP target after chrome.tabs reports 'complete'. These warrant a shorter retry 53 * delay (~200ms) because the new document is usually available quickly. 54 */ 55 const TARGET_NAVIGATION_PATTERNS = [ 56 'Inspected target navigated or closed', 57 ] as const; 58 59 function errorMessage(err: unknown): string { 60 return err instanceof Error ? err.message : String(err); 61 } 62 63 /** 64 * Classify a browser error and return retry advice. 65 * 66 * Single source of truth for "is this error transient?" across all layers. 67 */ 68 export function classifyBrowserError(err: unknown): RetryAdvice { 69 const msg = errorMessage(err); 70 71 // Extension/daemon transient errors — longer recovery time 72 if (EXTENSION_TRANSIENT_PATTERNS.some(p => msg.includes(p))) { 73 return { kind: 'extension-transient', retryable: true, delayMs: 1500 }; 74 } 75 76 // CDP target navigation errors — shorter recovery time 77 if (TARGET_NAVIGATION_PATTERNS.some(p => msg.includes(p))) { 78 return { kind: 'target-navigation', retryable: true, delayMs: 200 }; 79 } 80 81 // CDP protocol error with target context (e.g., -32000 "target closed") 82 if (msg.includes('-32000') && msg.toLowerCase().includes('target')) { 83 return { kind: 'target-navigation', retryable: true, delayMs: 200 }; 84 } 85 86 return { kind: 'non-retryable', retryable: false, delayMs: 0 }; 87 } 88 89 /** 90 * Check if an error is a transient browser error worth retrying. 91 * Convenience wrapper around classifyBrowserError(). 92 */ 93 export function isTransientBrowserError(err: unknown): boolean { 94 return classifyBrowserError(err).retryable; 95 } 96 97 // Re-export so callers don't need to import from two places 98 export type ConnectFailureKind = BrowserConnectKind; 99 100 export function formatBrowserConnectError(kind: ConnectFailureKind, detail?: string): BrowserConnectError { 101 switch (kind) { 102 case 'daemon-not-running': 103 return new BrowserConnectError( 104 'Cannot connect to opencli daemon.' + (detail ? `\n\n${detail}` : ''), 105 `The daemon should auto-start. If it keeps failing, make sure port ${DEFAULT_DAEMON_PORT} is available.`, 106 kind, 107 ); 108 case 'extension-not-connected': 109 return new BrowserConnectError( 110 'Browser Bridge extension is not connected.' + (detail ? `\n\n${detail}` : ''), 111 'Install the extension from GitHub Releases, then reload.', 112 kind, 113 ); 114 case 'command-failed': 115 return new BrowserConnectError( 116 `Browser command failed: ${detail ?? 'unknown error'}`, 117 undefined, 118 kind, 119 ); 120 default: 121 return new BrowserConnectError( 122 detail ?? 'Failed to connect to browser', 123 undefined, 124 kind, 125 ); 126 } 127 }