#3553·CopilotKit

Bug: InMemoryAgentRunner.connect() fails to restore sessions on serverless platforms (Vercel, Cloud Run)

Author: umax-imagination-mediaCreated Mar 30, 2026Updated Sep 17, 2026
Labelsbug

♻️ Reproduction Steps

Summary

When using CopilotKit with a proxied AG-UI agent on serverless platforms (Vercel, Google Cloud Run), session restoration via threadId fails intermittently because InMemoryAgentRunner stores thread events in an in-process GLOBAL_STORE (Map on globalThis) that is lost on cold starts or when requests hit different instances.

Bugfix / Feature request: Allow InMemoryAgentRunner.connect() to fall back to the upstream agent's /connect endpoint (or a configurable middleware API) when a threadId is not found in the in-memory store, so the backend agent can provide the conversation history.

Environment

  • CopilotKit: @copilotkit/runtime ^1.54.0 (resolved 1.54.1), @copilotkit/react-core ^1.54.0
  • AG-UI Client: @ag-ui/client ^0.0.48
  • Framework: Next.js 16 (App Router)
  • Deployment: Vercel (serverless) / Google Cloud Run
  • Backend agent: Python (Google ADK), connected via HttpAgent / AGENT_URL

Architecture

Browser (CopilotKit React)
  → POST /api/copilotkit (Next.js API route)
    → CopilotRuntime (with HttpAgent pointing to Python backend)
      → Python ADK agent at AGENT_URL

More info about backend environment (Python Google ADK part) for this issue described in https://github.com/ag-ui-protocol/ag-ui/issues/1243

API route setup:

typescript
const runtime = new CopilotRuntime({
  agents: {
    default: new HttpAgent({
      url: process.env.AGENT_URL || 'http://localhost:8000/',
    }),
  },
})

Session restoration is initiated client-side by passing threadId as a URL param:

typescript
const threadId = new URLSearchParams(globalThis.location?.search)
  .get('sessionId') || undefined

<CopilotKit key={threadId} runtimeUrl="/api/copilotkit" threadId={threadId}>

Impact

This affects any CopilotKit deployment using the proxied agent pattern on serverless platforms — which is likely the most common production deployment model. The "Share Conversation URL" feature (a key UX feature in our client's project deployed to production) becomes unreliable, and users cannot depend on session continuity across page reloads or shared links.

Related

  • InMemoryAgentRunner and GLOBAL_STORE in @copilotkit/runtime
  • ProxiedCopilotRuntimeAgent.connect() flow
  • AG-UI protocol's /connect endpoint specification

✅ Expected Behavior

When InMemoryAgentRunner.connect() does not find a threadId in GLOBAL_STORE, it should fall back to the upstream agent (or a configurable middleware endpoint) to attempt session restoration from the backend's persistent storage.

Proposed Solution: Fallback to upstream agent's connect endpoint

When the thread is not found in GLOBAL_STORE, InMemoryAgentRunner (or ProxiedCopilotRuntimeAgent) should forward the connect request to the configured HttpAgent's /connect endpoint, similar to how run() already proxies to the agent's /run endpoint.

Pseudocode:

typescript
class InMemoryAgentRunner {
  async connect({ threadId, agent }) {
    // 1. Try in-memory first (current behavior)
    const stored = GLOBAL_STORE.get(threadId)
    if (stored) {
      return replayEvents(stored)
    }

    // 2. NEW: Fall back to upstream agent's connect endpoint
    if (agent?.connect) {
      return agent.connect({ threadId })
    }

    // 3. Return empty if no fallback available
    return emptyStream()
  }
}

❌ Actual Behavior

Current Behavior

  1. User shares a conversation URL containing ?sessionId=<threadId>
  2. Recipient opens the URL; CopilotKit sends an agent/connect request with the threadId
  3. CopilotRuntime handles the connect request via runtime.runner.connect({ threadId })
  4. InMemoryAgentRunner.connect() looks up threadId in GLOBAL_STORE (an in-memory Map stored on globalThis)
  5. On cold start or different instance: GLOBAL_STORE is empty → no events returned → session appears blank
  6. On warm instance (same process): GLOBAL_STORE has the data → session restores correctly

This makes session restoration intermittent and unreliable on any serverless/multi-instance deployment.

Root Cause

The connect() flow in InMemoryAgentRunner is entirely self-contained — it only checks the in-process memory store. It never reaches the upstream agent (the Python backend at AGENT_URL), even though:

  • The Python ADK agent has its own thread/session persistence
  • The run() flow correctly proxies to the backend agent
  • The backend agent's AG-UI /connect endpoint could serve the conversation history

The asymmetry is:

Operation Reaches Python backend? Serverless-safe?
run() (send message) ✅ Yes, via HttpAgent ✅ Yes
connect() (restore session) ❌ No, in-memory only ❌ No

Screenshots

No response

CopilotKit Version

bash
├── @copilotkit/[email protected] -> ./node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected]_@types+react-dom@19_c70e6720046be18078d01f4dc322c726/node_modules/@copilotkit/react-core
├── @copilotkit/[email protected] -> ./node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected]_@[email protected]_87473fd4311520c6bffa467f72ff0ad2/node_modules/@copilotkit/react-ui
├── @copilotkit/[email protected] -> ./node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected]_@copilotki_111661355162fbdc80297afa697d54d2/node_modules/@copilotkit/runtime

Logs (Optional)

bash