Adds a minimal, runnable test rig for the channels HITL path that nothing in
the repo previously exercised: `onInterrupt` + `thread.resume()`, driven by a
real LangGraph graph that suspends in its checkpointer.
Channels has two unrelated HITL models, and they suspend in different places:
thread.awaitChoice() a channel-side tool handler blocks; the agent run
stays open; the waiter is an in-memory Map
onInterrupt/resume the AGENT's graph suspends; the run ENDS; the click
starts a new run carrying the resume value
Only the first had coverage. `examples/integrations/langgraph-python` has zero
`interrupt()` calls, and `examples/slack/e2e/restart-recovery.ts` — written for
this exact path — no longer compiles (it imports `@copilotkit/slack` plus
`appComponents`/`appHitl`, none of which still exist).
What's here:
- `agent-py/` — a LangGraph agent whose `create_thing` tool calls `interrupt()`
and writes nothing, so the interrupt fires deterministically with no
Linear/Notion credentials. Served over AG-UI, so the channel dials it
directly and `runtime.ts` is not involved.
- `agent-py/probe.py` — drives the agent alone (no channel, no Slack).
- `e2e/route-b-interrupt.ts` — drives agent + channel with a `FakeAdapter`
standing in for the platform, so a failure is unambiguously channels' fault.
Asserts the resume leaves in the legacy `forwardedProps.command.resume` shape.
- `app/route-b.ts` — the Slack-facing probe, plus a boot-time preflight that
rejects an unreachable/mangled `AGENT_URL` with a specific diagnosis instead
of a bare `fetch failed` on the first message.
Two wire facts worth recording, since both fail silently:
- `on_interrupt` is not a label you choose. It's the AG-UI custom event name
LangGraph's adapter emits, and adapters default `interruptEventNames` to
exactly that. Rename either side and the graph stays suspended forever.
- the interrupt payload crosses as a JSON *string*. All four real adapters
parse it before the handler runs; `FakeAdapter` does not, so handlers that
assume an object work in Slack and break headless. Both handlers here
normalize defensively.
Nothing in `packages/` changes; this is examples-only.
## Release monorepo v1.65.0
**Scope:** `monorepo` | **Bump:** `minor`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `monorepo` packages to `1.65.0`
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.65.0`
- Creates git tag `monorepo/v1.65.0`
- 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 v0.6.0
**Scope:** `channels` | **Bump:** `minor`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `channels` packages to `0.6.0`
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` packages to npm at version `0.6.0`
- Creates git tag `channels/v0.6.0`
- 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
Teams prepared deliveries compose the adapter, tenant, and provider user
ID. A real personal-chat identity is 133 characters, but the SDK
validates `appUserId` with the 128-character single-external-ID bound
and rejects the delivery before `onMessage` runs.
## Why
The composed app-user ID contains several opaque provider-owned parts.
It needs its own bounded validator; widening every external ID would
weaken unrelated protocol fields.
## Fix
- Validate only `appUserId` with a 2,048-character composite bound.
- Keep the same safe character set and the 128-character bound for other
external IDs.
- Add regression coverage for the observed 133-character Teams shape and
the upper bound.
Paired Intelligence contract fix:
https://github.com/CopilotKit/Intelligence/pull/720
## Summary
- Require `identifyUser` at Channel creation and separate application
users from provider actors.
- Add user/project Memory grants for Channels and the web runtime,
including project-only Channel access.
- Bind HITL continuations to immutable identity snapshots and consume
each continuation once.
- Update first-party adapters, examples, public docs, and cross-surface
tests.
## Why
Channel threads are provider conversations, not personal owners. Each
turn must resolve identity and Memory access explicitly so a shared
channel cannot inherit the wrong person.
## Impact
This is a hard API cut. It removes `PlatformUser`, `userKey`, and
`store.identity`; makes Channel `user` nullable; adds `actor`; and uses
one Memory policy for agent and browser access.
Intelligence counterpart:
https://github.com/CopilotKit/Intelligence/pull/706
## Validation
- `pnpm nx run-many -t test,build,check-types --projects
@copilotkit/channels-core,@copilotkit/channels-intelligence,@copilotkit/channels-slack,@copilotkit/channels-teams,@copilotkit/channels-discord,@copilotkit/channels-telegram,@copilotkit/channels-whatsapp,@copilotkit/channels-ui,@copilotkit/channels,@copilotkit/core,@copilotkit/runtime
--outputStyle=static`
- `pnpm lint`
- `npx vitest run && npm run typecheck && npm run build` in
`showcase/shell-docs`
- Pre-commit test, publint, and attw checks for 22 public projects.
## Release monorepo v1.64.2
**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.64.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 `monorepo` packages to npm at version `1.64.2`
- Creates git tag `monorepo/v1.64.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 v0.5.0
**Scope:** `channels` | **Bump:** `minor`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `channels` packages to `0.5.0`
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` packages to npm at version `0.5.0`
- Creates git tag `channels/v0.5.0`
- 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.
Adds `examples/showcases/reskinnable-demo` — the banking showcase
restructured onto a reskinnable shell/skin architecture, shipping
**banking** and **airline** as two swappable skins.
`examples/showcases/banking` is untouched.
## What this is
One Next.js app whose entire experience — brand, theme, layout, pages,
frontend tools, a2ui catalog, agent — swaps at runtime from a floating
selector. A skin-agnostic **shell** hosts one **skin** per route segment
`/[skin]/...`.
```
src/shell/ skin-contract · provider · registries · chat/ · canvas/ · threads/
src/skins/banking/ REST-backed (/api/banking/v1/*)
src/skins/airline/ in-memory client state
src/app/[skin]/ resolves the skin from the URL, mounted keyed by skin.id
```
The contract deliberately spans **both data substrates** — banking talks
to a REST backend, airline holds plain client state. A contract that
only ever hosted one kind of skin proves much less.
## How it was built
Started as a byte-identical clone of banking (159 tracked files, one
commit), then restructured in verifiable steps. Every intermediate step
kept the app **building and running**, not merely compiling. Most of the
diff is relocation; the genuinely new code is `src/shell/**`,
`src/skins/airline/**`, and banking's skin assembly.
Because `examples/showcases/banking` still exists, git cannot infer
renames — the diff shows all 203 files as additions. Reviewing it that
way is misleading. The real surface: **63 files are byte-identical to
banking, 90 changed (mostly import specifiers), 48 are genuinely new.**
## Decisions worth a reviewer's attention
**Banking keeps its REST backend, namespaced to the skin.** The
standalone prototype this architecture came from had replaced banking's
`/api/v1/*` with a client-side store. That was never a design decision —
the prototype was greenfield and simply never ported it (no commit
deletes it; the only deletion in its history is two stub skins). Since
every route under it is banking domain — cards, transactions,
exceptions, policies, reports, users, dev/reset — it moved to
`/api/banking/v1/*` rather than being discarded. Mounting one skin's
whole backend at the app root would contradict the premise that skins
are self-contained.
**`v1` is retained** even though it is vestigial (it arrived with the
pre-monorepo demo; there is no `v2` and nothing negotiates on it) so the
path shape is otherwise unchanged.
**The routing cutover is one commit on purpose.** Banking's pages *were*
the Next.js routes, so moving them, adding `/[skin]` routing, swapping
the registry off the placeholder, assembling the skin object and cutting
the shell's banking coupling had to land together — any split produces a
commit that builds and serves 404s.
**Four contract fields were added** beyond the prototype's version, each
closing a gap that would otherwise make a shipped banking feature
inexpressible:
- `toolLabels` — tool-activity chip labels are skin-domain strings
- `chatHeaderActions` — a skin contributes buttons to the shared chat
header (banking's invoice paperclip)
- `onSuggestionSelect` — a suggestion pill may stage an attachment and
drive the composer instead of sending text
- corrected OGUI semantics — surfaces render full-region on the canvas.
The prototype's "inline in the chat" note described a *published-SDK*
limitation that does not apply on `workspace:*`.
## Bugs fixed along the way
Several were pre-existing in banking and only surfaced by moving the
code:
- **`/cards?operation=change-pin` opened nothing.** Card operations are
registered on the index route, but banking's `/cards` re-exported the
*dashboard*, which ignores `?operation=`. Both `/` and `/cards` now
resolve to the skin base, so the PIN dialog actually opens.
- **The presenter-reset test asserted a pre-#6136 shape.** That route
also forgets the default persona (3 ids, not 2) and re-seeds; the test
was never updated, and its seeding dependency was unmocked so it
executed real network code.
- **Two memory smoke scripts posted to
`/api/copilotkit/agent/default/run`** — no `default` agent exists once
agents are keyed per skin. Nothing in the toolchain reads those `.mjs`
files, so no build or test could have caught it.
- **Both Docker stacks shared a compose project name**
(`banking-memory`), so containers *and named volumes* were shared:
`docker compose down` in either app destroyed the other's memory stack.
- **The skin selector covered the chat and swallowed clicks** on its
toggle. Now positioned from a `--nw-chat-width` custom property the chat
publishes, in a `pointer-events: none` strip — structurally unable to
intercept.
- **A dark-mode leak and a token-ownership leak**: `.dark` persisted
across skin switches into a light-only skin's shared chrome, and the
shell's `@theme inline` block still carried banking-violet shadow
values.
- **971 lines of dead code dropped** — `threads-drawer`, `threads-panel`
and their CSS module were superseded by `chat-inbox` in #6136 and
imported by nothing.
## Verification
- `nx run reskinnable-demo:build` green; **45 unit tests pass, 0 fail**;
`oxlint` 0 errors; `eslint` clean
- **6/6 Playwright smoke**, including 5 new tests covering reskinning
itself — the index redirect, both skins rendering their own chrome,
unknown-skin 404, and switching in both directions. Each was verified by
*deliberately breaking the app and watching the test fail*, because the
first draft of these assertions passed against a broken airline (the
shell selector renders every skin's brand on every page, so a brand-text
check proved nothing).
- Route behaviour confirmed over HTTP: `/` → 307 → `/banking`;
`/banking{,/cards,/charges,/dashboard,/team}` and `/airline` → 200;
unknown skin → 404
- The agent prompt was verified byte-identical to banking's (19,382
chars, compared programmatically) — it encodes demo behaviour including
the never-markdown-tables rule
- All 8 of #6136's demo beats traced end-to-end, plus the three
regressions it fixed (globally-registered action tools, non-HITL closing
step, the two non-interchangeable `actions.ts` helper families)
- Reviewed by a 7-lens pass scoped to the delta-from-banking. It found 4
significant issues, all fixed here: the runtime route applied banking's
identity scheme to every skin; per-user memory scoping depended on an
accidental effect ordering; dark mode leaked into a light-only skin's
shared chrome; and the skin selector overlapped skin navigation below
~1190px. One reported finding was investigated and **rejected** as
factually wrong rather than applied.
### How to read the diff
Because `examples/showcases/banking` still exists, git cannot infer
renames and shows all 203 files as additions. Reviewing it that way is
misleading:
- **63 files are byte-identical to banking** — no review needed
- **90 changed**, most only in their import specifiers
- **48 are genuinely new** — `src/shell/**`, `src/skins/airline/**`,
banking's skin assembly, `/[skin]` routing
The 8 commits are grouped by area of concern and read in order:
workspace → shell → banking-as-a-skin → airline → routing → tests → docs
→ tooling.
## Isolation from the banking demo
This app vendors the same Intelligence stack as banking with the same
seeded persona ids, so it is isolated on two independent axes:
**Ports** — banking's `7050/7053/715x` shifted by +200
(`7250/7253/725x`). Without this, `pnpm dev` here while banking's stack
was up attached silently to banking's backend: same memory buckets, and
the presenter reset button clearing the neighbour's demo state. Verified
live — the stack comes up as project `reskinnable-demo-memory` with its
own volumes and all six ports bound, banking untouched. The native Metal
TEI on `:7067` stays deliberately shared: same version and model give
byte-identical embeddings and it holds no demo state.
**Organization** — a different seeded `cpk` key, so this app resolves to
`haus-von-haskell` rather than banking's `casa-de-erlang`. Org comes
from the authenticated key and `seed.sql` already provisions three orgs
for exactly this, so it is a key swap with no backend change. This axis
matters because ports are a local convention that copying banking's
`.env` over this one silently undoes, whereas the org key still holds
when someone does.
Verified against a freshly seeded stack: a memory written under one key
is invisible to the other **even for an identical user id**, and the
app's own reset seeded 3 memories into org 2 that org 1 cannot see.
Neither demo can read or delete the other's memories.
The presenter reset and the runtime now also name the backend they
resolved (`apiUrl` in the reset response, one startup log line), so a
misconfiguration is visible rather than silent.
**One inherited doc claim corrected.** `.env.example` warned that
non-seeded ids `403` against the Intelligence stack. Measured, they do
not — `GET`/`POST /api/memories` returns 200/201 for an unseeded id and
for a nonsense one, the scope being created on demand. It mattered
because `DEMO_DEFAULT_USER_ID` is absent from `seed.sql`, so the warning
implied the unpinned interactive config the same file recommends was
broken. It is not. **banking still carries the claim** and deserves the
same correction.
## Known follow-ups (not blockers)
- `/banking/<unknown>` returns HTTP 200 while rendering the 404 UI —
`notFound()` in a client subtree, after the status is committed. Correct
UX, wrong status code; a clean fix needs a server-component validation
boundary.
- `e2e/ogui-routing.spec.ts` is `test.describe.fixme`. It clicks 7
suggestion pills, 6 of which #6136 had already removed from the
registered set — it has been broken on `main` since July, unnoticed
because **no CI workflow runs banking's e2e at all**. Rebuilding it
against the new catalog is real work, and the OGUI/a2ui routing it
guarded currently has no e2e coverage.
- Banking's own copy of the `dev/reset` test has the same stale
assertion and deserves a one-file fix.
- The two demo stacks still share host ports 7050/7053, so they cannot
run simultaneously (a loud "port already allocated", not silent
corruption). Moving them needs lockstep edits across compose, env,
playwright and `src` test defaults.
- Airline has no dark palette and renders no theme toggle.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01VgnriWLrxhfKnK8g1gHa5S
The previous comment presented the effect-based ref sync as merely avoiding
the react-hooks/refs lint rule. It is a trade-off: the render reads
cardsRef.current, and an effect writes after commit, so a card mutation
landing while the PIN card is open can leave the picker one render behind.
Records why that is accepted and what fixing it properly would require.
Replays three generative-UI changes from the banking showcase (#6259) into
the reskinnable demo's banking skin:
- setCardPin keys its collapsed render on the tool result and remembers
answered calls in a module-scope map, so a resolved PIN change survives
re-render. Drops the [cards] dependency in favour of a ref, which is what
stopped the tool tearing down mid-flight.
- showCharges becomes human-in-the-loop: it asks before moving the user's
whole screen, and routes through the skin-scoped base prefix.
- Notes about reported charges are prefixed with a red alert emoji.
Replays the report chart curation from the banking showcase (#6259, #6263)
into the reskinnable demo's banking skin. The three columns become spend
breakdown, spend over time and largest charges — the last of which answers
"which line items drive the total", a question a three-team aggregate
cannot. Additions carry a policyId so their bars colour by owning team.
Ports the brand-tint treatment for explicitly-set filter selects from the
banking showcase charges page (#6259) into the reskinnable demo's banking
skin, so an agent-applied filter reads as deliberate.
Ports the seed rebalance from the banking showcase (#6259) into the
reskinnable demo's banking skin so the report's spend-share donut shows a
meaningful split rather than three near-equal slices.
Ports the ranked largest-charges chart from the banking showcase (#6263)
into the reskinnable demo's banking skin. The report-card wiring that
consumes it lands separately.
Ports the red-alert-emoji instruction from the banking showcase seeded
memories (#6259) into the reskinnable demo's banking skin, so recalled
memory drives the same note formatting.
Ports the "format prose the same way every time" instruction block from
the banking showcase agent prompt (#6259) into the reskinnable demo's
banking skin, where the prompt lives in agent.ts rather than the route.
Ports the confirm-before-navigate chat card from the banking showcase
(#6259) into the reskinnable demo's banking skin. The showCharges
human-in-the-loop wiring that consumes it lands separately.
## Summary
- Send the first bounded Slack text delta in stream.start for direct and
managed delivery.
- Keep later appends, continuation messages, and empty-stream cleanup
behavior.
- Add managed delivery integration tests for first-text, empty-chunk,
and empty-stream paths.
## Root cause
Slack received stream.start without text, so it rendered Thinking...
until a separate append arrived.
## Tests
- pnpm nx run-many -t test,check-types,build -p
@copilotkit/channels-slack @copilotkit/channels-intelligence
--skip-nx-cache
- pnpm nx test @copilotkit/react-core --skip-nx-cache
- pre-commit test, publint, and attw checks for 12 affected projects
Points this app at the stack's second seeded organization
(haus-von-haskell) instead of the one banking uses (casa-de-erlang). Org is
resolved from the authenticated cpk key, and seed.sql already provisions three
orgs for exactly this, so it is a key swap with no backend change.
Ports and orgs isolate on independent axes and the second one matters: ports are
a local convention that copying banking's .env over this one silently undoes,
whereas the org key still holds when someone does. Verified against a freshly
seeded stack — writing under one key is invisible to the other even for an
identical user id, so neither demo can read or delete the other's memories.
Also corrects an inherited claim. banking's .env.example warns that non-seeded
ids 403; measured, they do not — GET/POST /api/memories returns 200/201 for an
unseeded id and for a nonsense one, the scope being created on demand. That
mattered because DEMO_DEFAULT_USER_ID is absent from seed.sql, so the warning
implied the unpinned interactive config it recommends was broken. It is not.