## Summary
Pressing **Stop** while an assistant message is streaming
(CopilotRuntime + `HttpAgent` proxy) crashed the chat with:
```
Cannot send event type 'TEXT_MESSAGE_END': The run has already errored with 'RUN_ERROR'. No further events can be sent.
```
Root cause: `finalizeRunEvents` appended a trailing `TEXT_MESSAGE_END`
**after** the `RUN_ERROR` that the aborted agent had already emitted.
Fixes#5812.
## Root cause
When the upstream agent (e.g. pydantic-ai's `AGUIAdapter`) is aborted
mid-stream it emits a live `RUN_ERROR` while a text message is still
open — it does **not** close the message first. All runners
(`in-memory`, `intelligence`, `sqlite`) stream `finalizeRunEvents`'
output *after* everything the agent already emitted, so the appended
closer landed past the terminal:
| | outgoing event order |
|---|---|
| **Before** | `… TEXT_MESSAGE_CONTENT → RUN_ERROR → TEXT_MESSAGE_END` ❌
verifier throws |
| **After** | `… TEXT_MESSAGE_CONTENT → RUN_ERROR` ✅ terminal closes the
message client-side |
Per the AG-UI invariant: at most one terminal event per run, and no
sub-events after it. I confirmed against the real `@ag-ui/client`
`verifyEvents` (the verifier the browser runs) that a terminal arriving
with a message still open is valid — the terminal implicitly closes it.
## Fix
`finalizeRunEvents` (in `@copilotkit/shared`) now returns early and
appends **nothing** when the stream already contains a terminal event
(`RUN_FINISHED` or `RUN_ERROR`). The abrupt-end path (no terminal →
close open streams + synthesize a terminal, in the correct order) is
unchanged. No API/signature change; the in-memory, intelligence, and
sqlite runners all inherit the fix.
## Testing
RED→GREEN verified — each new/updated assertion was confirmed to fail
against the pre-fix code:
- **`finalize-events.test.ts`** — terminal-present appends nothing
(parametrized over `RUN_FINISHED` and `RUN_ERROR`) + a named #5812 case.
- **`in-memory-runner.test.ts`** — end-to-end mid-stream-stop
regression: a fake `HttpAgent`-style agent is stopped between
`TEXT_MESSAGE_START` and `TEXT_MESSAGE_END`; asserts no events follow
`RUN_ERROR` **and** that the collected stream passes `verifyEvents`
(before the fix this threw the exact browser error).
- **`intelligence-runner.test.ts`** — corrected a pre-existing assertion
that had encoded the buggy post-terminal `TEXT_MESSAGE_END`.
Green: full `@copilotkit/runtime` suite, `@copilotkit/sqlite-runner`,
`@copilotkit/shared`, `check-types`, `oxlint` (0 errors), and build.
## Reviewer notes
- The behavior change is a single early-return in `finalize-events.ts`;
the `terminalEventMissing` guards simplify away because they're only
reachable when no terminal exists.
- Diff is +204/−55 across 4 files, the bulk of it tests.
Fixes#5775.
## Problem
After #5099 scoped the streamdown markdown/table styles under
`[data-copilotkit] [data-streamdown="…"]`, the **table action controls**
(copy / download) are still unstyled for hosts that import
`@copilotkit/react-core/v2/styles.css` but don't also ship streamdown's
raw Tailwind utilities. streamdown renders the controls row, per-button
wrappers, trigger buttons, dropdown popovers and menu items with
unprefixed utilities (`flex`, `items-center`, `justify-end`, `gap-1`,
`cursor-pointer`, `p-1`, …) and **no stable `data-streamdown`
attribute**, so CopilotKit's packaged CSS didn't cover them — the
controls rendered as vertically stacked plain icons instead of a
right-aligned row.
## Fix
Add scoped fallback selectors under `[data-copilotkit]
[data-streamdown="table-wrapper"]`, targeting the controls chrome
**structurally** (since it has no `data-streamdown` hook):
- controls row → `> div:first-child:not(:last-child)` (flex,
right-aligned, gap)
- per-button wrapper → `… > div` (relative, positions the popover)
- trigger buttons → `… > div > button` (matches the code-block
copy/download button styling)
- dropdown popover → `… > div > div`
- popover menu items → `… > div > div > button`
The controls row is `table-wrapper`'s first child **only when controls
are enabled**; `:not(:last-child)` leaves a control-less table (whose
single child is the scroll container, already styled by #5099)
untouched.
## Verification
- **Compiles.** Built `globals.css` through the Tailwind v4 CLI — every
`@apply` resolves (e.g. `bg-background` → `var(--background)`,
`shadow-lg` → the shadow vars, `min-w-[120px]` → `min-width:120px`) and
all five rules emit with correct values.
- **Selectors match the real DOM.** A new DOM test
(`streamdown-table-controls.test.tsx`) renders a real `<Streamdown>`
table and asserts the controls row is `table-wrapper`'s first-non-only
child, carries no `data-streamdown` attribute, and contains the trigger
buttons under `> div > button` — i.e. the scoped selectors target real
elements. This also guards against streamdown markup drift.
- **Selector presence** guarded by `streamdown-styles.test.ts`
(whitespace-robust).
- Full `styles/__tests__` suite green; `oxlint`/`oxfmt` clean.
## Note (out of scope, discovered while fixing)
At runtime in streamdown `1.6.11` the `<table>` element is stamped
`data-streamdown="table-wrapper"` (not `"table"`): `MarkdownTable`
passes `data-streamdown="table-wrapper"` as a prop that leaks through
`...rest` onto the `<table>`, overriding the intended `"table"`. So
#5099's `[data-streamdown="table"]` selector currently matches nothing,
and the table also matches the `table-wrapper` rules. The controls fix
here is unaffected (its child-combinator selectors don't match the
table's `thead`/`tbody` children), but the `[data-streamdown="table"]`
selector is worth a separate follow-up / upstream report.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Addresses two P1s on #5940: the re-home path (a) injected unscoped frontend
tools/readable context into the background run — `buildFrontendTools`/
`getContextForAgent` include entries with no agentId, so the thread-A run
received thread-B's live context and could execute global frontend handlers
against B — and (b) lost continuity across multiple queued follow-ups (fresh
per-item proxy + enqueue-time snapshot, so run 2 never saw run 1's result).
Both stem from *running* the stale follow-up. Switch to skip-stale: when the
shared agent's threadId no longer matches the thread the follow-up was enqueued
for, drop it (with a warning) rather than run it against the now-foreground
thread. This removes the proxy/registerProxiedAgent machinery entirely and
resolves both P1s by construction. The MCP app still gets its ui/message ack at
enqueue time; only the optional agent turn on an abandoned thread is skipped.
Removes the re-home unit/integration tests; the e2e regression test (no
cross-thread run after a switch) and simplified unit tests cover the behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drives the real CopilotKitCore + RunHandler + ProxiedCopilotRuntimeAgent via
registerProxiedAgent against a mocked transport (mirrors
proxied-runtime-transport.test.ts). Asserts the re-homed run reaches the runtime
addressed to the ORIGINAL threadId (not the foreground one), carries the captured
message, runs on an isolated instance whose events never reach the shared agent,
and unregisters the transient proxy after. Closes the delegate/replay-lifecycle
gap the mocked-host unit tests couldn't reach — in CI, no live runtime needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Queued MCP app ui/message follow-up work executed against whatever thread the
shared registry agent pointed at when the queue drained. If the host switched
threads while the follow-up was queued (agent busy), the run — and its streamed
events — leaked into the now-foreground thread.
Capture the thread context at enqueue and route the follow-up through
ɵrunMcpFollowUp: same thread runs live on the shared agent (unchanged); a
changed thread re-homes the run onto an isolated registerProxiedAgent sibling
pinned to the original thread (own event stream, persists + reconciles on
return); a changed thread on a non-runtime agent drops the follow-up rather
than leaking it.
Regression from 762370a4e5 (revert of per-thread activity-renderer clone
routing, #3630); uses the sanctioned registerProxiedAgent primitive (#4629)
instead of reintroducing implicit clones.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After #5099 the table wrapper/cells are styled via [data-streamdown] selectors,
but the copy/download controls row, button wrappers, trigger buttons, dropdown
popovers and menu items render with raw Tailwind utilities and no stable
data-streamdown attribute — so hosts that import @copilotkit/react-core/v2
styles without shipping streamdown's own utilities saw them unstyled (icons
stacked vertically instead of a right-aligned row).
Add scoped fallback selectors under [data-copilotkit] [data-streamdown=
"table-wrapper"], targeting the controls chrome structurally. The controls row
is the first child ONLY when controls are enabled, so :not(:last-child) leaves a
control-less table (single child = the scroll container) untouched.
Verified against streamdown 1.6.11's actual rendered DOM (a new DOM test guards
that the structure the selectors assume still holds) and by compiling the CSS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review found the original provider tests passed even with the fix
removed: a single committed mount yields exactly one /info whether the ctor or
an effect fires it, and the ctor's fetch is several microtasks deep so ordering
can't distinguish it — only the multi-instance (discarded-render) case differs,
which Testing Library can't reproduce.
- Add CopilotKitProvider.deferWiring.test.tsx (mocked core): asserts the provider
constructs with `deferInitialConnection: true` and calls `connect()` from an
effect. This FAILS if the deferral wiring is dropped (verified).
- Keep the two real-core tests as normal-mount regression guards (one /info on
mount; idempotent under StrictMode) and document that the multi-instance proof
lives in core-defer-runtime-connection.test.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CopilotKitProvider constructs the core during React's render phase, and React
can start-and-discard renders (concurrent rendering / Suspense / StrictMode).
Because the constructor fired the `/info` request synchronously, every discarded
-and-recreated core issued its own request — a single page load was observed
firing 70-80 `/info` requests instead of one.
Separate construction (pure) from connection (network I/O):
- core: `deferInitialConnection` lets the constructor record the runtime config
(so `runtimeUrl` stays available synchronously to hooks) WITHOUT starting the
`/info` fetch. `connect()` starts the single connection and is idempotent
(bails unless status is Disconnected), so a double-invoked mount effect
collapses to one request. `updateRuntimeConnection` also gains an in-flight
guard keyed by url+transport so concurrent same-target calls de-dupe.
- react-core: the provider constructs with `deferInitialConnection: true` and
calls `copilotkit.connect()` from its commit-phase mount effect — so renders
discarded before commit never fetch.
Backward compatible: without `deferInitialConnection` the constructor still
connects (Vue/Angular/vanilla unaffected).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Authoritative wire shape for the Action.Submit button + the Message activity Teams
delivers on click (activity.value carries { ckActionId, value }, text empty; not
Action.Execute). Round-trip contract test locks emit↔decode.
finish() now performs the final send outside the error-swallowing throttle path
and rejects on transport failure, so a consumer never marks a turn delivered when
the last post/update didn't land. Mid-stream edits stay tolerant (log + retry).
## Release channels-whatsapp v0.0.2
**Scope:** `channels-whatsapp` | **Bump:** `patch`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `channels-whatsapp` packages to `0.0.2`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `channels-whatsapp` packages to npm at version `0.0.2`
- Creates git tag `channels-whatsapp/v0.0.2`
- 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.
## Release channels-telegram v0.0.4
**Scope:** `channels-telegram` | **Bump:** `patch`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `channels-telegram` packages to `0.0.4`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `channels-telegram` packages to npm at version `0.0.4`
- Creates git tag `channels-telegram/v0.0.4`
- 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.
## Release channels-intelligence v0.1.1
**Scope:** `channels-intelligence` | **Bump:** `patch`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `channels-intelligence` packages to `0.1.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 `channels-intelligence` packages to npm at version
`0.1.1`
- Creates git tag `channels-intelligence/v0.1.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.
## Release channels-teams v0.1.2
**Scope:** `channels-teams` | **Bump:** `patch`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `channels-teams` packages to `0.1.2`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `channels-teams` packages to npm at version `0.1.2`
- Creates git tag `channels-teams/v0.1.2`
- 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.
## Release channels-slack v0.1.2
**Scope:** `channels-slack` | **Bump:** `patch`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `channels-slack` packages to `0.1.2`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `channels-slack` packages to npm at version `0.1.2`
- Creates git tag `channels-slack/v0.1.2`
- 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.
## Release channels-discord v0.0.3
**Scope:** `channels-discord` | **Bump:** `patch`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `channels-discord` packages to `0.0.3`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `channels-discord` packages to npm at version `0.0.3`
- Creates git tag `channels-discord/v0.0.3`
- 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.
## Problem
`@copilotkit/channels-intelligence` still used the retired Bot
HTTP/realtime contract and exposed Phoenix implementation details. The
managed Slack entrypoint used the same legacy API.
## Why
Intelligence now exposes a clean-break Channels contract. The SDK must
use it consistently, while keeping the Phoenix-backed transport private
behind a product-neutral Realtime Gateway API. Compatibility aliases
would hide integration mismatches.
## Fix
- Migrated HTTP paths, payloads, config, and KV state to Channel
terminology.
- Added the Realtime Gateway abstraction and Channel realtime wire
contract.
- Renamed remaining APIs/types, updated the managed Slack example, and
added forbidden-term coverage.
- Preserved framework and vendor `Bot` terminology only where it remains
semantically correct.
## Summary
Adds production onboarding for the Inspector Threads empty state:
- Moves the Threads `Talk to an Engineer` CTA into the main inspector
tab nav when Threads is active.
- Replaces the enabled-empty `No threads yet` state with example thread
rows and a deselected overview.
- Lets users select an example thread to preview the real thread-details
UI with Timeline, Raw AG-UI Events, and State data.
- Adds a dismissible/reopenable example tour that persists dismissal in
local storage.
- Hides examples once real threads are present.
- Adds the `Learn how Threads work` and `Explore self-hosted
Intelligence` CTAs.
- Adds a deferred autoplay video preview to the enabled-empty deselected
overview.
## Telemetry
New/updated Threads events in this PR:
- `oss.inspector.threads_tab_clicked` — fires when the rendered Threads
nav tab is clicked.
- `oss.inspector.threads_locked_viewed` — fires once per inspector
instance for the locked state.
- `oss.inspector.threads_empty_enabled_viewed` — fires once per
inspector instance when Threads are enabled with zero real threads.
- `oss.inspector.threads_enabled_viewed` — fires once per inspector
instance when real threads are present.
- `oss.inspector.threads_intelligence_signup_clicked` — fires from
locked-state Intelligence signup CTAs.
- `oss.inspector.threads_talk_to_engineer_clicked` /
`oss.inspector.talk_to_engineer_clicked` — fire from Threads-specific
and shared Talk to an Engineer CTAs.
- `oss.inspector.threads_example_viewed` — fires once per example thread
shown in the empty state.
- `oss.inspector.threads_example_selected` — fires once per example
thread selection.
- `oss.inspector.threads_example_tour_started` — fires when the tour
auto-starts for the first selected example.
- `oss.inspector.threads_example_tour_step_viewed` — fires once per
example thread/tour step.
- `oss.inspector.threads_example_tour_dismissed` — fires when the user
skips the tour.
- `oss.inspector.threads_example_tour_completed` — fires when the user
finishes the tour.
- `oss.inspector.threads_example_tour_reopened` — fires when the user
clicks `Show tour` after dismissal.
Telemetry properties are limited to product metadata and funnel context:
package/version, inspector distinct IDs,
intelligence/thread-service/license/runtime status, runtime URL type,
CTA surface/type, telemetry-disabled status, thread count, example
thread ID, tour step/tab, and dismiss method. We do **not** send message
content, AG-UI event payloads, agent state, prompts, completions, or
thread bodies.
No telemetry was added for passive video loading; it is a visual
affordance rather than a user intent signal.
## Outbound Attribution
Threads onboarding CTAs now include existing `ref` attribution plus
these UTM parameters:
- `utm_source=copilotkit_inspector`
- `utm_medium=in_product`
- `utm_campaign=threads_onboarding`
Affected links are limited to Threads onboarding surfaces:
- Threads tab-nav `Talk to an Engineer`
- Threads locked-state `Sign up for Intelligence`
(`https://dashboard.operations.copilotkit.ai/sign-in`)
- Empty Threads overview `Learn how Threads work`
- Empty Threads overview `Explore self-hosted Intelligence`
The UTM params are opt-in for these Threads onboarding CTAs and do not
apply to generic announcement/banner links or locked Memories CTAs. The
inspector spec includes a regression test to keep locked Memories CTAs
free of the Threads campaign params.
## Video Asset + Performance
- The overview video uses the CDN-hosted asset at
`https://cdn.copilotkit.ai/corp-site/videos/copilotkit-generative-ui-agentic-frontend-demo.webm`
instead of committing a binary to `@copilotkit/web-inspector`.
- Verified the URL serves `200`, `Content-Type: video/webm`,
`Content-Length: 6765736`, and a CloudFront cache hit.
- `@copilotkit/web-inspector` currently only inlines CSS and SVG assets
in its package build, while larger docs/showcase media commonly lives on
hosted/CDN-style URLs.
- The video `src` is not rendered on the initial overview paint. It is
deferred until `requestIdleCallback` or a short timeout fallback, uses
`preload="metadata"`, fades in after `loadeddata`, and does not load for
`prefers-reduced-motion: reduce`.
## Validation
- `NX_TUI=false npx -y pnpm@10.33.4 nx run
@copilotkit/web-inspector:test -- web-inspector.spec.ts`
- `NX_TUI=false npx -y pnpm@10.33.4 nx run
@copilotkit/web-inspector:check-types`
<img width="1662" height="1382" alt="CleanShot 2026-07-10 at 12 01
03@2x"
src="https://github.com/user-attachments/assets/e2031570-f602-40dc-a54c-e9c7690fc0ba"
/>
<img width="1680" height="1388" alt="CleanShot 2026-07-10 at 12 01
12@2x"
src="https://github.com/user-attachments/assets/2c8db42f-3ab0-47e2-a235-ea54e8dd292b"
/>
## Problem
A managed bot with **both** a Slack and a Teams adapter attached only
ever received its **Slack** deliveries. Teams deliveries stayed `queued`
forever — never claimed, never sent.
## Root cause
`channels-intelligence`'s runtime claim loop (`http-transports.ts` →
`claimOnce()`) posted a per-provider filter to
`/api/bots/listener/claim`:
```ts
{
runtimeInstanceId: this.cfg.runtimeInstanceId,
adapters: [this.cfg.adapter], // defaults to "slack"
}
```
app-api filters claimable deliveries by that list (`$adapters IS NULL OR
bie.provider = ANY($adapters)`), so a runtime declaring only `"slack"`
is never handed the same bot's Teams deliveries.
But the managed runtime is **provider-agnostic**: it emits abstract
render frames and Intelligence renders each reply per the delivery's own
reply target. There is no reason for the runtime to constrain claims by
provider — one `intelligenceAdapter()` should serve every channel its
bot has attached.
## Fix
Drop the `adapters` field from the claim body. `adapters` is already
optional on the app-api side (absent → `NULL` → no provider filter → all
providers), so this needs no coordinated backend change.
`this.cfg.adapter` is still used for the heartbeat's declared bots and
for egress, both unaffected.
## Testing
Verified end-to-end locally against a managed Teams bot: inbound Bot
Framework JWT → claim → agent run → render → Bot Connector egress all
`succeed` with this change. Slack continues to work unchanged.
Follow-up to the provider-agnostic claim change on this branch. Now that the
runtime claims deliveries for every provider its bot has attached, Teams
deliveries flow through the same bridge — and their reply target is a distinct
shape (serviceUrl/conversationId/tenantId, no teamId/channel/threadTs). Deriving
conversationKey from Slack-only fields collapsed every Teams conversation onto
one degenerate key, and conversationKey keys the agent/session
(getOrCreate -> makeAgent), so distinct Teams conversations would share
state/memory.
Make replyTarget a discriminated union (slack|teams) and derive conversationKey
per provider: teams:{tenantId}:{conversationId}, matching Intelligence app-api's
thread_key (OSS-441 slice 2, Intelligence #511) so client and server agree on
conversation identity. Unknown adapters fail loud (the claim loop's existing
catch nacks, not wedges).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The managed runtime's claimOnce() declared `adapters: [this.cfg.adapter]`
(defaulting to "slack"), which Intelligence used to filter claimable
deliveries by provider. A runtime serving a bot with both Slack and Teams
adapters would therefore never receive the bot's Teams deliveries — they
stayed queued forever while Slack worked.
The managed runtime is provider-agnostic: it emits abstract render frames
and Intelligence renders per the delivery's own reply target. So the claim
must not filter by provider. Drop the adapter filter from the claim body;
one config-free `intelligenceAdapter()` now serves every channel its bot
has attached.
Verified end-to-end locally against managed Teams: inbound JWT -> claim ->
agent -> render -> Bot Connector egress all succeed with this change.