## Release monorepo v1.64.2
**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.64.2`
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.64.2`
- Creates git tag `monorepo/v1.64.2`
- 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.
## Release channels v0.5.0
**Scope:** `channels` | **Bump:** `minor`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `channels` packages to `0.5.0`
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 `channels` packages to npm at version `0.5.0`
- Creates git tag `channels/v0.5.0`
- 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.
Adds `examples/showcases/reskinnable-demo` — the banking showcase
restructured onto a reskinnable shell/skin architecture, shipping
**banking** and **airline** as two swappable skins.
`examples/showcases/banking` is untouched.
## What this is
One Next.js app whose entire experience — brand, theme, layout, pages,
frontend tools, a2ui catalog, agent — swaps at runtime from a floating
selector. A skin-agnostic **shell** hosts one **skin** per route segment
`/[skin]/...`.
```
src/shell/ skin-contract · provider · registries · chat/ · canvas/ · threads/
src/skins/banking/ REST-backed (/api/banking/v1/*)
src/skins/airline/ in-memory client state
src/app/[skin]/ resolves the skin from the URL, mounted keyed by skin.id
```
The contract deliberately spans **both data substrates** — banking talks
to a REST backend, airline holds plain client state. A contract that
only ever hosted one kind of skin proves much less.
## How it was built
Started as a byte-identical clone of banking (159 tracked files, one
commit), then restructured in verifiable steps. Every intermediate step
kept the app **building and running**, not merely compiling. Most of the
diff is relocation; the genuinely new code is `src/shell/**`,
`src/skins/airline/**`, and banking's skin assembly.
Because `examples/showcases/banking` still exists, git cannot infer
renames — the diff shows all 203 files as additions. Reviewing it that
way is misleading. The real surface: **63 files are byte-identical to
banking, 90 changed (mostly import specifiers), 48 are genuinely new.**
## Decisions worth a reviewer's attention
**Banking keeps its REST backend, namespaced to the skin.** The
standalone prototype this architecture came from had replaced banking's
`/api/v1/*` with a client-side store. That was never a design decision —
the prototype was greenfield and simply never ported it (no commit
deletes it; the only deletion in its history is two stub skins). Since
every route under it is banking domain — cards, transactions,
exceptions, policies, reports, users, dev/reset — it moved to
`/api/banking/v1/*` rather than being discarded. Mounting one skin's
whole backend at the app root would contradict the premise that skins
are self-contained.
**`v1` is retained** even though it is vestigial (it arrived with the
pre-monorepo demo; there is no `v2` and nothing negotiates on it) so the
path shape is otherwise unchanged.
**The routing cutover is one commit on purpose.** Banking's pages *were*
the Next.js routes, so moving them, adding `/[skin]` routing, swapping
the registry off the placeholder, assembling the skin object and cutting
the shell's banking coupling had to land together — any split produces a
commit that builds and serves 404s.
**Four contract fields were added** beyond the prototype's version, each
closing a gap that would otherwise make a shipped banking feature
inexpressible:
- `toolLabels` — tool-activity chip labels are skin-domain strings
- `chatHeaderActions` — a skin contributes buttons to the shared chat
header (banking's invoice paperclip)
- `onSuggestionSelect` — a suggestion pill may stage an attachment and
drive the composer instead of sending text
- corrected OGUI semantics — surfaces render full-region on the canvas.
The prototype's "inline in the chat" note described a *published-SDK*
limitation that does not apply on `workspace:*`.
## Bugs fixed along the way
Several were pre-existing in banking and only surfaced by moving the
code:
- **`/cards?operation=change-pin` opened nothing.** Card operations are
registered on the index route, but banking's `/cards` re-exported the
*dashboard*, which ignores `?operation=`. Both `/` and `/cards` now
resolve to the skin base, so the PIN dialog actually opens.
- **The presenter-reset test asserted a pre-#6136 shape.** That route
also forgets the default persona (3 ids, not 2) and re-seeds; the test
was never updated, and its seeding dependency was unmocked so it
executed real network code.
- **Two memory smoke scripts posted to
`/api/copilotkit/agent/default/run`** — no `default` agent exists once
agents are keyed per skin. Nothing in the toolchain reads those `.mjs`
files, so no build or test could have caught it.
- **Both Docker stacks shared a compose project name**
(`banking-memory`), so containers *and named volumes* were shared:
`docker compose down` in either app destroyed the other's memory stack.
- **The skin selector covered the chat and swallowed clicks** on its
toggle. Now positioned from a `--nw-chat-width` custom property the chat
publishes, in a `pointer-events: none` strip — structurally unable to
intercept.
- **A dark-mode leak and a token-ownership leak**: `.dark` persisted
across skin switches into a light-only skin's shared chrome, and the
shell's `@theme inline` block still carried banking-violet shadow
values.
- **971 lines of dead code dropped** — `threads-drawer`, `threads-panel`
and their CSS module were superseded by `chat-inbox` in #6136 and
imported by nothing.
## Verification
- `nx run reskinnable-demo:build` green; **45 unit tests pass, 0 fail**;
`oxlint` 0 errors; `eslint` clean
- **6/6 Playwright smoke**, including 5 new tests covering reskinning
itself — the index redirect, both skins rendering their own chrome,
unknown-skin 404, and switching in both directions. Each was verified by
*deliberately breaking the app and watching the test fail*, because the
first draft of these assertions passed against a broken airline (the
shell selector renders every skin's brand on every page, so a brand-text
check proved nothing).
- Route behaviour confirmed over HTTP: `/` → 307 → `/banking`;
`/banking{,/cards,/charges,/dashboard,/team}` and `/airline` → 200;
unknown skin → 404
- The agent prompt was verified byte-identical to banking's (19,382
chars, compared programmatically) — it encodes demo behaviour including
the never-markdown-tables rule
- All 8 of #6136's demo beats traced end-to-end, plus the three
regressions it fixed (globally-registered action tools, non-HITL closing
step, the two non-interchangeable `actions.ts` helper families)
- Reviewed by a 7-lens pass scoped to the delta-from-banking. It found 4
significant issues, all fixed here: the runtime route applied banking's
identity scheme to every skin; per-user memory scoping depended on an
accidental effect ordering; dark mode leaked into a light-only skin's
shared chrome; and the skin selector overlapped skin navigation below
~1190px. One reported finding was investigated and **rejected** as
factually wrong rather than applied.
### How to read the diff
Because `examples/showcases/banking` still exists, git cannot infer
renames and shows all 203 files as additions. Reviewing it that way is
misleading:
- **63 files are byte-identical to banking** — no review needed
- **90 changed**, most only in their import specifiers
- **48 are genuinely new** — `src/shell/**`, `src/skins/airline/**`,
banking's skin assembly, `/[skin]` routing
The 8 commits are grouped by area of concern and read in order:
workspace → shell → banking-as-a-skin → airline → routing → tests → docs
→ tooling.
## Isolation from the banking demo
This app vendors the same Intelligence stack as banking with the same
seeded persona ids, so it is isolated on two independent axes:
**Ports** — banking's `7050/7053/715x` shifted by +200
(`7250/7253/725x`). Without this, `pnpm dev` here while banking's stack
was up attached silently to banking's backend: same memory buckets, and
the presenter reset button clearing the neighbour's demo state. Verified
live — the stack comes up as project `reskinnable-demo-memory` with its
own volumes and all six ports bound, banking untouched. The native Metal
TEI on `:7067` stays deliberately shared: same version and model give
byte-identical embeddings and it holds no demo state.
**Organization** — a different seeded `cpk` key, so this app resolves to
`haus-von-haskell` rather than banking's `casa-de-erlang`. Org comes
from the authenticated key and `seed.sql` already provisions three orgs
for exactly this, so it is a key swap with no backend change. This axis
matters because ports are a local convention that copying banking's
`.env` over this one silently undoes, whereas the org key still holds
when someone does.
Verified against a freshly seeded stack: a memory written under one key
is invisible to the other **even for an identical user id**, and the
app's own reset seeded 3 memories into org 2 that org 1 cannot see.
Neither demo can read or delete the other's memories.
The presenter reset and the runtime now also name the backend they
resolved (`apiUrl` in the reset response, one startup log line), so a
misconfiguration is visible rather than silent.
**One inherited doc claim corrected.** `.env.example` warned that
non-seeded ids `403` against the Intelligence stack. Measured, they do
not — `GET`/`POST /api/memories` returns 200/201 for an unseeded id and
for a nonsense one, the scope being created on demand. It mattered
because `DEMO_DEFAULT_USER_ID` is absent from `seed.sql`, so the warning
implied the unpinned interactive config the same file recommends was
broken. It is not. **banking still carries the claim** and deserves the
same correction.
## Known follow-ups (not blockers)
- `/banking/<unknown>` returns HTTP 200 while rendering the 404 UI —
`notFound()` in a client subtree, after the status is committed. Correct
UX, wrong status code; a clean fix needs a server-component validation
boundary.
- `e2e/ogui-routing.spec.ts` is `test.describe.fixme`. It clicks 7
suggestion pills, 6 of which #6136 had already removed from the
registered set — it has been broken on `main` since July, unnoticed
because **no CI workflow runs banking's e2e at all**. Rebuilding it
against the new catalog is real work, and the OGUI/a2ui routing it
guarded currently has no e2e coverage.
- Banking's own copy of the `dev/reset` test has the same stale
assertion and deserves a one-file fix.
- The two demo stacks still share host ports 7050/7053, so they cannot
run simultaneously (a loud "port already allocated", not silent
corruption). Moving them needs lockstep edits across compose, env,
playwright and `src` test defaults.
- Airline has no dark palette and renders no theme toggle.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01VgnriWLrxhfKnK8g1gHa5S
The previous comment presented the effect-based ref sync as merely avoiding
the react-hooks/refs lint rule. It is a trade-off: the render reads
cardsRef.current, and an effect writes after commit, so a card mutation
landing while the PIN card is open can leave the picker one render behind.
Records why that is accepted and what fixing it properly would require.
Replays three generative-UI changes from the banking showcase (#6259) into
the reskinnable demo's banking skin:
- setCardPin keys its collapsed render on the tool result and remembers
answered calls in a module-scope map, so a resolved PIN change survives
re-render. Drops the [cards] dependency in favour of a ref, which is what
stopped the tool tearing down mid-flight.
- showCharges becomes human-in-the-loop: it asks before moving the user's
whole screen, and routes through the skin-scoped base prefix.
- Notes about reported charges are prefixed with a red alert emoji.
Replays the report chart curation from the banking showcase (#6259, #6263)
into the reskinnable demo's banking skin. The three columns become spend
breakdown, spend over time and largest charges — the last of which answers
"which line items drive the total", a question a three-team aggregate
cannot. Additions carry a policyId so their bars colour by owning team.
Ports the brand-tint treatment for explicitly-set filter selects from the
banking showcase charges page (#6259) into the reskinnable demo's banking
skin, so an agent-applied filter reads as deliberate.
Ports the seed rebalance from the banking showcase (#6259) into the
reskinnable demo's banking skin so the report's spend-share donut shows a
meaningful split rather than three near-equal slices.
Ports the ranked largest-charges chart from the banking showcase (#6263)
into the reskinnable demo's banking skin. The report-card wiring that
consumes it lands separately.
Ports the red-alert-emoji instruction from the banking showcase seeded
memories (#6259) into the reskinnable demo's banking skin, so recalled
memory drives the same note formatting.
Ports the "format prose the same way every time" instruction block from
the banking showcase agent prompt (#6259) into the reskinnable demo's
banking skin, where the prompt lives in agent.ts rather than the route.
Ports the confirm-before-navigate chat card from the banking showcase
(#6259) into the reskinnable demo's banking skin. The showCharges
human-in-the-loop wiring that consumes it lands separately.
## Summary
- Send the first bounded Slack text delta in stream.start for direct and
managed delivery.
- Keep later appends, continuation messages, and empty-stream cleanup
behavior.
- Add managed delivery integration tests for first-text, empty-chunk,
and empty-stream paths.
## Root cause
Slack received stream.start without text, so it rendered Thinking...
until a separate append arrived.
## Tests
- pnpm nx run-many -t test,check-types,build -p
@copilotkit/channels-slack @copilotkit/channels-intelligence
--skip-nx-cache
- pnpm nx test @copilotkit/react-core --skip-nx-cache
- pre-commit test, publint, and attw checks for 12 affected projects
Points this app at the stack's second seeded organization
(haus-von-haskell) instead of the one banking uses (casa-de-erlang). Org is
resolved from the authenticated cpk key, and seed.sql already provisions three
orgs for exactly this, so it is a key swap with no backend change.
Ports and orgs isolate on independent axes and the second one matters: ports are
a local convention that copying banking's .env over this one silently undoes,
whereas the org key still holds when someone does. Verified against a freshly
seeded stack — writing under one key is invisible to the other even for an
identical user id, so neither demo can read or delete the other's memories.
Also corrects an inherited claim. banking's .env.example warns that non-seeded
ids 403; measured, they do not — GET/POST /api/memories returns 200/201 for an
unseeded id and for a nonsense one, the scope being created on demand. That
mattered because DEMO_DEFAULT_USER_ID is absent from seed.sql, so the warning
implied the unpinned interactive config it recommends was broken. It is not.
## Summary
- stop the Channels agent loop when a tool handler reports an
already-terminal provider delivery
- freeze managed renderer fanout while canonical ingestion records
`RUN_ERROR`
- immediately observe Slack native-stream queue failures while
preserving them for `finish()`
- treat uncertain managed file-delivery errors as terminal delivery
outcomes
## Root cause
The Core run loop converted every tool-handler exception into a
model-visible tool result. After `ChannelProviderDeliveryError` closed
the effect path, the model could continue and emit text, causing Slack
native rendering to call `slack.stream.start` against a closed delivery.
The native stream also retained that rejection in an unobserved internal
promise until `finish()`, leaving a Node unhandled-rejection window.
## Validation
- `pnpm nx run-many -t test check-types -p
@copilotkit/channels-core,@copilotkit/channels-slack,@copilotkit/channels-intelligence
--skip-nx-cache`
- `pnpm nx run-many -t build publint attw -p
@copilotkit/channels-core,@copilotkit/channels-slack,@copilotkit/channels-intelligence
--skip-nx-cache`
- `pnpm nx run-many -t test check-types build publint attw -p
@copilotkit/channels --skip-nx-cache`
- pre-commit affected-package matrix: 17 projects / 24 tasks
This app and the sibling banking demo vendor the same durable-memory stack with
identical seeded ids and api key, so which backend a process attached to was
invisible — a `pnpm dev` pointed at the neighbour's live stack shared its memory
buckets silently, and the presenter reset could mutate the other demo's state.
Surface the target so a human notices:
- Runtime route logs the resolved Intelligence API URL once at startup when
memory is enabled ("[reskinnable-demo] Intelligence: <url> (memory enabled)").
Never logs the api key or license token.
- The destructive presenter reset now names its target: it logs a warning with
the apiUrl and the exact user ids before forgetting, and includes the resolved
apiUrl in both the success and error response bodies so the caller can see
which backend was mutated. Behaviour is otherwise unchanged (same ids forgotten,
same PRESENTER_RESET_ENABLED gate, no confirmation prompt); only observability
improves. route.test.ts updated to assert the new apiUrl field, still exact.
reskinnable-demo was cloned from examples/showcases/banking and vendors an
identical Intelligence (durable-memory) docker stack — same seeded ids
(jordan-beamson / morgan-fluxx / northwind-demo-user), same INTELLIGENCE_API_KEY
and org. Both apps identified their backend purely by address, and both pointed
at http://localhost:7050. So with banking's stack already up, a bare `pnpm dev`
here (which, unlike run-demo.sh, checks no port bind) would silently attach to
banking's backend and read/write the SAME memory buckets — worst case, the
presenter reset button forgetting the neighbour demo's memories.
Shift every published host port by +200 (705x/715x -> 725x/727x) in lockstep
across compose, env, scripts, e2e config and tests so attaching to the wrong
stack is impossible by accident:
app-api 7050 -> 7250
gateway 7053 -> 7253
postgres 7156 -> 7256
redis 7158 -> 7258
minio API 7160 -> 7260
minio console 7161 -> 7261
bundled TEI 7167 -> 7267
The native Metal TEI on :7067 is left shared ON PURPOSE: it holds no demo state
(buckets live in the now-isolated postgres/redis), the same TEI version + model
yields byte-identical embeddings, and run-demo.sh reuses it when healthy rather
than forcing a second ~20x-slower model load. A comment records why.
Two related Inspector-telemetry tickets: **OSS-566** and **OSS-568**.
## OSS-566 — explicit "Inspector opened" event
There was no event recording that the panel was opened. Opens could only
be inferred from in-panel activity (~1,655/90d, a floor) or from
`banner_clicked` cta=`body` (~511), which misses the common
floating-button path entirely.
Adds `oss.inspector.opened` with:
| property | values |
|---|---|
| `open_source` | `floating_button` \| `announcement_preview` |
| `has_unseen_announcement` | whether an announcement was on screen at
open time |
| `license_status` / `runtime_mode` / `runtime_url_type` | same
segmentation the threads events already carry |
| `package_name` / `package_version` / `inspector_distinct_id` | version
segmentation |
**Restoring a persisted-open panel deliberately does not count.**
Restore assigns `isOpen` directly instead of routing through
`openInspector()`, so page reloads — and every `next dev` hot reload —
stay out of the number.
## OSS-568 — banner surface + first-class dismissal
1. **`surface` on `banner_viewed`** — `collapsed_preview` (bubble on the
collapsed widget) vs `expanded_card` (card inside the opened panel),
stamped at fire time. Dedup is now per `(banner, surface)` instead of
per banner, so opening the panel records the card impression as its own
signal.
2. **`oss.inspector.banner_dismissed`** — emitted **in addition to**
`banner_clicked { cta: "dismiss" }`, not replacing it, so dashboards
reading the `cta` value keep working. Carries `surface` too, separating
"swatted the bubble away" from "dismissed the card after opening".
Both new events clear the sink's `oss.inspector.` prefix gate, so **no
telemetry-sink deploy is needed**.
## Testing
- **`packages/web-inspector` full suite — 112 passed (4 files)**, run
locally in the worktree:
```
✓ dev/css-raw-import.spec.ts (1 test) 1ms
✓ src/__tests__/telemetry-egress-guard.spec.ts (3 tests) 2ms
✓ src/lib/__tests__/telemetry.test.ts (28 tests) 8ms
✓ src/__tests__/web-inspector.spec.ts (80 tests) 890ms
Test Files 4 passed (4)
Tests 112 passed (112)
```
- **New coverage**: payload shape for `opened` / `banner_dismissed`,
incl. an allow-list assertion that no content/PII key can be added
accidentally; collapsed→expanded surface sequence on open; per-surface
dedup; open attribution for both sources; no event for an already-open
panel; no event for a restored-open panel; nothing emitted when the
runtime reports `telemetryDisabled`; an open still recorded while the
runtime is disconnected.
- **`tsc --noEmit`** on `@copilotkit/web-inspector`: clean (after
building `core` + `shared` dist in the worktree).
- **`tsdown` build**: succeeds; the test-only egress-guard helper is
**not** present in `dist/`.
- **`oxfmt --check`**: clean. **`oxlint`**: 9 warnings, all
pre-existing.
- `@copilotkit/runtime` (1,760) and `@copilotkit/shared` (199) also
green — both are back on main's own test files in this PR.
## A test-only egress guard rides along
`vitest.setup.ts` installs a fetch guard that swallows requests to the
telemetry sink. This is **not** CI plumbing — it is a prerequisite for
the new events. These tests run in jsdom, where a real `fetch` exists,
and inspector telemetry is fire-and-forget, so any test that drives a
banner / threads / open path without stubbing fetch POSTs a real
`oss.inspector.*` event to the live sink, from developer machines as
well as CI. The announcement-dismissal tests were already doing this;
the new `opened` / `banner_dismissed` tests hit the same send path. No
environment variable can prevent it, because the inspector's opt-out
arrives in the runtime's `/info` response and these tests never boot a
runtime.
## Not in scope
Suppressing telemetry from CI jobs that boot real apps (**OSS-565**) was
explored on this branch and removed. It needs a mechanism that does not
depend on the `/info` handshake — the env → `/info` → core chain is
asynchronous, so an early interaction beats it. That ticket stays open
and unaddressed here.
Closes OSS-566, OSS-568.
## Summary
- keep participant actor metadata model-visible in Slack transcript
history
- keep own-channel assistant history equal to the provider-visible
message content
- preserve structured actor and provider message metadata returned by
`thread.getMessages()`
## Root cause
The delivery adapter prefixed every transcript entry with the untrusted
participant metadata envelope before assigning AG-UI roles. Own-channel
transcript entries were then assigned the assistant role with that
participant-style prefix still in their content, so the model received
the prefix as prior assistant output and could reproduce it on the next
turn.
## Validation
- `pnpm nx test @copilotkit/channels-intelligence --skip-nx-cache` (17
files, 117 tests)
- `pnpm nx run-many -t check-types build -p
@copilotkit/channels-intelligence --skip-nx-cache`
- repository pre-commit Nx test/publint/attw suite
Resolves [OSS-641](https://linear.app/copilotkit/issue/OSS-641). Mike's
report: *"You have to `await channels.ready()` for it to connect to the
Realtime Gateway. Seems like there's some clunkiness to creating the
runtime and getting it connected."* He then picked the fix: *"I think it
should autostart in the long running wrappers."*
## What changes
**`createCopilotNodeListener` and `createCopilotExpressHandler` start
activation at creation.** A declared Channel connects because it was
declared; `channels.ready()` becomes await-and-observe rather than the
call you must remember. Failure-mode asymmetry is the argument:
forgetting `ready()` today gives you a process that serves HTTP, looks
healthy, and is silently disconnected with **zero output**, while
auto-start's worst case is an activation error in the logs.
**`createCopilotRuntimeHandler` and `createCopilotHonoHandler` stay
lazy.** The generic Fetch handler is the serverless/edge entry point —
isolates freeze and recycle per request, so separate cold starts would
mint competing listeners for the same Channel (the reason activation was
deferred in `fbf35ac59` in the first place). Hono keeps that behavior
because it is our Next.js App Router surface in practice: every route
handler in `examples/showcases/*` (banking, mcp-apps,
generative-ui-playground, oracle-agent-memory) plus the vue/nuxt demo
builds one at module scope. Its TSDoc now states why, loudly, so nobody
"finishes the job" later.
`activateChannels: false` remains the clean opt-out that opens no
socket.
## Consequence for host code: the shutdown boundary moves earlier
Signal handlers must now be registered **before the listener is
created**, not merely before `ready()`. Otherwise a Ctrl-C during the
connect window hits Node's default handler and leaks a live gateway
session. `examples/slack`, `examples/teams`, and the docs snippets are
restructured to wire teardown before the listener exists (a
`stopChannels`/`teardown` binding assigned in the same tick as
creation). **Worth calling out in the changelog** — it is the general
hazard for any user code that registers shutdown after mounting.
## Failure semantics
Fire-and-forget by necessity, since a factory is synchronous. Set-level
failures log at `error`; per-Channel failures keep their existing `warn`
breadcrumbs; an up-front misconfiguration (duplicate/missing Channel
names) now surfaces as a logged error at creation rather than a throw
out of the factory — the factory still never throws. `ready()` stays
idempotent and one-shot, so a host that *does* await it observes this
activation's outcome, including its rejection, rather than triggering a
second one.
## READMEs
Every `channels-*/README.md` quickstart built the *generic* handler and
needed `await handler.channels.ready()` — for a socket-mode Slack bot, a
request handler you construct and never serve, which is likely closer to
what actually felt clunky. All seven now use the Node listener, so they
inherit auto-start and agree with the docs-site quickstarts. No new
public surface: a bot-only `startChannels(runtime)` host was the
alternative and is deliberately not taken here.
## Testing
- **`packages/runtime` unit suite: 1815 passed / 128 files** (`npx
vitest run`), including 9 tests in `endpoints-channels.test.ts`
covering: auto-start on node + express; Hono still lazy;
`activateChannels: false` opens no socket; a failed auto-start logs
instead of leaving an unhandled rejection (asserted via an
`unhandledRejection` listener) and the reason survives to a later
`ready()`; a duplicate-name misconfig logs without throwing; and two
wrappers over one runtime activate once (the per-runtime manager cache
is load-bearing now that *construction* activates).
- **`examples/slack`: 63 passed / 12 files; `examples/teams`: 2 passed /
1 file** (`npm test` in each).
- **Typecheck clean:** `examples/slack` and `examples/teams` (`tsc
--noEmit`), plus a full `@copilotkit/runtime` tsdown build.
- **Lint/format clean:** `oxlint` reports 0 findings in every changed
file (the 21 warnings in that run are pre-existing, all in untouched
example render/tool files), `oxfmt --check` passes on all 9 changed
source files.
- **Docs:** verified no stale lifecycle claims remain (`opens no
connection` / `ready() is required` / `control surface` guards) across
`docs/channels/**` and the Slack + Teams platform guides.
🤖 Generated with [Claude Code](https://claude.com/claude-code)