Skip to content

Commit 4cd8564

Browse files
committed
chore: release 2.10.44 — WebSocket resilience, ingress UI (Codex, Antigravity), changelog
1 parent ffb5f1f commit 4cd8564

9 files changed

Lines changed: 365 additions & 88 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [2.10.44] - 2026-04-17
6+
7+
**Thanks:** [Chris Lennon](https://github.com/chrislennon) for the Home Assistant WebSocket resilience work shipped in [PR #38](https://github.com/Coolver/home-assistant-vibecode-agent/pull/38). [SpryNM](https://github.com/sprynm) for practical feedback that improved the **Codex** setup steps in the ingress UI (aligned with [OpenAI’s Codex MCP guide](https://developers.openai.com/codex/mcp/)).
8+
9+
### Steadier connection when Home Assistant restarts or the network blips
10+
11+
If Home Assistant restarted, the watchdog kicked in, or the network dropped for a moment, the agent could leave a lot of work waiting a long time for answers that would never come—so you saw long stretches of timeout noise even after things were fine again. It also sometimes gave up waiting for the link to come back a little too eagerly. This release smooths that out: work in flight is cleared promptly when the link drops, reconnects pause briefly instead of hammering the server, the agent waits longer for a healthy connection before reporting “not connected,” and very large registry exports get more headroom on busy but healthy systems.
12+
13+
### Setup panel (ingress UI)
14+
15+
- **Codex:** Explains that the shared config file may not exist yet; adds a one-line `codex mcp add …` option with a copy button; points to official MCP documentation.
16+
- **Antigravity (Gemini):** New tab (after Cursor) with install notes, config path under `.gemini`, and merge guidance.
17+
- **Tabs:** Claude Code stays the default first tab; Cursor second; Antigravity follows Cursor.
18+
519
## [2.10.42] - 2026-04-10
620

721
**Release prepared with thanks to:** [@ctaylor86](https://github.com/ctaylor86), [@johny-mnemonic](https://github.com/johny-mnemonic), and [@wilsto](https://github.com/wilsto).

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
ARG BUILD_FROM
22
FROM ${BUILD_FROM}
33

4-
# Version: 2.3.12 - Force rebuild for repository parsing fix
5-
# Build timestamp: 2025-11-09 15:25:00 UTC
4+
# Version: 2.10.44 - WebSocket disconnect / reconnect resilience (see CHANGELOG)
5+
# Build timestamp: 2026-04-17
66
# Install system dependencies
77
RUN apk add --no-cache \
88
git \

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# HA Vibecode Agent - Home Assistant Add-on
22

3-
[![Version](https://img.shields.io/badge/version-2.10.43-blue.svg)](https://github.com/Coolver/home-assistant-vibecode-agent)
3+
[![Version](https://img.shields.io/badge/version-2.10.44-blue.svg)](https://github.com/Coolver/home-assistant-vibecode-agent)
44
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
55
[![MCP Package](https://img.shields.io/npm/v/@coolver/home-assistant-mcp?label=MCP%20Package)](https://www.npmjs.com/package/@coolver/home-assistant-mcp)
66

app/ingress_panel.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,21 @@ def generate_ingress_html(api_key: str, agent_version: str) -> str:
6868
}}
6969
}}'''
7070

71+
# Google Antigravity (Gemini) — same mcpServers JSON; stored under ~/.gemini/antigravity/mcp_config.json
72+
# See https://antigravity.google/docs/mcp
73+
antigravity_json_config = f'''{{
74+
"mcpServers": {{
75+
"home-assistant": {{
76+
"command": "npx",
77+
"args": ["-y", "@coolver/home-assistant-mcp@latest"],
78+
"env": {{
79+
"HA_AGENT_URL": "http://homeassistant.local:8099",
80+
"HA_AGENT_KEY": "{api_key}"
81+
}}
82+
}}
83+
}}
84+
}}'''
85+
7186
# Load Jinja2 template
7287
template_path = Path(__file__).parent / 'templates' / 'ingress_panel.html'
7388
template_content = template_path.read_text(encoding='utf-8')
@@ -80,7 +95,8 @@ def generate_ingress_html(api_key: str, agent_version: str) -> str:
8095
cursor_json_config=cursor_json_config,
8196
claude_json_config=claude_json_config,
8297
vscode_json_config=vscode_json_config,
83-
vscode_codex_toml_config=vscode_codex_toml_config
98+
vscode_codex_toml_config=vscode_codex_toml_config,
99+
antigravity_json_config=antigravity_json_config,
84100
)
85101

86102
return html

app/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
logger = setup_logger('ha_cursor_agent', LOG_LEVEL)
2727

2828
# Agent version
29-
AGENT_VERSION = "2.10.43"
29+
AGENT_VERSION = "2.10.44"
3030

3131
# FastAPI app
3232
app = FastAPI(

app/services/ha_websocket.py

Lines changed: 53 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ def is_connected(self) -> bool:
5353
"""Check if WebSocket is connected"""
5454
return self._connected and self.ws is not None and not self.ws.closed
5555

56+
def _fail_pending_requests_on_disconnect(self) -> None:
57+
"""Fail in-flight request futures immediately when the socket is gone."""
58+
if not self.pending_requests:
59+
return
60+
err = ConnectionError("WebSocket disconnected")
61+
pending = list(self.pending_requests.items())
62+
self.pending_requests.clear()
63+
for _msg_id, future in pending:
64+
if not future.done():
65+
future.set_exception(err)
66+
5667
async def start(self):
5768
"""Start WebSocket client in background"""
5869
if self._running:
@@ -114,59 +125,51 @@ async def _connect_and_listen(self):
114125
if self.session is None or self.session.closed:
115126
self.session = aiohttp.ClientSession()
116127

117-
async with self.session.ws_connect(self.url) as ws:
118-
self.ws = ws
119-
120-
# Step 1: Receive auth_required
121-
msg = await ws.receive_json()
122-
if msg.get('type') != 'auth_required':
123-
raise Exception(f"Expected auth_required, got: {msg.get('type')}")
124-
125-
logger.debug("Received auth_required, sending auth...")
126-
127-
# Step 2: Send auth
128-
await ws.send_json({
129-
'type': 'auth',
130-
'access_token': self.token
131-
})
132-
133-
# Step 3: Receive auth_ok or auth_invalid
134-
auth_response = await ws.receive_json()
135-
if auth_response.get('type') == 'auth_invalid':
136-
raise Exception(f"Authentication failed: {auth_response.get('message')}")
137-
138-
if auth_response.get('type') != 'auth_ok':
139-
raise Exception(f"Unexpected auth response: {auth_response}")
140-
141-
logger.info("✅ WebSocket connected and authenticated")
142-
self._connected = True
143-
self._reconnect_delay = 1 # Reset backoff on successful connect
144-
145-
# Step 4: Listen for messages
146-
async for msg in ws:
147-
if msg.type == aiohttp.WSMsgType.TEXT:
148-
try:
149-
data = json.loads(msg.data)
150-
await self._handle_message(data)
151-
except json.JSONDecodeError as e:
152-
logger.error(f"Failed to parse WebSocket message: {e}")
128+
try:
129+
async with self.session.ws_connect(self.url) as ws:
130+
self.ws = ws
153131

154-
elif msg.type == aiohttp.WSMsgType.CLOSED:
155-
logger.warning("WebSocket closed by server")
156-
break
132+
# Step 1: Receive auth_required
133+
msg = await ws.receive_json()
134+
if msg.get('type') != 'auth_required':
135+
raise Exception(f"Expected auth_required, got: {msg.get('type')}")
157136

158-
elif msg.type == aiohttp.WSMsgType.ERROR:
159-
logger.error(f"WebSocket error: {ws.exception()}")
160-
break
161-
162-
# Fail all in-flight requests immediately instead of letting them
163-
# wait 30s for a response that will never arrive
164-
for msg_id, future in list(self.pending_requests.items()):
165-
if not future.done():
166-
future.set_exception(ConnectionError("WebSocket disconnected"))
167-
self.pending_requests.clear()
168-
137+
logger.debug("Received auth_required, sending auth...")
138+
139+
await ws.send_json({
140+
'type': 'auth',
141+
'access_token': self.token
142+
})
143+
144+
auth_response = await ws.receive_json()
145+
if auth_response.get('type') == 'auth_invalid':
146+
raise Exception(f"Authentication failed: {auth_response.get('message')}")
147+
148+
if auth_response.get('type') != 'auth_ok':
149+
raise Exception(f"Unexpected auth response: {auth_response}")
150+
151+
logger.info("✅ WebSocket connected and authenticated")
152+
self._connected = True
153+
self._reconnect_delay = 1 # Reset backoff on successful connect
154+
155+
async for msg in ws:
156+
if msg.type == aiohttp.WSMsgType.TEXT:
157+
try:
158+
data = json.loads(msg.data)
159+
await self._handle_message(data)
160+
except json.JSONDecodeError as e:
161+
logger.error(f"Failed to parse WebSocket message: {e}")
162+
163+
elif msg.type == aiohttp.WSMsgType.CLOSED:
164+
logger.warning("WebSocket closed by server")
165+
break
166+
167+
elif msg.type == aiohttp.WSMsgType.ERROR:
168+
logger.error(f"WebSocket error: {ws.exception()}")
169+
break
170+
finally:
169171
self._connected = False
172+
self._fail_pending_requests_on_disconnect()
170173

171174
async def _handle_message(self, data: dict):
172175
"""Handle incoming WebSocket message"""

0 commit comments

Comments
 (0)