From 0d0d60600d2ab85946b586db38d915f4710a41cc Mon Sep 17 00:00:00 2001 From: Carlos Escalante Date: Sat, 4 Jul 2026 20:23:05 -0600 Subject: [PATCH] Assistant: intercept connect on the single-route transport too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The connect intercept only matched POST …/connect, but CopilotKit talks to the runtime in single-route mode: everything POSTs to /api/copilotkit with the operation in the body envelope ({method:"agent/connect"}). So the in-memory runner kept replaying the BFF process's full event history on every CopilotChat mount — every conversation since server start, across clears — which is exactly the stacked resurrected conversations seen in prod after navigating away and back. Now both transports get the immediately-completed stream. Verified with the reported repro: three conversations with clears between, navigate away and back → only the third remains; reload hydration unaffected. Co-Authored-By: Claude Fable 5 --- frontend/server.ts | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/frontend/server.ts b/frontend/server.ts index c05efc5..a429435 100644 --- a/frontend/server.ts +++ b/frontend/server.ts @@ -384,17 +384,38 @@ app.use("*", async (c, next) => { }); app.all("/api/copilotkit/*", async (c) => { - // CopilotChat fires POST …/agent//connect for explicit threadIds and - // the runtime's in-memory runner replays THIS PROCESS's event history — - // stale relative to the DB store (it resurrects cleared conversations and - // double-feeds page-load hydration). The empty-messages hydration run - // against MAF is our single restore path, so connect gets an - // immediately-completed stream instead. - if (new URL(c.req.url).pathname.endsWith("/connect")) { - return new Response("", { + // CopilotChat fires "connect" on every mount with an explicit threadId, + // and the runtime's in-memory runner answers it by replaying THIS + // PROCESS's event history — every run since the server started, across + // "Nueva conversación" clears (the DB delete can't touch process memory). + // That resurrected cleared conversations on navigation. The + // empty-messages hydration run against MAF's snapshot store is our single + // restore path, so connect gets an immediately-completed stream instead. + // + // Two transports must be caught: multi-route (POST …/agent//connect) + // and single-route (POST /api/copilotkit with {method:"agent/connect"} in + // the body — the one CopilotKit actually uses here; the URL-only check + // silently missed it). + const emptyStream = () => + new Response("", { status: 200, headers: { "Content-Type": "text/event-stream" }, }); + if (new URL(c.req.url).pathname.endsWith("/connect")) { + return emptyStream(); + } + if ( + c.req.method === "POST" && + (c.req.header("content-type") ?? "").includes("application/json") + ) { + try { + const envelope = (await c.req.raw.clone().json()) as { method?: string }; + if (envelope?.method === "agent/connect") { + return emptyStream(); + } + } catch { + // not JSON — fall through to the runtime + } } const cookieHeader = c.req.header("cookie") ?? "";