Commit Graph

2394 Commits

Author SHA1 Message Date
Benjamin Taylor 060cebf304 fix(channels-intelligence): fail loud on empty-text egress update
HttpEgressSink.emit ran the `if (!text) return { ok: true }` no-op guard for
both post and update ops. An empty POST is a legitimate no-op (nothing to say),
but an empty UPDATE — a real intent such as clearing a message body that this
post-only fallback egress cannot express — was acked as success while nothing
was sent, inconsistent with the module's fail-loud posture. Return
`{ ok: false, code: 'empty_update' }` for that case; empty posts still no-op.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:28:25 -05:00
Benjamin Taylor 0b69bb4b4d docs(channels): clarify read_thread getMessages is text-only (OSS-488)
The getMessages JSDoc cited "what was in the image" as a use case, but
image/file parts contribute no text in this mapping — read_thread is
text-only. Image content reaches the model only via conversationStore's
seeding of agent.messages. Tweak the comment so it no longer implies
read_thread can see image content.
2026-07-17 13:52:36 -05:00
AlemTuzlak 21b9a7138b chore: release channels v0.2.1 2026-07-17 09:13:35 +00:00
Ben Taylor 0c9d639b13 chore: release monorepo v1.63.1 (#6019)
## Release monorepo v1.63.1

**Scope:** `monorepo` | **Bump:** `patch`

---

### How this release process works

1. **This PR was created automatically** by the "release / create-pr"
workflow.
   It bumped the `monorepo` packages to `1.63.1`
   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.63.1`
   - Creates git tag `monorepo/v1.63.1`
   - 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.
2026-07-16 13:43:24 -05:00
tylerslaton a7459f4fb2 chore: release monorepo v1.63.1 2026-07-16 18:23:58 +00:00
Benjamin Taylor 052dc0948e fix(channels): address CR — case-insensitive Teams hex + test/comment hygiene
- emoji: match Teams codepoint hex case-insensitively (providers may send
  upper-case) + document single-codepoint-only parsing; cover 1F504_refresh.
- emoji: note the dormant outbound Teams asymmetry in toPlatformEmoji
  (unicode vs `<codepoint>_<name>`) as a TODO for when outbound reactions land.
- channels-slack: split the tool-only status-clear test so one mirrors the
  real event order (RUN_FINISHED before finish()) and one isolates the
  finish() backstop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 12:55:57 -05:00
Alem Tuzlak db65524aad fix(channels-slack): clear the "is thinking" status on finish for tool-only replies
The run renderer only cleared Slack's native "is thinking…" assistant
status from onFirstReply() (streamed-text paths) or the error/interrupt
paths. A turn whose reply streamed NO text — a tool-only / file-only
reply such as a posted chart — never triggered onFirstReply, so the
status indicator lingered forever.

finish() now clears the status as a backstop when statusMode is on and
no reply was posted. The postedReply guard prevents a redundant clear on
the normal streamed-text path (onFirstReply already cleared it).

Adds regression tests covering both the tool-only clear and the
postedReply guard.
2026-07-16 12:55:57 -05:00
Alem Tuzlak a06c8cf1be feat(channels): canonical cross-platform reaction normalization for Teams & WhatsApp
Normalize inbound reaction emoji to a canonical cross-platform name at the
central channels-core ingress, so onReaction handlers match one value
regardless of provider.

- channels-ui/emoji: add "teams" and "whatsapp" to EmojiPlatform and canonical
  entries for refresh/laugh/surprised/sad/angry. Teams normalizes its
  <codepoint>_<name> emoji codes (1f504_refresh -> refresh) and classic bare
  names (like -> thumbs_up); an out-of-range codepoint degrades to passthrough
  instead of throwing. WhatsApp uses the unicode path like Discord/Telegram.
- channels-core: add teams+whatsapp to EMOJI_PLATFORMS; IncomingReaction gains
  an optional source `platform`; onReaction normalizes by
  evt.platform ?? adapter.platform.
- channels-intelligence: the managed reaction dispatch passes the delivery's
  source platform (env.platform) so managed reactions normalize too.
2026-07-16 12:55:57 -05: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
tylerslaton add6539dcd chore: release channels v0.2.0 2026-07-15 20:29:29 +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
Maxim 1e8457d889 fix(a2ui-renderer): preserve themeSchema (and functions) in filterCatalog
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:23:35 -07:00
Benjamin Taylor 237cbf4cf9 chore(channels): CR hygiene — real test coverage + doc/log cleanups (OSS-476)
Trivial items from the CR confirmation rounds (no behavior change to production
paths):

- intelligence-adapter.test.ts: `delete source.getHistory` was inert (getHistory
  is a prototype method; delete only removes own props), so the "no getHistory"
  branch was never actually exercised. Shadow with an own `undefined` instead so
  the `source?.getHistory?.()` short-circuit is genuinely tested.
- in-memory-transports.ts: mirror the production `limit <= 0 -> []` guard in
  InMemoryDeliverySource.getHistory (was `slice(-limit)` → returns ALL for 0).
- realtime ack(): log the empty-turn (no accepted frames) drop so the OSS-491
  redelivery pile-up is diagnosable (every other drop path here logs).
- http-transports.ts: remove two orphaned JSDoc comments dangling over
  ClaimResponse (leftovers from the file/history extraction).
- intelligence-adapter.ts: correct an inaccurate op-id comment (mintOp is the
  ${turnId}:${seq} source; the render path keys on ${turnId}:${slot}:${seq}).

channels-intelligence 170 tests + check-types green.
2026-07-15 12:58:53 -05:00
Maxim 2e08939960 fix(react-core): guard A2UI catalog registration on Catalog instance to match filtering
The catalog-component registration effect read `rawCatalog.components.values()`
for any `.components`-shaped object, registering its components onto core so
they became toggleable in the inspector. But `filteredCatalog` only filters
genuine `Catalog` instances, passing a non-`Catalog` object through unfiltered.
This diverged: disabling such a component showed it disabled in the inspector
yet the model still saw/painted it, breaking the "disabled = invisible to
model" guarantee (and `.components.values()` could throw at mount if
`.components` isn't a Map).

Guard the registration effect on `rawCatalog instanceof Catalog`, mirroring the
`filteredCatalog` guard: a non-`Catalog` object now registers nothing (not
toggleable) and is not filtered — consistent, no divergence, no mount-throw.
Real catalogs from `createCatalog()` are `Catalog` instances and are unchanged.

Adds a covering test rendering the provider with a non-`Catalog` catalog object
carrying a component: asserts it does not throw at mount and registers nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:55:06 -07:00
Maxim c055922f31 fix(web-inspector): remove broken optional fired-capability dot
Removes A3's optional "fired this session" capability-dot feature. Its
identity model was broken and unfixable without a redesign:

- Global tools never lit up: the tool-call subscriber keyed fired entries
  as `${agentId}:${name}` with a real agentId, while a global tool's row
  key is `:${name}`.
- Dots leaked across a core swap because firedCapabilities was never reset
  in detachFromCore.
- A catalog component sharing a tool's name got a false dot (name-based
  collision).

Rather than repair the fragile keying, the feature is removed entirely.
The Capabilities tab, its toggles, and enablement enforcement are
unaffected; _capabilitiesVersion is retained for toggle re-render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:37:58 -07:00
Tyler Slaton 5811cab177 fix(channels): align rebase with channel API 2026-07-15 10:29:21 -07:00
Benjamin Taylor 2718474229 fix(channels): second CR round — graceful realtime stop() + parity/hygiene (OSS-476)
From the 7-agent CR confirmation round:

- RealtimeGatewayTransport.stop() now halts intake (a `stopped` guard in
  handleDeliveryAvailable — the session exposes no `off` to detach the
  DELIVERY_AVAILABLE listener) and DRAINS the in-flight delivery (awaits the
  serial `processing` chain) before clearing state, so a turn settling at stop
  time still sends its terminal signal instead of silently no-oping and
  redelivering. Mirrors HttpDeliverySource.stop().
- Realtime nack() truncates the reason to 500 chars (parity with HTTP).
- getMessages drops empty content parts before join(" ") so a read_thread
  transcript isn't corrupted with doubled/leading/trailing spaces (a test had
  enshrined "part one  part two").
- Removed dead imports (buildContentParts, AgentContentPart, ChannelFileRef)
  left in http-transports.ts after the file/history extraction.

Deferred (delivery-contract / design; need coordination — folded into OSS-491):
timeout-nack can redeliver a still-running turnId (overlap); realtime push()
no-state fallback vs HTTP fail-loud (documented intentional — parity Q); the
empty-turn completion signal.

Tests: stop() drains in-flight + ignores post-stop deliveries; getMessages
assertion corrected. channels-intelligence 170 tests + check-types green.
2026-07-15 12:21:53 -05:00
Tyler Slaton 3dba09b7c7 feat(channels): add the batteries-included umbrella 2026-07-15 10:12:23 -07:00
Tyler Slaton 06fb2ed34a refactor(channels): extract the platform-neutral core 2026-07-15 10:11:55 -07:00
github-actions[bot] c2e2b4c0fd style: auto-fix formatting 2026-07-15 17:08:16 +00:00
Benjamin Taylor 54fa303477 fix(channels): address adversarial CR findings on the realtime transport (OSS-476)
Pre-merge 7-agent CR of #5983 surfaced several realtime-transport defects (all
in channels-intelligence). Fixes:

- Poison-payload re-lease loop: an unmappable delivery (unmodeled reply-target
  adapter / unknown input.kind) with a valid lease was logged + dropped, so
  app-api re-leased the identical payload forever. It now fails NON-retryable
  (dead-letter), mirroring the HTTP path. nack() gains a `retryable` param.
- Double-terminal-signal race: realtime ack()/nack() deleted delivery state
  AFTER the wire push, so a per-turn-timeout nack could race a late dispatch ack
  and emit BOTH fail + complete_requested. Now delete-before-push (XOR).
- Concurrent dispatch: deliveries were handled fire-and-forget; now processed
  serially (parity with the HTTP runLoop) so an in-flight redelivery can't reset
  the shared per-turn seq counter or run two turns of one conversation at once.
- actor.displayName was carried through the claim mapper then dropped in
  dispatchTo (`{ id }` only) — now forwarded to handlers (OSS-476 identity).
- fetchFile enforced MAX_INBOUND_FILE_BYTES only against declared content-length;
  the actual read was unbounded. Now streams and aborts past the cap.
- getHistory returned the FULL history for limit<=0 (slice(-0)) and hydrated file
  bytes for over-returned messages it then discarded. Now caps to the most recent
  `limit` BEFORE hydrating; limit<=0 -> [].
- stream() posted an empty text frame for an empty stream; now skips the post.
- HTTP withTimeout/defaultSleep timers now unref() (parity with realtime).
- Corrected the inaccurate thread_started comment in claim-mapping.ts.

Deferred to OSS-491 (delivery-terminal-signal contract, needs app-api
coordination): an empty turn (reaction/command that posts nothing) has no valid
completion signal (acceptedThrough requires >=1) and redelivers; and the
run_error swallow on the HTTP-fallback render path.

Tests: poison non-retryable, single-terminal XOR, serial dispatch, displayName
forwarding, getHistory limit<=0 + cap. channels-intelligence 169 tests + check-types green.
2026-07-15 12:06:50 -05:00
Benjamin Taylor bc883e89c0 feat(channels-intelligence): realtime transport parity — kinds, identity, history/files, delete (OSS-476)
The realtime-gateway transport had drifted from the HTTP transport: it built a
text-only ingress envelope that coerced commands/reactions/interactions into
empty turns, keyed conversations per-turn (breaking threaded follow-ups),
dropped the provider actor identity, and implemented none of
fetchFile/getHistory/uploadFile (so the realtime path silently ran with no
history and no file support). It also had no `delete` render kind. This brings
the realtime path to full parity with direct.

- claim-mapping: extract the claim→ingress mapping (ClaimedDelivery,
  conversationKeyFromReplyTarget, mapDeliveryToEnvelope) into a shared module
  both transports use, so they cannot drift again. Add the provider `actor` to
  the claim turn and map it to `env.user` (fixes identity on BOTH paths).
- realtime transport: build the envelope via the shared mapper — real kind
  discrimination, thread-stable conversationKey, actor→user — instead of the
  text-only inline build. Fail-closed: an unmodeled reply-target/kind is
  dropped+logged, not crashed.
- file/history: extract fetchFile/getHistory/uploadFile into a shared
  IntelligenceFileHistoryClient (HTTP-only — the gateway never relays bytes or
  history). The realtime transport gains them when configured with
  `appApiBaseUrl` + `apiKey` (threaded through the launcher); absent that, the
  methods stay undefined and the adapter degrades exactly as before.
- delete render kind: add `{ kind: "delete"; ref }` to ChannelRenderEvent
  (mirrors the frozen Intelligence contract) and route thread.delete through a
  render frame when a render sink is wired (OSS-420), like post/update/file.

Tests: shared-mapper unit tests (actor→user, kind discrimination,
conversationKey, unmodeled-adapter throw); realtime tests for non-text kind +
identity + thread-stable key and file/history capability toggling; a
delete-render-frame test. Full suite + check-types + build green.
2026-07-15 11:41:01 -05:00
Ben Taylor 183b0d9dc9 fix(channels): realtime-transport reliability hardening + residual bot-naming scrub (closes OSS-485, OSS-482) (#5972)
Combines the two OSS-473 follow-ups (originally #5972 + #5973, now
folded here) into one PR for a single review/merge. Base is `main`
(OSS-473/#5963 has merged). Reviewable **commit-by-commit** — the
mechanical rename is isolated in commit 1; the behavior changes are
commits 2–4.

## Commit 1 — `refactor(channels)`: scrub residual internal "bot" naming
(closes OSS-485)

Mechanical, naming-only, **no behavior change** — the last internal
"bot" vestiges left out of 473's atomic telemetry-surface commit.

- `bot` local variable → `channel` in `create-channel.ts` internals +
the ~15 tests that exercise it.
- `BotNode` type → `ChannelNode` in `@copilotkit/channels-ui` and
**every** importer (channels,
slack/teams/discord/telegram/whatsapp/intelligence adapters, slack/teams
examples — 256 refs).
- `botName` adapter-SPI option → `channelName` on `AdapterStartContext`
+ its `create-channel` caller + the one consumer
(`IntelligenceAdapter.start` ctx), in one change so the SPI can't drift.
(Phoenix wire contract was already `channelName` as of 473.)
- Stale `bot-ui`/`bot-slack` comment refs →
`channels-ui`/`channels-slack`; `Symbol.for("copilotkit.bot-ui.*")` →
`"copilotkit.channels-ui.*"`.
- **Kept** (out of scope): the platform `isBot?` author flag (unrelated
semantic) and example `bot` instance vars / "demo bot" prose
(user-facing).

## Commits 2–4 — `fix`: realtime-transport reliability & parity
hardening (closes OSS-482)

The self-contained observability/robustness subset of the OSS-473 CR
follow-ups (`packages/channels-intelligence` transport +
`packages/runtime` manager). Each hardening ships with a test that fails
without the fix.

- **`emit()` fail-loud** (`intelligence-adapter.ts`) — returned a
synthetic `MessageRef` on `{ ok: false }`, acking a failed
post/update/delete as success (silent egress drop). Now throws → the
delivery is nacked/retried.
- **Realtime delivery dispatch** (`realtime-gateway-transport.ts`) — the
`void handleDeliveryAvailable(...)` fire-and-forget had no error
boundary and no deadline. Replaced with a `.catch` + a bounded per-turn
timeout that nacks + logs on failure/timeout (parity with the HTTP
`runLoop`).
- **`ChannelManager.stop()` per-handle timeout** — a wedged
`handle.stop()` hung teardown/SIGTERM forever. Each is now bounded by
`stopHandleTimeoutMs` (default 5000); on timeout it's logged and
abandoned so other entries still stop.
- **`ChannelManager.ready({ timeoutMs })`** — a set-wide timeout
discarded an erroring channel's reason when a sibling hung. The deadline
is now **per channel**, so the `AggregateError` carries both the real
activation error **and** a named timeout for each hanging channel.
- **Forward `ChannelManager` `log` down** to the launcher/transport so
transport-level drop diagnostics (e.g. a version-skew
missing-`leaseToken` outage) aren't silent in the managed path.
- **`examples/slack/.env.example`** — OpenAI-only `AGENT_MODEL`
guidance; removed never-read `ANTHROPIC_API_KEY`/`GOOGLE_API_KEY`;
blanked the presence-gated `LINEAR_API_KEY`/`NOTION_*` placeholders (a
non-blank value wires a broken MCP); documented `NOTION_MCP_PORT`; added
WhatsApp to the adapter list.

### Already landed in OSS-473 (not re-done here)
The reconnect **"gave-up → error" escalation** is fully implemented on
`main`: `realtime-gateway.ts` bounds the reconnect window
(`reconnectGiveUpMs`, default 60s) and emits a terminal `gave_up`; the
launcher exposes `onStateChange`;
`ChannelManager.registerConnectionObserver` maps `gave_up → error`
(covered by `realtime-gateway.test.ts` +
`channel-manager-reconnect.test.ts`).

### Explicitly EXCLUDED — owned by OSS-474 / OSS-476
Left untouched (overlap in-flight work): no cross-turn history on the
realtime path (per-turn `conversationKey`) → OSS-474/476; empty-turn
`ack()` redelivery livelock → OSS-474/475/476; non-text parity
(command/interaction/reaction) → OSS-476; `thread.delete()` render frame
→ OSS-476 (PR #554); durable StateStore on the managed realtime path →
OSS-474/476.

## Testing
- **DoD grep** — `grep -rn "\bbot\b\|botName\|BotNode" packages/channels
packages/channels-ui` (`.ts`/`.tsx`, excl. `dist/`) → **0**; `BotNode`
gone repo-wide.
- **check-types** green across all 8 channels packages +
`@copilotkit/runtime`.
- **tests** green: channels 155, channels-ui 23, slack 277, teams 88,
discord 193, telegram 149, whatsapp 73, channels-intelligence 144,
runtime `channel-manager` 40; slack-example 63, teams-example 2. Full CI
matrix (incl. `unit` 20.x/22.x/24.x) green on this branch.
- Each 482 hardening has a test that fails pre-fix (no silent ack;
dispatch nacks on throw/timeout; `stop()` resolves + logs on a wedged
handle; `ready()` aggregate preserves the real reason;
`defaultActivateChannel` forwards `log`).

Closes OSS-485. Closes OSS-482.
2026-07-15 11:29:09 -05: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 2b4b19cf3b fix(channels): fail loud on egress failure + bound realtime delivery dispatch (OSS-482)
Two transport reliability hardenings on the managed realtime path, from the
OSS-473 CR.

emit() fail-loud: IntelligenceAdapter.emit() returned a synthetic MessageRef on
`{ ok: false }`, acking a failed post/update/delete as success (silent egress
drop on the HTTP-fallback path). It now throws with the failure code so the
failure propagates up the render/run path and the delivery is nacked/retried.

Realtime delivery dispatch: `session.on(delivery.available)` fired
`void this.handleDeliveryAvailable(...)` with no error boundary and no per-turn
deadline — an onDelivery rejection became an unhandled rejection (silent drop)
and a hung handler pinned the delivery forever. Replaced the `void` with a
`.catch` and wrapped the turn in a bounded per-turn timeout that nacks + logs on
failure/timeout (parity with the HTTP runLoop's turnTimeoutMs).

Tests: egress `{ ok: false }` makes thread.post throw (no silent ack); an
onDelivery throw and an onDelivery that exceeds deliveryTimeoutMs both nack
(channel.delivery.fail.v1) and log.
2026-07-15 07:41:17 -05:00
Benjamin Taylor 483880583f refactor(channels): scrub residual internal "bot" naming → "channel" (closes OSS-485)
Follow-up to the OSS-473 clean-break rename (#5963). Scrubs the last
internal "bot" vestiges deliberately left out of 473's atomic
telemetry-surface commit. Naming-only, no behavior change.

- `bot` local variable → `channel` in create-channel.ts factory internals
  and the ~15 test files that exercise it.
- `BotNode` type → `ChannelNode` in @copilotkit/channels-ui and every
  importer (channels, slack/teams/discord/telegram/whatsapp/intelligence
  adapters, and the slack/teams examples).
- `botName` adapter-SPI option → `channelName`: renamed on
  AdapterStartContext, its create-channel caller, and the one adapter that
  reads it (IntelligenceAdapter.start ctx) in the same change so the SPI
  cannot drift. The Phoenix wire contract was already `channelName` (473).
- Stale `bot-ui`/`bot-slack` package refs and "bot core"/"the bot" prose in
  channels/channels-ui comments → `channels-ui`/`channels-slack`/"channel".
- `Symbol.for("copilotkit.bot-ui.*")` → `"copilotkit.channels-ui.*"`.

Deliberately kept: the platform `isBot?` author flag (an unrelated
"is this message author a bot account" semantic), and example `bot`
instance variables / "demo bot" prose (user-facing, out of this ticket's
internal-symbol scope).

Validation: DoD grep returns zero internal bot symbols in
packages/channels + packages/channels-ui; check-types + test green across
all 8 channels packages (1093 tests) and both examples.
2026-07-15 07:39:50 -05:00
Ben Taylor 0b00f6ea37 fix(channels-intelligence): Teams thread history (getHistory + getMessages) (#5969)
## Problem
On the managed Teams path the agent always reported "no earlier
messages" / "I don't see the image in this thread."
`HttpDeliverySource.getHistory` was **Slack-only**: it keyed off
`threadTs` and returned `[]` for any route without one. A Teams route is
`{ adapter: 'teams', tenantId, conversationId }` (no `threadTs`), so it
short-circuited *before making any request* — starving **both** history
mechanisms on Teams:
- `agent.messages` seeding (`conversationStore.getOrCreate`), and
- the `read_thread` tool (via `thread.getMessages()`).

## Fix
- **`getHistory` is now adapter-aware** (mirrors
`conversationKeyFromReplyTarget`'s per-adapter switch): Slack keys off
`teamId`/`channel`/`threadTs`; Teams sends `adapter=teams` + `tenantId`
+ `conversationId`, matching app-api's
`teams:{tenantId}:{conversationId}` thread_key. app-api's
`/api/channels/history` route already accepts this shape. **Slack query
and order are unchanged.**
- **Add `getMessages` to the adapter** so `thread.getMessages()` (the
`read_thread` tool) reads reconstructed history via the transport and
maps it to `ThreadMessage[]`. Without it `Thread.getMessages()` returns
`[]` and thread-reading tools (summaries, "what was in the image") see
nothing even when history exists.

## Tests
- New: Teams-shaped `getHistory` query, and the
missing-`tenantId`/`conversationId` short-circuit (no request).
- All 115 `channels-intelligence` tests pass; `check-types` clean.

## Notes
Verified end-to-end against a live managed Teams bot as a dist patch
before porting to source (history seeding + `read_thread` both start
returning the thread's messages). The app-api counterpart (Teams
ingress: reactions, inline-media/Graph file ingest, slash commands) is a
separate PR in the Intelligence repo.
2026-07-15 07:39:17 -05:00
Benjamin Taylor 9886d0b3f9 refactor(channels): rename ChannelProvider to ManagedChannelProvider, gate teams in docs (OSS-473) 2026-07-15 07:15:07 -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 0e5dc5f97c fix(channels-intelligence): make gave_up sticky until a successful rejoin (OSS-473) 2026-07-15 07:15:06 -05:00
Benjamin Taylor 4cc85a945b docs(channels): use channel vocabulary in the quickstart, not bot (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 decd4c20d5 fix(channels-intelligence): truthful connection-health for managed channels (OSS-473)
Classify per-channel state on channel_declaration_unavailable rejects so a
runtime_conflict is a hard error rather than being downgraded to setup_required;
make gave_up recoverable (a later rejoin restores online); and route Phoenix
channel-level close/error through the same health transition as socket drops.
2026-07-15 07:15:05 -05:00
Benjamin Taylor 496bb57e15 refactor(channels): rename telemetry surface oss.bot.* → oss.channel.* (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
Benjamin Taylor 04a1b10e26 fix(runtime): safe-integer projectId, trim adapter, named error classes, RC7 guard test (OSS-473) 2026-07-15 07:15:04 -05:00
Benjamin Taylor 8ae942efce fix(runtime): remove drift-prone channel-name replica, log activation errors under err, cover default engine (OSS-473)
RC15: getOrCreateChannelManager bridged the manager log as
`logger.warn({ meta }, msg)`, but pino only serializes an Error's
(non-enumerable) message/stack under the `err` key — under `meta` a
failed activation rendered as `{}`, losing the cause. Route an Error to
`err` and keep `meta` for everything else.

LEVER: removed the channel-name FORMAT/length validation block plus the
replicated CHANNEL_NAME_PATTERN / MIN / MAX constants from
channel-activation-config.ts. This was a third copy of managed-specific
rules whose source of truth is channels-intelligence's
assertValidChannelRealtimeScope + assertValidChannelNames, and it kept
drifting (omitted the reserved-name rule). Now that activation failures
are logged, recorded as `error` status, and surfaced via ready(), the
up-front check is not worth cross-package rule parity. Missing/empty
name still throws (the config's own precondition). Deleted the obsolete
"Slack"/"support_bot"/"cs"/65-char rejection tests.

projectId>0: parseProjectIdFromApiKey now throws ChannelConfigError when
the parsed id is <= 0 (`cpk-0_...` matched but failed deep in the
launcher). Parser validating its own output, not a channel-name replica,
so it stays here; reuses the existing key redaction.

adapter default hardening: `adapter ?? "slack"` -> truthiness/trim check
so ""/whitespace falls back to "slack".

activate() stopped-guard: short-circuits on `this.activated || this.stopped`
so a post-stop() activate() opens no transports on a dead manager.

coverage: exported defaultActivateChannel (the real engine) with an
injectable importer seam (optional param, default = the same non-literal
dynamic import) and covered its 3 branches — config->opts mapping (scope
carries only projectId+channelName), module-not-found friendly error, and
generic-error passthrough. Added cheap manager coverage: lazy-activate
duplicate-name reject via ready(), empty channels[] -> online + ready
resolves, non-default adapter reaches the engine config.

Call sites (no external breakage):
- CHANNEL_NAME_PATTERN/MIN/MAX: were module-private; zero references.
- parseProjectIdFromApiKey: only internal caller is
  deriveChannelActivationConfig (passes the real key) + tests; <=0 throw
  affects only malformed cpk-0 keys.
- defaultActivateChannel: only internal use is the ChannelManager
  constructor default (called 2-arg) + tests; new 3rd param is optional
  and the fn still satisfies ActivateChannelEngine.
- ChannelsIntelligenceModule: new additive export, referenced internally
  + tests only.
- activate() guard / logger bridge: internal only.
2026-07-15 07:15:04 -05:00
Benjamin Taylor 6703232f4f fix(channels-intelligence): isolate onClose callbacks, guard safeReason, cover join-failure teardown (OSS-473) 2026-07-15 07:15:04 -05:00
Benjamin Taylor 040b1a848f fix(runtime): wire channel activation logging, redact key fully, validate channel-name format up-front (OSS-473)
CR batch for packages/runtime managed-channels activation:

- RC11: getOrCreateChannelManager now passes a `log` adapter bridging the
  ChannelManager diagnostic sink to the shared logger
  (`log: (msg, meta) => logger.warn({ meta }, msg)`). Previously every
  breadcrumb (setup_required, failed-to-activate, dropped-session,
  teardown-stop failure) was a no-op, so a channel that failed to activate
  was permanently dead with zero output.
- f2: stopEntry logs a swallowed handle.stop() error via the sink instead of
  discarding it; teardown stays resilient (never rethrows).
- sync-throw guard: stopEntry wraps handle.stop() in
  `Promise.resolve().then(...)` so a foreign/injected handle that throws
  SYNCHRONOUSLY is caught by the same `.catch` — otherwise the throw escaped,
  skipped resolveSettled(), and hung `settled` forever.
- f3: ready() short-circuits and RESOLVES when the manager is stopped. A
  channel that settled to `error` before stop() had already rejected its
  `settled` promise, so a later ready() threw an AggregateError even though
  status().overall was "stopped" — now consistent with the after-stop case.
- RC12: parseProjectIdFromApiKey no longer slices a fixed 8 chars off an
  arbitrary key (which echoed secret bytes for a `cpk-_...`-shaped key). The
  failure message now echoes NONE of the key value, only the expected
  `cpk-{projectId}_` format hint.
- RC13: deriveChannelActivationConfig enforces the lowercase-kebab-case
  channel-name rule (/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, length 3-64) up front
  and throws a clear ChannelConfigError, instead of passing a bad name to the
  launcher where assertValidChannelRealtimeScope throws deep and the channel
  is silently degraded to `error`. Regex/bounds are a literal copy of
  channels-intelligence's assertValidChannelRealtimeScope (the source of
  truth; not statically imported — it's an optional pure-ESM peer).
- ready() docblock reworded: async ready() REJECTS (not throws) the
  ChannelConfigError.

Tests (red-green verified): RC11 (handler logger spy + manager log sink),
f3 (ready resolves post-stop after pre-stop error), sync-throw guard, RC12
(no secret-tail leak), RC13 (Slack/support_bot/cs/65-char reject; support
passes).

Call-site enumeration for changed signatures/behaviors:
- getOrCreateChannelManager (new internal `log` arg): single caller
  fetch-handler.ts:402; public signature unchanged, no caller impact.
- deriveChannelActivationConfig (RC13 now throws on bad name): single caller
  channel-manager.ts:323 inside activate()'s per-channel loop, already
  wrapped in try/catch that converts a throw to a rejected activation ->
  recorded as `error` status and surfaced via ready()'s AggregateError.
- parseProjectIdFromApiKey (RC12 message-only change): single caller
  channel-activation-config.ts:131; error type/behavior identical.
- ready() (f3 early-return): only affects a stopped manager (now resolves
  instead of throwing) — strictly more lenient; sole public reference is a
  doc example in endpoints/node.ts:42.

Skipped: a test exercising defaultActivateChannel's module-not-found friendly
error — the fn is unexported and the dynamic import specifier is not
injectable, and channels-intelligence IS installed in the workspace so a real
MODULE_NOT_FOUND can't be forced without contorting the code. isModuleNotFound
remains unit-tested.
2026-07-15 07:15:04 -05:00
Benjamin Taylor 2c5c1138ea fix(channels-intelligence): reset onClose latch on rejoin so it fires once per drop (OSS-473) 2026-07-15 07:15:03 -05:00