#7130·CopilotKit

ProxiedCopilotRuntimeAgent can retain stale runtimeMode when /info changes Intelligence → SSE

Author: amoai-techCreated Sep 14, 2026Updated Sep 14, 2026

Problem

ProxiedCopilotRuntimeAgent can retain a stale runtimeMode after the runtime's /info response changes from intelligence to sse, causing the client to send a run/connect request down the Intelligence delegate path against a runtime that is actually serving plain SSE. The Intelligence delegate then calls await response.json() on a text/event-stream body and throws.

Where

packages/core/src/core/agent-registry.ts, the runtime-info reconciliation loop (currently guarded by canReuseRuntimeAgent):

typescript
const existing = Object.prototype.hasOwnProperty.call(this.remoteAgents, id)
  ? this.remoteAgents[id]
  : undefined;
if (
  existing instanceof ProxiedCopilotRuntimeAgent &&
  this.canReuseRuntimeAgent(existing, runtimeUrl, this._runtimeTransport)
) {
  this.applyHeadersToAgent(existing);
  this.applyCredentialsToAgent(existing);
  this.applyRuntimeFetchToAgent(existing);
  return [id, existing]; // <-- runtimeMode / intelligence are never refreshed here
}
typescript
private canReuseRuntimeAgent(agent, runtimeUrl, transport): boolean {
  const connection = this.remoteAgentConnections.get(agent);
  return (
    connection?.runtimeUrl === runtimeUrl.replace(/\/$/, "") &&
    connection.transport === transport
  );
}

canReuseRuntimeAgent compares runtimeUrl and transport only. It does not compare runtimeMode. Only the "create a new agent" branch a few lines later sets runtimeMode: runtimeInfoResponse.mode ?? RUNTIME_MODE_SSE. So an already-existing ProxiedCopilotRuntimeAgent instance keeps whatever runtimeMode it was originally constructed with for its entire lifetime, even after subsequent /info calls report a different mode.

ProxiedCopilotRuntimeAgent.run()/connectAgent() branch on the agent's own (possibly stale) runtimeMode:

typescript
if (this.runtimeMode === RUNTIME_MODE_INTELLIGENCE) return this.#runViaDelegate(input);
// else: plain HTTP/SSE

Minimal reproduction (conceptual)

  1. Runtime initially reports mode: "intelligence" (e.g. CPK_INTELLIGENCE_API_KEY configured). CopilotKitCore connects; AgentRegistry creates a ProxiedCopilotRuntimeAgent for "default" with runtimeMode: "intelligence".
  2. Runtime is reconfigured to mode: "sse" (Intelligence disabled) — runtimeUrl and transport unchanged.
  3. CopilotKitCore (or the app) re-fetches /info and reconciles remote agents.
  4. canReuseRuntimeAgent returns true (URL/transport match) → the existing agent instance is reused as-is.
  5. agentRegistry._runtimeMode (the core-level value) is correctly updated to "sse", but existingAgent.runtimeMode is not.
  6. Any subsequent run()/connectAgent() call on that agent instance still takes the Intelligence delegate branch.
  7. POST /agent/:id/run hits the actual (SSE-mode) runtime, which returns Content-Type: text/event-stream with a body starting data: {"type":"RUN_STARTED",...}.
  8. The Intelligence delegate's requestJoinCredentials$ does await response.json() on that SSE body and throws Unexpected token 'd', "data: {...}" is not valid JSON.

Confirmed against

  • Installed @copilotkit/[email protected] (dist/index.mjs:1382-1409): identical reuse-without-refresh pattern.
  • Current main (packages/core/src/core/agent-registry.ts, fetched directly): the reuse guard is now the named canReuseRuntimeAgent helper, but it still only compares runtimeUrl/transportruntimeMode is still not part of the check, and no mode-based invalidation exists elsewhere in the file.

Expected behavior

A runtimeMode change reported by /info should invalidate/recreate the affected proxy agent(s) (or otherwise synchronize the existing instance's mode-specific state: runtimeMode, intelligence, and any delegate it may have created), the same way canReuseRuntimeAgent already invalidates on a runtimeUrl/transport change.

Real-world impact

Any browser tab left open across a runtime configuration change from Intelligence to SSE (a redeploy, an env var change, a rollback) gets permanently stuck attempting the Intelligence delegate path for the rest of that page's lifetime, with no visible indication beyond a response.json() parse error on send. A full page reload is currently the only recovery.

Related but not equivalent

#7069 addresses the delegate not honoring single-route transport configuration — a different bug in the same subsystem (delegate/transport handling), not this mode-staleness issue. #6469 previously fixed the delegate never refreshing headers after creation — same "cached once, never refreshed" pattern, but for headers rather than mode.