- Add telemetryDisabled to RuntimeInfo from COPILOTKIT_TELEMETRY_DISABLED/DO_NOT_TRACK env vars
- Mirror through AgentRegistry and expose via CopilotKitCore getter
- Guard track calls, URL param appending, and console disclosure on core.telemetryDisabled
- Move maybeShowDisclosure() to onRuntimeConnectionStatusChanged (fires after core attaches)
- Update docs to replace localStorage toggle description with env var approach
- Add telemetryDisabled test suite to get-runtime-info tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the chat's connect effect re-fires due to React effect-dep churn, it
calls copilotkit.connectAgent({ agent }) on the same thread again. The
RunHandler previously called agent.setMessages([]), agent.setState({}),
and (transitively) clearReconnectCursor on every such call. That forced
the realtime gateway to replay the topic's full event history on every
churn re-connect — sending the same persisted cpki_event_ids 2-3 times
per thread switch — and produced both halves of Tyler's bug: duplicate
rows in the inspector AG-UI Events tab plus "Message not found" toasts
when the next runAgent fired with an empty agent.messages.
This change makes the orchestrator detect actual thread switches:
- RunHandler tracks _lastConnectedThreadId across connectAgent calls.
On a fresh restore (different threadId from last call) it does the
reset and clears the replay cursor. On churn (same threadId) it
skips the reset entirely so local messages/state are preserved and
the gateway resumes from lastSeenEventId instead of replaying.
- IntelligenceAgent.connect() no longer auto-clears the cursor;
cursor management is the caller's decision now. clearReconnectCursor
is made public so RunHandler (and tests) can call it explicitly.
- ProxiedCopilotRuntimeAgent exposes a clearReplayCursor(threadId)
method that delegates to the IntelligenceAgent. Non-Intelligence
runtime modes are a safe no-op.
This supersedes #4720, which attempted to solve only the inspector
duplicate-row symptom by adding a dispatcher dedup. That approach
introduced a blocking regression in the A → B → A restore path: the
dedup persisted across thread switches and suppressed the gateway's
restore replay, leaving agent.messages at [] and triggering the very
"Message not found" toast we were trying to prevent. Mike's review
caught it. The dispatcher dedup is removed entirely; the
orchestrator-level gate is sufficient and a smaller surface change.
Tests:
- intelligence-agent.test.ts: updated 4 tests that codified the old
"always clear cursor" semantics. Added an explicit test that
clearReconnectCursor() empties the cursor for the next connect, and
Mike's regression — replay the same cpki_event_id after A → B → A
and verify the rehydrate still produces the user message.
- core-connect-thread-switch.test.ts (new): four tests for the
RunHandler gate covering churn, single switch, A → B → A, and the
no-clearReplayCursor non-Intelligence path.
All 430 core tests pass.
Renames the proxy-config field, the field on the agent instance, and
all matching references in tests and the useCopilotKit reference page.
"runtime" reads more naturally now that the proxy concept is documented
as "a local agent that delegates to a runtime agent" rather than
"remote agent" — the latter conflates with `remoteAgents` (the
registry of agents fetched from the runtime), which keeps its name.
No behavioral change; the field still controls the outbound REST URL
used by the proxy.
Round-2 review surfaced two cheap improvements; this commit lands them.
All 7 round-1 findings were confirmed fixed by round 2.
- core.ts onAgentsChanged: each iteration of the unregister loops is
now wrapped in try/catch. A throw on iteration [0] no longer stalls
cleanup for [1..n]; both registries' unregister paths are
idempotent so re-attempts on the next onAgentsChanged are safe.
- agent.ts abortRun: removed dead `if (!routedId) return;` after
`routedAgentId()` — that method now throws (or returns a non-empty
string), so the guard is unreachable.
Tests: core 425 — all green.
Round-1 review found seven actionable items; this commit lands fixes for
all of them. Tests: core 425, react-core 1151, web-inspector 7 — all green.
Real bugs fixed:
- run-handler.ts: dropped the stale `agent` argument on the
`reloadSuggestions(agentId, agent)` call. The signature was tightened
to `(agentId)` when the consumerAgent parameter was removed; the call
site wasn't updated, leaving a TS-2554 build break.
- agent.ts: tightened `routedAgentId(): string` to throw when both
`agentId` and `remoteAgentId` are unset, instead of returning
`string | undefined`. Removes two `!` non-null asserts in
`#runViaHttp` / `#connectViaHttp` and the silent
`/agent/undefined/connect` URL path.
- agent.ts: marked `remoteAgentId` `readonly`. The field was publicly
mutable but `super.url` is baked at construction — mutating
`remoteAgentId` post-construction silently desyncs the REST run URL
from the routing decision elsewhere. `readonly` prevents.
- agent-registry.ts: the registerProxiedAgent collision check now uses
`Object.prototype.hasOwnProperty.call(this._agents, agentId)` instead
of `agentId in this._agents`. The `in` operator walks the prototype
chain, so an agentId of `"__proto__"`, `"constructor"`, etc. would
falsely test as already-registered.
- core.ts: the onAgentsChanged handler now mirrors the thread-store
unregister loop with a parallel
`stateManager.unsubscribeFromAgent(agentId)` for any agentId in
previousAgentIds but absent from the current snapshot. Without this,
`unregister()`'s state-manager subscription leaked.
Comment / test cleanup:
- CopilotChatView.tsx:92: stale "empty cloned agent" reference in the
`isConnecting` JSDoc rewritten to "empty agent instance" — clones are
gone.
- core-register-proxied-agent.test.ts: split the misleading "registering
before runtime connects yields a proxy in pending runtimeMode" test
(which exercised the no-runtimeUrl path, never the pending path) into
two: one for the no-runtimeUrl case, one that actually constructs a
core with a runtimeUrl and asserts `runtimeMode === "pending"`.
Re-adds the isolation coverage that the per-thread cloning revert deleted,
rewritten against the explicit-registration model:
- 9 new core-level tests in core-register-proxied-agent.test.ts cover the
cases from the deleted use-agent-thread-isolation.test.tsx — distinct
instances when two proxies target the same remoteAgentId, message and
state isolation between proxies, independent threadId per proxy, shared
outbound URL (both route to remoteAgentId), getAgent identity, pending
registration before runtime connect, registration with a remote id the
runtime doesn't yet know, and re-register-after-unregister yielding a
fresh proxy.
- 1 new react-core test in CopilotChatActivityRendering.e2e.test.tsx
replaces the deleted "passes the per-thread clone to activity message
renderers" regression test. The clone-vs-registry trap is gone in the
new model, so the test is reframed: the renderer must receive the agent
registered under the local agentId, not any other agent in the registry
(e.g. the runtime-side id a proxy might route to).
Total: +10 tests. Core 415 → 424, react-core 1150 → 1151.
Adds a public CopilotKitCore.registerProxiedAgent({ agentId, remoteAgentId }) API
that mints a ProxiedCopilotRuntimeAgent under a local registry id and routes its
outbound HTTP requests to the named runtime agent. Returns { agent, unregister }
so React callers can clean up via useEffect.
Throws when agentId is already taken — collisions with agents__unsafe_dev_only or
a previous registerProxiedAgent are loud, not silent.
ProxiedCopilotRuntimeAgent gains a remoteAgentId field used only for outbound
routing — URL paths (/agent/<id>/run, /connect, /stop), single-route envelopes,
and the IntelligenceAgent delegate's agentId. The local agentId remains the
registry key and the source of truth for state-manager subscriptions, useAgent
caching, and onAgentsChanged. So multiple proxies (e.g. chat-1, chat-2) can
target the same runtime agent ("default") without cross-talk in any subscriber
bookkeeping.
Use case: replaces the implicit per-thread cloning previously offered by
useAgent({ threadId }). Callers now opt into multiple frontend agents
explicitly.
Includes 6 unit tests covering routing, duplicate-throw, idempotent unregister,
onAgentsChanged notification, and header inheritance.
Reverts the cloning design from #3525 (useAgent per-thread clones, getThreadClone,
globalThreadCloneMap, cloneForThread) and #3630 (clone routing in activity renderers),
plus the inspector machinery that existed only to handle clones (onAgentRunStarted
subscriber + run-handler emissions from #3869, the connect-time emission from #3872,
and the agentRunThreadId map that read from it).
State-manager isClone composite-key path and SuggestionEngine consumerAgent param —
both added in #3525 to keep clones visible to bookkeeping — are gone too.
Restores agent.threadId = resolvedThreadId in CopilotChat (pre-#3525 behavior) and
swaps the inspector's agentRunThreadId map for a direct agent.threadId read.
Removes the DemoButtonAgent and /a2ui-demo page from the demo (added by #3630 as a
clone-fix repro).
Re-opens the original issue #2957 (CPK-7155): two CopilotChat instances with the same
agentId and different threadIds will share message state again. The follow-up is a
public registerProxiedAgent API so callers can opt into multiple frontend agents
proxying to the same runtime agent, without implicit per-thread cloning.
Pins three integration cases for the thread-store auto-unregister branch
in CopilotKitCore's internal onAgentsChanged subscriber:
1. agent removed from agents → store IS unregistered; subscriber receives
the previous store via prevStore.
2. FIRST onAgentsChanged({ agents: {} }) on a published-style core where
the store was registered before any agents arrived → store SURVIVES.
Reproduces the race that the "previously had" guard exists to
prevent.
3. add → register → remove cycle → store IS unregistered. Complements (2)
by exercising the same code path's positive branch.
Test (1) surfaced a real bug in the seed: agentRegistry.initialize does
NOT emit onAgentsChanged, so the constructor's internal subscriber never
saw the agents__unsafe_dev_only set, and `previousAgentIds` stayed empty.
A later removeAgent__unsafe_dev_only call would then be guarded into a
no-op because the agentId looked "new". Fixed by seeding
`previousAgentIds = new Set(Object.keys(agents__unsafe_dev_only))` right
before wiring the subscriber.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CopilotKitCore subscribes to onAgentsChanged and unregisters thread
stores for any agentId not in the new agents map. For published cores,
core.agents is asynchronously populated, so the FIRST
onAgentsChanged({ agents: {} }) notification fires BEFORE published
agents are merged in. Without a guard, that empty notification rips out
a thread store that a consumer (e.g. useThreads) just registered.
Track previousAgentIds and only unregister an agentId that was present
in the previous snapshot AND missing from the new one. The first
empty-agents notification (where the agentId was never previously
present) becomes a no-op.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- thread-store-registry: makeStore now attaches a __testId via
intersection so callers can distinguish stubs at a glance during
debugging instead of relying on identity-by-allocation alone.
- thread-store-registry subscriber-isolation test: assert the
diagnostic content ("Subscriber onThreadStoreRegistered error") and
Error argument, not just that some error was logged.
- handle-threads identifyUser-throws test: assert
"Error identifying intelligence user" with an Error argument, since
the throw originates inside resolveIntelligenceUser which logs and
returns 500 before subscribeToThreads is reached.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
R1's notify-ordering fix only made the sync subscriber path safe; async
subscribers still race against the next register(). When notifySubscribers
awaits inside Promise.all, control returns to register() which assigns the
new store to the same agentId before the async handler resumes — at which
point registry.get(agentId) returns the new store, not the unregistered one.
Carry the previous store on the onThreadStoreUnregistered payload so
subscribers tearing down state don't need to consult the registry. Same
treatment for unregister(). Doc the "do not call registry.get(agentId) in
this callback" contract on the subscriber type itself.
Also tighten getAll(): cache the snapshot, freeze it (so the Readonly<>
claim is honest at runtime), and return the same reference between
mutations. Stable identity matters for useSyncExternalStore consumers that
compare snapshots to skip re-renders. Tests updated to assert prevStore
delivery, frozen-snapshot mutation throws, and reference stability across
calls. Mock notifySubscribers now uses Promise.all to mirror production
parallel dispatch.
vitest types invocationCallOrder as `number[]`, but TS narrows
indexed access to `number | undefined` under noUncheckedIndexedAccess.
Capture the entries first, assert each is defined, then compare —
keeps the ordering check intact while satisfying tsc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tests in @copilotkit/react-core need the WebSocket retry budget so they
can validate teardown semantics without hardcoding the threshold
separately from production. Exposing it under the ɵ-prefixed internal
namespace keeps it out of the public API surface while letting the
test bench import a single source of truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-part welcome-screen regression for `available: "always"` suggestion configs
when the chat connects to agents via `runtimeUrl` instead of registering them
locally:
1. SuggestionEngine.reloadSuggestions bailed early when the agent wasn't yet
in the registry. With runtimeUrl, the registry is empty during the initial
/info fetch, so the very first reload (fired by useConfigureSuggestions on
mount) skipped every config — static pills never appeared on the welcome
screen, dynamic pills never started generating. Now: don't bail, default
`messageCount` to 0, run static configs anyway. Dynamic configs still need
a real agent and skip until one arrives.
2. useConfigureSuggestions's global-config path only iterated the current
agents map, which compounded the problem above — the empty map meant zero
reloads. Now: also reload for the chat's resolved consumer agent (covers
the empty-map case), and subscribe to onAgentsChanged for dynamic configs
only, firing exactly once when the target agent first appears (so dynamic
pills catch up after the runtime fetch completes, without piling up
overlapping generations as multiple useConfigureSuggestions hooks mount).
`hasSuggestions` keeps the `!isConnecting && !isRunning` UI gate. `available:
"always"` controls eligibility windows (welcome screen vs. after first
message), not whether to render through connect/replay or through a run —
those still hide and the end-of-run reload regenerates against the new
context.
Tests:
- Engine: added unit coverage for reloadSuggestions when no agent is present.
- React: 4 integration tests covering welcome screen (specific + global
consumerAgentId) and the run lifecycle (hide during run, reappear after) in
default and pin-to-send modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- register() now snapshots and clears the previous slot before queuing the
unregister notification, then assigns the new store before queuing the
register notification. The unregister microtask is queued first so
subscribers tear down stale subscriptions before receiving the
replacement.
- All notifySubscribers calls now attach .catch handlers so notification
failures surface via console.error instead of being silently swallowed
by `void`.
- getAll() returns a shallow copy so callers cannot mutate the registry's
internal state through the returned reference.
- Test mock now mirrors the real notifySubscribers signature (handler +
errorMessage) and wraps each subscriber call in try/catch, exercising
the same error-isolation behaviour as production.
- Replaced the `as unknown as ɵThreadStore` cast with a satisfies-typed
minimal stub matching the real interface.
- Added tests for ordering (unregister before second register), throwing
subscriber isolation, and getAll() snapshot isolation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up ag-ui-protocol/ag-ui#1578 — `import * as jsonpatch from
"fast-json-patch"` produced an empty namespace under Node native ESM
because fast-json-patch@3.x populates exports via Object.assign, which
the CJS→ESM named-export detector cannot see. Result: every STATE_DELTA
and ACTIVITY_DELTA event threw "applyPatch is not a function", and
LangGraph generative UI streams floods the console with the failure on
each patch.
0.0.53 switches to a default import so the emitted bundle works under
both ESM and CJS consumers. Bumped @ag-ui/core and @ag-ui/encoder in
lockstep since they share the release.
Covers the shared-state thread-resume path that was missing coverage:
on resume, the /connect bootstrap plan replays STATE_SNAPSHOT events
captured during the original run, and agent.state (both on the direct
IntelligenceAgent and through the ProxiedCopilotRuntimeAgent bridge)
must reflect the final snapshot so UI reading from agent.state renders
the persisted state.
Both tests pass against the current implementation, ruling out these
layers as the source of the shared-state demo regression.
## Release monorepo v1.56.3
**Scope:** `monorepo` | **Bump:** `patch`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `monorepo` packages to `1.56.3`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `monorepo` packages to npm at version `1.56.3`
- Creates git tag `monorepo/v1.56.3`
- Creates a GitHub Release with the final release notes
### Before merging
- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)
---
> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
- hooks.ts: keep both threads/clear and cpk-debug-events in RouteInfo
- use-threads.tsx: keep registerThreadStore effect + adopt main's
runtimeStatus gating for context dispatch
- use-threads.test.tsx: keep both our register/unregister test and
main's new runtimeConnectionStatus=Connected gating test
- scripts/hooks/check-binaries.sh: add shell-docs and shell-dojo
demo-content.json exclusions (main introduced these >1MB files without
updating the exclusion list)
- lefthook.yml, pnpm-lock.yaml: accept main's version
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ThreadStoreRegistry.register: delete old store before notifyUnregistered
so callbacks that call getThreadStore(agentId) see undefined, not the
new store
- ThreadDetailsComponent: reset _expandedMessages on threadId change
alongside _expandedToolCalls (prevents stale expanded state across
thread switches)
- handle-threads.test: assert identifyUser called in getThreadMessages
intelligence path; add identifyUser-throws 500 test
- use-threads.test: add fetchMoreThreads end-to-end test (calls the
function, asserts cursor param on second fetch, asserts 3 threads)
- in-memory-runner.test: call clearThreads() in first describe's
beforeEach for GLOBAL_STORE consistency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename sortThreadsByUpdatedAt → sortThreadsByRecency to match the
lastRunAt-preferring sort introduced in the previous commit.
- useThreads: correct the context-dispatch comment to describe what the
code actually does (null only when runtimeUrl is absent; transient
status states leave the previous context in place).
- CopilotChatInput: rewrite the `bottomAnchored` prop doc so the
layout/positioning distinction is self-evident.
- Add changeset calling out the behavior change to suggestions (now
hidden while `isRunning`) and summarizing the ENT-314 fixes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Skip copilotkit.connectAgent when CopilotChat lacks a caller-supplied
threadId — a locally-minted UUID has no backend record, so /connect
would always 404 on the intelligence platform.
- Suppress the welcome screen while a connect is in flight and
unconditionally when the caller has supplied a threadId
(hasExplicitThreadId). Prevents the "How can I help you today?"
flash on thread switch.
- Gate suggestions on !isConnecting && !isRunning to avoid painting
them against a mid-replay message tree.
- Defer the isConnecting release by one animation frame so trailing
bootstrap renders commit before the flag flips.
- Reserve room for the "Powered by CopilotKit" license badge via a
new --copilotkit-license-banner-offset CSS var published by the
banner on mount; chat input consumes it only when bottom-anchored.
- Sort and display threads by lastRunAt (fallback to updatedAt →
createdAt) so metadata-only actions like archive/rename don't
reshuffle the list.
- useThreads waits for runtimeConnectionStatus === Connected before
dispatching the store context, eliminating the speculative /threads
fetch that fired before /info returned wsUrl.
Threads example polish: restore button + tooltips on
archive/restore/delete, segmented Active/All filter, graceful error
state, skeleton rows on initial load, stable scrollbar gutter,
pre-paint dark-mode class, logo position stable across app/chat
modes, drop dynamic-import drawer wrapper that caused null first
paint, archived-row dimming via child colors instead of opacity.
Tests:
- CopilotChat.absentThreadConnect: connect is skipped without a
threadId, fires when supplied via prop or config.
- CopilotChatView.connectingGate: isConnecting suppresses welcome;
hasExplicitThreadId suppresses welcome on empty chat.
- threads (core): lastRunAt sort fallback ordering.
- use-threads: Connecting-state gate defers /threads until Connected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ThreadStoreRegistry.register() now fires onThreadStoreUnregistered
before overwriting an existing store so subscribers (web inspector)
don't stay subscribed to stale stores on replacement; test extended
to verify both events fire in order
- Wire handleClearThreads to POST /threads/clear route; add RouteInfo
variant, router pattern, and fetch-handler case so the inspector can
actually call it
- Add handleGetThreadMessages tool-call mapping test using properly
typed Message objects (role as const, type as const) — exercises the
real mapping code path without mocking getThreadMessages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Excludes showcase/shell-docs and showcase/shell-dojo demo-content.json from
the size check in lefthook.yml — these data files were added by main but
weren't in the exemption list, causing the pre-commit hook to reject them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consolidate the repeated console.error + emitError + .catch pattern
into a single private logAndEmitError method on CopilotKitCore. All 4
call sites (setDefaultThrottleMs, subscribeToAgentWithOptions validation,
safeCall reportError, unsupported-keys warning) now go through the helper.
## Summary
- `buildFrontendTools` only checked `tool.available !== false` but the
`available` field can also be the string `"disabled"`, which is truthy
- Added `tool.available !== "disabled"` to the filter so disabled tools
are properly excluded
- Added test verifying tools with `available: "disabled"` are filtered
out
## Test plan
- [x] New test: tool with `available: "disabled"` is excluded from
`buildFrontendTools`
- [x] All existing available-filtering tests still pass
- [x] Full core test suite passes (338 tests)