diff --git a/clis/antigravity/serve.ts b/clis/antigravity/serve.ts index 82bb947f4..d336d34bf 100644 --- a/clis/antigravity/serve.ts +++ b/clis/antigravity/serve.ts @@ -11,10 +11,11 @@ */ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'; -import { CDPBridge } from '@jackwener/opencli/browser/cdp'; -import type { IPage } from '@jackwener/opencli/types'; -import { resolveElectronEndpoint } from '@jackwener/opencli/launcher'; -import { EXIT_CODES, getErrorMessage } from '@jackwener/opencli/errors'; +import { CDPBridge } from '../../src/browser/cdp.js'; +import type { IPage } from '../../src/types.js'; +import { resolveElectronEndpoint } from '../../src/launcher.js'; +import { EXIT_CODES, getErrorMessage } from '../../src/errors.js'; +import { parseEnvTimeout, parseTimeoutValue } from '../../src/runtime.js'; // ─── Types ─────────────────────────────────────────────────────────── @@ -303,8 +304,8 @@ async function sendMessage(page: IPage, message: string, bridge?: CDPBridge): Pr async function waitForReply( page: IPage, beforeText: string, - opts: { timeout?: number; pollInterval?: number } = {}, -): Promise { + opts: { timeout?: number; pollInterval?: number; reconnect?: () => Promise } = {}, +): Promise { const timeout = opts.timeout ?? 120_000; // 2 minutes max const pollInterval = opts.pollInterval ?? 500; // 500ms polling @@ -318,40 +319,62 @@ async function waitForReply( let stableCount = 0; const stableThreshold = 4; // 4 * 500ms = 2s of stability fallback + let reconnectCount = 0; while (Date.now() < deadline) { - const generating = await isGenerating(page); - const currentText = await getConversationText(page); - const textChanged = currentText !== beforeText && currentText.length > 0; - - if (generating) { - hasStartedGenerating = true; - stableCount = 0; // Reset stability while generating - } else { - if (hasStartedGenerating) { - // It actively generated and now it stopped -> DONE - // Provide a small buffer to let React render the final message fully - await sleep(500); - return; - } - - // Fallback: If it never showed "Generating/Cancel", but text changed and is stable - if (textChanged) { - if (currentText === lastText) { - stableCount++; - if (stableCount >= stableThreshold) { - return; // Text has been stable for 2 seconds -> DONE + try { + const generating = await isGenerating(page); + const currentText = await getConversationText(page); + const textChanged = currentText !== beforeText && currentText.length > 0; + + if (generating) { + hasStartedGenerating = true; + stableCount = 0; // Reset stability while generating + } else { + if (hasStartedGenerating) { + // It actively generated and now it stopped -> DONE + // Provide a small buffer to let React render the final message fully + await sleep(500); + return page; + } + + // Fallback: If it never showed "Generating/Cancel", but text changed and is stable + if (textChanged) { + if (currentText === lastText) { + stableCount++; + if (stableCount >= stableThreshold) { + return page; // Text has been stable for 2 seconds -> DONE + } + } else { + stableCount = 0; + lastText = currentText; } - } else { + } + } + } catch (err: any) { + const msg = err.message || String(err); + const isSessionLoss = /closed|lost|not open|websocket/i.test(msg); + + if (opts.reconnect && isSessionLoss && reconnectCount < 2) { + reconnectCount++; + console.error(`[serve] CDP session loss detected (${msg}), attempting to reconnect (${reconnectCount}/2)...`); + try { + page = await opts.reconnect(); + // Reset stability tracking after reconnect stableCount = 0; - lastText = currentText; + lastText = beforeText; + continue; + } catch (reconnectErr: any) { + console.error(`[serve] Reconnection failed: ${reconnectErr.message}`); + throw err; // Throw original error if reconnection itself fails } } + throw err; } await sleep(pollInterval); } - throw new Error('Timeout waiting for Antigravity reply'); + throw new Error(`Timeout waiting for Antigravity reply after ${timeout / 1000}s`); } // ─── Request Handlers ──────────────────────────────────────────────── @@ -359,8 +382,9 @@ async function waitForReply( async function handleMessages( body: AnthropicRequest, page: IPage, - bridge?: CDPBridge, + opts: { bridge?: CDPBridge; timeout?: number; reconnect?: () => Promise } = {}, ): Promise { + const { bridge, timeout, reconnect } = opts; // Extract the last user message const userMessages = body.messages.filter(m => m.role === 'user'); if (userMessages.length === 0) { @@ -393,7 +417,7 @@ async function handleMessages( // Poll for reply (change detection) console.error('[serve] Waiting for reply...'); - await waitForReply(page, beforeText); + page = await waitForReply(page, beforeText, { timeout, reconnect }); // Extract the actual reply text precisely from the DOM const replyText = await getLastAssistantReply(page, userText); @@ -416,8 +440,13 @@ async function handleMessages( // ─── Server ────────────────────────────────────────────────────────── -export async function startServe(opts: { port?: number } = {}): Promise { +export async function startServe(opts: { port?: number; timeout?: number } = {}): Promise { const port = opts.port ?? 8082; + const envTimeoutSeconds = parseEnvTimeout('OPENCLI_ANTIGRAVITY_TIMEOUT', 120); + const effectiveTimeoutSeconds = parseTimeoutValue(opts.timeout, '--timeout', envTimeoutSeconds); + const effectiveTimeout = effectiveTimeoutSeconds * 1000; + + console.error(`[serve] Starting Antigravity API proxy on port ${port} (timeout: ${effectiveTimeout / 1000}s)`); // Lazy CDP connection — connect when first request comes in let cdp: CDPBridge | null = null; @@ -546,7 +575,11 @@ export async function startServe(opts: { port?: number } = {}): Promise { // Lazy connect on first request const activePage = await ensureConnected(); - const response = await handleMessages(body, activePage, cdp ?? undefined); + const response = await handleMessages(body, activePage, { + bridge: cdp!, + timeout: effectiveTimeout, + reconnect: ensureConnected, + }); jsonResponse(res, 200, response); } finally { requestInFlight = false; diff --git a/src/cli.ts b/src/cli.ts index 0ed3a7bbe..b5b5c9796 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1002,9 +1002,14 @@ cli({ .command('serve') .description('Start Anthropic-compatible API proxy for Antigravity') .option('--port ', 'Server port (default: 8082)', '8082') + .option('--timeout ', 'Maximum time to wait for a reply (default: 120s)') .action(async (opts) => { const { startServe } = await import('../clis/antigravity/serve.js'); - await startServe({ port: parseInt(opts.port) }); + const { parseTimeoutValue } = await import('./runtime.js'); + await startServe({ + port: parseInt(opts.port, 10), + timeout: opts.timeout ? parseTimeoutValue(opts.timeout, '--timeout', 120) : undefined, + }); }); // ── Dynamic adapter commands ────────────────────────────────────────────── diff --git a/src/runtime.ts b/src/runtime.ts index d96b095fe..b0074f3fb 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -12,17 +12,23 @@ export function getBrowserFactory(site?: string): new () => IBrowserFactory { return BrowserBridge; } -function parseEnvTimeout(envVar: string, fallback: number): number { - const raw = process.env[envVar]; - if (raw === undefined) return fallback; - const parsed = parseInt(raw, 10); +/** + * Validates and parses a timeout value (seconds). + */ +export function parseTimeoutValue(val: string | number | undefined, label: string, fallback: number): number { + if (val === undefined) return fallback; + const parsed = typeof val === 'number' ? val : parseInt(String(val), 10); if (Number.isNaN(parsed) || parsed <= 0) { - console.error(`[runtime] Invalid ${envVar}="${raw}", using default ${fallback}s`); + console.error(`[runtime] Invalid ${label}="${val}", using default ${fallback}s`); return fallback; } return parsed; } +export function parseEnvTimeout(envVar: string, fallback: number): number { + return parseTimeoutValue(process.env[envVar], envVar, fallback); +} + export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_CONNECT_TIMEOUT', 30); export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_COMMAND_TIMEOUT', 60); export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_EXPLORE_TIMEOUT', 120);