Commit Graph

692 Commits

Author SHA1 Message Date
Ben Taylor 8a8e52d9e9 fix(runtime): non-optional listener.channels and honest lifecycle docs (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)
2026-07-28 14:11:53 -05:00
Maxim 0850c5e522 Merge remote-tracking branch 'origin/main' into blitz/glass-inspector/integration
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>
2026-07-28 21:10:21 +02:00
Benjamin Taylor a26990971e docs(runtime): note that the non-optional channels shape needs a literal channels tuple (OSS-646) 2026-07-28 13:26:05 -05:00
Benjamin Taylor 3f0bbe4a7b feat(runtime): default the Intelligence platform URLs to the managed service
`CopilotKitIntelligence` required `apiUrl` and `wsUrl` on every construction,
so the two correct hosts had to be found and copied by hand — which is how an
agent came to invent them. Both now default to CopilotKit's managed platform,
making `new CopilotKitIntelligence({ apiKey })` the whole managed-service setup.

Overrides are unchanged for self-hosted and non-production deployments, with two
guards that the previous required-field signature made unnecessary:

- A blank value counts as unset. These URLs are usually wired from env vars, and
  a declared-but-empty variable arrives as `""`, which would otherwise produce
  host-relative requests instead of falling back to the managed platform.
- Setting only one of the pair warns. The API and realtime planes are separate
  hosts, so a lone override silently splits the client across two deployments —
  and that failure surfaces as a hang, not an error.

Sweeps the doc, skill, README, and example surfaces to the short form so the
copy-paste path no longer hands anyone URLs to get wrong, and reattaches the
`CopilotKitIntelligence` class JSDoc, which was orphaned above an interface and
so never appeared on hover.

