mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-24 03:00:28 -05:00
fix(copilot): prevent double output from StreamFinish/mark_task_completed race (#SECRT-2021)
Requested by @0ubbe The CoPilot consistently displays every response twice. Root cause is a race condition between StreamFinish delivery and mark_task_completed: 1. Executor publishes StreamFinish to Redis stream 2. Frontend receives it, transitions to 'ready', invalidates cache 3. mark_task_completed runs AFTER StreamFinish (separate operation) 4. Session refetch sees task still 'running' → spurious resumeStream() 5. Resume replays entire Redis stream from '0-0' → double output Backend fix (processor.py): Intercept StreamFinish from the generator instead of publishing it directly. mark_task_completed atomically sets status to 'completed' THEN publishes StreamFinish, ensuring clients never see the finish event while the task is still marked as running. Frontend fix (useCopilotPage.ts): Only reset hasResumedRef on error (SSE drop), not on clean stream finish. This prevents the spurious resume even if the backend timing changes.
This commit is contained in:
@@ -257,10 +257,21 @@ class CoPilotProcessor:
|
||||
cluster_lock.refresh()
|
||||
last_refresh = current_time
|
||||
|
||||
# Intercept StreamFinish: don't publish it directly.
|
||||
# mark_task_completed atomically sets status to "completed"
|
||||
# and THEN publishes StreamFinish. Publishing StreamFinish
|
||||
# before the status update causes a race where the frontend
|
||||
# sees the stream as finished but the task is still "running",
|
||||
# triggering a spurious resume that replays the entire stream
|
||||
# (double output bug — SECRT-2021).
|
||||
if isinstance(chunk, StreamFinish):
|
||||
break
|
||||
|
||||
# Publish chunk to stream registry
|
||||
await stream_registry.publish_chunk(entry.task_id, chunk)
|
||||
|
||||
# Mark task as completed
|
||||
# Mark task as completed — this publishes StreamFinish AFTER
|
||||
# atomically setting status to "completed", preventing the race.
|
||||
await stream_registry.mark_task_completed(entry.task_id, status="completed")
|
||||
log.info("Task completed successfully")
|
||||
|
||||
|
||||
@@ -208,8 +208,13 @@ export function useCopilotPage() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2GetSessionQueryKey(sessionId),
|
||||
});
|
||||
// Allow re-resume if the backend task is still running.
|
||||
hasResumedRef.current = null;
|
||||
// Only allow re-resume on error (SSE drop without clean finish).
|
||||
// On clean finish (status === "ready"), the backend task is done —
|
||||
// resetting the ref would allow a spurious resume if the session
|
||||
// refetch races with mark_task_completed (SECRT-2021).
|
||||
if (status === "error") {
|
||||
hasResumedRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [status, sessionId, queryClient]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user