[pull] main from CopilotKit:main - #452
Merged
Merged
Conversation
…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.
…ral channels tuple (OSS-646)
…s before activation (OSS-646)
…er, nonzero on failure) (OSS-646)
…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.
## 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.
…oalescing revert (refs OSS-648)
…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`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 : )