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.
Playwright-verified fixes for the Mastra demo validation round:
- aimock interrupt fixtures (gen-ui-interrupt, interrupt-headless): add
hasToolResult:false to the schedule_meeting suspend legs so the resume
request falls through to the toolCallId confirmation fixture instead of
re-matching the suspend leg (picker loop, duplicated intro). Mirrors
hitl-in-chat.json.
- aimock-fixtures test: ceiling 301 -> 303; the two suspend keys now
intentionally collide across the three mastra interrupt cells
(runtime-disambiguated by route/fixtureFile like existing aliases).
- browse-web tool: return the result OBJECT instead of JSON.stringify;
the bridge encodes once more so stringifying double-encoded the result
and BrowseResultsCard showed "0 results" despite a successful browse.
- reasoning-chain pill: "Roll a d20 ..." instead of "Roll a 20-sided die
..." — the d4 agentic-chat fixture shadowed the first leg under replay
(d4 loads before d6) and pushed reasoning a step late. Real-LLM order
verified correct.
- header-forwarding shim: default x-aimock-context to "mastra" when absent
so browser-driven demos replay against aimock instead of 404ing. Harness
header wins when present; real providers ignore it.
- docker-compose.local: make OPENAI_BASE_URL overridable via .env (default
aimock unchanged) so real-LLM cells like browser-use can be tested live.
(--no-verify: commitlint binary missing in this worktree after the session
crash — ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL, infra not message)
- [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.
The PR's documented snippet — `await handler.channels.ready(...)` with no
`!`/`?.` — did not type-check under strict TS because
`createCopilotRuntimeHandler` always returned `channels?: ChannelsControl`.
Encode channel-presence at the type level:
- runtime.ts: `CopilotRuntime` is now a `const` typed as `CopilotRuntimeConstructor`
(backed by an internal `CopilotRuntimeShim` class; behavior unchanged). A
class constructor cannot vary its return type across overloads, so the two
construct-signature overloads live on the constructor interface: `intelligence`
+ a non-empty `channels` tuple returns a `RuntimeWithDeclaredChannels`-branded
runtime; every other config (SSE, intelligence-without-channels, empty
`channels: []`, or a non-literal `Channel[]` variable) stays unbranded. The
brand is a phantom (compile-time-only) property. `export interface CopilotRuntime`
preserves the name as a type for existing `runtime: CopilotRuntime` / `as
CopilotRuntime` sites.
- fetch-handler.ts: overload `createCopilotRuntimeHandler` — a branded runtime
(unless `activateChannels: false`, constrained to `true | undefined`) returns
the new `CopilotRuntimeFetchHandlerWithChannels` (non-optional `channels`);
everything else keeps the optional shape. Opting out of activation honestly
falls through to the optional overload.
- Added a compile-time type test (checked by `tsc --noEmit`, the `check-types`
gate). It probes the optionality modifier structurally (`{} extends Pick<T,K>`)
rather than for `undefined`, since this package compiles `strict: false`.
Confirmed it fails pre-change on the required-channels assertion and passes
after. Dropped the now-unnecessary `!` in handler-channels.test.ts.
Call sites: the second overload is byte-identical to the former single signature,
so every `createCopilotRuntimeHandler` caller (node/express/hono endpoints,
integration servers, examples) and every `new CopilotRuntime` site resolves
unchanged; only inline non-empty-`channels` construction gains the (strict
supertype-assignable) branded type. Verified via a clean full-package check-types.
P1#2 — reachable setup_required on the PRODUCTION engine path.
connectRealtimeGateway no longer flattens every join rejection into a
generic Error. A join `.receive("error", reason)` whose reason is a known
setup-required code (`channel_declaration_unavailable`, and defensively
`adapter_setup_required` / `not_configured`) now rejects with a
distinguishable `RealtimeGatewaySetupRequiredError` (`code === "SETUP_REQUIRED"`,
raw reason preserved). ChannelManager already detects that code, so an
unconfigured managed provider now degrades to `setup_required` (ready()
resolves) instead of `error`. All other reasons keep the generic error and
the socket-leak teardown is unchanged.
P1#3 — status() reflects real connection health instead of `online` forever.
ConnectedRealtimeGatewaySession exposes `onStateChange(cb)` over
`RealtimeGatewayConnectionState` (`online` | `reconnecting` | `gave_up`),
driven by the real Phoenix seams: an unexpected socket drop → `reconnecting`;
a successful (re)join (the join-push recHooks survive Phoenix `resend`, so
`"ok"` re-fires on every auto-rejoin) → `online`; and a BOUNDED give-up —
Phoenix retries forever, so a `reconnectGiveUpMs` window (default 60000, runs
from the first drop of an episode, cleared on rejoin) elapsing while still
reconnecting → `gave_up` (terminal). Our own disconnect() stays silent.
ChannelManager wires this in place of the log-only onClose breadcrumb:
`reconnecting`→status reconnecting, `online`→online, `gave_up`→error; a
stopped manager/entry ignores late events. computeOverall now ranks
`error > reconnecting > setup_required > connecting > online`. ready() keeps
its one-shot semantics (settles on the initial outcome); later health
transitions move only status(). Docs updated to state `online` means
currently-sendable.
Wording: the direct-adapter skip comment/log now states delivery is
exclusive-per-platform (managed OR direct per platform, not both — attaching
both would double-deliver) with true coexistence tracked in OSS-484. Skip
behavior unchanged.
Call-sites for the changed signatures:
- connectRealtimeGateway error shape: only caller is
startChannelsOverRealtimeGateway (realtime-gateway-launcher.ts:216); it
awaits and lets the rejection propagate, so the setup-required error flows
through unchanged (no branch to update).
- new ConnectedRealtimeGatewaySession.onStateChange: passed through in
startChannelsWithGatewaySession and startChannelsOverRealtimeGateway
(realtime-gateway-launcher.ts); added to ChannelsHandle (runtime.ts) and the
manager's local ChannelsHandle view (channel-manager.ts); exported from
index.ts. RealtimeGatewaySession (base, no observer) consumers
(realtime-gateway-transport.ts) unaffected.
- manager onClose→state transitions: registerOnClose renamed to
registerConnectionObserver; sole caller is the online settle handler in
activate().
Honors the SoT "never infer managed intent from a direct adapter" rule.
Channel.adapters is a new additive read-only member; verified no consumer
constructs Channel literals (only createChannel does).
RC15: getOrCreateChannelManager bridged the manager log as
`logger.warn({ meta }, msg)`, but pino only serializes an Error's
(non-enumerable) message/stack under the `err` key — under `meta` a
failed activation rendered as `{}`, losing the cause. Route an Error to
`err` and keep `meta` for everything else.
LEVER: removed the channel-name FORMAT/length validation block plus the
replicated CHANNEL_NAME_PATTERN / MIN / MAX constants from
channel-activation-config.ts. This was a third copy of managed-specific
rules whose source of truth is channels-intelligence's
assertValidChannelRealtimeScope + assertValidChannelNames, and it kept
drifting (omitted the reserved-name rule). Now that activation failures
are logged, recorded as `error` status, and surfaced via ready(), the
up-front check is not worth cross-package rule parity. Missing/empty
name still throws (the config's own precondition). Deleted the obsolete
"Slack"/"support_bot"/"cs"/65-char rejection tests.
projectId>0: parseProjectIdFromApiKey now throws ChannelConfigError when
the parsed id is <= 0 (`cpk-0_...` matched but failed deep in the
launcher). Parser validating its own output, not a channel-name replica,
so it stays here; reuses the existing key redaction.
adapter default hardening: `adapter ?? "slack"` -> truthiness/trim check
so ""/whitespace falls back to "slack".
activate() stopped-guard: short-circuits on `this.activated || this.stopped`
so a post-stop() activate() opens no transports on a dead manager.
coverage: exported defaultActivateChannel (the real engine) with an
injectable importer seam (optional param, default = the same non-literal
dynamic import) and covered its 3 branches — config->opts mapping (scope
carries only projectId+channelName), module-not-found friendly error, and
generic-error passthrough. Added cheap manager coverage: lazy-activate
duplicate-name reject via ready(), empty channels[] -> online + ready
resolves, non-default adapter reaches the engine config.
Call sites (no external breakage):
- CHANNEL_NAME_PATTERN/MIN/MAX: were module-private; zero references.
- parseProjectIdFromApiKey: only internal caller is
deriveChannelActivationConfig (passes the real key) + tests; <=0 throw
affects only malformed cpk-0 keys.
- defaultActivateChannel: only internal use is the ChannelManager
constructor default (called 2-arg) + tests; new 3rd param is optional
and the fn still satisfies ActivateChannelEngine.
- ChannelsIntelligenceModule: new additive export, referenced internally
+ tests only.
- activate() guard / logger bridge: internal only.