diff --git a/autogpt_platform/backend/backend/copilot/executor/processor.py b/autogpt_platform/backend/backend/copilot/executor/processor.py index 81d616a759..6d5b44c58a 100644 --- a/autogpt_platform/backend/backend/copilot/executor/processor.py +++ b/autogpt_platform/backend/backend/copilot/executor/processor.py @@ -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") diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts index 0f6a17e666..6b965d0635 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts @@ -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]);