## Release monorepo v1.64.1
**Scope:** `monorepo` | **Bump:** `patch`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `monorepo` packages to `1.64.1`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `monorepo` packages to npm at version `1.64.1`
- Creates git tag `monorepo/v1.64.1`
- Creates a GitHub Release with the final release notes
### Before merging
- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)
---
> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
Fixes FAC-61.
## Summary
- Documents the full Google ADK voice runtime route path:
`app/api/copilotkit-voice/[[...slug]]/route.ts`.
- Explains how browser calls to `/api/copilotkit-voice` map through the
Next.js runtime to the ADK backend `/voice` endpoint.
- Keeps the ADK-specific backend paragraph gated to integrations that
declare `voice_backend_pattern: adk-fastapi-agent-path`, so other
framework voice pages do not show Google ADK prose.
- Replaces the docs `voice-page` snippet with a minimal `CopilotKit` +
`CopilotChat` example so the docs no longer show the live demo-only
`VoiceChat` wrapper as copy-paste app code.
- Normalizes docs-only `*.snippet.*` caption display so
`page.snippet.tsx` renders as `page.tsx` while still remaining a
docs-only extraction file.
- Corrects the sample-audio button prose to match the actual
implementation: the button emits canned text through `onTranscribed`,
and the parent chat component performs the textarea insertion.
## Acceptance Evidence
- Full route path: `voice.mdx` names
`app/api/copilotkit-voice/[[...slug]]/route.ts`.
- Backend endpoint: `voice.mdx` explains that `voice-demo` is registered
with `HttpAgent` at `${AGENT_URL}/voice`, and that the Python ADK server
mounts agents at `path=f"/{agent_name}"`.
- Framework scoping: the ADK backend paragraph is wrapped in
`<WhenFrameworkHas flag="voice_backend_pattern"
equals="adk-fastapi-agent-path">`; only the Google ADK manifest declares
that value.
- No undefined docs component: the extracted `voice-page` region lives
in `page.snippet.tsx` and imports `CopilotChat` and `CopilotKit` from
`@copilotkit/react-core/v2`; the rendered caption is normalized to
`page.tsx`; the live route still uses `VoiceChat` outside the docs
region.
- Source inference removed: the page explicitly connects `runtimeUrl`,
`basePath`, catch-all runtime routes, and the backend `/voice` hop in
prose.
## Validation
- `git diff --check`
- `pnpm -C showcase/scripts validate-manifests`
- `pnpm -C showcase/scripts bundle-content`
- `npm run typecheck` in `showcase/shell-docs`
- `npm run lint` in `showcase/shell-docs`
- `npm run build` in `showcase/shell-docs`
## What this is now
Opt-in instrumentation for the channel egress path, plus a
replay-stability guard. **Measurement and safety only — no behaviour
change to how frames are sent.**
It started as a fix too. That fix is reverted here; see below.
## The problem it measures
A streaming reply pushes one render frame per AG-UI token and awaits
each durable acceptance receipt before sending the next, so reply
latency scales with token count. Nothing recorded that, so the cost was
invisible and got attributed to the Connector Outbox's 400ms poll
instead.
```bash
COPILOTKIT_CHANNELS_RENDER_METRICS=summary # one summary per turn
COPILOTKIT_CHANNELS_RENDER_METRICS=frames # also one line per frame
```
Unset means off and no collector is allocated. Output goes to the
transport `log` callback when configured, else the console.
## What it found, measured against production
Hosted api + realtime gateway, local runtime, fixture agent emitting
exactly 120 deltas 15ms apart:
```
pushMsMean: 478.43 pushMsP50: 463 pushMsP95: 589 pushBlockedPct: 99.67
```
**478ms per round trip against an ~85ms network RTT**, so roughly 390ms
of every push is server-side work — the `lookupClerkOrgId` query, the
off-cluster ops-api entitlement call, the read-model build, and the
Redis publish. Intelligence#627 removes those for `text_delta` frames.
At 478ms a push, a 120-token reply costs ~123 round trips ≈ **59s**.
That matches the original "sometimes many seconds" report.
## Why the fix was reverted
Coalescing adjacent text deltas worked — 123 frames to 4, a production
turn from ~59s to 3.4s — and broke replay.
A turn's id is a pure function of its delivery id (`turn_<deliveryId>`,
enforced by app-api) and the adapter resets the per-turn seq counter on
every dispatch, so a redelivery replays into the **same seq space**.
app-api treats that space as an idempotency key: same payload at a seq
is `duplicate_accepted`, a different payload is
`CHANNEL_RENDER_FRAME_CONFLICT`, which nacks the delivery. Merge
boundaries depend on wall-clock arrival, so a replay cuts them
differently:
| | attempt 1 (slow, merged) | attempt 2 (redelivery, faster) |
|---|---|---|
| seq 1–3 | *merged away, never pushed* | `"a"`, `"b"`, `"c"` → INSERT ✓
|
| seq 4 | `text_delta "abcd"` | `text_delta "d"` → **CONFLICT** ✗ |
Before coalescing, deltas were 1:1 with AG-UI events and a replay was
byte-identical. I verified both directions: the new test passes on the
pre-coalescing adapter and fails with coalescing. The regression was
mine.
`render-frame-replay.test.ts` stays as a standing guard — it passes with
frames 1:1 and fails for any change that makes a frame's payload at a
given seq depend on timing.
## The replacement
Batch the **transport**, don't merge frame **content**: send the
buffered frames as an array in one request. Same collapse in round
trips, `seq k` still carries `delta k` so replay stays byte-identical,
and seqs stay dense so the outbox can later require contiguity
(OSS-653).
That needs an app-api batch accept and a gateway envelope, so it ships
after launch with its own deploy ordering (app-api → gateway → SDK)
rather than two days before.
## Also found, filed separately
OSS-653 — the outbox advances an in-memory cursor to the highest seq it
read (`WHERE seq > cursor ORDER BY seq ASC`), so a frame accepted out of
order is skipped and never rendered. Pre-existing and independent of
this PR. Not durable corruption: thread history is rebuilt from all
acceptance rows ordered by seq, independent of that cursor, so threads
read back complete.
## Validation
```
nx run-many -t test check-types build --projects=@copilotkit/channels-intelligence --skip-nx-cache
```
216 tests pass (12 files), check-types and build clean, `oxlint` clean
on all changed files.
refs OSS-648
Coalescing adjacent text deltas cut a 120-token reply from 123 frames to 4 and
took a production turn from an extrapolated ~59s to 3.4s, but it broke replay.
A turn's id is a pure function of its delivery id (turn_<deliveryId>, which
app-api enforces) and the adapter resets the per-turn seq counter on every
dispatch, so a redelivery replays into the same seq space. app-api treats that
space as an idempotency key: the same payload at a given seq is
duplicate_accepted, a different payload is CHANNEL_RENDER_FRAME_CONFLICT, which
nacks the delivery. Merge boundaries depend on wall-clock arrival, so a replay
cuts them differently and collides — attempt 1 stores "abcd" at seq 4, attempt 2
pushes "d" there. Before coalescing, deltas were 1:1 with AG-UI events and a
replay was byte-identical, so this worked; the regression is mine.
Keep the instrumentation, which is what found the problem and is what will
measure the replacement. Add the replay-stability test as a standing guard: it
passes with frames 1:1 and fails for any change that makes a frame's payload at a
given seq depend on timing.
The cost problem is real and unfixed. The replacement is to batch the TRANSPORT
(many frames per request) rather than merge frame CONTENT: it collapses the same
number of round trips, keeps seq k carrying delta k so replay stays
byte-identical, and keeps seqs dense so the outbox can later require contiguity
(OSS-653). That needs an app-api batch accept and a gateway envelope, so it ships
after launch with its own deploy ordering rather than two days before.
## Summary
Restores Angular Showcase parity with the current React feature
contracts and makes the paired browser audit distinguish Angular
regressions from failures shared by both frontends.
The Angular host is still one shared application. Agent frameworks can
differ behind the runtime boundary, but BuiltInAgent no longer needs
frontend-only route or default-agent fallbacks: its current demos expose
the same named runtime and agent contracts used by React.
## What changed
### Angular runtime parity
- align BuiltInAgent runtime routes and named agent IDs with the current
React demos
- remove stale BuiltIn-only route, agent, reasoning, and interrupt
branches
- configure dynamically created chats with their agent and thread before
component construction, preventing the transient `default` agent lookup
against named-only runtimes
- preserve the remaining integration-specific behavior only where the
backend interaction contract genuinely differs
- preserve currency values in assistant messages without breaking inline
KaTeX rendering
- align Angular A2UI row and column schemas with the shared layout
contract
- expose authoritative running state and prevent an unnecessary
follow-up run in the complete headless demo
- reset interrupt selection state between repeated turns
### Audit and probe hardening
- merge exact-cell shard artifacts and validate source, fixture, and
feature-contract identity
- classify paired results as passed, Angular regression, shared failure,
Angular improvement, React-only, identity mismatch, or missing
counterpart
- make Angular-only failures, identity drift, and missing counterparts
blocking while keeping shared failures visible
- use rendered-surface completion for tool-rendered probes while
retaining lifecycle gates
- avoid duplicate or empty submissions in the headless-simple probe
- require interrupt resolution and resumed-run completion before
advancing
- use turn-scoped assistant content where narration is an accepted
shared contract
- add focused regression tests for Angular bindings, dynamic component
creation, matrix lifecycle handling, parity classification, and
representative probes
No new CI workflow is added by this PR.
## Scope of the parity claim
The audit proves the narrower parity condition: Angular has no failure
in a cell where React passes at the same source revision. Shared
React/Angular failures remain reported but are non-blocking, so this
does not claim that every supported cell is green.
## Validation
Final rebased head: `aeb503c37` on `d80cc9244` (`main`).
Current-head local validation:
- `pnpm nx run @copilotkit/showcase-scripts:test --skip-nx-cache
--output-style=static` — 74 files, 2,366 tests passed
- `pnpm nx run @copilotkit/showcase-harness:test:ci --skip-nx-cache
--output-style=static` — 175 files passed, 2 skipped; 3,672 tests
passed, 18 skipped
- `pnpm nx run @copilotkit/showcase-harness:test:quarantine-ratchet
--skip-nx-cache --output-style=static` — passed
- `pnpm nx run @copilotkit/showcase-harness:typecheck --skip-nx-cache
--output-style=static` — passed
- `pnpm nx run @copilotkit/angular:check-types --skip-nx-cache
--output-style=static` — passed with dependencies
- `pnpm nx run-many --targets=typecheck,test,build
--projects=@copilotkit/showcase-angular-host --parallel=1
--skip-nx-cache --output-style=static` — passed; 20 Angular files and
168 tests passed
- Angular browser build passed its performance-budget and artifact
audits
Focused BuiltInAgent regression rerun at the equivalent pre-rebase
implementation head `b9d51551c`:
- all six previously deterministic Angular failures passed: auth,
multimodal, agent-config, voice, shared-state-read, and
a2ui-fixed-schema
- 68 exact React/Angular cells classified into 35 feature comparisons
- 25 paired passes
- 7 shared failures
- 1 Angular improvement
- 2 React-only cells
- zero Angular regressions
- zero identity mismatches
- zero missing counterparts
The earlier exhaustive browser audit also passed in [GitHub
Actions](https://github.com/CopilotKit/CopilotKit/actions/runs/30065131330):
- 1,296 exact cells: 660 React and 636 Angular
- 497 paired passes
- 46 Angular improvements
- 93 shared failures
- 24 React-only cells
- zero Angular regressions, identity mismatches, or missing counterparts
## QA Factory Coding Update
**Ticket**
FAC-49: Starter CLI dev fails: intelligence container unhealthy, missing
intelligence_app DB
https://linear.app/copilotkit/issue/FAC-49/starter-cli-dev-fails-intelligence-container-unhealthy-missing
**Implemented Behavior**
For each of the 7 integration starters (`adk`, `agno`, `llamaindex`,
`mastra`, `ms-agent-framework-python`, `pydantic-ai`, `strands-python`),
the following files were created or modified:
**1. `examples/integrations/<slug>/docker-compose.intelligence.yml`** (7
new files)
A self-contained local dev stack with three services:
- `postgres` (pgvector/pgvector:0.8.2-pg16) — mounts
`./docker/postgres-init:/docker-entrypoint-initdb.d:ro` (not `./docker`
wholesale), healthcheck `pg_isready -U intelligence -d intelligence_app`
- `redis` (redis:7-alpine)
- `intelligence` (cpki/intelligence-composite:local, built from
`${INTELLIGENCE_REPO:-../../../Intelligence}`) — `MEMORY_ENABLED:
"false"`, `SL_ENABLED: "false"` for minimal dev, exposes `:4201` (api)
and `:4401` (gateway)
**Acceptance Criteria**
- **Per-starter docker-compose.intelligence.yml present and valid:** For
each of the 7 integration starters,
`examples/integrations/<slug>/docker-compose.intelligence.yml` exists
and defines services `postgres`, `redis`, and `intelligence`. The
postgres service mounts `./docker/postgres-init` (not `./docker`) to
`/doc...
- **Postgres init SQL creates the required databases:** For each of the
7 starters,
`examples/integrations/<slug>/docker/postgres-init/01-create-databases.sql`
exists and contains `CREATE DATABASE intelligence_app` and `CREATE
DATABASE intelligence_app_shadow`. Observable: `cat
examples/integrations/<slug>/docker/post...
- **Postgres healthcheck targets intelligence_app:** In each
`docker-compose.intelligence.yml`, the postgres healthcheck command
contains `pg_isready -U intelligence -d intelligence_app`. Observable:
`grep "intelligence_app"
examples/integrations/<slug>/docker-compose.intelligence.yml` returns a
match on the healthche...
- **README intelligence section present:** Each of the 7 `README.md`
files contains a section titled `## CopilotKit Intelligence` (or close
variant) that references `docker-compose.intelligence.yml` (not a
relative path outside the starter directory). Observable: `grep -E
"docker-compose.intelligence|CopilotKit Intell...
- **showcase/integrations is untouched:** No new files appear under
`showcase/integrations/` for any of the 7 slugs. Observable: `git diff
--name-only main HEAD -- showcase/integrations/` is empty for these
paths. Forbidden: any change to `showcase/integrations/`.
- **Existing CI tests still pass:** `nx run scripts:test` (or
equivalent) exits 0 — specifically the
`integration-intelligence-migration.test.ts` suite which validates the
route.ts shape for all 7 starters. Observable: test run exit code 0, no
skipped tests on the existing route.ts assertions. Forbidden: marking
exist...
**Testing Scope**
1. **HEAD unchanged**: `git rev-parse HEAD` →
`8a8e52d9e90ad09ec42455b203cc7a2aa6c5fd2f` ✓
2. **No remotes**: `git remote -v` → empty ✓
3. **All 7 docker-compose.intelligence.yml present**: `find ... -name
docker-compose.intelligence.yml` shows all 7 ✓
4. **All 7 SQL init files present**: checked with `find` + `cat` ✓
5. **YAML structure validation (Python yaml.safe_load)**: all 7 parse
successfully with services `{postgres, redis, intelligence}`, postgres
mounts `./docker/postgres-init`, healthcheck contains `intelligence_app`
✓
6. **Postgres init SQL**: all 7 contain `CREATE DATABASE
intelligence_app;` and `CREATE DATABASE intelligence_app_shadow;` ✓
**Showcase Impact**
Not applicable. Changes are entirely within `examples/integrations/` —
adding `docker-compose.intelligence.yml`,
`docker/postgres-init/01-create-databases.sql`, and README sections to
the 7 integration starters. No files under `showcase/integrations/` or
`showcase/` were touched. The showcase system is governed by s...
**Pull Request**
Branch `qa-factory/fac-49-20260728195649` from base `main` at
`8a8e52d9e90ad09ec42455b203cc7a2aa6c5fd2f`. 21 files are left as
uncommitted worktree changes for the QA Factory executor to commit and
push with QA-bot identity. HEAD remains at
`8a8e52d9e90ad09ec42455b203cc7a2aa6c5fd2f` (unchanged). No remotes in
checko...
Detailed current-head testing evidence belongs in the Review workflow PR
comment.
## Release channels v0.4.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.4.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.4.0`
- Creates git tag `channels/v0.4.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.
A streaming reply emitted one render frame per AG-UI token and awaited each
durable acceptance receipt before sending the next, so reply latency scaled
with token count: a 100-token reply cost 100 serialized round trips. Against a
hosted API that is tens of seconds for a long reply, and it dominated every
other cost in the egress path.
Frames now land in a buffer that a single pump drains, folding adjacent
text deltas for the same message into one frame. The buffer only ever holds
what arrived while a push was already in flight, which is exactly the set the
old serial chain kept queued, so this waits on no timer and adds no latency.
Batch size self-tunes: the slower the round trip, the more deltas merge.
Ordering is preserved by keeping seq allocation at enqueue time, so a discrete
post or update interleaved mid-run still orders correctly against the run's own
frames. A merged frame carries the seq of the last delta it absorbed; skipped
seqs are never pushed, and the accepted high-water mark still advances. Only
adjacent same-message deltas merge, so a tool call or a second message ends the
run. Merged text is capped at 12k chars, under the platform's 40k delta limit
and within one Slack appendStream call.
Measured with the instrumentation from the previous commit, on a 40-token reply
with a 100ms round trip and tokens arriving every 10ms: 42 frames and 4242ms
before, 7 frames and 706ms after, with 40 deltas folded into 5 pushes and no
text lost.
## Release monorepo v1.64.0
**Scope:** `monorepo` | **Bump:** `minor`
---
### 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.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 `monorepo` packages to npm at version `1.64.0`
- Creates git tag `monorepo/v1.64.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.
Channel replies are slow in proportion to how many tokens they contain. The
run renderer pushes one render frame per AG-UI event and awaits its durable
acceptance receipt before sending the next, so a streaming reply costs one
round trip per token. Nothing recorded that, so the cost was invisible: no
frame counts, no per-push latency, no measure of how much of a turn was spent
waiting.
Add opt-in instrumentation at the renderer's serial push chain, the one point
every frame passes through, so it covers the HTTP render-accept transport and
the realtime-gateway transport with one seam. Set
COPILOTKIT_CHANNELS_RENDER_METRICS=summary for a per-turn summary, or =frames
to also log a line per frame. Unset means off and no collector is built.
The summary reports frame counts by kind, characters carried per text_delta
frame, total/mean/p50/p95/max push latency, and the share of the turn spent
blocked on receipts. charsPerTextFrame and pushBlockedPct are the two numbers
that show whether a batching change helped: one round trip per token reads as
a single-digit charsPerTextFrame and a pushBlockedPct near 100.
Measurement only. No change to frame ordering, sequencing, or delivery.
## Problem
Slack legacy fallback posted `_thinking…_` before replacing it with the
reply, so native status and response content appeared as duplicate
thinking UI.
## Why
When `chat.startStream` falls back, the first legacy post should contain
available response text, not a second thinking indicator.
## Fix
Seed the first legacy Slack post with transformed response content and
retain continuation placeholders for later chunks. Add direct-legacy and
native-start-failure regressions, including status clearing.
Refs OSS-635. Global formatting currently awaits
[#6210](https://github.com/CopilotKit/CopilotKit/pull/6210).
## Problem
All five `OSS-599` references in
`packages/runtime/src/v2/runtime/core/channel-manager.ts` describe the
missing gateway/canonical/reliability wiring for **direct** Channels as
*"deferred"*:
> it is NOT wired into the Intelligence gateway/canonical/reliability
layer (deferred, OSS-599)
That reads as pending work — as though a direct Channel eventually
reaches managed parity.
**OSS-599 says the opposite.** Its boundary discipline places
run-correctness (canonical cross-surface history, fenced
outer-run/single-terminal, durable HITL-resume-across-restart, selection
pinning) and the reliability layer **Intelligence-side only**, and
states plainly that shipping an SDK-side equivalent *"collapses the
build-vs-buy moat"*. A direct Channel's ceiling is the SDK's in-process
run loop, permanently.
So the comments point the next reader at implementing precisely the
thing the ticket forbids.
## Change
Reword all five sites to say the boundary is by design, not pending:
| Site | Was | Now |
|---|---|---|
| `ChannelStatus` doc | "(deferred, OSS-599)" | "BY DESIGN, not a
deferral" + why, + "do not 'finish' this by pulling the layer into the
SDK" |
| `ChannelManager` class doc | "wiring … is deferred" | "stay below the
canonical/reliability layer by design" |
| `activate()` inline | "is deferred (OSS-599)" | "by design, not
pending work (OSS-599)" |
| `startDirectChannel` doc | "(deferred, OSS-599)" | "that boundary is
permanent, not a deferral" |
| direct-start log string | "wiring deferred (OSS-599)" | "stay below
the canonical/reliability layer by design (OSS-599)" |
Comments and one log string only. **No behavior change.**
## Testing
- **Log-string assertion preserved.** `channel-manager.test.ts:611-619`
asserts the direct-start breadcrumb contains `"direct adapter"` and
`"ɵruntime.start()"`. Both substrings survive the reword — only the
parenthetical changed.
- **Test run + control.** Ran `channel-manager.test.ts`,
`channel-manager-reconnect.test.ts`, and
`channel-activation-config.test.ts` in the worktree: `4 failed | 56
passed`. Ran the same suite on an **unmodified `origin/main`** worktree
as a control: `4 failed | 34 passed` — the *identical* four failures.
The four are a worktree artifact, not a regression:
`@copilotkit/channels` resolves to the outer checkout's pre-#6145 build,
which has no `ɵruntime`, so every "real direct transport" test fails
there. Same failures before and after the change; this diff adds none.
CI (with a correct install) is the real gate.
- **Formatted** with `oxfmt`.
## Notes
Pre-commit hooks were bypassed: `test-and-check-packages` runs `nx` in
the worktree, where `@copilotkit/core:build` fails for unrelated
environment reasons. The change is comments-only.
Follow-up, not in this PR: OSS-599 was written the day before Plan C
shipped, so its own framing ("a DIY runner is ~15 lines over
`ɵruntime.start()`", "a DIY runner gets this") is stale now that the DIY
path is removed. Its §2 response-policy and four-mode-binding scope is
unaffected. I'm leaving a reconcile note on the ticket.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Problem
Calling `thread.runAgent()` without an explicit prompt can omit the
inbound Channel message when reconstructed history excludes the
in-flight turn. Applying the fallback unconditionally would duplicate
that turn in native adapters whose conversation stores already seed it.
## Why
`Thread.runAgent()` had no adapter-declared signal for whether the
conversation store already supplied the inbound message, and a text-only
fallback would discard multimodal content parts.
## Fix
Add `ConversationStore.seedsInboundTurn`, declare the native-adapter and
Intelligence behaviors, and default an omitted prompt only when the
store has not already seeded the turn. Prefer non-empty inbound
`contentParts` over `message.text`, preserve explicit prompts, and add
composition plus adapter regression coverage for both paths.
Closes OSS-646. Split out of OSS-641 as the unambiguous half. This PR
does **not** change when activation happens — whether the long-running
wrappers should auto-connect stays open on OSS-641.
## Why
`createCopilotRuntimeHandler` builds the `ChannelManager` but opens no
connection; activation is lazy, triggered by the first
`channels.ready()`. That is deliberate (`fbf35ac59`, OSS-473) —
Cloudflare/Next isolates freeze and recycle per request, so cold starts
would mint conflicting listeners. Two things were left inconsistent with
it:
1. `endpoints/node.ts` still documented the pre-`fbf35ac59` world — "the
same `ChannelsControl` surface the underlying fetch handler **activates
at creation time**" — and labelled the one required call as `//
Optional:`. That's the TSDoc developers and coding agents see in-editor,
and it contradicted every channel-package README. Same failure class as
OSS-634.
2. `68349bc1f` gave the fetch handler a branded overload so
`handler.channels.ready()` type-checks without `?.`, but the node
wrapper never got it — so every call site, including our own example and
all nine showcase docs pages, was written defensively.
### A live consequence, found en route
`examples/slack/app/managed.ts` never called `ready()`. It built the
runtime, mounted the listener, logged `[channel] started managed Channel
"…"`, and only ever called `stop()` — so since activation went lazy it
has connected nothing while reporting success. It was written against
exactly the creation-time model the TSDoc described. Fixed here, with a
regression assertion.
## What changed
- **Types** — `createCopilotNodeListener` gets the branded overload pair
mirroring `createCopilotRuntimeHandler`: a runtime with at least one
declared Channel yields non-optional `.channels`; `activateChannels:
false` and channel-less runtimes keep the optional shape. Adds
`NodeCopilotListenerWithChannels`; both listener types are now exported
from `@copilotkit/runtime/v2/node`.
- **Docs** — node/express/hono TSDoc corrected: creation opens no
connection, `ready()` is what activates, and it is required on a
long-running host. Same stale claim fixed in the three example comments
and `examples/slack/README.md` that repeated it.
- **Call sites** — `?.` dropped from `examples/slack`, `examples/teams`,
both READMEs, and the nine `showcase/shell-docs` channel pages.
## Deliberate scope choices, called out
- **Express/Hono keep an optional `.channels`.** Only their TSDoc is
corrected here. Their own type docs name Node as the lifecycle-owning
surface and attach `.channels` best-effort, so the branded overload is
Node-only for now; `endpoints-channels.test.ts` still uses `!` for those
two. Say the word if the overload should extend to them.
- **The non-optional shape requires a literal `channels` tuple**
(`readonly [Channel, ...Channel[]]`). A runtime built from a
dynamically-assembled `Channel[]` is unbranded and still needs `?.`. Now
stated in the node TSDoc.
- **`examples/slack/app/managed.ts` now exits nonzero if activation
fails**, where before it stayed up serving HTTP with nothing connected.
Intentional — fail loud, and it matches `index.ts`. Note that `ready()`
resolves for `setup_required`, so a declared-but-unprovisioned channel
still logs as started.
- **Signal handlers are registered before awaiting activation** in
`managed.ts`, so a Ctrl-C inside the 30s activation window still tears
the Channel down instead of hitting Node's default handler.
## Verification
- **Type contract, red → green:** the new `KeyIsRequired<typeof
listener, "channels">` assertion in `handler-channels-types.test.ts`
failed to compile before the overload (`error TS2344: Type 'false' does
not satisfy the constraint 'true'`) and passes after.
- **Example bug, red → green:** stashing only `managed.ts` fails the new
guard with `expected "vi.fn()" to be called once, but got 0 times`.
- **Strict-null proof:** `slack-example` and `teams-example` both `tsc
--noEmit` clean under `strict: true` with the `?.` removed. This matters
because the runtime package compiles with `strict: false`, so its own
type test can only probe the optionality modifier structurally.
- Runtime channel suites 54/54; slack example 63/63.
- **Coverage limit:** the `managed.ts` guard is mocked — it proves the
example *calls* `ready()` with a bound, not that a Channel connects.
Nothing in CI exercises a real gateway connect for these examples.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Follow-up to #6136, which merged while this was being written. That PR
changed the demo substantially and the README still described an older
build.
## Corrected
| Was | Now |
| --- | --- |
| `CopilotPopup` | Left-docked `CopilotSidebar`, two columns (thread
rail + conversation), app nav rail on the right |
| learned memory is `procedural` | `operational` — what the code
actually writes |
| Memory tab is "top-k recall, not a full enumeration" | "Stored
memories (N)" — a complete enumeration aggregated across identities;
semantic search is a separate box |
| per-run reset is "a separate follow-up" | Ships behind
`PRESENTER_RESET_ENABLED`; also re-seeds memory and returns to the bare
root |
| `showTransactions` is a `useFrontendTool` | `useComponent`, so it
persists in the transcript |
| "change my Visa PIN from the team page" via navigation | PIN is a
component in the chat; the agent never receives the digits |
## Added
- The **eight-pill demo script** as a table, with which beats need
Intelligence mode
- The `/charges` page and the **PDF / Q2 multimodal** beat
- **Seeded memory** — and why the over-limit procedure is deliberately
*not* seeded (seeding it would remove the teach beat entirely)
- The **components-not-markdown-tables** rule, including why `showTable`
renders nothing below two rows
- Per-team chart colour via `teamColor()`
- The two **incompatible helper families** in `actions.ts` —
destructuring `ok` off the wrong one reports a success as a failure,
which bit twice during #6136
## Screenshots
All three replaced. They showed the old right-docked chat layout, so
they contradicted the first paragraph of the README. New captures are
from the shipped build (filed report dashboard, chat with components +
tool-activity line, recording vignette).
Docs only — no source changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Makes `new CopilotKitIntelligence({ apiKey })` the whole managed-service
setup. `apiUrl` and `wsUrl` were required on every construction, so the
two correct hosts had to be found and copied by hand — which is how an
agent came to invent them. Linear:
[OSS-638](https://linear.app/copilotkit/issue/OSS-638/add-default-rest-and-ws-api-urls-to-intelligence-sdk).
## The change
Both fields are now optional and default to CopilotKit's managed
platform (`https://api.intelligence.copilotkit.ai` /
`wss://realtime.intelligence.copilotkit.ai`). Overrides are unchanged
for self-hosted and non-production deployments, with two guards that the
previous required-field signature made unnecessary:
**A blank value counts as unset.** These URLs are almost always wired
from env vars, and a declared-but-empty variable
(`COPILOTKIT_INTELLIGENCE_URL=`, common in generated `.env` files and
container configs) arrives as `""`. Left as-is it slips past a `??`
default and produces host-relative requests — the failing test showed a
request to `/api/threads?...` with no host at all.
**Setting only one of the pair warns.** The API and realtime planes are
separate hosts, so a lone override silently pairs (say) a self-hosted
API with the managed gateway. That failure surfaces as a 30s hang rather
than an error, because the socket layer treats an unreachable host as a
retryable reconnect — so it is worth a log line at construction.
## Docs
Swept the copy-paste surfaces to the short form so nobody is handed URLs
to get wrong: channels quickstart/index/mcp + the 5 platform pages, the
7 channels package READMEs, the runtime and react-core skill references
(package and root copies), and the slack/teams examples with their
READMEs and `.env.example`s. The "separate hosts, override both or
neither" lesson now lives in the quickstart's override note and the SDK
JSDoc instead of in every snippet.
Two things beyond the mechanical sweep:
- `dev-docs/architecture/setup-intelligence.md` documented `apiUrl:
"https://your-intelligence-host/api"` and `wsUrl:
"wss://your-intelligence-host/socket"` — the exact double-`/api` and
fake-`/socket` suffixes the runtime skill reference warns against. Fixed
while here.
- The `CopilotKitIntelligence` class JSDoc sat above
`ThreadDeletedPayload`, so it never appeared on hover of the class.
Reattached — that is the discoverability half of the original complaint.
The integration examples (langgraph, mastra, adk, …) are deliberately
untouched: they pass `process.env.INTELLIGENCE_API_URL ??
"http://localhost:4201"` to target a local stack, where the managed
defaults would be wrong.
## Verification
Tests written first and watched fail — 5 on undefined URLs, then the
blank-URL case on the host-relative request.
- `client.test.ts` 54/54; full `runtime/src/v2` suite 1128/1128
- slack example 63/63, teams example 2/2, channels-intelligence 195/195
- `tsc --noEmit` clean on `packages/runtime`; both edited examples
typecheck clean against the new signature (verified by resolving
`@copilotkit/runtime` at the worktree source, since the symlinked
`node_modules` otherwise reports stale required-field types)
- oxlint 0 warnings / 0 errors on touched files, oxfmt clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What
Two SDK-side delivery / terminal-signal hardening fixes in the managed
channels
adapter (`packages/channels-intelligence/src/intelligence-adapter.ts`),
from the
pre-merge adversarial CR of #5972. Closes OSS-491.
## Scope correction (verified end-to-end during review)
The ticket originally framed Finding 1 as a live "self-hosted /
no-Outbox"
silent-failure. That premise is **wrong**, and I traced the full render
path to
confirm it:
- Every real deployment wires a render sink: the realtime gateway
launcher passes
one, and the config-free `intelligenceAdapter()` auto-wires
`HttpRenderEventSink`
in `start()`. On that path a `run_error` is POSTed to app-api's
`/api/channels/deliveries/:id/render-events/accept` route, durably
persisted,
projected into canonical history as a `RUN_ERROR` terminal, **and** the
Connector
Outbox posts a user-visible `⚠️ Agent error: …` to Slack.
- The Connector Outbox is the **same app-api binary** and runs in
**self-hosted / composite** deployments too (gated only by the channels
feature
flag, not deployment mode). Self-hosted handles `run_error` identically
— there
is no server-side gap.
- The ad-hoc fallback sink in `renderSinkFor` — the code Finding 1
patches — is
reached **only** when a consumer injects both `source` and `egress` but
no
`renderSink` (in-memory tests, and hypothetical BYO custom-transport
integrations). It is not on any default or self-hosted path.
So Finding 1 is **defensive hardening of a fallback plus a lifecycle
fix**, not a
HIGH-severity production silent-failure (severity retriaged to Low on
the ticket).
Finding 2 is on the shared `dispatch()` path and does affect real
deployments.
## Finding 1 — `run_error` on the ad-hoc HTTP-fallback sink
`renderSinkFor`'s ad-hoc plain-text sink (used only when no `renderSink`
is wired)
had no `run_error` branch, so on that path a `run_error` was dropped,
the run
resolved, and `dispatch()` acked a text-less turn as success — silent,
nothing
logged. Now that sink logs the raw error via `config.log`, flushes any
buffered
partial text, and posts a generic user-visible error. Posted + logged +
**acked, not nacked**: a `run_error` is frequently a deterministic agent
failure
and the Channel path reprocesses redeliveries (`skipIngressDedup`), so
nacking
would livelock. A genuine egress failure of the error post itself still
nacks
(`emit` throws → `drain()` rejects).
Also — and this part **does** apply to the realtime path —
`onRunErrorEvent` now
calls `ensureRunStarted()`, so a `run_error` that precedes any text/tool
event
emits a preceding `run_started` (parity with the text/tool handlers; a
well-formed
run lifecycle on both paths). This is the one intentional realtime-path
change: a
text-less error now yields `[run_started, run_error, finalize]` instead
of
`[run_error, finalize]`. `run_error` **rendering** is unchanged — still
a frame to
the sink, never converted to an egress error post.
## Finding 2 — a failed `ack()` no longer nacks a completed turn
`ack()` sat inside the same `try` as `dispatchTo`, so an `ack()` network
failure
fell into the catch and nacked an already-processed turn with a
misleading reason;
with `skipIngressDedup`, the whole turn reran (duplicate agent run —
re-calls the
model, re-runs tools — even though egress op-id dedupe catches duplicate
posts).
Ack is now a distinct phase: a failed ack is logged, never nacked. This
kills the
**spurious** rerun — notably the common "ack landed server-side but the
response
was lost" case. A genuinely-failed ack still redelivers via lease expiry
(inherent
to at-least-once, unavoidable); the turn already posted its reply, so
only
delivery bookkeeping is delayed.
## Tests
TDD; the behavior tests were written failing first.
- Fallback text-less `run_error` ⇒ emits a user-visible error post
**and** logs it.
- `run_error` before any text/tool event ⇒ `run_started` precedes it.
- Realtime path ⇒ `run_error` still forwarded to the render sink,
**not** converted
to an egress post (regression guard).
- Failed `ack()` on a successful turn ⇒ not nacked, no rerun, ack
failure logged.
- Failed error-post emit on the fallback path ⇒ `drain()` rejects
(nacks/retries).
- Buffered partial text flushed before the error post (order preserved).
## Verification
- `nx run @copilotkit/channels-intelligence:check-types` ✅
- `nx run @copilotkit/channels-intelligence:test` ✅ (all green)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What does this PR do?
A runtime that declares a managed channel the Intelligence project does
not have
crashed startup: `ready()` rejected and the channel settled to `error`.
```
realtime gateway session join failed: channel_declaration_unavailable (channel states: runtime_not_declared)
status: { overall: "error", channels: { "managed-teams-probe": "error" } }
```
Creating the channel and attaching credentials is the setup step every
new
channel goes through, so this was the first run of *every* managed
channel,
before anyone reached the happy path.
### Cause
app-api had no declaration-response token for "the project has no
channel by that
name", so it sent `runtime_not_declared`. That token means the inverse
on a join —
a channel exists but no runtime declared it, while on a join the runtime
is the
one declaring. `classifyJoinError` read it literally, which was correct,
and so
treated a benign setup gap as a hard failure.
The fix is split by which side owns the mistake. app-api now sends
`channel_not_declared` for that case (Intelligence#615). This PR teaches
the
classifier the new token.
### Change
- `channel_not_declared` added to `SETUP_REQUIRED_CHANNEL_STATES`, so
the channel
settles to `setup_required` and `ready()` resolves.
- `runtime_not_declared` keeps its literal reading and stays a hard
error, as do
`runtime_conflict`, `platform_setup_failed`, `delivery_failed`,
`egress_failed`
and `runtime_offline`.
- Retired the `TODO (gateway-owner)` the classifier carried, which asked
whether
`runtime_not_declared` was ever benign. It was — but not for the reason
the
comment guessed. The server was overloading the token, not racing a
heartbeat.
Replaced the TODO with what the answer turned out to be.
- Documented that `SETUP_REQUIRED_CHANNEL_STATES` now draws on two
Intelligence
vocabularies: most members are read-model `CHANNEL_STATE_KINDS`, while
`channel_not_declared` is declaration-only and has no read-model
counterpart.
An SDK without this change treats the unknown token as a hard error,
which is the
pre-fix behavior — so old clients are no worse off, they just keep the
bug.
## Related PRs and Issues
- Server side: CopilotKit/Intelligence#615
- OSS-622
## Checklist
- [x] I have read the Contribution Guide
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked
## Tests
Added `classifies channel_not_declared as SETUP_REQUIRED (OSS-622)`,
plus an
`error-channel-not-declared` join mode. RED before the fix: the join
produced
`RealtimeGatewayChannelStateError` instead of
`RealtimeGatewaySetupRequiredError`.
`npx vitest run src/realtime-gateway.test.ts` — 21 pass.
33 failures elsewhere in this package reproduce on `origin/main` with
these
changes stashed (a stale `@copilotkit/channels` build missing the
`ɵruntime`
symbol) and are unrelated.
`CopilotKitIntelligence` required `apiUrl` and `wsUrl` on every construction,
so the two correct hosts had to be found and copied by hand — which is how an
agent came to invent them. Both now default to CopilotKit's managed platform,
making `new CopilotKitIntelligence({ apiKey })` the whole managed-service setup.
Overrides are unchanged for self-hosted and non-production deployments, with two
guards that the previous required-field signature made unnecessary:
- A blank value counts as unset. These URLs are usually wired from env vars, and
a declared-but-empty variable arrives as `""`, which would otherwise produce
host-relative requests instead of falling back to the managed platform.
- Setting only one of the pair warns. The API and realtime planes are separate
hosts, so a lone override silently splits the client across two deployments —
and that failure surfaces as a hang, not an error.
Sweeps the doc, skill, README, and example surfaces to the short form so the
copy-paste path no longer hands anyone URLs to get wrong, and reattaches the
`CopilotKitIntelligence` class JSDoc, which was orphaned above an interface and
so never appeared on hover.
Linear: OSS-638
`createCopilotNodeListener` now mirrors `createCopilotRuntimeHandler`'s branded
overload pair, so a runtime with at least one declared Channel yields a listener
whose `.channels` is non-optional and the documented `listener.channels.ready()`
call type-checks with no `!` and no `?.`. `activateChannels: false` and
channel-less runtimes keep the optional shape. Both listener types are exported
from `@copilotkit/runtime/v2/node`.
Corrects TSDoc on the node, express, and hono wrappers that still claimed
activation happens "at creation time" and labelled `ready()` as optional — stale
since activation was deferred to make the Fetch handler serverless-safe. On a
long-running host that call is required, not optional.
Fixes a live consequence of that stale model: `examples/slack/app/managed.ts`
never called `ready()`, so it mounted a listener, logged "started managed
Channel", and connected nothing. Covered by a regression assertion.
Drops the now-unnecessary `?.` from the examples, READMEs, and channel docs, and
adds compile-time contracts for the listener shape alongside the existing
handler ones. The examples compile with `strict: true`, so they prove the
`?.`-free call under strict null checks, which the runtime package (strict:
false) cannot.
Close two assert-without-a-test gaps found in review of the OSS-491 fix:
a failed error-post emit on the fallback run_error path propagates (drain
rejects → the delivery nacks/retries rather than acking a dropped error),
and buffered partial text is flushed before the error post (order preserved,
no lost or duplicated output).
Two pre-existing SDK delivery / terminal-signal defects in the managed
channels adapter, surfaced by the pre-merge adversarial CR of #5972 and
deliberately left out of that PR's scope. Both bite the HTTP-fallback /
self-hosted path (no Connector Outbox); the realtime + Outbox path is
unchanged.
Finding 1 (HIGH) — run_error swallowed on the HTTP-fallback render path,
delivery acked as SUCCESS. `renderSinkFor`'s ad-hoc plain-text sink had no
`run_error` branch, so an agent-side failure with no accompanying text (model
error, tool crash, upstream 5xx) resolved the run, drained, and acked the
delivery as success — no user-visible message and nothing logged ("bot goes
silent on every error"). Now the fallback sink logs the raw error via
`config.log`, flushes any buffered partial text, and posts a generic
user-visible error. Posted + logged + acked (not nacked) on purpose: a
run_error is frequently a deterministic agent failure and the Channel path
reprocesses redeliveries (skipIngressDedup), so nacking would livelock. A
genuine egress failure of the error post itself still nacks (emit throws →
drain rejects). Also: `onRunErrorEvent` now calls `ensureRunStarted()` so a
run_error that precedes any text/tool event still emits a preceding
run_started.
Finding 2 (LOW) — a failed ack() nacked an already-processed turn. `ack()`
sat inside the same try as `dispatchTo`, so an ack network failure fell into
the catch and nacked a completed turn with a misleading reason; because the
Channel path reprocesses redeliveries, the whole turn reran (duplicate agent
run / posts). Ack is now a distinct phase: a failed ack is logged, never
conflated with a turn failure and never nacked.