14017 Commits

Author SHA1 Message Date
Austin Merrick 87010e6f1f docs(threads): align React Native hosted setup 2026-08-03 09:41:07 -07:00
Austin Merrick 9900d8e82f docs(threads): harden hosted contracts 2026-08-03 09:35:55 -07:00
Austin Merrick 6d15ef52e2 docs(threads): close frontend contract gaps 2026-08-01 16:15:57 -07:00
Austin Merrick 0c1835181e docs(threads): add native architecture handoffs 2026-08-01 15:42:34 -07:00
Austin Merrick 248d3bb6f7 docs(threads): align hosted verification contracts 2026-08-01 15:07:42 -07:00
Austin Merrick 375a34e968 test(showcase): align Channels auto-start contract 2026-08-01 00:33:07 -07:00
Austin Merrick 89135d9635 Merge remote-tracking branch 'origin/main' into malabo
# Conflicts:
#	showcase/shell-docs/src/content/docs/premium/threads-explained.mdx
2026-07-31 23:57:15 -07:00
Austin Merrick 81daf49ee3 test(showcase): tighten hosted guide contract guards 2026-07-31 23:35:02 -07:00
Austin Merrick 1c15347485 test(showcase): harden hosted guide syntax guards 2026-07-31 23:35:02 -07:00
Austin Merrick a146d53272 test(showcase): harden hosted guide contract guards 2026-07-31 16:03:17 -07:00
Austin Merrick 5a035434e0 test(showcase): close hosted contract gaps 2026-07-31 15:24:52 -07:00
Austin Merrick bf19c155f2 test(showcase): harden hosted guide contracts 2026-07-31 14:34:40 -07:00
Austin Merrick 6a738d9394 test(showcase): ignore non-rendered Markdown links 2026-07-31 13:43:21 -07:00
Austin Merrick 341f57a7f3 docs(threads): clarify recency sort fallback 2026-07-31 13:38:58 -07:00
Austin Merrick b25f0d7e96 test(showcase): exercise hosted guide snippet inliner 2026-07-31 13:36:58 -07:00
Austin Merrick 030b172b70 test(showcase): guard Angular thread API inventory 2026-07-31 13:36:30 -07:00
Austin Merrick 8e3e5143ef test(showcase): compose shared hosted guide fixture 2026-07-31 12:38:01 -07:00
Austin Merrick 70740b469d docs(showcase): add hosted guide frontend variants 2026-07-31 12:34:41 -07:00
Austin Merrick af7980029c docs(react-core): clarify Drawer managed entitlement 2026-07-31 12:26:32 -07:00
Austin Merrick 595befbf3b docs(threads): align cross-frontend contracts 2026-07-31 12:26:32 -07:00
Austin Merrick d43a100b88 test(showcase): guard Drawer managed entitlement docs 2026-07-31 12:10:59 -07:00
Austin Merrick c19f9caece test(showcase): guard cross-frontend thread contracts 2026-07-31 12:05:20 -07:00
Austin Merrick e3c8a8d487 test(showcase): guard hosted guide frontend variants 2026-07-31 11:56:45 -07:00
Mike Ryan 62eb3bf1f8 fix(channels): start Slack streams with first text (#6278)
## Summary

- Send the first bounded Slack text delta in stream.start for direct and
managed delivery.
- Keep later appends, continuation messages, and empty-stream cleanup
behavior.
- Add managed delivery integration tests for first-text, empty-chunk,
and empty-stream paths.

## Root cause

Slack received stream.start without text, so it rendered Thinking...
until a separate append arrived.

## Tests

- pnpm nx run-many -t test,check-types,build -p
@copilotkit/channels-slack @copilotkit/channels-intelligence
--skip-nx-cache
- pnpm nx test @copilotkit/react-core --skip-nx-cache
- pre-commit test, publint, and attw checks for 12 affected projects
2026-07-31 10:37:58 -07:00
Mike Ryan eb6c8df0ad fix(channels): start Slack streams with first text 2026-07-31 10:26:41 -07:00
Ben Taylor 101fe27d80 fix(channels): contain terminal provider failures (#6269)
## Summary

- stop the Channels agent loop when a tool handler reports an
already-terminal provider delivery
- freeze managed renderer fanout while canonical ingestion records
`RUN_ERROR`
- immediately observe Slack native-stream queue failures while
preserving them for `finish()`
- treat uncertain managed file-delivery errors as terminal delivery
outcomes

## Root cause

The Core run loop converted every tool-handler exception into a
model-visible tool result. After `ChannelProviderDeliveryError` closed
the effect path, the model could continue and emit text, causing Slack
native rendering to call `slack.stream.start` against a closed delivery.

The native stream also retained that rejection in an unobserved internal
promise until `finish()`, leaving a Node unhandled-rejection window.

## Validation

- `pnpm nx run-many -t test check-types -p
@copilotkit/channels-core,@copilotkit/channels-slack,@copilotkit/channels-intelligence
--skip-nx-cache`
- `pnpm nx run-many -t build publint attw -p
@copilotkit/channels-core,@copilotkit/channels-slack,@copilotkit/channels-intelligence
--skip-nx-cache`
- `pnpm nx run-many -t test check-types build publint attw -p
@copilotkit/channels --skip-nx-cache`
- pre-commit affected-package matrix: 17 projects / 24 tasks
2026-07-31 10:09:55 -05:00
Ben Taylor 468995e8f5 feat(telemetry): inspector opened event and banner surface split (OSS-566/568) (#6203)
Two related Inspector-telemetry tickets: **OSS-566** and **OSS-568**.

## OSS-566 — explicit "Inspector opened" event

There was no event recording that the panel was opened. Opens could only
be inferred from in-panel activity (~1,655/90d, a floor) or from
`banner_clicked` cta=`body` (~511), which misses the common
floating-button path entirely.

Adds `oss.inspector.opened` with:

| property | values |
|---|---|
| `open_source` | `floating_button` \| `announcement_preview` |
| `has_unseen_announcement` | whether an announcement was on screen at
open time |
| `license_status` / `runtime_mode` / `runtime_url_type` | same
segmentation the threads events already carry |
| `package_name` / `package_version` / `inspector_distinct_id` | version
segmentation |

**Restoring a persisted-open panel deliberately does not count.**
Restore assigns `isOpen` directly instead of routing through
`openInspector()`, so page reloads — and every `next dev` hot reload —
stay out of the number.

## OSS-568 — banner surface + first-class dismissal

1. **`surface` on `banner_viewed`** — `collapsed_preview` (bubble on the
collapsed widget) vs `expanded_card` (card inside the opened panel),
stamped at fire time. Dedup is now per `(banner, surface)` instead of
per banner, so opening the panel records the card impression as its own
signal.
2. **`oss.inspector.banner_dismissed`** — emitted **in addition to**
`banner_clicked { cta: "dismiss" }`, not replacing it, so dashboards
reading the `cta` value keep working. Carries `surface` too, separating
"swatted the bubble away" from "dismissed the card after opening".

Both new events clear the sink's `oss.inspector.` prefix gate, so **no
telemetry-sink deploy is needed**.

## Testing

- **`packages/web-inspector` full suite — 112 passed (4 files)**, run
locally in the worktree:
  ```
   ✓ dev/css-raw-import.spec.ts (1 test) 1ms
   ✓ src/__tests__/telemetry-egress-guard.spec.ts (3 tests) 2ms
   ✓ src/lib/__tests__/telemetry.test.ts (28 tests) 8ms
   ✓ src/__tests__/web-inspector.spec.ts (80 tests) 890ms
   Test Files  4 passed (4)
        Tests  112 passed (112)
  ```
- **New coverage**: payload shape for `opened` / `banner_dismissed`,
incl. an allow-list assertion that no content/PII key can be added
accidentally; collapsed→expanded surface sequence on open; per-surface
dedup; open attribution for both sources; no event for an already-open
panel; no event for a restored-open panel; nothing emitted when the
runtime reports `telemetryDisabled`; an open still recorded while the
runtime is disconnected.
- **`tsc --noEmit`** on `@copilotkit/web-inspector`: clean (after
building `core` + `shared` dist in the worktree).
- **`tsdown` build**: succeeds; the test-only egress-guard helper is
**not** present in `dist/`.
- **`oxfmt --check`**: clean. **`oxlint`**: 9 warnings, all
pre-existing.
- `@copilotkit/runtime` (1,760) and `@copilotkit/shared` (199) also
green — both are back on main's own test files in this PR.

## A test-only egress guard rides along

`vitest.setup.ts` installs a fetch guard that swallows requests to the
telemetry sink. This is **not** CI plumbing — it is a prerequisite for
the new events. These tests run in jsdom, where a real `fetch` exists,
and inspector telemetry is fire-and-forget, so any test that drives a
banner / threads / open path without stubbing fetch POSTs a real
`oss.inspector.*` event to the live sink, from developer machines as
well as CI. The announcement-dismissal tests were already doing this;
the new `opened` / `banner_dismissed` tests hit the same send path. No
environment variable can prevent it, because the inspector's opt-out
arrives in the runtime's `/info` response and these tests never boot a
runtime.

## Not in scope

Suppressing telemetry from CI jobs that boot real apps (**OSS-565**) was
explored on this branch and removed. It needs a mechanism that does not
depend on the `/info` handshake — the env → `/info` → core chain is
asynchronous, so an early interaction beats it. That ticket stays open
and unaddressed here.

Closes OSS-566, OSS-568.
2026-07-31 08:33:57 -05:00
Mike Ryan 29d2721775 fix(channels): contain terminal delivery failures 2026-07-30 23:20:48 -07:00
Mike Ryan 2fe47bc969 Prevent Slack actor metadata from leaking into assistant history (#6268)
## Summary

- keep participant actor metadata model-visible in Slack transcript
history
- keep own-channel assistant history equal to the provider-visible
message content
- preserve structured actor and provider message metadata returned by
`thread.getMessages()`

## Root cause

The delivery adapter prefixed every transcript entry with the untrusted
participant metadata envelope before assigning AG-UI roles. Own-channel
transcript entries were then assigned the assistant role with that
participant-style prefix still in their content, so the model received
the prefix as prior assistant output and could reproduce it on the next
turn.

## Validation

- `pnpm nx test @copilotkit/channels-intelligence --skip-nx-cache` (17
files, 117 tests)
- `pnpm nx run-many -t check-types build -p
@copilotkit/channels-intelligence --skip-nx-cache`
- repository pre-commit Nx test/publint/attw suite
2026-07-30 22:58:29 -07:00
Mike Ryan 552268d47d fix: keep participant metadata out of assistant history 2026-07-30 22:50:27 -07:00
Tyler Slaton ca5e665e98 feat(channels): implement Channels V5 (#6266)
## Summary

- add the provider-neutral V5 message operation contract with stable
logical and revision identities
- route created, updated, and deleted messages through explicit mention
and message semantics
- add delivery-scoped transcript loading with retry coalescing and
delivery thread authorization
- charge managed deliveries on first substantive work while leaving
preparation free
- handle transcript failures by surface, render Slack status, and fence
superseded runs before output
- reconcile managed files with idempotent operation identities and bound
pending delivery capacity
- propagate actor kind through prepared deliveries
- normalize Slack, Teams, Telegram, WhatsApp, and Discord turns onto the
same operation contract

## Validation

- pnpm nx run-many -t test build -p @copilotkit/channels-intelligence
@copilotkit/channels-slack
  - channels-intelligence: 117 tests passed
  - channels-slack: 320 tests passed
- pnpm nx run-many -t build test -p @copilotkit/channels-telegram
@copilotkit/channels-whatsapp @copilotkit/channels-discord
  - Telegram: 150 tests passed
  - WhatsApp: 74 tests passed
  - Discord: 194 tests passed
- pre-commit affected package test, publint, attw, binary, lint, and
commitlint checks pass
- broad affected run found unrelated existing example lint/build
failures; the branch-owned cross-adapter type failures it revealed were
fixed and rerun green
- paired Intelligence Docker E2E passed all four realtime boundary
scenarios against real Postgres, Redis, MinIO, App API, and two Gateways
with fake Runtime and fake Slack
2026-07-30 20:41:38 -07:00
Mike Ryan 8e2d7a5cda test(channels): bind canonical run delivery 2026-07-30 20:05:31 -07:00
Mike Ryan 4680d2f579 fix(channels): normalize remaining provider turns 2026-07-30 19:58:08 -07:00
Mike Ryan 81d192ad22 fix(channels): suppress exact provider self output 2026-07-30 19:51:08 -07:00
Mike Ryan 7259bae438 feat(channels): bound pending delivery capacity 2026-07-30 19:51:08 -07:00
Mike Ryan 9dc343ddf8 feat(channels): confirm managed file delivery 2026-07-30 19:51:08 -07:00
Mike Ryan ddfa6f3453 feat(channels): supersede pre-output runs 2026-07-30 19:51:08 -07:00
Mike Ryan 2a00a5a16a feat(channels): render native Slack status 2026-07-30 19:51:08 -07:00
Mike Ryan bff87d1428 feat(channels): handle transcript failures by surface 2026-07-30 19:51:07 -07:00
Mike Ryan f7437daad3 feat(channels): charge deliveries on first work 2026-07-30 19:51:07 -07:00
Mike Ryan f1eef91fed feat(channels): authorize delivery thread access 2026-07-30 19:51:07 -07:00
Mike Ryan b376c901f6 feat(channels): load delivery-scoped transcripts 2026-07-30 19:51:07 -07:00
Mike Ryan bf8abc3cef feat(channels): add V5 message operation routing 2026-07-30 19:51:07 -07:00
Ben Taylor 5a8a487df3 feat(runtime): auto-start managed Channels on long-running hosts (OSS-641) (#6258)
Resolves [OSS-641](https://linear.app/copilotkit/issue/OSS-641). Mike's
report: *"You have to `await channels.ready()` for it to connect to the
Realtime Gateway. Seems like there's some clunkiness to creating the
runtime and getting it connected."* He then picked the fix: *"I think it
should autostart in the long running wrappers."*

## What changes

**`createCopilotNodeListener` and `createCopilotExpressHandler` start
activation at creation.** A declared Channel connects because it was
declared; `channels.ready()` becomes await-and-observe rather than the
call you must remember. Failure-mode asymmetry is the argument:
forgetting `ready()` today gives you a process that serves HTTP, looks
healthy, and is silently disconnected with **zero output**, while
auto-start's worst case is an activation error in the logs.

**`createCopilotRuntimeHandler` and `createCopilotHonoHandler` stay
lazy.** The generic Fetch handler is the serverless/edge entry point —
isolates freeze and recycle per request, so separate cold starts would
mint competing listeners for the same Channel (the reason activation was
deferred in `fbf35ac59` in the first place). Hono keeps that behavior
because it is our Next.js App Router surface in practice: every route
handler in `examples/showcases/*` (banking, mcp-apps,
generative-ui-playground, oracle-agent-memory) plus the vue/nuxt demo
builds one at module scope. Its TSDoc now states why, loudly, so nobody
"finishes the job" later.

`activateChannels: false` remains the clean opt-out that opens no
socket.

## Consequence for host code: the shutdown boundary moves earlier

Signal handlers must now be registered **before the listener is
created**, not merely before `ready()`. Otherwise a Ctrl-C during the
connect window hits Node's default handler and leaks a live gateway
session. `examples/slack`, `examples/teams`, and the docs snippets are
restructured to wire teardown before the listener exists (a
`stopChannels`/`teardown` binding assigned in the same tick as
creation). **Worth calling out in the changelog** — it is the general
hazard for any user code that registers shutdown after mounting.

## Failure semantics

Fire-and-forget by necessity, since a factory is synchronous. Set-level
failures log at `error`; per-Channel failures keep their existing `warn`
breadcrumbs; an up-front misconfiguration (duplicate/missing Channel
names) now surfaces as a logged error at creation rather than a throw
out of the factory — the factory still never throws. `ready()` stays
idempotent and one-shot, so a host that *does* await it observes this
activation's outcome, including its rejection, rather than triggering a
second one.

## READMEs

Every `channels-*/README.md` quickstart built the *generic* handler and
needed `await handler.channels.ready()` — for a socket-mode Slack bot, a
request handler you construct and never serve, which is likely closer to
what actually felt clunky. All seven now use the Node listener, so they
inherit auto-start and agree with the docs-site quickstarts. No new
public surface: a bot-only `startChannels(runtime)` host was the
alternative and is deliberately not taken here.

## Testing

- **`packages/runtime` unit suite: 1815 passed / 128 files** (`npx
vitest run`), including 9 tests in `endpoints-channels.test.ts`
covering: auto-start on node + express; Hono still lazy;
`activateChannels: false` opens no socket; a failed auto-start logs
instead of leaving an unhandled rejection (asserted via an
`unhandledRejection` listener) and the reason survives to a later
`ready()`; a duplicate-name misconfig logs without throwing; and two
wrappers over one runtime activate once (the per-runtime manager cache
is load-bearing now that *construction* activates).
- **`examples/slack`: 63 passed / 12 files; `examples/teams`: 2 passed /
1 file** (`npm test` in each).
- **Typecheck clean:** `examples/slack` and `examples/teams` (`tsc
--noEmit`), plus a full `@copilotkit/runtime` tsdown build.
- **Lint/format clean:** `oxlint` reports 0 findings in every changed
file (the 21 warnings in that run are pre-existing, all in untouched
example render/tool files), `oxfmt --check` passes on all 9 changed
source files.
- **Docs:** verified no stale lifecycle claims remain (`opens no
connection` / `ready() is required` / `control surface` guards) across
`docs/channels/**` and the Slack + Teams platform guides.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-30 21:26:06 -05:00
Ben Taylor 9607d3a684 fix(channels): isolate factory-returned agents per turn (#6265)
## Summary

Every Channel turn now runs on an agent instance nobody else holds a
reference to, for **both** configured shapes — the singleton config and
the result of an `agent: (threadId) => …` factory.

This is a fresh start on #6260 (thanks @AlemTuzlak and @maxkorp — the
isolation change is theirs). That PR carried a per-run runner thread id
and a per-run lock namespace, both of which broke managed ingestion and
were removed during review. Rather than keep iterating on a branch whose
description had drifted from its diff, this restates the change from
`main` with an accurate root cause and the two corrections below.

**This is a concurrency-isolation fix.** Two overlapping mentions on one
conversation currently get prompted with each other's questions; that is
reproduced below and fixed here. It is *not* a fix for "only the first
mention gets a reply" — see [What the original report actually
was](#what-the-original-report-actually-was), which turns out to have
been fixed already in #6256.

## Root cause

`createChannel` resolves an agent per turn through `agentFactory`. The
singleton config was cloned per turn; a factory's result was returned
raw:

```ts
if (typeof a === "function") return (threadId: string) => sanitize(a(threadId));
```

A factory is free to return the same object on every call — `agent:
(threadId) => shared` — which is easy to write by accident, and is what
a singleton becomes when someone refactors to get at the `threadId`.

That matters because **turn concurrency defaults to `"parallel"`**
(`ChannelConcurrency`), and the only per-thread admission gate lives in
`channels-intelligence` (`ChannelDeliveryTransport.activeThreads`, added
in #6257). A **directly connected** adapter — Slack, Discord, Teams,
Telegram, WhatsApp — has no such gate, so two turns in one conversation
can genuinely run at the same time. On one shared instance they corrupt
each other:

- `messages` is a single array both runs append into, so each run's
new-message diff picks up the other's
- `isRunning`, `activeRunDetach$` and `activeRunCompletionPromise` are
single-slot fields the second run overwrites while the first is still
streaming
- managed delivery instead serializes on **object identity**
(`DeliveryAdapter.acquireAgent`), so two *different* conversations
sharing one instance head-of-line block each other
- managed `historyIds` is a `WeakMap` keyed on the agent, so a second
concurrent turn clobbers the first's "which messages are new" baseline

## Changes

- **Clone for both shapes.** A fresh factory (`() => new Agent()`) is
unaffected beyond one unused instance.
- **Guard the failure that mandatory cloning introduces.**
`AbstractAgent.prototype.clone()` copies a fixed field list, so a
subclass declaring its own state (auth client, config, cache) reads it
back as `undefined` — and nothing surfaces it, because the base method
always exists and returns a correctly-typed instance. Comparing own
enumerable keys catches exactly that and names the dropped fields.
- Own **functions** are exempt. Assigning a method on the instance is
how spies and instrumentation wrap an agent, and losing that wrapper
leaves the prototype method intact, so the clone still behaves
correctly. This is not hypothetical — an unconditional check failed 8
existing tests in this repo, all of which patch `runAgent` on the
instance.
- **Reset `isRunning` / `abortController` on the clone**, commented as
hygiene rather than as a fix: `runAgent` assigns `abortController =
params?.abortController ?? new AbortController()` before every run and
the run loop passes none, so an inherited aborted controller cannot
reach the next request. Noted inline that this discards
`HttpAgent.clone()`'s deliberate propagation of the source's aborted
state.
- **Correct the `clone()` error text** — it offered the factory as an
escape hatch, which cloning factory results removes.
- **Test scaffolding**: one `patchAgentAndClones` /
`captureSessionAgents` pair replaces five hand-rolled copies of the same
recursive clone-patching setup.
- **Remove `ChannelAgentConcurrencyError`** — unthrown since #6256. Its
doc said it was kept so older importers would not break, but
`channels-intelligence` exports only `.` and never re-exported it from
`index.ts` (`git log -S` over that file confirms it never did), so
nothing outside the package could ever reach it.
- **Preventive regression guard** in `channel-canonical-run.test.ts`:
the thread id reaching `runner.run` must stay the canonical product
thread id. `IntelligenceAgentRunner` sends it as `thread_id` when
joining `ingestion:<runId>` and the gateway resolves it against
`cpki.threads`, so decorating it fails the join with `thread_not_found`.
Nothing decorates it on `main` — this pins the invariant so that stays
true.

### Two things #6260 asserted that are not true

Recording these so they don't get re-derived:

1. **There is no lock-key leak to fix.** #6260 forwarded `lockKeyPrefix`
on lock cleanup, described as fixing a prefixed lock acquired under one
key and released under another. Intelligence `main` has `threadLockKey =
(organizationId, threadId) => \`thread:${org}:${threadId}:lock\`` — no
prefix parameter — and `lockKeyPrefix` is read **nowhere** in `apps/`.
Acquire, renew and cleanup all hit the same unprefixed key, so nothing
leaks. My own review said the same thing and was equally wrong. Omitted
here.
2. **A loud failure for dropped subclass fields *is* available** — #6260
states none is. It's the guard above.

## What the original report actually was

#6260 framed this as fixing "2+ **sequential** mentions, only the first
gets a reply." That framing was wrong, and the sequential mechanism it
implied does not exist. #6256's own root cause is the accurate one:

> Tagging a Slack bot multiple times (or five people asking in one
thread) only answered the first turn. Root cause was SDK turn locking
(`onLockConflict: drop`) and managed per-thread exclusive agent
execution — not Intelligence ingress.

Those are **overlapping** mentions, and both causes were fixed in #6256.
There was never a sequential bug: the stated mechanism cannot produce
one, because both real adapters replace the message list before the run
— managed does `agent.messages = [...history]`, Slack does
`agent.messages = history` plus a fresh per-turn threadId. Subscribers
are passed per-run to `runAgent(params, subscriber)` rather than
accumulated, `session.release?.()` sits in a `finally`, and `isRunning`
is cleared by `runAgent`'s own `finally`.

What #6256 left behind is the last hole in *its* story, and it is what
this PR closes: under the new parallel default, `agent: (id) => shared`
still hands both turns one instance. Both turns reply — so the symptom
is not a dead turn — but each is prompted with the other user's
question. Reproduced above.

## Follow-ups worth filing (not in this PR)

Two of the four I flagged are handled in this PR (the dead error class,
and the sequential mystery — resolved above). Two are not, deliberately:

1. **`lockKeyPrefix` is a no-op end to end**, yet configured in two
mainline Intelligence demos (`demos/splat-demo/bff/src/main.ts:371`,
`demos/simple-agent/bff/src/main.ts:457`). Deleting it is a breaking
removal of a public `CopilotRuntime` option spanning ~30 references
across `runtime.ts`, `channel-manager.ts`, `client.ts`,
`fetch-handler.ts` and `handlers/intelligence/run.ts`, and it needs a
lockstep Intelligence PR for the demos. It also carries a real product
question — implement per-prefix locks server-side (the closed #669) or
drop the concept. Too big and too breaking to ride along here.
2. **Directly connected adapters have no equivalent of #6257's
per-thread admission gate.** A design decision, not a removal: #6256
deliberately made same-conversation turns parallel. Adjacent to
OSS-686's cross-replica question, where the unprefixed thread lock is
the only fence spanning replicas.

## Testing

Everything below was run in-session on this branch, based on
`fcc8616e91`.

**Automated**

| Suite | Result |
|---|---|
| `nx run @copilotkit/channels-core:test` | 190 passed (33 files) — was
187, +3 new |
| `nx run @copilotkit/channels-intelligence:test` | 92 passed (15 files)
|
| `nx run @copilotkit/channels-slack:test` | 318 passed (23 files) |
| `nx run @copilotkit/channels-teams:test` | 90 passed (14 files) |
| `nx run @copilotkit/runtime:test` | 1813 passed (128 files) |
| `nx run @copilotkit/channels-core:check-types` | pass |
| `oxfmt` + `oxlint` on changed files | clean (4 pre-existing warnings)
|

The first `runtime` run had one failure — `[Fetch] Debug Events >
streams debug event envelopes…` in `node-servers.integration.test.ts`.
It passes in isolation and passed on a full re-run; it is a full-suite
flake, unrelated to this change (the only runtime change here is a test
assertion in a different file).

**The bug is reproduced at the symptom level.** Two overlapping turns, a
factory returning a shared instance, each run reporting what it believes
it was asked — read while both turns are in flight. Against `main`'s
source:

```
expected [ 'first+second', 'first+second' ] to deeply equal [ 'first', 'second' ]
```

Both runs saw both users' questions. With the fix each run sees only its
own. The test asserts the symptom before the mechanism, so a regression
reports that rather than an object-identity puzzle.

**The new tests fail without the fix.** Verified by swapping in `main`'s
`create-channel.ts` via `git show` and re-running with the tests kept:

```
× factory returning a shared instance isolates each turn from the others
  AssertionError: expected FakeAgent{…} not to be FakeAgent{…} // Object.is equality
× agent whose clone() drops subclass state fails loud
  AssertionError: promise resolved "undefined" instead of rejecting
✓ does not fail loud when clone() drops an instance-patched method   ← passes both ways, by design
```

The third is a regression guard on the guard itself: it must pass before
*and* after, or the check is too strict.

**`clone()` behavior probed directly against the installed
`@ag-ui/client` 0.0.57**, rather than assumed — this is what the guard
is built on:

```
CustomAgent  source own keys: …,authClient,…,retries,…
CustomAgent  clone  own keys: (authClient and retries absent)
CustomAgent  authClient on clone: undefined | retries: undefined
CustomAgent  instanceof CustomAgent: true        ← typed correctly, silently gutted
CustomAgent  dropped: [ 'authClient', 'retries' ] ← detector fires
HttpAgent    dropped: [ ]                         ← no false positive
```

`HttpAgent`, `LangGraphAgent`, `BuiltInAgent` and `IntelligenceAgent`
all override `clone()` and stay quiet. `SanitizingHttpAgent`
(channels-slack/teams) declares no own fields, so its prototype methods
survive untouched. `ChannelOuterAgent` has three own fields and no
override, but never passes through `isolateAgentInstance` — it goes
straight to `runner.run`.

**Not verified:** a live managed-gateway turn. Carried over from #6260
as still unchecked.

## Behavior change to be aware of

The new guard turns a previously silent failure into a startup-time
throw for anyone using a custom `AbstractAgent` subclass **with its own
state and no `clone()` override**. Via the singleton config that shape
was already broken (silently); via a factory it used to work. It's a
real break, it's loud, and the message names the fields and the fix.
Flagging it explicitly rather than burying it in a doc comment.
2026-07-30 21:07:31 -05:00
Benjamin Taylor eb9643a2f2 fix(channels): assert per-turn isolation by symptom, drop dead concurrency error
Strengthen the shared-instance-factory test to assert the user-visible defect
rather than object identity. Two overlapping turns through
`agent: (id) => shared` both read the one shared `messages` array, so each run
is prompted with the other user's question too. Against main's source the test
now reports exactly that:

  expected [ 'first+second', 'first+second' ] to deeply equal [ 'first', 'second' ]

The symptom is asserted before the mechanism so a regression names the defect
instead of posing an object-identity puzzle.

This also settles what the original "only the first mention gets a reply" report
was: overlapping mentions dropped by the old `onLockConflict: drop` default plus
the managed per-thread exclusive gate, both already fixed in #6256. Both turns
do reply here; what was left was cross-contaminated context, not a dead turn.

Remove `ChannelAgentConcurrencyError`, unthrown since #6256. Its doc claimed it
was kept so older importers would not break, but it was never re-exported from
`channels-intelligence`'s entrypoint — `git log -S` over index.ts confirms it was
never reachable from outside the package, whose only export is `.`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:56:33 -05:00
Benjamin Taylor 9725663035 fix(channels): isolate factory-returned agents per turn
`createChannel` resolved an agent per turn through `agentFactory`, which
cloned the singleton config but returned a factory's result raw. A factory
is free to hand back the same object every call — `agent: (threadId) =>
shared` — which is easy to write by accident and is what a singleton
becomes when someone needs the `threadId`.

Turn concurrency defaults to `"parallel"`, and only the managed adapter
serializes same-thread deliveries, so on a directly connected adapter two
turns in one conversation can run at once. On one shared instance they
corrupt each other: `messages` is a single array both runs append into, so
each run's new-message diff picks up the other's, and `isRunning` /
`activeRunDetach$` / `activeRunCompletionPromise` are single-slot fields
the second run overwrites while the first is still streaming. Managed
delivery instead serializes on object identity, head-of-line blocking two
different conversations that share one instance.

Clone for both shapes so the object a turn runs on is never one the caller
still holds. A fresh factory is unaffected beyond an unused instance.

Because cloning is now mandatory everywhere, add a guard for the failure it
introduces: `AbstractAgent.prototype.clone()` copies a fixed field list, so
a subclass declaring its own state gets it back as `undefined` with no
error — the base method always exists and returns a correctly-typed
instance. Comparing own enumerable keys catches that and names the dropped
fields. Own functions are exempt: assigning a method on the instance is how
spies and instrumentation wrap an agent, and losing that wrapper leaves the
prototype method intact.

Also reset `isRunning` and `abortController` on the clone as hygiene — not a
fix for a dead turn, since `runAgent` assigns a fresh controller before each
run and the run loop passes none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:42:38 -05:00
Benjamin Taylor 4d74bdc5c3 feat(runtime): auto-start managed Channels on long-running hosts (refs OSS-641)
Creating a Node listener or an Express handler now STARTS activation of the
runtime's declared managed Channels, so `channels.ready()` becomes
await-and-observe instead of the thing you must remember to call. A declared
Channel connects because it was declared.

The failure mode this removes: forget `ready()` and you get a process that
serves HTTP, looks healthy, and is silently disconnected with zero output.
Auto-start's worst case is an activation error in the logs.

The generic Fetch handler stays LAZY — it is the serverless/edge entry point,
where isolates freeze and recycle per request and separate cold starts would
mint competing listeners for the same Channel. `createCopilotHonoHandler` stays
lazy for the same reason: it is our Next.js App Router surface in practice
(every `examples/showcases/*` route handler builds one at module scope), and its
TSDoc now says so loudly. `activateChannels: false` remains the opt-out that
opens no socket.

Consequence for host code: the shutdown-handler boundary moves earlier. Signal
handlers must be registered before the listener is CREATED, not merely before
`ready()` — otherwise a Ctrl-C during the connect window hits Node's default
handler and leaks a live gateway session. The slack and teams examples and the
docs snippets are restructured accordingly.

Also migrates the seven channel-package README quickstarts off the generic
handler (a request handler a socket-mode bot constructs and never serves) onto
the Node listener, so they inherit auto-start and agree with the docs site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:37:37 -05:00
Sam Julien 2a8d76399b docs(channels): clarify availability and self-hosting (#6264)
## Summary

- broaden the shared Channels overview copy from Slack and Teams to
Slack, Microsoft Teams, and more
- clarify that CopilotKit Intelligence owns the channel connection
across providers
- add an availability CTA for upcoming Discord, WhatsApp, Telegram, and
SMS support
- add a production self-hosting section with an engineering contact path
- extend the Channels docs assertions to cover the new copy and tracked
CTAs

## User impact

The Slack and Microsoft Teams overview routes now set clearer
expectations about currently available and upcoming providers, while
giving enterprise evaluators direct paths for roadmap needs and
self-hosted deployments.

## Validation

- `pnpm exec oxfmt --check
showcase/shell-docs/src/content/docs/channels/index.mdx
showcase/shell-docs/src/lib/__tests__/channels-docs.test.ts`
- `npm run lint` (passes with existing package warnings)
- `npm run typecheck`
- `npm test` (51 files, 351 tests)
- `npm run build`
- verified the shared copy rendered on both `/slack` and `/teams`
locally
2026-07-30 15:30:53 -07:00
Sam Julien 625bcd30cd docs(channels): clarify availability and self-hosting 2026-07-30 15:21:02 -07:00