fix(copilot): address round-2 PR review and fix tool loading on stop

Backend:
- Add _validate_and_get_session() call to cancel endpoint (404 for
  invalid sessions, consistent with other endpoints)
- Reduce polling max_wait from 10s to 5s (stay below reverse-proxy
  read timeouts)
- Return cancelled=True with reason="cancel_published_not_confirmed"
  on timeout (cancel event IS published, just not yet confirmed)

Frontend:
- Mark in-progress tool parts as output-error on stop so spinners
  clear immediately instead of spinning forever
- Toast on cancel API failure (network error / 5xx)
This commit is contained in:
Zamil Majdy
2026-02-20 02:21:28 +07:00
parent 76e0c96aa9
commit b6064d0155
3 changed files with 26 additions and 14 deletions

View File

@@ -334,8 +334,10 @@ async def cancel_session_task(
Publishes a cancel event to the executor via RabbitMQ FANOUT, then
polls Redis until the task status flips from ``running`` or a timeout
(10 s) is reached. Returns only after the cancellation is confirmed.
(5 s) is reached. Returns only after the cancellation is confirmed.
"""
await _validate_and_get_session(session_id, user_id)
active_task, _ = await stream_registry.get_active_task_for_session(
session_id, user_id
)
@@ -350,8 +352,9 @@ async def cancel_session_task(
)
# Poll until the executor confirms the task is no longer running.
# Keep max_wait below typical reverse-proxy read timeouts.
poll_interval = 0.5
max_wait = 10.0
max_wait = 5.0
waited = 0.0
while waited < max_wait:
await asyncio.sleep(poll_interval)
@@ -365,9 +368,11 @@ async def cancel_session_task(
return CancelTaskResponse(cancelled=True, task_id=task_id)
logger.warning(
f"[CANCEL] Task ...{task_id[-8:]} still running after {max_wait}s"
f"[CANCEL] Task ...{task_id[-8:]} not confirmed after {max_wait}s"
)
return CancelTaskResponse(
cancelled=True, task_id=task_id, reason="cancel_published_not_confirmed"
)
return CancelTaskResponse(cancelled=False, task_id=task_id, reason="timeout")
@router.post(

View File

@@ -114,16 +114,23 @@ export function useCopilotPage() {
// the cancel API to actually stop the executor and wait for confirmation.
const stop = useCallback(async () => {
sdkStop();
// Mark any in-progress tool parts as errored so spinners stop.
setMessages((prev) =>
prev.map((msg) => ({
...msg,
parts: msg.parts.map((part) =>
"state" in part &&
(part.state === "input-streaming" || part.state === "input-available")
? { ...part, state: "output-error" as const, errorText: "Cancelled" }
: part,
),
})),
);
if (!sessionId) return;
try {
const res = await postV2CancelSessionTask(sessionId);
if (res.status === 200 && !res.data.cancelled) {
toast({
title: "Could not stop the task",
description: "The task may still be running in the background.",
variant: "destructive",
});
}
await postV2CancelSessionTask(sessionId);
} catch {
toast({
title: "Could not stop the task",
@@ -131,7 +138,7 @@ export function useCopilotPage() {
variant: "destructive",
});
}
}, [sdkStop, sessionId]);
}, [sdkStop, sessionId, setMessages]);
// Abort the stream if the backend doesn't start sending data within 12s.
const stopRef = useRef(stop);

View File

@@ -1267,7 +1267,7 @@
"post": {
"tags": ["v2", "chat", "chat"],
"summary": "Cancel Session Task",
"description": "Cancel the active streaming task for a session.\n\nPublishes a cancel event to the executor via RabbitMQ FANOUT, then\npolls Redis until the task status flips from ``running`` or a timeout\n(10 s) is reached. Returns only after the cancellation is confirmed.",
"description": "Cancel the active streaming task for a session.\n\nPublishes a cancel event to the executor via RabbitMQ FANOUT, then\npolls Redis until the task status flips from ``running`` or a timeout\n(5 s) is reached. Returns only after the cancellation is confirmed.",
"operationId": "postV2CancelSessionTask",
"security": [{ "HTTPBearerJWT": [] }],
"parameters": [