Linear: OSS-638
2026-07-28 13:12:15 -05:00
Benjamin Taylor 956c6448a7 fix(runtime): non-optional listener.channels and honest lifecycle docs (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.
2026-07-28 13:11:55 -05:00
Tyler Slaton 1d1ca9ea8b fix(docs): correct the documented Intelligence endpoints for Channels (#6189)
Fixes the Channels docs/examples pointing at an Intelligence host that
does not serve the API, and the websocket URL guidance that can never
produce a working prod value. Linear:
[OSS-621](https://linear.app/copilotkit/issue/OSS-621).

## The two bugs

**1. The documented host does not serve the API.** Probed every
plausible path, not just `/`:

| URL | Result |
| --- | --- |
| `api.copilotkit.ai` — `/`, `/api`, `/api/health`, `/health`,
`/api/threads`, `/api/v1/threads` | **404 on all**, `server:
awselb/2.0`, `content-length: 0` — an ALB with no target-group rule
behind it |
| `realtime.copilotkit.ai` | **no DNS record at all** |
| `api.intelligence.copilotkit.ai/` and `/api/health` | 200,
`x-powered-by: Express` |
| `api.intelligence.copilotkit.ai/api/threads` | **401** — a real,
auth-gated endpoint |
| `realtime.intelligence.copilotkit.ai/runner/websocket` | **403** —
mounted and auth-gated (a 404 would mean unmounted) |

So the documented host is not merely returning 404 at the root — nothing
is routed there on any path, and it is not the app (no `x-powered-by`).
The working pair, matching the CLI's baked-in prod defaults and
`gitops/environments/prod/values.yaml`, is
`https://api.intelligence.copilotkit.ai` +
`wss://realtime.intelligence.copilotkit.ai`.

**2. `wsUrl` was documented as derivable from `apiUrl`.** Prod splits
the API and realtime planes across *different hosts*, so a scheme-only
swap yields `wss://api.intelligence.copilotkit.ai` — wrong host. That
failure is silent: a wrong `apiUrl` returns a clean HTTP error, but a
wrong `wsUrl` sits in `connecting` until the settle timeout and reports
only "did not settle in time". The derive is not even correct locally,
where the API and gateway are on different ports (4201 vs 4401) and the
swap preserves the port. It is essentially never right, so it is deleted
rather than relabelled.

Demonstrated end to end rather than asserted — running `examples/teams`
with the WS URL unset:

```
# on main: derive(https://api.intelligence.copilotkit.ai) -> wss://api.intelligence.copilotkit.ai   (wrong host, 30s hang)
# on this branch:
exit code: 1
Missing COPILOTKIT_INTELLIGENCE_WS_URL.
  export COPILOTKIT_INTELLIGENCE_URL=https://api.intelligence.copilotkit.ai
  export COPILOTKIT_INTELLIGENCE_WS_URL=wss://realtime.intelligence.copilotkit.ai
The API and websocket URLs are DIFFERENT hosts (api.… vs realtime.…), so
the websocket URL cannot be derived from the API URL — set both.
```

## Scope note

The ticket listed 8 sites; the actual blast radius was 30 across 22
files. Beyond the ticket's list:

- **Five more channels package READMEs** — `channels`, `channels-core`,
`channels-discord`, `channels-slack`, `channels-teams` (the ticket named
only telegram + whatsapp).
- **The live product docs** —
`showcase/shell-docs/src/content/docs/channels/` taught the broken
derive in 8 files. These escaped the ticket's grep because their host
was already a `your-intelligence-url` placeholder; only bug 2 was
present. This is the surface developers actually read.
- **A generated skills mirror** — `skills/runtime/` is produced from
`packages/runtime/skills/runtime/` by `pnpm sync:plugin-skills`; fixing
one without the other leaves the bug live and fails the
`check-plugin-skills` gate.
- `skills/copilotkit-debug/references/runtime-debugging.md` and a third
occurrence in `client.ts`.

## Acceptance item 4 — resolved, chain verified

The ticket flagged a contradiction: `realtime-gateway.ts` documented
`wss://gateway.example/socket` while the runtime skill listed `/socket`
as a mistake. **The skill is right**, confirmed by tracing the real
runtime path rather than inferring it:

1. `channel-activation-config.ts:145` — `const wsUrl =
intelligence.ɵgetRunnerWsUrl()`, i.e. base + `/runner`.
2. → `channel-manager.ts:237` `wsUrl: config.wsUrl` →
`startChannelsOverRealtimeGateway` → `connectRealtimeGateway`.
3. `realtime-gateway.ts:253` hands that to Phoenix's `Socket`, which
appends `/websocket`.
4. The gateway mounts exactly `/runner` and `/client`
(`realtime_gateway/endpoint.ex:10,17`). There is no `/socket`.

The two docs survived contradicting each other because they describe
**different layers**: the public `wsUrl` is a bare base, while
`connectRealtimeGateway` receives the already-derived runner URL. Its
doc comment and the test fixtures move to `/runner` and now name the
layer. Independently, `get-runtime-info.ts:93` uses `ɵgetClientWsUrl()`,
which confirms the `/info` sample in the debug skill correctly keeps its
`/client` suffix — only the host there was wrong.

## Acceptance item 3 — split out

Failing loudly instead of hanging is a behavior change in the launcher's
connect path that overlaps OSS-622's error classification, so it is
[OSS-623](https://linear.app/copilotkit/issue/OSS-623) rather than
smuggled into a docs fix.

## Verification

- 44/44 gateway tests in `packages/channels-intelligence`;
`examples/slack/app/managed.test.ts` passes; `examples/teams` typechecks
clean.
- `nx run-many -t test,publint,attw` across the 15 affected projects
passed via the pre-commit hook; full CI green (38 checks, including
`build-check (shell-docs)`).
- Behavior proven by running the example, not by mocks (above).
- 45 commits behind `main` at time of review with **zero overlap** on
changed files, and the diff covers every dead-host and derive site
present on *current* `main`.
- No logic changes in either published package — `client.ts` and
`realtime-gateway.ts` are JSDoc/field-comment only. Behavior changes are
confined to the three example apps.

## Self-review pass

An adversarial pass over this PR tried to falsify its central claim
(that `api.copilotkit.ai` does not serve the API) by probing non-root
paths — the evidence got stronger, not weaker. It also cleared a
suspected regression: three `channels-intelligence` files read
`COPILOTKIT_INTELLIGENCE_URL` without the WS var, but they are the
HTTP-only transport path and need no socket URL. Two genuine gaps it
*did* find are fixed in the last commit: the five platform pages had
lost deployment neutrality (managed hosts now carry a self-hosted note),
and `index.mdx`/`mcp.mdx` referenced the new variable without telling
the reader where it comes from.

**One product decision for the reviewer:** `api.copilotkit.ai` is a live
ALB that routes nothing. If it is meant to become the public API alias,
this PR is documenting the wrong long-term string and the ALB wants a
listener rule instead. I used the host the product actually hands users
today (CLI prod defaults + gitops).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-28 08:46:39 -07:00
tylerslaton 17564afd2b chore: release monorepo v1.64.0 2026-07-28 02:40:43 +00:00
David McKay 6632e48a89 Merge branch 'main' into david/fix-cpk-7634-activity-snapshot-order 2026-07-27 15:21:27 -07:00
Benjamin Taylor 3c1c85937b fix(docs): point Intelligence docs at the hosts that actually serve prod
Every documented Intelligence endpoint used api.copilotkit.ai for both the
REST and websocket planes. That host returns 404, and the realtime host it
implied (realtime.copilotkit.ai) has no DNS record at all, so a developer
copying any of these snippets could not connect.

The real managed endpoints are split across two hosts, matching what the CLI
bakes in as its prod defaults and what gitops deploys:

  apiUrl: https://api.intelligence.copilotkit.ai
  wsUrl:  wss://realtime.intelligence.copilotkit.ai

Because the planes are separate hosts, wsUrl cannot be produced from apiUrl by
swapping the scheme, so each site now says so explicitly. The runtime skill's
"common mistakes" section gains that as a third failure mode, and the
CopilotKitIntelligenceConfig JSDoc documents it on the field itself — a wrong
apiUrl fails fast with an HTTP error, but a wrong wsUrl only hangs until the
settle timeout, which is what made this expensive to diagnose.

skills/runtime is the generated mirror of packages/runtime/skills/runtime,
regenerated with pnpm sync:plugin-skills. skills/copilotkit-debug is
standalone; its /info sample keeps the /client suffix, which is correct there
because that response carries the already-derived client URL.

Refs OSS-621
2026-07-27 16:44:38 -05:00
adrienplg 0d679c7fc0 fix(runtime): bump uuid off deprecated v10.0.0
packages/runtime declared "uuid": "^10.0.0", but nothing in the package's
source imports uuid directly — id generation goes through randomUUID()
re-exported from @copilotkit/shared, which already depends on uuid@^11.1.0.
The unused v10 pin only served to emit an npm deprecation warning for every
consumer installing @copilotkit/runtime.

Bump to ^11.1.0 to match @copilotkit/shared and clear the warning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:03:06 -07:00
Stefano Maffeis 182c2c754b fix(runtime): type-check agentId spy in sse-connect-agent-id test
Add AgentRunnerConnectRequest import and type the connect spy parameter so
connectSpy.mock.calls[0][0].agentId passes tsc --noEmit.
2026-07-27 06:40:20 +00:00
Ben Taylor 8806f668d1 fix(runtime): coalesce consecutive same-role Anthropic messages before dispatch (#5360)
## Summary

In multi-turn conversations using the Anthropic adapter, a model turn
that contains both assistant text and a tool call can be replayed as two
consecutive `{role: "assistant"}` entries in the Anthropic payload.
Anthropic expects one message object per turn with alternating roles, so
that split payload can blur turn boundaries on the next request. This PR
coalesces same-role Anthropic messages before dispatch so one assistant
turn stays one assistant message.

## Root cause


[`convertMessageToAnthropicMessage`](https://github.com/CopilotKit/CopilotKit/blob/005aebbededbfdd7978b3c1ee221580b74dd088d/packages/runtime/src/service-adapters/anthropic/utils.ts#L142-L219)
maps each CopilotKit message independently. A mixed assistant turn, a
`TextMessage(role=assistant)` followed by an `ActionExecutionMessage`,
therefore becomes two separate assistant entries. The Anthropic Messages
API requires all content blocks for one assistant turn to be sent in a
single message object: https://docs.anthropic.com/en/api/messages


[`AnthropicAdapter.process()`](https://github.com/CopilotKit/CopilotKit/blob/005aebbededbfdd7978b3c1ee221580b74dd088d/packages/runtime/src/service-adapters/anthropic/anthropic-adapter.ts#L282-L366)
already deduplicates `tool_result` blocks on the user side, but it
previously forwarded the mapped assistant-side payload without a
coalescing pass. Rebasing onto current `main` also exposed test-fixture
drift in the two new regression tests, so the final branch updates those
fixtures to the current object-form `TextMessage` constructor and
removes one stale `ActionInput` field while keeping the production fix
unchanged.

## Changes

- `packages/runtime/src/service-adapters/anthropic/utils.ts`: add
`coalesceConsecutiveSameRoleMessages(messages)` to merge adjacent
equal-role
  Anthropic messages by concatenating their `content` arrays.
-
`packages/runtime/src/service-adapters/anthropic/anthropic-adapter.ts`:
call
`coalesceConsecutiveSameRoleMessages` before
`limitMessagesToTokenCount`.
-
`packages/runtime/tests/service-adapters/anthropic/anthropic-adapter.test.ts`:
add same-role regression coverage, align the new fixtures with the
current
object-form `TextMessage` constructor, and remove the stale `parameters`
field
  from the action fixture.
- `.changeset/coalesce-anthropic-same-role-messages.md`: patch changeset
for
  `@copilotkit/runtime`.

## Scope

The `tool_result` allowlist deduplication stays unchanged. Token
trimming and the orphan-removal post-processor still run after
coalescing, so `tool_use` and `tool_result` pairing remains intact. The
OpenAI, Google, LangChain, and Groq adapters are unaffected.

The reported regression and the explicit regression coverage are
assistant-side. The coalescing helper itself is role-agnostic for
adjacent equal-role array content, but this PR does not add a separate
user-side regression case.

## Related PRs and Issues

Prior attempted fix: #2864, which addressed unrelated message callbacks
rather than the adapter payload.

## Test plan

- [x] `pnpm -C packages/runtime exec vitest run
tests/service-adapters/anthropic/anthropic-adapter.test.ts`
  10/10 tests pass, including the two same-role coalescing cases.
- [x] `pnpm -C packages/runtime exec vitest run
tests/service-adapters/anthropic/utils-token-trimming.test.ts`
  9/9 tests pass.
- [ ] CI green (`static / quality`, `test / unit` on Node 20/22/24)

## Upstream

Closes #2910.
Reported by @alonronin.
2026-07-25 08:47:11 -05:00
Tyler Slaton bc5454f848 fix(channels): surface fenced listener sessions 2026-07-24 19:14:58 -04:00
Rod Boev 005aebbede test(runtime): align Anthropic coalescing fixtures with current message types 2026-07-24 18:59:25 -04:00
Rod Boev b94c1be8c2 fix(runtime): coalesce consecutive same-role Anthropic messages before dispatch (#2910) 2026-07-24 18:45:10 -04:00
Benjamin Taylor 05b3b2b55b docs(runtime): direct Channels stay below the canonical/reliability layer 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>
2026-07-24 17:07:46 -05:00
David McKay 7cbc2b5d4f fix(runtime): preserve anyOf/oneOf unions in frontend tool schemas (#6143)
## Problem

A frontend/client tool (`useFrontendTool`) whose Zod `parameters` use
`z.discriminatedUnion(...)` — or any schema that serializes to a
JSON-schema `anyOf`/`oneOf` node — silently loses the union-typed field
when calling OpenAI. The tool call arrives with that field missing or
empty. Switching the same tool to a flat object with an enum
discriminant works, which points at schema conversion rather than the
model.

## Root cause

The runtime has **two** JSON-Schema → Zod converters:

- `@copilotkit/shared`'s `convertJsonSchemaToZodSchema` already handles
`anyOf`/`oneOf` as `z.union` (and `$ref`, null-unions, graceful
fallback).
- The **local copy** in `packages/runtime/src/agent/index.ts` — used by
the classic `BuiltInAgent` / AI SDK path via
`convertToolsToVercelAITools` — never did.

A union node carries no top-level `type`, so it hit the empty-schema
guard (`if (!jsonSchema.type)`) and collapsed to `z.object({})`. The
reconstructed tool schema therefore dropped the union entirely — most
visibly for a union nested inside array `items` — so the model was never
offered those fields and could not emit them. (The legacy GraphQL OpenAI
adapter is unaffected: it forwards the JSON schema directly.)

## Fix

Handle `anyOf`/`oneOf` as `z.union` **before** the empty-schema guard,
mirroring the already-proven shared converter. A single-variant union
unwraps to that variant.

## Backward compatibility

Only previously-broken union nodes change behavior (empty object → real
union). Empty `{}` schemas, typed nodes, and the `isJsonSchema` gate are
untouched. Two regression tests added: a direct `anyOf` conversion and
the exact nested-`items` `oneOf` trap.

## Repro

A `useFrontendTool` with

```ts
parameters: z.object({
  blocks: z.array(z.discriminatedUnion("type", [
    z.object({ type: z.literal("heading"), level: z.number() }),
    z.object({ type: z.literal("paragraph"), content: z.string() }),
  ])),
})
```

against OpenAI: before, `blocks` items arrived empty; after, both
variants survive into the model call.

## Note on OpenAI strict mode

This path does not enable OpenAI strict function-calling, so once the
union survives conversion it serializes back to `anyOf` and OpenAI
accepts it. The data loss was upstream, in CopilotKit's own converter,
not an OpenAI strict-mode limitation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-24 11:49:50 -07:00
David McKay e5493bca5f fix(runtime): honor *_BASE_URL env vars in resolveModel for OpenAI-compatible endpoints (#6140)
## Problem

`resolveModel()` builds each provider with only an `apiKey`, so a
**string model spec** (e.g. `"openai/gpt-4o-mini"`) cannot target an
**OpenAI-compatible endpoint** — Azure OpenAI, OpenRouter, an LLM
gateway, or a local server (vLLM / LM Studio / Ollama).
`OPENAI_BASE_URL` is silently ignored, and the only workaround is to
drop the ergonomic string form and construct a `LanguageModel` instance
yourself. The same gap exists for Anthropic and Google.

## Fix

`resolveModel` now passes `baseURL` from the standard env var for each
provider:

| Provider | Env var |
|---|---|
| OpenAI | `OPENAI_BASE_URL` |
| Anthropic | `ANTHROPIC_BASE_URL` |
| Google | `GOOGLE_GENERATIVE_AI_BASE_URL` |

It is `undefined` when the var is unset, so each provider falls back to
its default endpoint — **fully backward compatible** (no behavior change
unless you opt in).

## Tests

Adds `resolve-model-baseurl.test.ts` (4 cases): each provider forwards
its env var to the SDK factory, and an unset var leaves `baseURL`
undefined. All existing `resolveModel` tests pass unchanged (118 runtime
tests green locally).

## Context

Found while building an OpenAI-compatible agent on the V2 runtime +
`@copilotkit/react-native`, where the string model form couldn't reach
the configured endpoint.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-24 11:49:23 -07:00
Ben Taylor 955a7e9d82 feat(channels)!: Plan C — channels run only through the Intelligence runtime (#6145)
## Summary

**Plan C: channels are an Intelligence capability.** A valid
Intelligence configuration (API key) is required to run any channel.
There's a **free tier**, so this is "connect your account," not "pay."

The Intelligence runtime runs every channel: managed (Slack/Teams) over
the gateway, and **direct-adapter** channels (Discord/Telegram/WhatsApp,
or self-hosted Slack/Teams) via their own credentialed transport,
started by the runtime. The SSE runtime type-rejects `channels`, and the
handler builds channel activation for an Intelligence runtime.

> Note: the `channel.ɵruntime.{start,stop,addAdapter}` seam is an
internal (`ɵ`) contract the runtime and the managed launcher drive — not
a public API.

Design doc: [Channels = Intelligence-only (Plan
C)](https://app.notion.com/p/3a63aa381852814e83dafedc0742e7cc)

## What changed

- **`refactor(channels-core)`** — relocate the `Channel` lifecycle onto
an internal `channel.ɵruntime.{start,stop,addAdapter}` seam.
- **`feat(channels-core)!`** — **remove** public
`Channel.start()/stop()/addAdapter()`. Channels are runtime-driven only.
- **`feat(runtime)!`** — the `ChannelManager` (built only for an
Intelligence runtime) now **starts direct-adapter channels** via
`ɵruntime.start()` instead of recording them `"unmanaged"` and skipping.
Dead `"unmanaged"` status removed. Direct channels reuse the same
bounded/idempotent/resilient teardown via a synthetic handle.
- **`fix(channels-core)`** — a channel whose adapters **all** fail to
start now **errors** instead of falsely reporting `online` (a partial
start, ≥1 adapter live, still counts as started).
- **`fix(examples)`** — examples bound `channels.ready({ timeoutMs })`
so a wedged adapter start can't hang readiness.
- **`docs`** — 7 channels READMEs + the teams/slack examples reworked to
`new CopilotRuntime({ intelligence, channels })` +
`handler.channels.ready()/stop()`; no `channel.start()`, no DIY.
Multi-platform slack now runs under Intelligence (one `ɵruntime.start()`
starts all its direct adapters).

**Breaking:** `Channel.start()/stop()/addAdapter()` removed
(`channels-core` 0.2.x) — channels are driven by the runtime (`new
CopilotRuntime({ intelligence, channels })`).

## Validation

**Unit** (per-package raw `tsc` + `vitest`): channels-core **156**,
channels-intelligence **182**, runtime channel-manager **38** (incl. a
new `failStart` regression test), channels-integration 8.

**Independent whole-branch review:** direct-channel lifecycle traced
correct for every start/stop interleaving (no wedge, single-stop,
bounded/idempotent teardown shared with the managed path); public-API
removal complete (zero callers); no DIY/`channelRunner`/guard-relaxation
debris; SSE guard intact.

**Adversarial review** (tried to break it) surfaced two real items, both
handled here:
- **False `online`** — `ɵruntime.start()` swallowed adapter start
failures and resolved, so a dead channel read `online`. **Fixed**
(`fix(channels-core)` + `failStart` test).
- **Overstated invariant** — "no standalone path" is enforced by the
runtime gate + `ɵ` convention, not the type system. **Clarified** (see
the note above). Also bounded example `ready()` (`fix(examples)`).

**Live run** against a real Intelligence key (**7/7**): the gate rejects
channels-without-Intelligence; the runtime starts a direct channel via
`ɵruntime.start()` (a real turn invoked the agent, clean `stop()`); and
the real key **authenticated against the real gateway** (a managed join
was rejected by a project feature-flag, not an auth error — so the key
was accepted).

> Examples typecheck only in CI — the worktree can't build every
`@copilotkit/*` sibling; those `TS2307`/`TS2305` are environmental.

## Related

- **Improvements (fast-follow):**
[OSS-599](https://linear.app/copilotkit/issue/OSS-599) — §2 response
policy, four-mode agent binding, run-correctness (Intelligence-side).
- **Managed-provider parity:**
[OSS-600](https://linear.app/copilotkit/issue/OSS-600) +
[601](https://linear.app/copilotkit/issue/OSS-601)/[602](https://linear.app/copilotkit/issue/OSS-602)/[603](https://linear.app/copilotkit/issue/OSS-603)
(Discord/Telegram/WhatsApp) — hosted transport for the remaining
platforms; also closes the direct-channel key-validation gap (managed =
gateway-validated).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-24 11:41:21 -05:00
Stefano Maffeis 3525c74c3e test(runtime): add agentId forwarding assertion to runner.connect
CopilotKit/CopilotKit#6120

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-24 08:24:04 +00:00
Benjamin Taylor 476682330b fix(channels-core): a channel whose adapters ALL fail to start now errors, not false-online
Adversarial-review finding: ɵruntime.start() swallowed every adapter start failure
and resolved, so the runtime reported status "online" on a dead channel (revoked
token, port-in-use). Now: if a channel has adapters and NONE started, start() rejects
so ChannelManager surfaces "error" (a partial start, >=1 adapter live, still counts as
started). Adds a channel-manager failStart regression test + updates the telemetry test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:20:30 -05:00
Benjamin Taylor e6dba4519b feat(runtime)!: Intelligence runtime starts direct-adapter channels (Plan C)
The ChannelManager (only constructed for an Intelligence runtime) now STARTS
direct-adapter channels via channel.ɵruntime.start() instead of recording them
"unmanaged" and skipping — so every channel runs only because Intelligence is
configured; there is no standalone path. Removed the now-dead "unmanaged" status;
direct channels reuse the bounded/idempotent teardown via a synthetic handle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:26:24 -05:00
github-actions[bot] 814aa40d4e style: auto-fix formatting 2026-07-23 22:33:28 +00:00
David McKay 87c6b114db test: fix mock spy arity so check-types (tsc) passes
The vi.fn mock factories were 0-arg but invoked with the provider options in
the vi.mock adapters, tripping tsc (TS2554). vitest transpiles without type-
checking, so it passed vitest locally but failed the check-types job.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:31:47 -07:00
David McKay 885e392e70 test: assert union via toBeInstanceOf(z.ZodUnion) instead of untyped _def.typeName
_def.typeName isn't on ZodTypeDef's public type (TS2339 in check-types);
instanceof is the type-safe equivalent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:29:14 -07:00
David McKay 4a7e4c8bdc fix(runtime): preserve anyOf/oneOf unions when converting frontend tool schemas
A frontend/client tool (`useFrontendTool`) whose Zod `parameters` use
`z.discriminatedUnion(...)` — or any schema that serializes to a JSON-schema
`anyOf`/`oneOf` node — silently lost the union-typed field when calling OpenAI.
The tool call arrived with that field missing or empty.

Root cause: the runtime has two JSON-Schema -> Zod converters. The shared one
(`@copilotkit/shared`) already handles `anyOf`/`oneOf` as `z.union`, but the
local copy in `packages/runtime/src/agent/index.ts` — used by the classic
`BuiltInAgent` / AI SDK path via `convertToolsToVercelAITools` — never did.
A union node carries no top-level `type`, so it hit the empty-schema guard and
collapsed to `z.object({})`. The reconstructed tool schema therefore dropped
the union entirely (most visibly for a union nested inside array `items`), so
the model was never offered those fields and could not emit them.

Fix: handle `anyOf`/`oneOf` as `z.union` before the empty-schema guard,
mirroring the shared converter. A single-variant union unwraps to that variant.

Backward compatible: only previously-broken union nodes change behavior
(empty object -> real union). Empty `{}` schemas, typed nodes, and the
`isJsonSchema` gate are untouched.

Repro: a `useFrontendTool` with `parameters: z.object({ blocks: z.array(
z.discriminatedUnion("type", [...])) })` against OpenAI — before, `blocks`
items arrived empty; after, both variants survive into the model call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:57:47 -07:00
github-actions[bot] 93173c672b style: auto-fix formatting 2026-07-23 21:36:53 +00:00
David McKay 5952ea8943 fix(runtime): honor *_BASE_URL env vars in resolveModel for OpenAI-compatible endpoints
resolveModel() built each provider with only an apiKey, so a string model
spec (e.g. "openai/gpt-4o-mini") could not target an OpenAI-compatible
endpoint — OPENAI_BASE_URL was silently ignored, and the only workaround was
to construct the LanguageModel yourself and pass the instance. Same gap for
Anthropic and Google.

Now resolveModel passes baseURL from the standard env var
(OPENAI_BASE_URL / ANTHROPIC_BASE_URL / GOOGLE_GENERATIVE_AI_BASE_URL). It is
undefined when unset, so the provider falls back to its default endpoint —
fully backward compatible.

Tests: adds resolve-model-baseurl.test.ts (4 cases). Existing resolveModel
tests unchanged (118 runtime tests pass locally).

Discovered while building an OpenAI-compatible agent on the V2 runtime +
@copilotkit/react-native.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:35:12 -07:00
Mark Fogle c336514519 chore(deps): widen @anthropic-ai/sdk optional peer range
The optional peer dependency on @anthropic-ai/sdk was pinned to
^0.57.0, which conflicts with current SDK versions (e.g. 0.109) and
forces consumers to install @copilotkit/runtime with --legacy-peer-deps.

The anthropic adapter only relies on stable @anthropic-ai/sdk APIs
(client construction, messages.create, ephemeral cache_control), so
loosen the range to >=0.57.0 to remove the false peer conflict.

pnpm auto-installs this optional peer, so its specifier is mirrored in
the lockfile; update that specifier line to match. The resolved version
(0.57.0) still satisfies the range, so resolution is otherwise unchanged
and `pnpm install --frozen-lockfile` passes.

Reported by an outside contributor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:42:12 +00:00
MikeRyanDev 69861f13df chore: release monorepo v1.63.2 2026-07-23 16:15:51 +00:00
Stefano Maffeis 1d96334d64 feat(runtime): add agentId to AgentRunnerConnectRequest
Pass agentId through handleSseConnect to runner.connect() so custom
agent runners can hydrate messages when cache is empty.

Closes #5911
2026-07-23 06:10:12 +00:00
tylerslaton a7459f4fb2 chore: release monorepo v1.63.1 2026-07-16 18:23:58 +00:00
Martha Schumann 9e9ce128dd test(runtime): verify packed managed channels dependency 2026-07-16 10:44:10 -07:00
Martha Schumann fea464de52 fix(runtime): install managed channels activation dependency 2026-07-16 10:27:31 -07:00
tylerslaton 6c354037fc chore: release monorepo v1.63.0 2026-07-15 22:18:07 +00:00
Tyler Slaton 9504e0666c refactor(channels): add batteries-included package architecture (#5948)
## Summary

- Extract the platform-neutral Channels foundation into
`@copilotkit/channels-core`.
- Make `@copilotkit/channels` the batteries-included consumer entry
point, with adapter and UI subpaths.
- Release the umbrella, core, UI, and all six adapters as one shared
`channels` version scope.
- Verify the packed consumer contract and migrate the Slack and Teams
examples to the umbrella.

## Why

Consumers should be able to install one Channels package without making
the runtime or selective integrations depend on every platform SDK.
Shipping the complete Channels family together prevents adapter/core
version drift and makes the umbrella's exact dependency set release as a
compatible unit.

## How

- Move shared bot/runtime primitives into `channels-core` and invert
adapter/runtime dependencies.
- Add exact workspace dependencies and export subpaths from the umbrella
package.
- Consolidate the existing release configuration and all three release
workflow selectors into one shared `channels` scope containing all nine
packages.
- Use `@copilotkit/channels` as the version source: the next minor
release resolves to `0.2.0` and bumps every Channels package together.
- Publish scoped packages in dependency order for both stable and canary
releases: UI, core, adapters, then the umbrella.
- For a stable Channels release, publish the UI/core/adapters first, run
the registry-backed packed-consumer verifier against those newly
published exact versions, then publish the umbrella.
- Generate Channels release notes from `channels/v*` tags rather than
the monorepo `v*` tags.
- Verify builds, type checks, tests, package artifacts, examples, and
release workflow scope synchronization locally.

### First stable release sequence

1. Bootstrap the currently unpublished `@copilotkit/channels-core`
package on npm and configure npm trusted publishing for every Channels
package against this repository's `release / publish` workflow and `npm`
environment.
2. Create and merge the `channels` minor release PR. The stable workflow
publishes the Channels family in the staged order above and validates
the packed umbrella from the registry before the umbrella is released.
3. Create and merge a subsequent `monorepo` release PR so the published
`@copilotkit/runtime` switches from the historical umbrella dependency
to `@copilotkit/channels-core`.
2026-07-15 13:12:16 -07:00
github-actions[bot] 8e40a7e33c style: auto-fix formatting 2026-07-15 19:18:36 +00:00
Maxim 80221845ff Merge remote-tracking branch 'origin/main' into blitz/glass-inspector/integration
# Conflicts:
#	packages/web-inspector/src/styles/generated.css
2026-07-15 12:15:40 -07:00
Benjamin Taylor a8407f84fd fix(channels): address Tyler CR — wire app-api URL through runtime activation + map user name (OSS-476)
- [P1] History/files were absent on the NORMAL managed path: defaultActivateChannel
  never forwarded the app-api HTTP URL, so the transport (which installs
  fetchFile/getHistory/uploadFile only when appApiBaseUrl is set) ran without
  them for Channels started by the CopilotRuntime handler — only manual
  low-level launcher callers got file/history. Thread intelligence.ɵgetApiUrl()
  through ChannelActivationConfig.apiUrl → defaultActivateChannel →
  startChannelsOverRealtimeGateway({ appApiBaseUrl }). The launcher + transport
  already accepted it.
- [P2] Managed turns exposed the provider profile under a non-public `displayName`
  field, leaving PlatformUser.name undefined. Map env.user.displayName -> name
  (parity with the direct Slack adapter, which populates `name`).

Tests: deriver returns apiUrl; defaultActivateChannel forwards appApiBaseUrl to
the launcher opts; onMessage sees message.user.name. channel-activation-config +
channels-intelligence (170) green; runtime build type-checks. (channel-manager.test
executes in CI — local vitest hits the known optional-peer-dep resolution flake.)
2026-07-15 13:36:36 -05:00
Tyler Slaton 5811cab177 fix(channels): align rebase with channel API 2026-07-15 10:29:21 -07:00
Tyler Slaton 06fb2ed34a refactor(channels): extract the platform-neutral core 2026-07-15 10:11:55 -07:00
Benjamin Taylor a758faa19d fix(channels): CR follow-ups — honest stopped status + correct .env WS port (OSS-482)
Two fixes from the pre-merge adversarial CR of this PR (both pre-existing,
flagged as in-subject):

- ChannelManager.status() reported overall "online" for a manager stopped
  BEFORE activate() (e.g. SIGTERM during startup): `entries` is empty, so the
  empty-set fold returned "online" — a torn-down manager reading healthy. Now
  short-circuits to "stopped" when `this.stopped`, matching the documented
  status() contract. New red-green test covers the stop()-before-activate() case.

- examples/slack/.env.example: COPILOTKIT_INTELLIGENCE_WS_URL example was
  ws://localhost:4401, but derivation is a scheme-only swap of the :4201 API URL
  (→ ws://localhost:4201) and 4401 is used nowhere — a user uncommenting it hit a
  dead port. Corrected to :4201 and clarified the derivation note.
2026-07-15 10:50:20 -05:00
Benjamin Taylor ed37e418ca fix(runtime): bound ChannelManager.stop(), surface ready() AggregateError on hang, forward log to transport (OSS-482)
Three ChannelManager observability/robustness hardenings from the OSS-473 CR.

stop() per-handle timeout: stopEntry awaited handle.stop() unbounded, so a
wedged stop() (e.g. a socket.disconnect that never returns) hung teardown — and
thus SIGTERM shutdown — forever. Each handle.stop() is now bounded by
stopHandleTimeoutMs (default 5000); on timeout it is logged and abandoned so
every other entry still reaches `stopped`.

ready() surfaces the real reason on hang: ready({ timeoutMs }) wrapped the whole
`allSettled` in one timeout, so when one channel settled to `error` while a
sibling hung, it rejected with only a generic timeout and DISCARDED the erroring
channel's reason. The deadline is now applied PER CHANNEL, so the AggregateError
carries both each failed channel's real reason AND a named timeout for each
still-hanging channel.

Log forwarding: the manager's `log` reached activation-level events only; the
default engine (defaultActivateChannel → startChannelsOverRealtimeGateway) never
passed it down, so transport-level drop diagnostics (e.g. a version-skew
missing-leaseToken outage) were silent in the managed path. `log` is now
forwarded down to the launcher/transport.

Note: the reconnect "gave-up → error" escalation from the same CR is already
implemented end-to-end in OSS-473 (realtime-gateway.ts reconnectGiveUpMs +
ChannelManager.registerConnectionObserver) and is not re-done here.

Tests: stop() resolves + logs a timeout when handle.stop() never settles;
ready() aggregate contains both a real activation error and the hung sibling's
named timeout; defaultActivateChannel forwards its log sink to the launcher opts.
2026-07-15 07:44:35 -05:00
Benjamin Taylor 2a72ca15e3 docs(runtime): describe channel skip as exclusive per Channel, not per platform (OSS-473) 2026-07-15 07:15:06 -05:00
Benjamin Taylor fbf35ac593 fix(runtime): defer channel activation to ready() so the Fetch handler is serverless-safe (OSS-473) 2026-07-15 07:15:06 -05:00
Benjamin Taylor 5042266824 fix(runtime): declare managed provider per-Channel instead of a hard-coded slack default (OSS-473) 2026-07-15 07:15:06 -05:00
Benjamin Taylor 6fb748c368 fix(runtime): report skipped direct-adapter channels as unmanaged, not healthy (OSS-473) 2026-07-15 07:15:05 -05:00
Benjamin Taylor 68349bc1f8 fix(runtime): non-optional handler.channels for intelligence runtimes with declared channels (OSS-473)
The PR's documented snippet — `await handler.channels.ready(...)` with no
`!`/`?.` — did not type-check under strict TS because
`createCopilotRuntimeHandler` always returned `channels?: ChannelsControl`.

Encode channel-presence at the type level:

- runtime.ts: `CopilotRuntime` is now a `const` typed as `CopilotRuntimeConstructor`
  (backed by an internal `CopilotRuntimeShim` class; behavior unchanged). A
  class constructor cannot vary its return type across overloads, so the two
  construct-signature overloads live on the constructor interface: `intelligence`
  + a non-empty `channels` tuple returns a `RuntimeWithDeclaredChannels`-branded
  runtime; every other config (SSE, intelligence-without-channels, empty
  `channels: []`, or a non-literal `Channel[]` variable) stays unbranded. The
  brand is a phantom (compile-time-only) property. `export interface CopilotRuntime`
  preserves the name as a type for existing `runtime: CopilotRuntime` / `as
  CopilotRuntime` sites.
- fetch-handler.ts: overload `createCopilotRuntimeHandler` — a branded runtime
  (unless `activateChannels: false`, constrained to `true | undefined`) returns
  the new `CopilotRuntimeFetchHandlerWithChannels` (non-optional `channels`);
  everything else keeps the optional shape. Opting out of activation honestly
  falls through to the optional overload.
- Added a compile-time type test (checked by `tsc --noEmit`, the `check-types`
  gate). It probes the optionality modifier structurally (`{} extends Pick<T,K>`)
  rather than for `undefined`, since this package compiles `strict: false`.
  Confirmed it fails pre-change on the required-channels assertion and passes
  after. Dropped the now-unnecessary `!` in handler-channels.test.ts.

Call sites: the second overload is byte-identical to the former single signature,
so every `createCopilotRuntimeHandler` caller (node/express/hono endpoints,
integration servers, examples) and every `new CopilotRuntime` site resolves
unchanged; only inline non-empty-`channels` construction gains the (strict
supertype-assignable) branded type. Verified via a clean full-package check-types.
2026-07-15 07:15:05 -05:00
Benjamin Taylor 08d14dcc8c fix(runtime): reachable setup_required + connection-health status from the real gateway (OSS-473)
P1#2 — reachable setup_required on the PRODUCTION engine path.
connectRealtimeGateway no longer flattens every join rejection into a
generic Error. A join `.receive("error", reason)` whose reason is a known
setup-required code (`channel_declaration_unavailable`, and defensively
`adapter_setup_required` / `not_configured`) now rejects with a
distinguishable `RealtimeGatewaySetupRequiredError` (`code === "SETUP_REQUIRED"`,
raw reason preserved). ChannelManager already detects that code, so an
unconfigured managed provider now degrades to `setup_required` (ready()
resolves) instead of `error`. All other reasons keep the generic error and
the socket-leak teardown is unchanged.

P1#3 — status() reflects real connection health instead of `online` forever.
ConnectedRealtimeGatewaySession exposes `onStateChange(cb)` over
`RealtimeGatewayConnectionState` (`online` | `reconnecting` | `gave_up`),
driven by the real Phoenix seams: an unexpected socket drop → `reconnecting`;
a successful (re)join (the join-push recHooks survive Phoenix `resend`, so
`"ok"` re-fires on every auto-rejoin) → `online`; and a BOUNDED give-up —
Phoenix retries forever, so a `reconnectGiveUpMs` window (default 60000, runs
from the first drop of an episode, cleared on rejoin) elapsing while still
reconnecting → `gave_up` (terminal). Our own disconnect() stays silent.
ChannelManager wires this in place of the log-only onClose breadcrumb:
`reconnecting`→status reconnecting, `online`→online, `gave_up`→error; a
stopped manager/entry ignores late events. computeOverall now ranks
`error > reconnecting > setup_required > connecting > online`. ready() keeps
its one-shot semantics (settles on the initial outcome); later health
transitions move only status(). Docs updated to state `online` means
currently-sendable.

Wording: the direct-adapter skip comment/log now states delivery is
exclusive-per-platform (managed OR direct per platform, not both — attaching
both would double-deliver) with true coexistence tracked in OSS-484. Skip
behavior unchanged.

Call-sites for the changed signatures:
- connectRealtimeGateway error shape: only caller is
  startChannelsOverRealtimeGateway (realtime-gateway-launcher.ts:216); it
  awaits and lets the rejection propagate, so the setup-required error flows
  through unchanged (no branch to update).
- new ConnectedRealtimeGatewaySession.onStateChange: passed through in
  startChannelsWithGatewaySession and startChannelsOverRealtimeGateway
  (realtime-gateway-launcher.ts); added to ChannelsHandle (runtime.ts) and the
  manager's local ChannelsHandle view (channel-manager.ts); exported from
  index.ts. RealtimeGatewaySession (base, no observer) consumers
  (realtime-gateway-transport.ts) unaffected.
- manager onClose→state transitions: registerOnClose renamed to
  registerConnectionObserver; sole caller is the online settle handler in
  activate().
2026-07-15 07:15:05 -05:00
Benjamin Taylor 6ab2d57ff3 fix(runtime): skip managed activation for channels carrying a direct adapter (OSS-473)
Honors the SoT "never infer managed intent from a direct adapter" rule.
Channel.adapters is a new additive read-only member; verified no consumer
constructs Channel literals (only createChannel does).
2026-07-15 07:15:05 -05:00