## 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.
Narrows this PR to OSS-566 + OSS-568. The OSS-565 CI-suppression effort is
backed out entirely:
- the 10 workflows' `COPILOTKIT_TELEMETRY_DISABLED` / `DO_NOT_TRACK` env and the
egress-block steps
- `examples/e2e/playwright.config.ts` env defaults
- the runtime/shared test-env manipulation and the `telemetry-opt-out-env`
helper
- the `navigator.webdriver` check and its tests
Every reverted path is byte-identical to the merge-base — verified rather than
assumed, since `git checkout origin/main -- <path>` would otherwise have
imported main's newer content for files that had moved on.
One piece is deliberately kept, because it is not about CI: the jsdom egress
guard. These tests run with a real `fetch`, and the inspector's telemetry is
fire-and-forget, so any test touching a banner / threads / open path without
stubbing fetch POSTs a real `oss.inspector.*` event to the live sink — from
developer machines as well as CI. The announcement-dismissal tests did exactly
that before the guard, and the new `opened` / `banner_dismissed` tests hit the
same send path, so shipping the events without it would make our own dataset
dirtier. Its comments no longer cite OSS-565; the rationale stands on its own.
What remains: `oss.inspector.opened` with open-source attribution, the
`banner_viewed` surface split, and first-class `banner_dismissed`. No inspector
telemetry waits on the /info handshake.
Verified: web-inspector 112, runtime 1,760, shared 199 (the last two now on
main's own test files). tsc clean, oxfmt clean, oxlint 9 warnings all
pre-existing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The script-based version broke all 15 dojo suites with exit 127. Two reasons,
both my error: the step carried no `if:` guard while its neighbours are gated on
`steps.should-run.outputs.skip`, and the dojo job checks out into a
`CopilotKit/` subdirectory, so a repo-relative script path does not resolve
there at all.
Inlining the two-line hosts write and moving it to each job's FIRST step removes
the dependency entirely — nothing to check out, nothing to guard, no path
assumption. Audited all 12 jobs first: every one is an Ubuntu runner with no
`container:`, so `sudo` + /etc/hosts is available uniformly.
Verified the printf survives YAML escaping (literal `\n` in the file) by running
the extracted command in bash against a fake hosts file: exit 0, exactly the two
expected entries. actionlint still 344 findings, unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the /info-coupled approach with two mechanisms that need no handshake,
and backs the coupling out entirely.
The problem with gating on /info: `telemetryDisabled` only reaches the browser
through an async handshake, so either events race it (the hole) or they wait for
it (fail-closed). Waiting means an inspector whose runtime never connects — or
whose /info 404s, which sets status to `Error`, not `Connected` — goes silent for
the whole session. That penalizes real users with broken setups, who are exactly
the people most likely to have the inspector open.
Instead:
1. `navigator.webdriver` in `track()`. Spec'd, synchronous, available on the
first line of script, and true under every Playwright launch — verified
against this repo's Playwright 1.59.1 in both headless and headed mode. Needs
no per-app env plumbing and covers pages served by deployed environments,
which no CI-side variable could reach. Suppresses only on an explicit `true`:
absent/falsy means "not automation", never "unknown, so stay quiet".
2. A job-wide egress block (`.github/scripts/block-telemetry-egress.sh`) pointing
the sink at loopback for both address families, added to the 12 CI jobs that
run or boot real CopilotKit code. Backstop for anything that reads neither the
env nor the DOM. The frozen integration container gets the same via
`--add-host`, since neither the workflow env nor the runner's hosts file
crosses that boundary.
Backed out of the inspector: the egress gate and held queue, `pendingOpened`,
and `pendingBannerViewed` (the last pre-dating this PR). Inspector telemetry no
longer references the handshake at all — `index.ts` is net −94 lines and the
opt-out is now a single boolean check per call site, as it reads.
Two bugs this surfaced, both caught by existing or new tests rather than
shipped: removing the flush step also removed the only `telemetryDisabled` check
on the `banner_viewed` path, and a thread-refetch test counted calls on a stubbed
global `fetch`, so telemetry that now sends immediately inflated it — scoped to
the thread URLs it actually means.
Also corrects the workflow comments, which credited the /info chain for
browser-side suppression it never reliably provided.
Verified: web-inspector 116, runtime 1,760, shared 199. tsc clean, oxfmt clean,
oxlint back to the baseline 8 warnings (fixed a dead type import and a
non-capturing test helper I introduced). actionlint 344 findings before and
after, byte-identical modulo shifted line numbers. shellcheck clean. tsdown
build ships the check in dist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## 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).
The previous commit closed the pre-handshake hole for the three events this PR
touches, but every other inspector event still posted on the placeholder.
`AgentRegistry._telemetryDisabled` starts `false`, so the per-call-site
`if (this.core?.telemetryDisabled) return` check passes while disconnected and
the event goes out — reproduced with the threads tab, which emitted
`threads_tab_clicked` before /info could report the opt-out. 15 call sites
shared the flaw (threads tab, memories tab, view-state, CTAs, example tour).
Rather than queue at each of them, the gate now lives in `telemetry.ts` at the
single egress boundary: `track()` holds events while the runtime decision is
unknown, and the inspector calls `resolveTelemetryGate("allowed" | "denied")`
once the handshake lands. Nothing reaches the network without an explicit
allow.
That makes the per-event queues added in e7e8922 redundant, so
`pendingBannerViewed` and `pendingBannerInteractions` are gone (net −101 lines
in index.ts). `pendingOpened` stays: it exists to resolve `license_status` /
`runtime_mode` / `runtime_url_type` at flush time, which is a payload concern
rather than a deferral one. The gate resets on `detachFromCore` so a later core
starts from "unknown" instead of inheriting the previous runtime's decision.
Two fidelity details the refactor forced, both covered: held events carry the
timestamp of the interaction rather than of the release, and the opt-out is
re-checked at release in case the user opts out mid-handshake.
Trade worth stating: an inspector whose runtime never connects now emits
nothing. Consent cannot be established without the handshake, so silence is
the correct default.
Coverage: threads tab held then dropped on disabled / sent on allowed, plus a
gate unit suite (unknown holds, allowed releases in order, denied discards,
post-denial drops, queue bound, reset, opt-out precedence, event-time ts).
web-inspector 127 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## 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.
Three review findings on the inspector-telemetry PR.
Banner interactions bypassed the runtime opt-out before the handshake.
`telemetryDisabled` reads `false` until /info answers, so a dismissal (or a
body click) handled while disconnected posted on that placeholder — even when
the runtime was about to report `telemetryDisabled: true`. `banner_clicked`
and `banner_dismissed` now defer behind the same connected gate as
`banner_viewed`. Dedup still happens at interaction time, so a double-click
queues one event rather than two, and the queue carries the same bound as the
others.
Queued opens preserved pre-handshake placeholder segmentation. `trackOpened`
snapshotted `licenseStatus` (undefined) and `runtimeMode` (its `sse` default)
at queue time, and the flush sent them unchanged — permanently attributing an
early open against an Intelligence runtime to SSE. Only the open *intent* is
queued now; `license_status` / `runtime_mode` / `runtime_url_type` are
resolved at flush time, once /info has landed.
Test env overrides left the opt-out cleared. Suites that must clear
`COPILOTKIT_TELEMETRY_DISABLED` / `DO_NOT_TRACK` now capture the originals and
restore them in `afterAll`, via a shared `telemetry-opt-out-env` helper. The
hoisted cases snapshot inline and return it, since hoisted callbacks run
before the imports they would otherwise need. Measured: under the repo's
default `isolate: true`, vitest forks a fresh process per file, so the delete
does not currently cross files (pids differ; a `--no-isolate` control shares
one pid and does leak). Restoring is the defensive fix, not a live-bug fix.
Coverage: pre-handshake dismissal dropped when the runtime then reports
disabled; sent when it allows; double-click dedup; and a queued open flushed
against an Intelligence runtime carrying `runtime_mode: intelligence` +
`license_status: valid`. Three of the four fail against the pre-fix code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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)
Realigns the inspector/memory work onto the banking demo as it shipped in
#6136 (ChatGPT-style shell, gen-UI beats, durable-memory self-learning) and
#6202 (README refresh).
All six conflicts were the same collision: this branch removes the bespoke
Glass Engine inspector, while #6136 kept and rebuilt around it.
- run-handler.ts: kept both sides (our CopilotKitCoreCatalogComponent and
main's MAX_FOLLOW_UP_DEPTH landed at the same spot).
- wrapper.tsx / layout.tsx: took main's rewritten provider tree and
right-hand icon rail, minus the Glass Engine providers, pane, and
telescope toggle. Also dropped main's `padClass` (it reserved space for
the Glass pane and referenced a now-removed `glassActive`) and
`<ProactiveNotice />` (main removed it; the import is already gone).
- memory-tab.tsx, lib/intelligence/memory.ts: confirmed the deletions.
Their only remaining importers were the bespoke inspector and the
banking-local /api/memories routes, all removed here. seed-memories.ts
is unaffected: it POSTs to INTELLIGENCE_API_URL, not the local route.
- README.md: kept our product-inspector section over main's Glass Engine
availability/activation prose, and documented the Capabilities tab.
Drive-by fixes to comment rot the migration created: user-id.ts and the
copilotkit route doc comments referenced the deleted Memory-panel proxies,
and the README pointed the presenter-reset control at the removed
telescope toggle.
Also replaces a literal NUL byte in capabilityKey() with a unicode escape.
The raw control character made tsc/grep/diff treat run-handler.ts as a
binary file, which hid this very merge's conflict markers from grep.
Behavior is unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
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)