Assistant: intercept connect on the single-route transport too
All checks were successful
Deploy to VPS / test (push) Successful in 1m35s
Deploy to VPS / deploy (push) Successful in 22s

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 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-07-04 20:23:05 -06:00
parent 700e1edbb4
commit 0d0d60600d

View File

@@ -384,17 +384,38 @@ app.use("*", async (c, next) => {
});
app.all("/api/copilotkit/*", async (c) => {
// CopilotChat fires POST …/agent/<id>/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/<id>/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") ?? "";