[eric] fix browser agent stop: cancel mid-flight API calls, stop children before parent, guard frontend state

This commit is contained in:
ciregenz
2026-04-01 16:22:31 -07:00
parent b75c6f8fe2
commit e1c22dfaba
4 changed files with 53 additions and 23 deletions
+20 -17
View File
@@ -1353,23 +1353,25 @@ class AgentManager:
async def stop_agent(self, session_id: str):
"""Stop a running agent and all its browser-agent children."""
task = self.tasks.get(session_id)
if task and not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
# Stop children first so browser agents get cancelled before parent
children = [
s for s in self.sessions.values()
if s.parent_session_id == session_id and s.mode == "browser-agent"
]
for child in children:
await self.stop_agent(child.id)
session = self.sessions.get(session_id)
if session:
# Set cancel event BEFORE cancelling the task so in-flight
# browser agent loops see it immediately
if hasattr(session, '_cancel_event'):
session._cancel_event.set()
for req in list(session.pending_approvals):
ws_manager.resolve_approval(req.id, {"behavior": "deny", "message": "Agent stopped"})
session.pending_approvals = []
if hasattr(session, '_cancel_event'):
session._cancel_event.set()
session.status = "stopped"
if not session.closed_at:
session.closed_at = datetime.now()
@@ -1379,12 +1381,13 @@ class AgentManager:
"session": session.model_dump(mode="json"),
})
children = [
s for s in self.sessions.values()
if s.parent_session_id == session_id and s.mode == "browser-agent"
]
for child in children:
await self.stop_agent(child.id)
task = self.tasks.get(session_id)
if task and not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
def handle_approval(self, request_id: str, decision: dict):
"""Resolve a pending HITL approval."""
+22 -4
View File
@@ -337,18 +337,33 @@ async def run_browser_agent(
"message": user_msg.model_dump(mode="json"),
})
async def _cancellable(coro):
"""Race any awaitable against the cancel event. Returns None if cancelled."""
task = asyncio.ensure_future(coro)
cancel_wait = asyncio.ensure_future(cancel_event.wait())
done, pending = await asyncio.wait(
[task, cancel_wait], return_when=asyncio.FIRST_COMPLETED,
)
for p in pending:
p.cancel()
if cancel_event.is_set():
return None
return task.result()
try:
for turn in range(MAX_TURNS):
if cancel_event.is_set():
break
response = await client.messages.create(
response = await _cancellable(client.messages.create(
model=api_model,
max_tokens=4096,
system=SYSTEM_PROMPT,
tools=BROWSER_TOOLS_SCHEMA,
messages=messages,
)
))
if response is None:
break
assistant_content = []
text_parts = []
@@ -444,9 +459,12 @@ async def run_browser_agent(
continue
start = time.time()
result = await execute_browser_tool(
result = await _cancellable(execute_browser_tool(
tu.name, tu.input, browser_id, tab_id,
)
))
if result is None:
cancelled = True
break
elapsed_ms = int((time.time() - start) * 1000)
action_log.append({
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "openswarm",
"version": "1.0.18",
"version": "1.0.19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openswarm",
"version": "1.0.18",
"version": "1.0.19",
"hasInstallScript": true,
"dependencies": {
"electron-updater": "^6.3.0",
+9
View File
@@ -536,6 +536,11 @@ const agentsSlice = createSlice({
}
}
const existing = state.sessions[action.payload.id];
// Don't let a stale "running" message overwrite a terminal status
const terminal = ['stopped', 'error'] as const;
if (existing && terminal.includes(existing.status as any) && action.payload.status === 'running') {
return;
}
// Preserve local pending_approvals if the server payload has none but
// the frontend has some (avoids race where backend clears approvals
// before the frontend processes the removal).
@@ -559,6 +564,10 @@ const agentsSlice = createSlice({
) {
const session = state.sessions[action.payload.sessionId];
if (session) {
const terminal = ['stopped', 'error'] as const;
if (terminal.includes(session.status as any) && action.payload.status === 'running') {
return;
}
session.status = action.payload.status;
}
if (action.payload.status === 'running' && !state.trackedNotificationIds.includes(action.payload.sessionId)) {