Bug: InMemoryAgentRunner.connect() fails to restore sessions on serverless platforms (Vercel, Cloud Run)
♻️ 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_URLMore 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:
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:
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
InMemoryAgentRunnerandGLOBAL_STOREin@copilotkit/runtimeProxiedCopilotRuntimeAgent.connect()flow- AG-UI protocol's
/connectendpoint 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:
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
- User shares a conversation URL containing
?sessionId=<threadId> - Recipient opens the URL; CopilotKit sends an
agent/connectrequest with thethreadId CopilotRuntimehandles the connect request viaruntime.runner.connect({ threadId })InMemoryAgentRunner.connect()looks upthreadIdinGLOBAL_STORE(an in-memoryMapstored onglobalThis)- On cold start or different instance:
GLOBAL_STOREis empty → no events returned → session appears blank - On warm instance (same process):
GLOBAL_STOREhas 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
/connectendpoint 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
├── @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/runtimeLogs (Optional)
Source: CopilotKit/CopilotKit