## Summary
- Extract the platform-neutral Channels foundation into
`@copilotkit/channels-core`.
- Make `@copilotkit/channels` the batteries-included consumer entry
point, with adapter and UI subpaths.
- Release the umbrella, core, UI, and all six adapters as one shared
`channels` version scope.
- Verify the packed consumer contract and migrate the Slack and Teams
examples to the umbrella.
## Why
Consumers should be able to install one Channels package without making
the runtime or selective integrations depend on every platform SDK.
Shipping the complete Channels family together prevents adapter/core
version drift and makes the umbrella's exact dependency set release as a
compatible unit.
## How
- Move shared bot/runtime primitives into `channels-core` and invert
adapter/runtime dependencies.
- Add exact workspace dependencies and export subpaths from the umbrella
package.
- Consolidate the existing release configuration and all three release
workflow selectors into one shared `channels` scope containing all nine
packages.
- Use `@copilotkit/channels` as the version source: the next minor
release resolves to `0.2.0` and bumps every Channels package together.
- Publish scoped packages in dependency order for both stable and canary
releases: UI, core, adapters, then the umbrella.
- For a stable Channels release, publish the UI/core/adapters first, run
the registry-backed packed-consumer verifier against those newly
published exact versions, then publish the umbrella.
- Generate Channels release notes from `channels/v*` tags rather than
the monorepo `v*` tags.
- Verify builds, type checks, tests, package artifacts, examples, and
release workflow scope synchronization locally.
### First stable release sequence
1. Bootstrap the currently unpublished `@copilotkit/channels-core`
package on npm and configure npm trusted publishing for every Channels
package against this repository's `release / publish` workflow and `npm`
environment.
2. Create and merge the `channels` minor release PR. The stable workflow
publishes the Channels family in the staged order above and validates
the packed umbrella from the registry before the umbrella is released.
3. Create and merge a subsequent `monorepo` release PR so the published
`@copilotkit/runtime` switches from the historical umbrella dependency
to `@copilotkit/channels-core`.
## What & why
The Realtime Gateway transport had drifted from the HTTP polling
transport, so managed channels running over the gateway behaved
differently from direct. This brings the realtime path to full parity.
Before, the realtime transport:
- built a **text-only** ingress envelope —
commands/reactions/interactions were coerced into empty turns;
- keyed conversations **per-turn** (`conversationKey = turn.id`), so
threaded follow-ups didn't share agent/session state;
- **dropped the provider actor** — `env.user` was never populated (on
*both* transports, in fact);
- implemented **none** of `fetchFile`/`getHistory`/`uploadFile`, so the
realtime path silently ran with no history and no file support;
- had **no `delete` render kind**, so `thread.delete` couldn't render
over the gateway.
## Changes
- **`claim-mapping.ts` (new, shared):** extract the claim→ingress
mapping — `ClaimedDelivery`, `conversationKeyFromReplyTarget`,
`mapDeliveryToEnvelope` — into one module both transports import, so
they can't drift again (drift *was* the bug). Adds the provider `actor`
to the claim turn and maps it to `env.user` — fixing identity on
**both** paths.
- **realtime transport:** builds the envelope via the shared mapper
(real kind discrimination, thread-stable `conversationKey`, actor→user)
instead of the inline text-only build. Fail-closed: an unmodeled
reply-target/kind is dropped+logged, not crashed.
- **`intelligence-file-history.ts` (new, shared):** extract
`fetchFile`/`getHistory`/`uploadFile` into
`IntelligenceFileHistoryClient`. These are **HTTP-only** — the gateway
relays the render-event stream but never file bytes or history. The
realtime transport gains them when configured with `appApiBaseUrl` +
`apiKey` (threaded through the launcher); without those, the methods
stay `undefined` and the adapter degrades exactly as before.
- **`delete` render kind:** add `{ kind: "delete"; ref }` to
`ChannelRenderEvent` (mirrors the frozen Intelligence contract) and
route `thread.delete` through a render frame when a render sink is
wired, like post/update/file.
## Tests
- `claim-mapping.test.ts`: actor→user, kind discrimination
(command/reaction/interaction/text), thread-stable conversationKey
(slack/teams), unmodeled-adapter throw.
- `render-events.test.ts`: realtime non-text kind + identity +
thread-stable key; file/history capability toggling on config; a
delete-render-frame test.
- Full suite (160) + `check-types` + `build` green.
## Companion
This is the CopilotKit SDK half of the managed-transport parity work;
the Intelligence-side half (gateway `render_event/1` validator for
file/delete, DB CHECK, Connector Outbox `chat.delete`, actor on the
claim) landed separately. Together they close the direct-vs-managed
parity matrix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Self-activate CVDIAG_LOG_STDOUT=0 when CVDIAG_PB_URL is wired (safe: only silences stdout when PB is receiving; explicit override preserved). Extend the watchdog to poll the public $PORT /api/health and, on sustained failure, POST a loud #oss-alerts Slack alert BEFORE kill-restart; add the same alert to the agent-\:8000 branch (no silent recovery). Add uvicorn --no-access-log to cut the access-log flood. Together these keep the shared log stream under the 500/sec cap so the pipe never backs up.
Shared cvdiag_bootstrap: gate the per-LLM-call breadcrumb capture handler and the emit_cvdiag stdout write behind CVDIAG_LOG_STDOUT (default ON so every other integration is byte-for-byte unchanged; opt-out per service). Enqueue to the non-blocking PocketBase sink BEFORE the stdout write so a wedged fd1 cannot cost the durable breadcrumb. No sampling; full fidelity to PB. Red-green unit tests incl. a hostile-stdout durability test.
Faithful node:22-slim repro: fd1 through the same awk process-substitution as entrypoint.sh, a Railway-capped drain reader, a uvicorn+CVDIAG-shaped flood, and the static no-log /api/health as victim. RED wedges (200->502, CPU->0, heartbeat frozen); the FIXED lane stays 200 throughout. run.sh asserts the outcome (exit 3/4/5 on a false result, proven). watchdog.sh runs the entrypoint public-guard loop verbatim and needle-anchors it against entrypoint.sh.
- [P1] History/files were absent on the NORMAL managed path: defaultActivateChannel
never forwarded the app-api HTTP URL, so the transport (which installs
fetchFile/getHistory/uploadFile only when appApiBaseUrl is set) ran without
them for Channels started by the CopilotRuntime handler — only manual
low-level launcher callers got file/history. Thread intelligence.ɵgetApiUrl()
through ChannelActivationConfig.apiUrl → defaultActivateChannel →
startChannelsOverRealtimeGateway({ appApiBaseUrl }). The launcher + transport
already accepted it.
- [P2] Managed turns exposed the provider profile under a non-public `displayName`
field, leaving PlatformUser.name undefined. Map env.user.displayName -> name
(parity with the direct Slack adapter, which populates `name`).
Tests: deriver returns apiUrl; defaultActivateChannel forwards appApiBaseUrl to
the launcher opts; onMessage sees message.user.name. channel-activation-config +
channels-intelligence (170) green; runtime build type-checks. (channel-manager.test
executes in CI — local vitest hits the known optional-peer-dep resolution flake.)
## What & why
Consolidates three community docs fixes that were each opened against
the retired `docs/content/docs/` tree (now just a symlink to
`showcase/shell-docs/`), so none of them could merge as-is even though
the underlying doc bugs are still live. This re-applies them against the
current shell-docs source and, where the original proposals had drifted,
uses commands verified against the current repo.
### 1. Contributing / package-linking guides — supersedes #3509
Turborepo has been **fully removed** from the repo (no `turbo`
dependency, no `turbo.json`), but every contributing guide still
instructs `turbo run …`. Across the root guide, all per-integration
copies, and the shared snippets:
- Drop the "Turborepo v2.x installed globally" prerequisite; bump pnpm
to **v10.x** to match the root `packageManager` (`pnpm@10.33.4`).
- Reframe the monorepo as a pnpm workspace orchestrated by **Nx**.
- Replace commands with equivalents verified against the current root
`package.json` / Nx targets:
- `turbo run build|dev|format|lint` → `pnpm run build|dev|format|lint`
- `turbo run link:global` / `unlink:global` → `pnpm exec nx run-many -t
link:global` / `unlink:global`
- per-package dev (`turbo run dev --filter=…`) → `pnpm exec nx watch
--projects=packages/<name> -- pnpm run build`
- also fixed a pre-existing stale reference: `pnpm run example-dev` →
`pnpm run dev:examples` (the real script)
### 2. Anthropic model IDs — supersedes #3656
`built-in-agent/model-selection` listed dotted IDs that aren't valid;
hyphenate `claude-3-7-sonnet`, `claude-opus-4-1`, `claude-3-5-haiku`.
### 3. LangGraph `RunnableConfig` imports — supersedes #4069
Twelve langgraph reference pages annotate `config: RunnableConfig` in
Python snippets without importing it. Add `from langchain_core.runnables
import RunnableConfig` to each such block. (Tutorial pages were
intentionally left out to avoid disrupting their step-by-step
narrative.)
## Credit
Thanks to the original authors whose fixes this incorporates:
@electricalen (#3509), @Abubakar-01 (#3656), and @Koushik-Salammagari
(#4069). Those PRs can be closed in favor of this one once merged.
## Testing
- `grep` confirms **0** remaining `turbo run` / `Turborepo` / `turbo@2`
references and **0** remaining `example-dev` references across the docs
content.
- All replacement scripts/targets verified present in the root
`package.json` (`build`, `dev`, `format`, `lint`, `dev:examples`) and as
per-package Nx targets (`link:global`, `unlink:global`).
- Confirmed **0** langgraph Python blocks remain that use
`RunnableConfig` without importing it (excluding the
intentionally-skipped tutorial pages).
- Spot-checked insertion placement/indentation in both deeply-nested
(`interrupt-flow.mdx`) and flat (`auth.mdx`) code blocks.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Trivial items from the CR confirmation rounds (no behavior change to production
paths):
- intelligence-adapter.test.ts: `delete source.getHistory` was inert (getHistory
is a prototype method; delete only removes own props), so the "no getHistory"
branch was never actually exercised. Shadow with an own `undefined` instead so
the `source?.getHistory?.()` short-circuit is genuinely tested.
- in-memory-transports.ts: mirror the production `limit <= 0 -> []` guard in
InMemoryDeliverySource.getHistory (was `slice(-limit)` → returns ALL for 0).
- realtime ack(): log the empty-turn (no accepted frames) drop so the OSS-491
redelivery pile-up is diagnosable (every other drop path here logs).
- http-transports.ts: remove two orphaned JSDoc comments dangling over
ClaimResponse (leftovers from the file/history extraction).
- intelligence-adapter.ts: correct an inaccurate op-id comment (mintOp is the
${turnId}:${seq} source; the render path keys on ${turnId}:${slot}:${seq}).
channels-intelligence 170 tests + check-types green.
From the 7-agent CR confirmation round:
- RealtimeGatewayTransport.stop() now halts intake (a `stopped` guard in
handleDeliveryAvailable — the session exposes no `off` to detach the
DELIVERY_AVAILABLE listener) and DRAINS the in-flight delivery (awaits the
serial `processing` chain) before clearing state, so a turn settling at stop
time still sends its terminal signal instead of silently no-oping and
redelivering. Mirrors HttpDeliverySource.stop().
- Realtime nack() truncates the reason to 500 chars (parity with HTTP).
- getMessages drops empty content parts before join(" ") so a read_thread
transcript isn't corrupted with doubled/leading/trailing spaces (a test had
enshrined "part one part two").
- Removed dead imports (buildContentParts, AgentContentPart, ChannelFileRef)
left in http-transports.ts after the file/history extraction.
Deferred (delivery-contract / design; need coordination — folded into OSS-491):
timeout-nack can redeliver a still-running turnId (overlap); realtime push()
no-state fallback vs HTTP fail-loud (documented intentional — parity Q); the
empty-turn completion signal.
Tests: stop() drains in-flight + ignores post-stop deliveries; getMessages
assertion corrected. channels-intelligence 170 tests + check-types green.
Addresses @samjulien's review on #5982:
1. Contributing example command: the guide changed into a nonexistent
`examples/next-openai` and ran `dev:examples` (a workspace build/watch
script that never starts a server). Point it at the real
`examples/v1/next-openai` package and its `example-dev` (`next dev`)
script, which actually serves http://localhost:3000/presentation.
Fixed in the shared snippet + all per-integration copies.
2. LangGraph auth: langgraph variants are docs_mode: generated, so the
`/auth` route renders the root `docs/auth.mdx`, not the framework copy.
Add the missing `from langchain_core.runnables import RunnableConfig`
to the two Python blocks in the root source that route renders.
3. Self-contained fences: add the import to the non-tutorial
`langgraph/shared-state/predictive-state-updates.mdx` Python fence and
the `snippets/integrations/langgraph/frontend-tools.mdx` fence, so the
zero-missing-import claim holds for every non-tutorial langgraph block.
4. Contributor prerequisites: bump the `docs-contributions` guides from
pnpm 9 to pnpm 10 to match the code-contributions requirement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pre-merge 7-agent CR of #5983 surfaced several realtime-transport defects (all
in channels-intelligence). Fixes:
- Poison-payload re-lease loop: an unmappable delivery (unmodeled reply-target
adapter / unknown input.kind) with a valid lease was logged + dropped, so
app-api re-leased the identical payload forever. It now fails NON-retryable
(dead-letter), mirroring the HTTP path. nack() gains a `retryable` param.
- Double-terminal-signal race: realtime ack()/nack() deleted delivery state
AFTER the wire push, so a per-turn-timeout nack could race a late dispatch ack
and emit BOTH fail + complete_requested. Now delete-before-push (XOR).
- Concurrent dispatch: deliveries were handled fire-and-forget; now processed
serially (parity with the HTTP runLoop) so an in-flight redelivery can't reset
the shared per-turn seq counter or run two turns of one conversation at once.
- actor.displayName was carried through the claim mapper then dropped in
dispatchTo (`{ id }` only) — now forwarded to handlers (OSS-476 identity).
- fetchFile enforced MAX_INBOUND_FILE_BYTES only against declared content-length;
the actual read was unbounded. Now streams and aborts past the cap.
- getHistory returned the FULL history for limit<=0 (slice(-0)) and hydrated file
bytes for over-returned messages it then discarded. Now caps to the most recent
`limit` BEFORE hydrating; limit<=0 -> [].
- stream() posted an empty text frame for an empty stream; now skips the post.
- HTTP withTimeout/defaultSleep timers now unref() (parity with realtime).
- Corrected the inaccurate thread_started comment in claim-mapping.ts.
Deferred to OSS-491 (delivery-terminal-signal contract, needs app-api
coordination): an empty turn (reaction/command that posts nothing) has no valid
completion signal (acceptedThrough requires >=1) and redelivers; and the
run_error swallow on the HTTP-fallback render path.
Tests: poison non-retryable, single-terminal XOR, serial dispatch, displayName
forwarding, getHistory limit<=0 + cap. channels-intelligence 169 tests + check-types green.
The realtime-gateway transport had drifted from the HTTP transport: it built a
text-only ingress envelope that coerced commands/reactions/interactions into
empty turns, keyed conversations per-turn (breaking threaded follow-ups),
dropped the provider actor identity, and implemented none of
fetchFile/getHistory/uploadFile (so the realtime path silently ran with no
history and no file support). It also had no `delete` render kind. This brings
the realtime path to full parity with direct.
- claim-mapping: extract the claim→ingress mapping (ClaimedDelivery,
conversationKeyFromReplyTarget, mapDeliveryToEnvelope) into a shared module
both transports use, so they cannot drift again. Add the provider `actor` to
the claim turn and map it to `env.user` (fixes identity on BOTH paths).
- realtime transport: build the envelope via the shared mapper — real kind
discrimination, thread-stable conversationKey, actor→user — instead of the
text-only inline build. Fail-closed: an unmodeled reply-target/kind is
dropped+logged, not crashed.
- file/history: extract fetchFile/getHistory/uploadFile into a shared
IntelligenceFileHistoryClient (HTTP-only — the gateway never relays bytes or
history). The realtime transport gains them when configured with
`appApiBaseUrl` + `apiKey` (threaded through the launcher); absent that, the
methods stay undefined and the adapter degrades exactly as before.
- delete render kind: add `{ kind: "delete"; ref }` to ChannelRenderEvent
(mirrors the frozen Intelligence contract) and route thread.delete through a
render frame when a render sink is wired (OSS-420), like post/update/file.
Tests: shared-mapper unit tests (actor→user, kind discrimination,
conversationKey, unmodeled-adapter throw); realtime tests for non-text kind +
identity + thread-stable key and file/history capability toggling; a
delete-render-frame test. Full suite + check-types + build green.
Combines the two OSS-473 follow-ups (originally #5972 + #5973, now
folded here) into one PR for a single review/merge. Base is `main`
(OSS-473/#5963 has merged). Reviewable **commit-by-commit** — the
mechanical rename is isolated in commit 1; the behavior changes are
commits 2–4.
## Commit 1 — `refactor(channels)`: scrub residual internal "bot" naming
(closes OSS-485)
Mechanical, naming-only, **no behavior change** — the last internal
"bot" vestiges left out of 473's atomic telemetry-surface commit.
- `bot` local variable → `channel` in `create-channel.ts` internals +
the ~15 tests that exercise it.
- `BotNode` type → `ChannelNode` in `@copilotkit/channels-ui` and
**every** importer (channels,
slack/teams/discord/telegram/whatsapp/intelligence adapters, slack/teams
examples — 256 refs).
- `botName` adapter-SPI option → `channelName` on `AdapterStartContext`
+ its `create-channel` caller + the one consumer
(`IntelligenceAdapter.start` ctx), in one change so the SPI can't drift.
(Phoenix wire contract was already `channelName` as of 473.)
- Stale `bot-ui`/`bot-slack` comment refs →
`channels-ui`/`channels-slack`; `Symbol.for("copilotkit.bot-ui.*")` →
`"copilotkit.channels-ui.*"`.
- **Kept** (out of scope): the platform `isBot?` author flag (unrelated
semantic) and example `bot` instance vars / "demo bot" prose
(user-facing).
## Commits 2–4 — `fix`: realtime-transport reliability & parity
hardening (closes OSS-482)
The self-contained observability/robustness subset of the OSS-473 CR
follow-ups (`packages/channels-intelligence` transport +
`packages/runtime` manager). Each hardening ships with a test that fails
without the fix.
- **`emit()` fail-loud** (`intelligence-adapter.ts`) — returned a
synthetic `MessageRef` on `{ ok: false }`, acking a failed
post/update/delete as success (silent egress drop). Now throws → the
delivery is nacked/retried.
- **Realtime delivery dispatch** (`realtime-gateway-transport.ts`) — the
`void handleDeliveryAvailable(...)` fire-and-forget had no error
boundary and no deadline. Replaced with a `.catch` + a bounded per-turn
timeout that nacks + logs on failure/timeout (parity with the HTTP
`runLoop`).
- **`ChannelManager.stop()` per-handle timeout** — a wedged
`handle.stop()` hung teardown/SIGTERM forever. Each is now bounded by
`stopHandleTimeoutMs` (default 5000); on timeout it's logged and
abandoned so other entries still stop.
- **`ChannelManager.ready({ timeoutMs })`** — a set-wide timeout
discarded an erroring channel's reason when a sibling hung. The deadline
is now **per channel**, so the `AggregateError` carries both the real
activation error **and** a named timeout for each hanging channel.
- **Forward `ChannelManager` `log` down** to the launcher/transport so
transport-level drop diagnostics (e.g. a version-skew
missing-`leaseToken` outage) aren't silent in the managed path.
- **`examples/slack/.env.example`** — OpenAI-only `AGENT_MODEL`
guidance; removed never-read `ANTHROPIC_API_KEY`/`GOOGLE_API_KEY`;
blanked the presence-gated `LINEAR_API_KEY`/`NOTION_*` placeholders (a
non-blank value wires a broken MCP); documented `NOTION_MCP_PORT`; added
WhatsApp to the adapter list.
### Already landed in OSS-473 (not re-done here)
The reconnect **"gave-up → error" escalation** is fully implemented on
`main`: `realtime-gateway.ts` bounds the reconnect window
(`reconnectGiveUpMs`, default 60s) and emits a terminal `gave_up`; the
launcher exposes `onStateChange`;
`ChannelManager.registerConnectionObserver` maps `gave_up → error`
(covered by `realtime-gateway.test.ts` +
`channel-manager-reconnect.test.ts`).
### Explicitly EXCLUDED — owned by OSS-474 / OSS-476
Left untouched (overlap in-flight work): no cross-turn history on the
realtime path (per-turn `conversationKey`) → OSS-474/476; empty-turn
`ack()` redelivery livelock → OSS-474/475/476; non-text parity
(command/interaction/reaction) → OSS-476; `thread.delete()` render frame
→ OSS-476 (PR #554); durable StateStore on the managed realtime path →
OSS-474/476.
## Testing
- **DoD grep** — `grep -rn "\bbot\b\|botName\|BotNode" packages/channels
packages/channels-ui` (`.ts`/`.tsx`, excl. `dist/`) → **0**; `BotNode`
gone repo-wide.
- **check-types** green across all 8 channels packages +
`@copilotkit/runtime`.
- **tests** green: channels 155, channels-ui 23, slack 277, teams 88,
discord 193, telegram 149, whatsapp 73, channels-intelligence 144,
runtime `channel-manager` 40; slack-example 63, teams-example 2. Full CI
matrix (incl. `unit` 20.x/22.x/24.x) green on this branch.
- Each 482 hardening has a test that fails pre-fix (no silent ack;
dispatch nacks on throw/timeout; `stop()` resolves + logs on a wedged
handle; `ready()` aggregate preserves the real reason;
`defaultActivateChannel` forwards `log`).
Closes OSS-485. Closes OSS-482.
Two fixes from the pre-merge adversarial CR of this PR (both pre-existing,
flagged as in-subject):
- ChannelManager.status() reported overall "online" for a manager stopped
BEFORE activate() (e.g. SIGTERM during startup): `entries` is empty, so the
empty-set fold returned "online" — a torn-down manager reading healthy. Now
short-circuits to "stopped" when `this.stopped`, matching the documented
status() contract. New red-green test covers the stop()-before-activate() case.
- examples/slack/.env.example: COPILOTKIT_INTELLIGENCE_WS_URL example was
ws://localhost:4401, but derivation is a scheme-only swap of the :4201 API URL
(→ ws://localhost:4201) and 4401 is used nowhere — a user uncommenting it hit a
dead port. Corrected to :4201 and clarified the derivation note.
Consolidates three stale-docs fixes that were opened against the retired
`docs/content/docs/` tree (now a symlink) and so could no longer merge:
- Contributing/package-linking guides (root + all integration copies +
shared snippets): Turborepo is fully removed from the repo (no dep, no
turbo.json). Drop the Turborepo prerequisite, bump pnpm to v10.x to match
`packageManager`, describe the monorepo as a pnpm workspace orchestrated by
Nx, and replace `turbo run <task>` with verified equivalents:
`pnpm run build|dev|format|lint`, `pnpm exec nx run-many -t (un)link:global`,
`pnpm exec nx watch` for a single package, and `pnpm run dev:examples`
(the real script; `example-dev` did not exist). Supersedes #3509.
- built-in-agent/model-selection: hyphenate the Anthropic model IDs
(`claude-3-7-sonnet`, `claude-opus-4-1`, `claude-3-5-haiku`). Supersedes #3656.
- langgraph reference docs: add the missing
`from langchain_core.runnables import RunnableConfig` import to Python code
blocks that annotate `config: RunnableConfig`. Supersedes #4069.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
|
[zizmorcore/zizmor-action](https://redirect.github.com/zizmorcore/zizmor-action)
| action | minor | `v0.5.7` → `v0.6.0` |
---
> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/592) for more information.
---
### Release Notes
<details>
<summary>zizmorcore/zizmor-action (zizmorcore/zizmor-action)</summary>
###
[`v0.6.0`](https://redirect.github.com/zizmorcore/zizmor-action/compare/v0.5.7...v0.6.0)
[Compare
Source](https://redirect.github.com/zizmorcore/zizmor-action/compare/v0.5.7...v0.6.0)
</details>
---
### Configuration
📅 **Schedule**: (in timezone America/Los_Angeles)
- Branch creation
- "before 9am every weekday"
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Enabled.
♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/CopilotKit/CopilotKit).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNTkuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI1OS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Pre-existing .env.example issues noted in the OSS-473 CR:
- AGENT_MODEL example was `anthropic/claude-sonnet-4.5`, but runtime.ts is
OpenAI-only (web search is an OpenAI hosted tool; it strips a leading
`openai/` and passes the rest to `openaiText`). Use an OpenAI example and say
so.
- Removed ANTHROPIC_API_KEY / GOOGLE_API_KEY — never read by this runtime.
- LINEAR_API_KEY, NOTION_TOKEN and NOTION_MCP_AUTH_TOKEN were non-blank
placeholders, but runtime.ts turns the Linear/Notion MCPs ON purely on the
presence of LINEAR_API_KEY / NOTION_MCP_AUTH_TOKEN — a placeholder wires a
broken MCP. Blanked them (they're optional integrations, like the commented
Discord/Telegram/WhatsApp creds).
- Documented NOTION_MCP_PORT (the `pnpm notion-mcp` sidecar port, default 3001;
must match NOTION_MCP_URL).
- Added WhatsApp to the header list of supported adapters.
Three ChannelManager observability/robustness hardenings from the OSS-473 CR.
stop() per-handle timeout: stopEntry awaited handle.stop() unbounded, so a
wedged stop() (e.g. a socket.disconnect that never returns) hung teardown — and
thus SIGTERM shutdown — forever. Each handle.stop() is now bounded by
stopHandleTimeoutMs (default 5000); on timeout it is logged and abandoned so
every other entry still reaches `stopped`.
ready() surfaces the real reason on hang: ready({ timeoutMs }) wrapped the whole
`allSettled` in one timeout, so when one channel settled to `error` while a
sibling hung, it rejected with only a generic timeout and DISCARDED the erroring
channel's reason. The deadline is now applied PER CHANNEL, so the AggregateError
carries both each failed channel's real reason AND a named timeout for each
still-hanging channel.
Log forwarding: the manager's `log` reached activation-level events only; the
default engine (defaultActivateChannel → startChannelsOverRealtimeGateway) never
passed it down, so transport-level drop diagnostics (e.g. a version-skew
missing-leaseToken outage) were silent in the managed path. `log` is now
forwarded down to the launcher/transport.
Note: the reconnect "gave-up → error" escalation from the same CR is already
implemented end-to-end in OSS-473 (realtime-gateway.ts reconnectGiveUpMs +
ChannelManager.registerConnectionObserver) and is not re-done here.
Tests: stop() resolves + logs a timeout when handle.stop() never settles;
ready() aggregate contains both a real activation error and the hung sibling's
named timeout; defaultActivateChannel forwards its log sink to the launcher opts.
Two transport reliability hardenings on the managed realtime path, from the
OSS-473 CR.
emit() fail-loud: IntelligenceAdapter.emit() returned a synthetic MessageRef on
`{ ok: false }`, acking a failed post/update/delete as success (silent egress
drop on the HTTP-fallback path). It now throws with the failure code so the
failure propagates up the render/run path and the delivery is nacked/retried.
Realtime delivery dispatch: `session.on(delivery.available)` fired
`void this.handleDeliveryAvailable(...)` with no error boundary and no per-turn
deadline — an onDelivery rejection became an unhandled rejection (silent drop)
and a hung handler pinned the delivery forever. Replaced the `void` with a
`.catch` and wrapped the turn in a bounded per-turn timeout that nacks + logs on
failure/timeout (parity with the HTTP runLoop's turnTimeoutMs).
Tests: egress `{ ok: false }` makes thread.post throw (no silent ack); an
onDelivery throw and an onDelivery that exceeds deliveryTimeoutMs both nack
(channel.delivery.fail.v1) and log.
Follow-up to the OSS-473 clean-break rename (#5963). Scrubs the last
internal "bot" vestiges deliberately left out of 473's atomic
telemetry-surface commit. Naming-only, no behavior change.
- `bot` local variable → `channel` in create-channel.ts factory internals
and the ~15 test files that exercise it.
- `BotNode` type → `ChannelNode` in @copilotkit/channels-ui and every
importer (channels, slack/teams/discord/telegram/whatsapp/intelligence
adapters, and the slack/teams examples).
- `botName` adapter-SPI option → `channelName`: renamed on
AdapterStartContext, its create-channel caller, and the one adapter that
reads it (IntelligenceAdapter.start ctx) in the same change so the SPI
cannot drift. The Phoenix wire contract was already `channelName` (473).
- Stale `bot-ui`/`bot-slack` package refs and "bot core"/"the bot" prose in
channels/channels-ui comments → `channels-ui`/`channels-slack`/"channel".
- `Symbol.for("copilotkit.bot-ui.*")` → `"copilotkit.channels-ui.*"`.
Deliberately kept: the platform `isBot?` author flag (an unrelated
"is this message author a bot account" semantic), and example `bot`
instance variables / "demo bot" prose (user-facing, out of this ticket's
internal-symbol scope).
Validation: DoD grep returns zero internal bot symbols in
packages/channels + packages/channels-ui; check-types + test green across
all 8 channels packages (1093 tests) and both examples.
## Problem
On the managed Teams path the agent always reported "no earlier
messages" / "I don't see the image in this thread."
`HttpDeliverySource.getHistory` was **Slack-only**: it keyed off
`threadTs` and returned `[]` for any route without one. A Teams route is
`{ adapter: 'teams', tenantId, conversationId }` (no `threadTs`), so it
short-circuited *before making any request* — starving **both** history
mechanisms on Teams:
- `agent.messages` seeding (`conversationStore.getOrCreate`), and
- the `read_thread` tool (via `thread.getMessages()`).
## Fix
- **`getHistory` is now adapter-aware** (mirrors
`conversationKeyFromReplyTarget`'s per-adapter switch): Slack keys off
`teamId`/`channel`/`threadTs`; Teams sends `adapter=teams` + `tenantId`
+ `conversationId`, matching app-api's
`teams:{tenantId}:{conversationId}` thread_key. app-api's
`/api/channels/history` route already accepts this shape. **Slack query
and order are unchanged.**
- **Add `getMessages` to the adapter** so `thread.getMessages()` (the
`read_thread` tool) reads reconstructed history via the transport and
maps it to `ThreadMessage[]`. Without it `Thread.getMessages()` returns
`[]` and thread-reading tools (summaries, "what was in the image") see
nothing even when history exists.
## Tests
- New: Teams-shaped `getHistory` query, and the
missing-`tenantId`/`conversationId` short-circuit (no request).
- All 115 `channels-intelligence` tests pass; `check-types` clean.
## Notes
Verified end-to-end against a live managed Teams bot as a dist patch
before porting to source (history seeding + `read_thread` both start
returning the thread's messages). The app-api counterpart (Teams
ingress: reactions, inline-media/Graph file ingest, slash commands) is a
separate PR in the Intelligence repo.
## Summary
Makes the normal `createCopilotRuntimeHandler` own Managed Channels
activation, readiness, reconnect, and shutdown. A developer declares a
Channel next to their agents in an existing `CopilotRuntime`, adds
`intelligence`, and mounts the normal handler — **no separate launcher,
no gateway URLs, no org/project/channel/runtime-instance IDs**.
```ts
const support = createChannel({ name: "support", agent: () => supportAgent });
const runtime = new CopilotRuntime({ agents, intelligence, identifyUser, channels: [support] });
export const handler = createCopilotRuntimeHandler({ runtime, basePath: "/api/copilotkit" });
await handler.channels.ready({ timeoutMs: 10_000 });
handler.channels.status();
await handler.channels.stop();
```
Implements the CopilotKit-SDK half of the Managed Channels SoT
**workstream A**. Closes OSS-473.
## What changed
- **`channels:` runtime option** on the Intelligence runtime (public
alias replacing the internal `bots` field); the SSE runtime rejects it.
- **`ChannelManager`** (`packages/runtime/.../core/channel-manager.ts`)
owns the managed lifecycle: lazy, idempotent `activate()`; `ready({
timeoutMs })`; `status()` (`connecting | online | setup_required |
reconnecting | stopped | unmanaged | error`); idempotent, resilient
`stop()`. Activation config (`wsUrl`, `apiKey`, `projectId`,
`channelName`, `provider`, `runtimeInstanceId`) is **derived from the
`intelligence` config + the declared channel** — `projectId` is parsed
from the `cpk-{projectId}_` API-key prefix, and the managed `provider`
is declared **per-Channel** via `createChannel({ provider })` (type
`ManagedChannelProvider`, defaults to `slack`; `teams` is
gated/coordinated — the gateway accepts only `slack` at join today, so
`teams` is SDK-ready but not GA until Intelligence OSS-450/#511 lands) —
so no infrastructure IDs are supplied by the developer.
- **`createCopilotRuntimeHandler` returns a callable object** — for an
Intelligence runtime with declared channels the `channels` control is
**non-optional** (`((req) => Promise<Response>) & { channels:
ChannelsControl }`; optional for SSE / channel-less runtimes).
Activation is **lazy and serverless-safe**: handler creation opens
**no** connection; the persistent gateway socket opens on the first
`await handler.channels.ready()`, so a Fetch host that cannot own a
listener (Cloudflare Workers, Next.js App Router) never opens one. Call
`ready()` once at startup on a long-running host. Idempotent per-runtime
via a `WeakMap`; additive and non-breaking; propagated through the
node/express/hono endpoint wrappers.
- **Clean-break rename** `createBot → createChannel` / `Bot → Channel`
(and `CreateChannelOptions`, `ChannelHandler/Component/Tool/Command`,
`defineChannelTool/Command`) across `@copilotkit/channels`,
`-intelligence`, the adapter packages, `@copilotkitnext/teams`, and the
examples — no public `Bot` aliases, per the SoT.
- **`examples/slack`** managed path rewritten to the no-launcher DX
(`createChannel` + `channels:` + `createCopilotNodeListener` +
`handler.channels.stop()` on SIGTERM/SIGINT); the six `INTELLIGENCE_*`
launcher env vars removed.
- The `channels-intelligence` realtime launcher is kept working and is
now driven internally by the handler.
## Reconnection & connection health (note for reviewers)
The actual reconnect/rejoin is **delegated to the Phoenix connection
layer**, not a runtime-managed backoff loop. Verified against the live
gateway contract (Intelligence #511): Phoenix's `Socket` auto-reconnects
and auto-rejoins, which re-runs the gateway `join/3` →
`record_heartbeat` (re-registers the listener), and `terminate/2`
releases the dead socket's lease. A manager-level re-activation loop
would be both **redundant** and **incorrect** (the `Channel` is
single-start — re-running `addAdapter`/`start` throws).
What the handler owns is **observation of that connection**. The session
exposes `onStateChange` and the manager reflects it in `status()`: a
dropped socket **or a Phoenix channel-level close/error** moves the
channel to `reconnecting`; a successful (re)join moves it back to
`online` — so `online` means the managed path can currently send. A
**bounded give-up window** (`reconnectGiveUpMs`, default 60s) surfaces a
prolonged outage as `error`, but it is **recoverable**: if Phoenix
rejoins afterwards the channel returns to `online` — the state always
tracks the live transport, never latching. Join rejections are
**classified per-Channel** from the gateway's declaration states:
genuinely unconfigured/waiting states (`adapter_setup_required`,
`*_waiting_for_runtime`, `no_channels_yet`, …) surface as
`setup_required` (and `ready()` resolves); a `runtime_conflict` or a
`*_failed`/hard state surfaces as an **error** and is never silently
downgraded to setup-required. `ready()` keeps one-shot semantics — it
settles on the initial activation outcome; later health transitions move
only `status()`.
## Testing
Unit + integration: handler-owned activation with config derived purely
from `intelligence` (asserts **no** org/channelId on the wire); **first
request does not trigger activation**; `ready`/`status`/`stop`; a
socket-drop path; duplicate-name fail-loud; setup-required surfacing;
the default engine's opts mapping + module-not-found path. `build` +
`check-types` + `test` green across `@copilotkit/channels`,
`-intelligence`, and `runtime` (1726 tests).
## Residual risk / verification
The dynamic `import()` of `@copilotkit/channels-intelligence`, the real
Phoenix socket join, and the example's node listener are proven against
fakes but not a live gateway. A **packaged-SDK → Realtime Gateway →
provider e2e smoke** is recommended before production reliance (SoT
workstream D; pairs with landing Intelligence #511).
## Follow-ups (out of this PR's activation-lifecycle subject)
Filed separately: managed transport parity/reliability — cross-turn
history (OSS-436), command/interaction/reaction ingress over the
realtime path (OSS-416/419/434), `thread.delete()` egress, empty-turn
redelivery, egress-failure fail-loud, durable StateStore on the managed
path, and the reconnect-observability / `stop()`-timeout hardenings.
Docs update tracked in #5925. Id-less gateway topic in OSS-480.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Adapter transport model (clarified per review)
This PR is **exclusive per Channel**, not per platform, and there is no
simultaneous coexistence. **Any** developer-supplied **direct** adapter
on a Channel declared in `channels:` makes the **whole** Channel
`unmanaged` and skips it for managed activation — regardless of platform
— rather than dual-activating, because attaching the managed gateway
transport beside a direct adapter would open two live connections to the
provider and deliver every event twice (`assertExclusive` enforces the
adapter exclusivity). The skipped Channel is not silently "healthy": it
is reported with an explicit **`unmanaged`** status (a runtime whose
only Channel is direct reads `overall: "unmanaged"`, never `online`),
and the handler neither starts nor stops it — the developer owns its
`channel.start()`. Having the handler own the lifecycle of a direct
Channel too (either-or per Channel) is **OSS-486**; true managed+direct
coexistence on one Channel is **OSS-484**.
Classify per-channel state on channel_declaration_unavailable rejects so a
runtime_conflict is a hard error rather than being downgraded to setup_required;
make gave_up recoverable (a later rejoin restores online); and route Phoenix
channel-level close/error through the same health transition as socket drops.