From 962114d1d3af190e77dc5b6a81895fc4c8865a90 Mon Sep 17 00:00:00 2001 From: mawkone Date: Mon, 15 Jun 2026 12:47:32 -0700 Subject: [PATCH] Allow agent to run in background after browser refresh --- vibn-frontend/app/api/chat/route.ts | 35 +++++++++++++++++------------ 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/vibn-frontend/app/api/chat/route.ts b/vibn-frontend/app/api/chat/route.ts index 9af6133e..ed96fb3b 100644 --- a/vibn-frontend/app/api/chat/route.ts +++ b/vibn-frontend/app/api/chat/route.ts @@ -905,13 +905,11 @@ export async function POST(request: Request) { process.env.VERCEL_URL || "https://vibnai.com"; - // Honor client-side abort (Stop button). When the user clicks Stop - // the browser's AbortController fires `request.signal.aborted` and - // the fetch stream is closed; we use it as a polite checkpoint - // between rounds and tool calls so we (a) don't keep paying Gemini - // for tokens the user no longer wants and (b) persist whatever the - // assistant produced before the cancel. - const clientSignal = request.signal; + // We no longer honor client-side abort (Stop button / refresh) to cancel the AI. + // We use a dummy AbortController so that the backend can still be cancelled internally if needed, + // but the browser refresh / disconnect will not stop the agent's work. + const internalAbortController = new AbortController(); + const clientSignal = internalAbortController.signal; // Stream response const encoder = new TextEncoder(); @@ -1188,11 +1186,18 @@ export async function POST(request: Request) { const flushTimeline = () => { if (!currentTimelineKind) return; if (currentTimelineKind === "thought") { - assistantTimeline.push({ kind: "thought", text: currentTimelineText }); + assistantTimeline.push({ + kind: "thought", + text: currentTimelineText, + }); } else if (currentTimelineKind === "text") { - assistantText += (assistantText ? "\n\n" : "") + currentTimelineText; + assistantText += + (assistantText ? "\n\n" : "") + currentTimelineText; assistantTextSegments.push(currentTimelineText); - assistantTimeline.push({ kind: "text", text: currentTimelineText }); + assistantTimeline.push({ + kind: "text", + text: currentTimelineText, + }); } currentTimelineKind = null; currentTimelineText = ""; @@ -1200,7 +1205,7 @@ export async function POST(request: Request) { for await (const chunk of stream) { if (aborted) break; - + if (chunk.type === "thinking_delta" && chunk.text) { if (currentTimelineKind !== "thought") { flushTimeline(); @@ -1947,9 +1952,11 @@ export async function POST(request: Request) { } }, cancel() { - // Browser disconnected (tab closed, navigated away). Clear the - // heartbeat so we stop writing to a closed stream. - // The abort handler above already flipped the flag so the loop bails. + // Browser disconnected (tab closed, navigated away, refreshed). + // We do NOT call internalAbortController.abort() here because we want + // the agent to continue its work in the background! + // We just clear the heartbeat to stop writing to the closed stream. + clearInterval(heartbeat); }, });