From 5f2681bc8ca82236d75ea3370fc484fc5f58c28f Mon Sep 17 00:00:00 2001 From: deepziyu Date: Tue, 7 Apr 2026 16:28:17 +0800 Subject: [PATCH 1/3] fix(antigravity): implement configurable timeout and auto-reconnect for serve --- clis/antigravity/serve.ts | 89 ++++++++++++++++++++++++--------------- src/cli.ts | 6 ++- 2 files changed, 59 insertions(+), 36 deletions(-) diff --git a/clis/antigravity/serve.ts b/clis/antigravity/serve.ts index 82bb947f4..c8b300166 100644 --- a/clis/antigravity/serve.ts +++ b/clis/antigravity/serve.ts @@ -11,10 +11,10 @@ */ 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'; // ─── Types ─────────────────────────────────────────────────────────── @@ -303,8 +303,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 @@ -319,39 +319,49 @@ async function waitForReply( const stableThreshold = 4; // 4 * 500ms = 2s of stability fallback 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 { - stableCount = 0; - lastText = currentText; } } + } catch (err) { + if (opts.reconnect) { + console.error('[serve] Error during waitForReply, attempting to reconnect:', (err as Error).message); + page = await opts.reconnect(); + // Continue monitoring with new page instance + continue; + } + 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 +369,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 +404,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 +427,12 @@ 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 envTimeout = process.env.OPENCLI_ANTIGRAVITY_TIMEOUT ? parseInt(process.env.OPENCLI_ANTIGRAVITY_TIMEOUT) * 1000 : undefined; + const effectiveTimeout = opts.timeout ? opts.timeout * 1000 : envTimeout ?? 120_000; + + 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 +561,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 17e7ec823..b0a216b75 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1002,9 +1002,13 @@ 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) }); + await startServe({ + port: parseInt(opts.port), + timeout: opts.timeout ? parseInt(opts.timeout) : undefined, + }); }); // ── Dynamic adapter commands ────────────────────────────────────────────── From 956ab7dfc698aa4ef9fd314d944e5a5b83bd01aa Mon Sep 17 00:00:00 2001 From: deepziyu Date: Tue, 7 Apr 2026 19:23:47 +0800 Subject: [PATCH 2/3] fix(antigravity): refine serve reconnection logic with error filtering and max retries --- clis/antigravity/serve.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/clis/antigravity/serve.ts b/clis/antigravity/serve.ts index c8b300166..df85c813a 100644 --- a/clis/antigravity/serve.ts +++ b/clis/antigravity/serve.ts @@ -318,6 +318,7 @@ async function waitForReply( let stableCount = 0; const stableThreshold = 4; // 4 * 500ms = 2s of stability fallback + let reconnectCount = 0; while (Date.now() < deadline) { try { const generating = await isGenerating(page); @@ -348,12 +349,23 @@ async function waitForReply( } } } - } catch (err) { - if (opts.reconnect) { - console.error('[serve] Error during waitForReply, attempting to reconnect:', (err as Error).message); - page = await opts.reconnect(); - // Continue monitoring with new page instance - continue; + } 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 = beforeText; + continue; + } catch (reconnectErr: any) { + console.error(`[serve] Reconnection failed: ${reconnectErr.message}`); + throw err; // Throw original error if reconnection itself fails + } } throw err; } From c2f16e6c99261940c40e4e876b2481ca5783b687 Mon Sep 17 00:00:00 2001 From: deepziyu Date: Wed, 8 Apr 2026 11:12:39 +0800 Subject: [PATCH 3/3] fix(antigravity): standardize timeout parsing and address review feedback --- clis/antigravity/serve.ts | 6 ++++-- src/cli.ts | 5 +++-- src/runtime.ts | 16 +++++++++++----- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/clis/antigravity/serve.ts b/clis/antigravity/serve.ts index df85c813a..d336d34bf 100644 --- a/clis/antigravity/serve.ts +++ b/clis/antigravity/serve.ts @@ -15,6 +15,7 @@ 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 ─────────────────────────────────────────────────────────── @@ -441,8 +442,9 @@ async function handleMessages( export async function startServe(opts: { port?: number; timeout?: number } = {}): Promise { const port = opts.port ?? 8082; - const envTimeout = process.env.OPENCLI_ANTIGRAVITY_TIMEOUT ? parseInt(process.env.OPENCLI_ANTIGRAVITY_TIMEOUT) * 1000 : undefined; - const effectiveTimeout = opts.timeout ? opts.timeout * 1000 : envTimeout ?? 120_000; + 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)`); diff --git a/src/cli.ts b/src/cli.ts index c74eb7474..b5b5c9796 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1005,9 +1005,10 @@ cli({ .option('--timeout ', 'Maximum time to wait for a reply (default: 120s)') .action(async (opts) => { const { startServe } = await import('../clis/antigravity/serve.js'); + const { parseTimeoutValue } = await import('./runtime.js'); await startServe({ - port: parseInt(opts.port), - timeout: opts.timeout ? parseInt(opts.timeout) : undefined, + port: parseInt(opts.port, 10), + timeout: opts.timeout ? parseTimeoutValue(opts.timeout, '--timeout', 120) : undefined, }); }); 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);