Skip to content

[pull] main from CopilotKit:main - #452

Merged
pull[bot] merged 43 commits into
TheTechOddBug:mainfrom
CopilotKit:main
Jul 29, 2026
Merged

[pull] main from CopilotKit:main#452
pull[bot] merged 43 commits into
TheTechOddBug:mainfrom
CopilotKit:main

Conversation

@pull

@pull pull Bot commented Jul 29, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

BenTaylorDev and others added 30 commits July 24, 2026 17:07
…ayer by design

All five OSS-599 references in `channel-manager.ts` described the missing
gateway/canonical/reliability wiring for DIRECT Channels as "deferred",
implying a direct Channel gets pulled up to managed parity later.

OSS-599 says the opposite. Its boundary discipline puts 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 that reproducing them in the SDK
"collapses the build-vs-buy moat". A direct Channel's ceiling is the SDK's
in-process run loop, permanently.

Reword all five sites so the next reader does not treat the gap as pending
work — which would lead them to implement exactly what OSS-599 forbids.

Comments and one log string only; no behavior change. The log string keeps
the `direct adapter` / `ɵruntime.start()` substrings that
`channel-manager.test.ts` asserts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s (OSS-646)

`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.
…s (OSS-646) (#6207)

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)
## 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.
…ayer by design (#6155)

## 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

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](#6210).
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.
## 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.
…rame (OSS-648)

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 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.
…ing intelligence_app DB (#6209)

## 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.
MikeRyanDev and others added 13 commits July 28, 2026 15:25
## 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
… with tokens (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.
…OSS-648) (#6212)

## 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
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`
@pull pull Bot locked and limited conversation to collaborators Jul 29, 2026
@pull pull Bot added the ⤵️ pull label Jul 29, 2026
@pull
pull Bot merged commit 501e899 into TheTechOddBug:main Jul 29, 2026
2 of 48 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants