fix(hub): reassign pending_owners on WS reconnect — prevents approval response loss

This commit is contained in:
Variet Worker
2026-03-17 21:52:50 +09:00
parent 0fae7e32aa
commit 9ccfa83439

69
hub.py
View File

@@ -269,6 +269,19 @@ class WSHub:
self.project_connections[conn.project] = set() self.project_connections[conn.project] = set()
self.project_connections[conn.project].add(conn.conn_id) self.project_connections[conn.project].add(conn.conn_id)
# FIX: Reassign orphaned pending_owners (from dead conn_ids) to this new connection.
# When Extension reconnects, old conn_id entries become stale.
reassigned = 0
for rid, cid in list(self.pending_owners.items()):
if cid not in self.connections:
self.pending_owners[rid] = conn.conn_id
reassigned += 1
if reassigned:
logger.info(
f"[HUB] Reassigned {reassigned} orphaned pending_owners "
f"to new conn {conn.conn_id} (project={conn.project})"
)
# Broadcast instance update to all project connections # Broadcast instance update to all project connections
asyncio.create_task(self._broadcast_instance_update(conn.project)) asyncio.create_task(self._broadcast_instance_update(conn.project))
@@ -292,10 +305,26 @@ class WSHub:
if not self.project_connections[project]: if not self.project_connections[project]:
del self.project_connections[project] del self.project_connections[project]
# Clean up pending ownership # Reassign pending ownership to another connection in same project
# (instead of deleting — prevents approval responses from being lost)
stale = [rid for rid, cid in self.pending_owners.items() if cid == conn_id] stale = [rid for rid, cid in self.pending_owners.items() if cid == conn_id]
for rid in stale: if stale:
del self.pending_owners[rid] remaining = self.project_connections.get(project, set()) - {conn_id}
new_owner = None
for cid in remaining:
c = self.connections.get(cid)
if c and c.authenticated:
new_owner = cid
break
for rid in stale:
if new_owner:
self.pending_owners[rid] = new_owner
logger.info(
f"[HUB] Reassigned pending {rid[:12]}{new_owner} "
f"(disconnected {conn_id})"
)
else:
del self.pending_owners[rid]
# Close WebSocket if still open # Close WebSocket if still open
if not conn.ws.closed: if not conn.ws.closed:
@@ -374,13 +403,39 @@ class WSHub:
return False return False
async def send_response_to_pending_owner(self, request_id: str, message: dict): async def send_response_to_pending_owner(self, request_id: str, message: dict):
"""Route a response to the Extension that created the pending request.""" """Route a response to the Extension that created the pending request.
Falls back to any active connection in the same project if the
original owner disconnected (e.g. Extension WS reconnected with
a new conn_id).
"""
conn_id = self.pending_owners.get(request_id) conn_id = self.pending_owners.get(request_id)
if conn_id: if conn_id:
await self.send_to_connection(conn_id, message) conn = self.connections.get(conn_id)
# Clean up after response delivered if conn and conn.authenticated and not conn.ws.closed:
await self.send_to_connection(conn_id, message)
self.pending_owners.pop(request_id, None)
return True
# Original owner dead — try to find any active connection in same project
project = conn.project if conn else None
if project:
for cid in self.project_connections.get(project, set()):
c = self.connections.get(cid)
if c and c.authenticated and not c.ws.closed:
await self.send_to_connection(cid, message)
self.pending_owners.pop(request_id, None)
logger.info(
f"[HUB] Rerouted response {request_id[:12]}{cid} "
f"(original {conn_id} dead)"
)
return True
# No active connection found — clean up
self.pending_owners.pop(request_id, None) self.pending_owners.pop(request_id, None)
return True logger.warning(
f"[HUB] Response {request_id[:12]} lost: owner {conn_id} dead, "
f"no active connections in project"
)
return False
logger.warning(f"[HUB] No owner for pending {request_id[:12]}") logger.warning(f"[HUB] No owner for pending {request_id[:12]}")
return False return False