## Summary
- Add Slack `respondTo` routing options with mention-only channel thread
replies by default.
- Keep DMs and assistant pane conversational while allowing legacy
owned-thread continuation by config.
- Update the Slack example config, manifests, docs, and Discord README
trigger wording.
## Why
Slack bots should stay quiet in channel threads unless explicitly
mentioned or configured for continuation. This makes Kite-style bots
less noisy by default while preserving the old behavior for teams that
want it.
## How
- Resolve `respondTo` once in `SlackAdapter.start()` and pass the
resolved policy into the Slack listener.
- Gate `app_mention`, `message.im`, and non-DM thread replies against
that policy.
- Add listener coverage for defaults, disabled routes, in-channel
mention replies, and legacy thread continuation.
## Test Plan
- `pnpm exec nx run @copilotkit/bot-slack:test --
src/__tests__/slack-listener.test.ts`
- `pnpm exec nx run @copilotkit/bot-slack:test`
- `pnpm exec nx run @copilotkit/bot-slack:check-types`
- `pnpm exec nx run slack-example:test`
- `pnpm exec nx run slack-example:check-types`
- targeted `pnpm exec oxfmt --check ...`
- targeted `git diff --check ...`
Fixes#5635.
## What
Headers set directly on an `HttpAgent` registered via
`agents__unsafe_dev_only` were silently replaced by the provider
headers. Per-agent auth headers (like an `Authorization` for a
self-hosted backend) got dropped, causing 401s.
## Why
`AgentRegistry.applyHeadersToAgent` did `agent.headers = {
...core.headers }`, a full overwrite. The run handler and the react-core
`useAgent` hook did the same. So an agent built with its own headers
lost them on registration, on every `setHeaders`, and before each
request.
## Fix
Merge instead of replace. The registry captures each agent's own headers
once (in a WeakMap, before the first apply) and rebuilds `{
...ownHeaders, ...coreHeaders }`. Core wins on key conflicts, which
keeps the existing "provider headers are authoritative" and logout/clear
behavior. All header application now routes through one method,
`CopilotKitCore.applyHeadersToAgent`, so runs never clobber per-agent
headers.
Vue and Angular benefit too: they dispatch runs through `core.runAgent`
/ `connectAgent`, so the merge is re-applied before every request.
## Tests
- core: 3 new cases in `core-headers.test.ts` (preserve, merge,
retain-across-setHeaders); existing overwrite and clear tests still
pass.
- react-core: new `use-agent-provider-headers.e2e.test.tsx` with a real
provider and an HttpAgent that has its own headers.
Verified locally: format, lint, full core + react-core suites, and both
builds.
## Summary
Adds `unarchiveThread(id)` to the v2 thread store (`@copilotkit/core`)
and the `useThreads` hook (`@copilotkit/react-core/v2`), restoring an
archived thread via the existing generic `PATCH /threads/:id { archived:
false }` update path — no new runtime endpoint. Mirrors `archiveThread`
across the store and hook.
This is the durable, architecture-independent piece extracted from the
threads-drawer effort. The drawer UI itself is being restarted as a
framework-agnostic **CopilotDrawer** (Lit web component + React/Angular
wrappers) under a separate spec; this hook method stands on its own and
is needed regardless.
## Testing
TDD. New core store test (`PATCH … { archived: false }`) and
`useThreads` hook test; full suites green (core, react-core).
Restores an archived thread via the existing generic PATCH /threads/:id
update path with { archived: false } — the same mechanism example apps
already use for restore — so no new runtime route is required. Mirrors
archiveThread across the core thread store and the v2 useThreads hook.
addReaction/removeReaction resolved the channel from messageRef.channelId only,
unlike Slack and Telegram which fall back to the target channel. The shipped
example reacts with `{ id }` (no channelId) — the channel is meant to come from
the thread's reply target — so on Discord fetchSendable("") threw, the call
returned { ok: false }, and the 👀/✅/⚠️ acks silently never fired (contradicting
the example's "works on Slack, Discord, and Telegram alike" claim).
Fall back to the target channel when the reacted ref carries no channelId, for
parity with the other two adapters. Adds a regression test.
Repairs TypeScript check-types across the monorepo and adds a CI gate so
regressions are caught going forward:
- core: bundler module resolution and strict-mode fixes
- sdk-js: bundler module resolution; keep codegen, formatter, packaging working
- react-core: fixes across components, hooks, and tests
- react-native: restore catch binding referenced by TypeError cause
- runtime: repair check-types and bound AI SDK schema inference
- web-inspector: nodenext import extensions, export Anchor
- remaining packages and node example: assorted check-types repairs
- deps: add missing type-only devDependencies
- license context driven from /info licenseStatus
- ci: run check-types in the static quality workflow
Squashed from 12 commits for a single, easily-revertable change.
CR round 2 follow-ups (no behavior change):
- Add a core-headers regression test proving the agentOwnHeaders baseline
stays pristine across remove + re-add (the WeakMap is intentionally not
cleared on removal; clearing would re-capture polluted headers).
- Correct the stale `headers` config doc ("appended" -> merged on top of each
HttpAgent's own headers, core wins).
- Tighten the e2e no-provider-headers assertion to toEqual.
## Release monorepo v1.61.1
**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.61.1`
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.61.1`
- Creates git tag `monorepo/v1.61.1`
- 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.
Addresses PR review feedback:
- Resolve the license token once (option ?? COPILOTKIT_LICENSE_TOKEN) into a
protected readonly field on BaseCopilotRuntime, and have
CopilotIntelligenceRuntime's licenseChecker reuse it. Collapses the duplicated
resolution and structurally enforces that telemetry attribution and feature
gating can never disagree, instead of relying on a "keep in sync" comment.
- Add an integration test for the env-var-only path (no licenseToken option) —
the exact self-hosted scenario this PR targets — proving the env-resolved
token reaches lambdaClient.send through a real request. Kept in its own file
so the process-wide telemetry singleton (last-write-wins) can't false-pass it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds genuine end-to-end coverage beyond the SSE-via-Express case:
- SSE via the Hono adapter
- SSE via the framework-agnostic fetch handler (what node + custom adapters wrap)
- Intelligence mode end-to-end (real CopilotIntelligenceRuntime, WS runner stubbed)
Each constructs a real runtime (so the base-class setLicenseToken runs), drives a
real request through the adapter, and asserts the token reaches lambdaClient.send
on oss.runtime.copilot_request_created.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Only CopilotIntelligenceRuntime called telemetry.setLicenseToken in its
constructor; BaseCopilotRuntime and CopilotSseRuntime did not. As a result,
self-hosted SSE users got anonymous runtime telemetry (no telemetry_id) even
with a license token configured — and those events were additionally throttled
to the 5% anonymous sample rate, leaving runtime telemetry_id stuck at ~1%.
Hoist the licenseToken resolution (option ?? COPILOTKIT_LICENSE_TOKEN env
fallback) and telemetry.setLicenseToken call into BaseCopilotRuntime so SSE and
Intelligence runtimes attribute telemetry identically. Remove the now-redundant
duplicate from CopilotIntelligenceRuntime (its licenseChecker stays).
Tests cover every construction path into the endpoints:
- runtime-license-telemetry.test.ts: SSE/Intelligence direct + CopilotRuntime
shim (both delegates) x {explicit option, env fallback, none}; asserts the
token is set exactly once (guards against a double-set after the hoist).
- sse-license-telemetry.integration.test.ts: end-to-end proof the token rides
to lambdaClient.send through a real Express endpoint request.
- copilot-runtime-license-telemetry.test.ts: regression guard for the v1
CopilotRuntime path (already worked, previously untested).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
- Mint a GitHub App token for the stable release workflow and reuse it
for PR creation and follow-up API calls
- Disable lefthook during automation commits so release PR generation
does not depend on local developer hooks
- Relax the CopilotChat perf regression test to assert correctness
without a hard 5s wall-clock check
## Testing
- Unit/UI test updated to allow longer async rendering while still
verifying 100 messages render successfully
- Not run (not requested)
Add capability-gated surface methods across the bot packages so one handler
degrades gracefully per platform:
- thread.react / thread.unreact — emoji reactions (Slack, Discord, Telegram)
- thread.postEphemeral — only-you messages with a required DM fallback
- openModal (interaction/command-scoped) — structured forms (Slack rich,
Discord text-only), omitted on surfaces with no live interaction trigger
Spans @copilotkit/bot-ui, bot, bot-slack, bot-discord, bot-telegram with
per-adapter capability flags. Discord: open modals from slash-command
interactions, and never clear registered commands on an empty publish.
CR round 1 follow-ups (no behavior change):
- Document on setHeaders + applyHeadersToAgent that the merge baseline is the
agent's construction-time headers, so setHeaders can override but cannot
remove a per-agent header (the agent's own value re-surfaces on clear), and
that dynamic updates go through setHeaders, not direct agent.headers mutation.
- Tighten the agentOwnHeaders field comment (captured on first apply, never
re-captured) and the applyHeadersToAgent method doc.
- Add a core-headers test pinning the clear-reveals-baseline contract.
- Soften the two useAgent test-mock comments: they are an additive stand-in,
not a faithful model of core's frozen baseline.
Adds the AG-UI standard interrupt flow (RUN_FINISHED outcome:interrupt + resume array) alongside the legacy on_interrupt path.
- core: forward the standard resume array through runAgent.
- react-core / vue / react-native: useInterrupt handles standard interrupts with resolve()/cancel(), surfaces the primary + full interrupt set, and persists each resolved tool-backed interrupt as a tool-result message so multi-turn conversations stay well-formed (no dangling tool call -> no tool-call loop).
- runtime BuiltInAgent: native interrupts for the aisdk + tanstack factory paths via each SDK's needsApproval primitive (tool-approval-request / CUSTOM approval-requested -> outcome:interrupt); classic interrupt-tool emission + ctx.interrupt() factory primitive; idempotent resume injection mapped to each SDK's native tool-result; getCapabilities advertises humanInTheLoop.interrupts.
- docs: document standard interrupt support.
Verified across core/react-core/runtime unit suites and a real-model multi-turn run on both aisdk and tanstack.
## Problem
Fixes#5581.
When `enableInspector={true}` and `useThreads()` is **not** mounted, the
inspector creates its own thread store per agent
(`ensureOwnedThreadStore`). That store initialized its context with
empty headers:
```ts
store.setContext({
runtimeUrl: core.runtimeUrl,
headers: {}, // ← ignores headers configured on <CopilotKit>
agentId,
});
```
So the inspector's `/threads` requests omitted the headers configured on
`<CopilotKit>` (e.g. `X-CSRF`, `Authorization`). In environments that
enforce CSRF/auth checks this returns **HTTP 403**; in lax local envs it
200s but still sends no headers. This is the inspector-side counterpart
to the `useThreads()` fix in #5300.
## Solution
1. Source the headers from `core.headers` when the owned store's context
is created.
2. Add an `onHeadersChanged` subscriber that re-applies headers to all
owned stores when the host updates them at runtime (e.g. a refreshed
auth/CSRF token via `core.setHeaders`). This mirrors `useThreads()`,
which re-dispatches the context whenever `core.headers` change, so the
owned stores' requests stay authorized.
Headers are spread (`{ ...core.headers }`) to match the existing pattern
in `use-threads.tsx` and produce a fresh mutable object. Stores
registered by `useThreads()` are untouched — only inspector-owned stores
are affected.
## Testing
Added two regression tests in
`packages/web-inspector/src/__tests__/web-inspector.spec.ts` (stub
`globalThis.fetch`, drive the owned store via the agents-changed path):
- the owned store's `/threads` request carries `core.headers`;
- an `onHeadersChanged` update re-applies the new headers on the next
request.
`pnpm --filter @copilotkit/web-inspector test` → 34 passed. Verified the
first test fails when the fix is reverted. Lint (oxlint) and formatting
(oxfmt) clean on the changed files.
HttpAgent headers configured directly on an agent registered via
agents__unsafe_dev_only were silently replaced by core headers, dropping
per-agent auth headers and causing 401s against self-hosted backends.
Core headers are now merged ON TOP of each agent's construction-time
headers (captured once in a WeakMap before the first apply), with the
core-level value winning on a key conflict. Header application is
centralized in CopilotKitCore.applyHeadersToAgent so the run handler and
the react-core useAgent hook share one merge path and never clobber
per-agent headers.
## Summary
Fixes#5554
When a backend agent calls a frontend tool via
`renderAndWaitForResponse` (the `useHumanInTheLoop` hook) and the run is
aborted (`stopAgent`/`abortRun`) while the form is still pending, the
handler promise was settled only by `respond()` — so on abort it either
hung forever or silently resolved to an empty string. The backend
received an empty `tool_call_result` (no error), which downstream agent
logic interpreted as "no input" — a silent state corruption.
## Root cause
`useHumanInTheLoop`
(`packages/react-core/src/v2/hooks/use-human-in-the-loop.tsx`) created
its handler promise capturing only `resolve`, and ignored the
`AbortSignal` that the core `RunHandler` already passes to tool handlers
(`packages/core/src/core/run-handler.ts`). An aborted run therefore
never settled the pending promise; an `undefined` result is stringified
to `""` (run-handler.ts) → silent empty tool result.
## Fix
Honor the existing `AbortSignal` in the HITL handler:
- If the signal is already aborted when the handler runs, reject
immediately.
- Otherwise attach a one-shot `abort` listener that rejects the pending
promise with an explicit `Error("Human-in-the-loop interaction
aborted")`.
- `respond()` detaches the listener before resolving, so a normal
response is unchanged and abort cannot fire after a normal resolve.
Core's existing catch path converts the rejection into an explicit error
tool result instead of a silent `""`. Scoped to the one hook; no
protocol change, no new timeout API, and **unmount is deliberately not
touched** (to avoid regressing reconnect/remount-resume).
## Tests added
`packages/react-core/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx`
— drives a HITL tool to the executing state, aborts the run without
calling `respond()`, and asserts an explicit non-empty error surfaces
(via `onToolExecutionEnd`) rather than a silent empty result. Fails
before the fix (handler hangs), passes after.
## Checklist
- [x] Failing test written and confirmed failing before the fix
- [x] Fix applied, test passes
- [x] Full `@copilotkit/react-core` suite passes (1291 passed;
reconnect/remount tests green)
- [x] Build succeeds (`nx build @copilotkit/react-core`)
- [x] Formatter passes
Adds a durable persistence layer for @copilotkit/bot, replacing the
in-memory-only ActionStore with a pluggable StateStore.
- StateStore interface (kv/list/lock/dedup/queue) with a shared
conformance suite; MemoryStore default plus @copilotkit/bot-store-redis
and @copilotkit/bot-store-postgres backends.
- createBot({ store }): typed per-thread state via Standard Schema,
action snapshots persisted through the store, per-conversation turn
lock (onLockConflict drop|force), and inbound-event dedup keyed on a
stable eventId. ActionStore is kept as a deprecated alias.
- Cross-platform transcripts (bot.transcripts + identity resolver) with
age-bounded retention (prune on append + filter on read), and
runAgent({ transcript: true }) to auto-inject history and capture the
reply.
- createBot({ components }) re-registers components so durable actions
re-fire after a restart; restart-durability demo in examples/slack.
- Dedup is marked seen only after the turn lock is acquired, so a turn
dropped on lock-conflict does not burn its eventId (no lost retries).
- Release lockstep: bot-store-redis/postgres version with bot + bot-ui.
The legacy Markdown renderer enabled rehype-raw with no HTML sanitizer,
so raw HTML embedded in assistant/model output reached the DOM (CWE-79).
Add rehype-sanitize as the terminal rehype pass so it runs after any
consumer-supplied rehypePlugins and cannot be bypassed. Add a regression
test covering the dangerous-HTML vectors (script/style/base/form/iframe,
event handlers, javascript: URLs) and the consumer-plugin injection path,
and assert legitimate Markdown/GFM features still render. Pin react-dom to
a caret range for the SSR-based test.
`updateRuntimeConnection` unconditionally rebuilt the `remoteAgents` map
with a fresh `ProxiedCopilotRuntimeAgent` for every id on each connect,
discarding the already-registered live instance along with its
accumulated `messages`, `threadId`, and subscriptions. A re-connection
(an `/info` re-settle, or a header/config/transport change) therefore
swapped the live instance for an empty one. Downstream the `use-agent`
memo keys on the instance identity returned by `getAgent(id)`, so the
swap unmounted an already-rendered conversation — the source of the
showcase auth `dom-missing` flap.
Reuse the existing instance for ids still advertised by the runtime
(re-applying only registry-owned headers/credentials in place); mint a
new proxy only for genuinely-new ids; drop ids no longer present. The
disconnect/no-runtime and error paths still clear `remoteAgents`.
## What
Adds **`@copilotkit/bot-whatsapp`** — a WhatsApp Business **Cloud API**
`PlatformAdapter` for the platform-agnostic `@copilotkit/bot` engine —
plus a runnable **`examples/whatsapp`** app and docs. This brings
WhatsApp to the bots ecosystem alongside the existing Slack support,
reusing the engine, the `@copilotkit/bot-ui` IR, and the pluggable
`ActionStore` untouched.
## How it works
- **Ingress:** the adapter owns its own HTTP server — GET verification
handshake (`hub.challenge`) + POST intake validated by
`X-Hub-Signature-256` HMAC (timing-safe), acked `200` immediately then
processed async.
- **No streaming:** WhatsApp messages are immutable, so the run renderer
**buffers** text and sends once on `TEXT_MESSAGE_END`
(`supportsStreaming: false`; `update()` posts fresh, `delete()` no-ops).
- **Interactive mapping:** text/section → text; ≤3 buttons →
reply-button message; `Select` or 4–10 actions → list message; >10 →
numbered-text fallback. A control's `value` round-trips by encoding it
into the reply id (`ck:…::<json>`), since WhatsApp replies carry no
value field; oversized encodings fail loud rather than corrupt silently.
- **Memory:** WhatsApp exposes no readable history, so a pluggable
**`HistoryStore`** (default `InMemoryHistoryStore`) holds it and replays
it into `agent.messages` each turn (fresh threadId per turn, mirroring
`bot-slack`). Swap in a durable backend to persist across restarts.
- **Commands:** leading-keyword matching (`commandPrefix`, default `/`);
the command text is injected via the engine's `runAgent({ prompt })`
path (not persisted at ingress).
- **Inbound media** → AG-UI multimodal content parts; **HITL** via
interactive replies.
## Example
`examples/whatsapp` mirrors `examples/slack`: a CopilotKit
`BuiltInAgent` over MCP (Linear + Notion), with `issue_list`, an
interactive `show_incident`, and a `confirm_write` HITL gate.
## Tests & verification
- 62 unit tests across the package (render mapping, markdown→WhatsApp,
signature verification incl. wrong-but-equal-length, interaction
decode/round-trip, buffered renderer, webhook listener/server, stores,
media, adapter).
- `build` ✅, package `check-types` ✅, `publint`/`attw` (ESM-only) ✅,
example `check-types` ✅. Full `nx run-many -t test
--projects=packages/**` passes.
- Two rounds of code review (APPROVE) — fixed slash-command history
double-append and silent value-truncation; minors (HMAC over raw bytes,
conversationKey invariant, offset-correct Blob, unused-dep pruning,
added tests).
## Docs
Package `README.md` + `ARCHITECTURE.md`, example setup guide (Meta app +
webhook + tunnel), and a `shell-docs` WhatsApp guide page (registered in
`meta.json` + early-access gate).
## Notes / out of scope (v1)
- No template-send path for messaging outside WhatsApp's 24-hour
customer-service window (documented limitation).
- Pre-existing, unrelated `@copilotkit/core` `phoenix-observable.ts`
typecheck error exists on the branch base (missing `@types/phoenix`) —
not introduced here.
A catalog on <CopilotKit a2ui={{ catalog }}> is now enough to use A2UI
end to end. Previously developers also had to set a2ui.injectA2UITool: true
on the runtime.
The provider forwards an a2uiCatalogAvailable signal per run whenever it has
a catalog (and renders surfaces locally). handle-run reads that signal and
hands it to configureAgentForRequest, which enables A2UIMiddleware and defaults
injectA2UITool to true. An explicit injectA2UITool value (including false) and
an explicit a2ui.enabled: false are always respected via ?? / short-circuit, so
this only fills the default and never overrides a deeper opt-out.