Google Antigravity
Public Made by Adomby adom
Google Antigravity in Adom Hydrogen: the editor extension, the agy CLI and a dock dashboard that installs both into your container.
Fix Antigravity dock startup with slow editor discovery #1
fix/dashboard-startup-health → mainDock launch fails with “Opening the Antigravity dashboard (no URL within 60s)” when editor discovery takes longer than the launcher's 0.5–1 second health timeout. Reproduced on winvm (editor on port 8823; status takes about 3 seconds) and ConfRoomROG (port 8822; about 1.5 seconds). The HTTP server is already running, so subsequent launches also attempt to bind its occupied port.
Add a lightweight /api/health readiness endpoint and use it in the launcher and dock manifest. Reusing a predecessor without that endpoint falls back to /api/status with enough time for its bounded external probes. Stop startup promptly if its child exits, and detach the child's stdin.
Validation:
python3 -m unittest -v test_startup: four passing regressions covering slow predecessor reuse, diagnostics-free readiness, cold start plus idempotent relaunch, and an unrelated service holding the port.- Applied the source patch to the existing 0.1.5 installations on winvm and ConfRoomROG after checking predecessor hashes and saving backups. Existing slow servers were successfully reused before restarting only the verified Antigravity port owners.
- Cold dock launch: winvm 1.476 seconds; ConfRoomROG 0.526 seconds. Repeat dock launch: 0.781 and 0.157 seconds, with unchanged server PIDs.
- Both live Hydrogen documents rendered, fetched the new health endpoint in approximately 4–9 milliseconds, and passed the starter-panel self-check. Screenshots inspected on both machines. ConfRoomROG's original minimized window state was restored without changing foreground focus.
Both machines currently lack Google's extension and agy CLI; the dashboards correctly display those as not installed. This validates dashboard startup and rendering, not an authenticated Antigravity conversation. The installed source is locally patched; no package release has been published. A release version bump and rollout remain separate from this draft.
Diff Skip to comments
adom_antigravity.py+21−3@@ -222,6 +222,10 @@ class Handler(SimpleHTTPRequestHandler): b = r.read(); self.send_response(r.status); self.send_header('Content-Type', r.headers.get('Content-Type', 'application/json')); self.send_header('Content-Length', str(len(b))); self.end_headers(); self.wfile.write(b) def do_GET(self):+ if self.path == '/api/health':+ # Dock readiness must not wait for agy or editor port discovery.+ return self.json({'app': 'adom/antigravity', 'version': VERSION,+ 'pid': os.getpid(), 'state': 'healthy'}) if self.path in ['/api/status', '/state']: return self.json(context()) if self.path == '/events':@@ -269,6 +273,18 @@ class Handler(SimpleHTTPRequestHandler): return self.send_error(404) +def server_identity(url):+ """Cheap readiness probe; allow older servers time for their full diagnostics."""+ try:+ return json.load(urllib.request.urlopen(url + 'api/health', timeout=1))+ except urllib.error.HTTPError as e:+ if e.code != 404:+ raise+ # 0.1.5's status can take 8s for agy, 4.5s for editor ports, and 1.5s+ # for the hub. A short timeout falsely declares that live server dead.+ return json.load(urllib.request.urlopen(url + 'api/status', timeout=20))++ def main(): p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument('command', choices=['status', 'context', 'install-extension', 'install-cli', 'open', 'serve', '_serve'])@@ -286,14 +302,16 @@ def main(): ThreadingHTTPServer(('127.0.0.1', a.port), Handler).serve_forever(); return url = f'http://127.0.0.1:{a.port}/' try:- r = json.load(urllib.request.urlopen(url + 'api/status', timeout=1)); assert r.get('app') == 'adom/antigravity' and r['version'] == VERSION+ r = server_identity(url); assert r.get('app') == 'adom/antigravity' and r['version'] == VERSION except Exception: log = Path.home() / '.local/state/adom/antigravity'; log.mkdir(parents=True, exist_ok=True) with (log / 'dashboard.log').open('ab') as out:- subprocess.Popen([sys.executable, str(Path(__file__).resolve()), '_serve', '--port', str(a.port)], stdout=out, stderr=out, start_new_session=True)+ process = subprocess.Popen([sys.executable, str(Path(__file__).resolve()), '_serve', '--port', str(a.port)], stdin=subprocess.DEVNULL, stdout=out, stderr=out, start_new_session=True) for _ in range(50):+ if process.poll() is not None:+ raise RuntimeError('Dashboard process exited; inspect ' + str(log / 'dashboard.log')) try:- ready = json.load(urllib.request.urlopen(url + 'api/status', timeout=.5))+ ready = server_identity(url) if ready.get('app') != 'adom/antigravity' or ready.get('version') != VERSION: raise RuntimeError('Port is occupied by a different service') break
dockbar.json+1−1@@ -1,7 +1,7 @@ { "dock_manifest": "1.2.0", "webview": { "title": "Antigravity", "favicon": "dashboard/icon.svg" },- "health": { "url": "/api/status", "poll_seconds": 15 },+ "health": { "url": "/api/health", "poll_seconds": 15 }, "launch": { "mode": "container_command", "console": "show",
test_startup.pyadded+95@@ -0,0 +1,95 @@+"""Launch contract regressions: slow diagnostics must not break the dock."""+import json+import os+from pathlib import Path+import signal+import socket+import subprocess+import sys+import tempfile+import threading+import time+import unittest+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer+from unittest.mock import patch++import adom_antigravity as app+++class LegacyHandler(BaseHTTPRequestHandler):+ def log_message(self, *args):+ pass++ def do_GET(self):+ if self.path != '/api/status':+ self.send_error(404)+ return+ time.sleep(1.6) # ConfRoomROG; longer than both original startup budgets.+ body = json.dumps({'app': 'adom/antigravity', 'version': app.VERSION}).encode()+ self.send_response(200)+ self.send_header('Content-Length', str(len(body)))+ self.end_headers()+ self.wfile.write(body)+++class StartupTests(unittest.TestCase):+ def serve_fixture(self, handler):+ server = ThreadingHTTPServer(('127.0.0.1', 0), handler)+ thread = threading.Thread(target=server.serve_forever, daemon=True)+ thread.start()+ self.addCleanup(server.server_close)+ self.addCleanup(server.shutdown)+ return server.server_port++ def launch(self, port, home):+ return subprocess.run(+ [sys.executable, str(Path(app.__file__)), 'serve', '--print-url', '--port', str(port)],+ env={**os.environ, 'HOME': home}, capture_output=True, text=True, timeout=25)++ def test_legacy_slow_status_is_reused_without_spawning(self):+ port = self.serve_fixture(LegacyHandler)+ with tempfile.TemporaryDirectory() as home:+ result = self.launch(port, home)+ self.assertEqual(result.returncode, 0, result.stderr)+ self.assertEqual(result.stdout.strip(), f'http://127.0.0.1:{port}/')+ self.assertFalse((Path(home) / '.local/state/adom/antigravity').exists())++ def test_health_does_not_run_external_diagnostics(self):+ port = self.serve_fixture(app.Handler)+ with patch.object(app, 'context', side_effect=AssertionError('diagnostics ran')):+ self.assertEqual(app.server_identity(f'http://127.0.0.1:{port}/')['pid'], os.getpid())++ def test_cold_start_then_relaunch_keeps_same_process(self):+ with socket.socket() as sock:+ sock.bind(('127.0.0.1', 0))+ port = sock.getsockname()[1]+ with tempfile.TemporaryDirectory() as home:+ first = self.launch(port, home)+ self.assertEqual(first.returncode, 0, first.stderr)+ url = first.stdout.strip()+ pid = app.server_identity(url)['pid']+ try:+ second = self.launch(port, home)+ self.assertEqual(second.returncode, 0, second.stderr)+ self.assertEqual(second.stdout.strip(), url)+ self.assertEqual(app.server_identity(url)['pid'], pid)+ finally:+ os.kill(pid, signal.SIGTERM)++ def test_other_service_is_not_terminated(self):+ class OtherHandler(LegacyHandler):+ def do_GET(self):+ body = b'{"app":"another-app","version":"1"}'+ self.send_response(200)+ self.send_header('Content-Length', str(len(body)))+ self.end_headers()+ self.wfile.write(body)+ port = self.serve_fixture(OtherHandler)+ with tempfile.TemporaryDirectory() as home:+ result = self.launch(port, home)+ self.assertNotEqual(result.returncode, 0)+ self.assertEqual(app.server_identity(f'http://127.0.0.1:{port}/')['app'], 'another-app')+++if __name__ == '__main__':+ unittest.main()
Comments
Log in to comment.
No comments yet.