Addresses review on #5969:
- Add getMessages tests: string content, content-part array (text parts joined,
non-text contributes ""), and role→isBot/user derivation; plus the no-getHistory
transport case returning [].
- Log on the unexpected getMessages catch (matches conversationStore's seeding
path) instead of degrading to [] silently — HttpDeliverySource already swallows
its own fetch failures, so this outer catch only fires on a real throw.
- Fold the duplicated role boolean (isBot computed once, drives both fields).
parseRecallBody under-validated two inputs of the /memories/recall endpoint:
- `limit`: the `typeof limit !== "number"` guard let NaN, Infinity, -5, and
3.5 through. Infinity/NaN then JSON-serialize to null (silently corrupting
the forwarded platform request); negatives/fractions are nonsensical.
Now require a finite positive integer (Number.isInteger && > 0) when
provided, else 400.
- `query`: the `length === 0` guard missed whitespace-only queries (" "),
forwarding a useless query to the platform (reachable from the inspector
recall box). Now trim and reject when empty, forwarding the trimmed value.
`scope` validation is unchanged; docstring updated to match.
Added behavioral tests asserting 400 for limit = Infinity/NaN/-1/2.5 and for
a whitespace-only query, plus a valid { query, limit } case that passes 200
with the trimmed query forwarded.
Call sites: parseRecallBody is called only by handleRecallMemories (same file).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reported bug (capabilityKey joining agentId+name with a SPACE) is NOT
present: run-handler.ts already joins with a NUL byte (0x00), matching the
"NUL-separated" docstring. The space appearance was a rendering artifact —
a NUL byte displays as an invisible/space-like gap in editors and grep.
This adds the mandatory covering regression test so a future edit back to a
space separator cannot regress silently. It registers two agent-scoped tools
whose (name, agentId) pairs collide under a space separator but are distinct
under NUL — A {name:"b c", agentId:"a"} and B {name:"c", agentId:"a b"}, both
"a b c" with a space — disables only A, and asserts B stays enabled in
isToolEnabled and buildFrontendTools. Red/green verified: fails against a space
separator, passes against the shipped NUL separator.
capabilityKey callers (setToolEnabled/isToolEnabled/buildFrontendTools) are all
internal and all rely on this collision-free identity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parse optional projectJoinToken/projectJoinCode from /memories/subscribe.
When present, open a second project_meta:memories:<code> realtime channel
alongside the user channel, feeding the same id-keyed reducer. Both channels
share the same session stamp so the reducer's superseded-context guard drops
stale deltas from both identically (the D4 landmine). Absent project creds ->
user-only, silent degrade, realtimeStatus unaffected.
Remove the user-scope snapshot filter now that project rows stay in sync via
the project channel, and invert the snapshot test to expect project rows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds recall(query, opts?) to the MemoryStore (POST /memories/recall,
hybrid RAG), a score? field on Memory, the recallResponseToMemory
projector, MEMORIES_RECALL_PATH, and the MEMORY_RECALL_FAILED error
code. The user-scope list filter is intentionally retained (its
removal is relocated to a later slot co-landing with the project
realtime channel).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the memory subscribe passthrough so the response carries the
optional project-scoped realtime credentials { projectJoinToken?,
projectJoinCode? } alongside the existing user { joinToken, joinCode }.
- Widen SubscribeToMemoriesResponse with optional projectJoinToken /
projectJoinCode; the platform client passes them through verbatim.
- handleSubscribeToMemories forwards both fields only when present and
omits them when absent (silent-degrade contract; client then opens
only the user channel). Field names match Intelligence B0a verbatim.
- Tests: handler forwards project creds when present and omits when
absent (no present-with-undefined keys); client passes them through.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getHistory was Slack-only: it keyed off threadTs and returned [] for any route
without one, so a Teams route ({adapter:'teams', tenantId, conversationId})
never hit /api/channels/history. That starved BOTH history paths on Teams --
agent.messages seeding (conversationStore.getOrCreate) and the read_thread tool
-- so the agent always reported "no earlier messages" / "no image in thread".
- 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. The /api/channels/history route
already accepts this shape. Slack query order is 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 see
nothing even when history exists.
Adds tests for the Teams getHistory query and the missing-id short-circuit.
Follow-up to #5812/#5885. When the user presses Stop, an agent may emit a
terminal RUN_ERROR (often code "abort") as its cancellation signal. The client's
onRunErrorEvent surfaced that unconditionally via emitAgentError →
AGENT_RUN_ERROR_EVENT → the default chat error banner ("This operation was
aborted"), even though a user stop is expected, not a failure.
Suppress the error emission in onRunErrorEvent when the run was user-aborted,
mirroring the local-abort suppression already on the runAgent/connectAgent
paths. Prefer the client's own _runAbortController.signal.aborted (robust) over
the agent-supplied code, with code === "abort" as a secondary signal. Normal
RUN_ERRORs are unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## 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>
Wrap the DropdownMenuTrigger and TooltipTrigger UI primitives in
React.forwardRef so they forward the ref to their underlying Radix
primitive. Radix `asChild` slots pass a ref to their child; as plain
function components these triggers dropped it, producing the React 18.3
"Function components cannot be given refs" warning on every render of
CopilotChatInput's AddMenuButton.
Fixes#5744
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.