2330 Commits

Author SHA1 Message Date
Austin Merrick 9900d8e82f docs(threads): harden hosted contracts 2026-08-03 09:35:55 -07:00
Austin Merrick 89135d9635 Merge remote-tracking branch 'origin/main' into malabo
# Conflicts:
#	showcase/shell-docs/src/content/docs/premium/threads-explained.mdx
2026-07-31 23:57:15 -07:00
Austin Merrick 5a035434e0 test(showcase): close hosted contract gaps 2026-07-31 15:24:52 -07:00
Austin Merrick 341f57a7f3 docs(threads): clarify recency sort fallback 2026-07-31 13:38:58 -07:00
Austin Merrick af7980029c docs(react-core): clarify Drawer managed entitlement 2026-07-31 12:26:32 -07:00
Austin Merrick 595befbf3b docs(threads): align cross-frontend contracts 2026-07-31 12:26:32 -07:00
Mike Ryan eb6c8df0ad fix(channels): start Slack streams with first text 2026-07-31 10:26:41 -07:00
Ben Taylor 101fe27d80 fix(channels): contain terminal provider failures (#6269)
## Summary

- stop the Channels agent loop when a tool handler reports an
already-terminal provider delivery
- freeze managed renderer fanout while canonical ingestion records
`RUN_ERROR`
- immediately observe Slack native-stream queue failures while
preserving them for `finish()`
- treat uncertain managed file-delivery errors as terminal delivery
outcomes

## Root cause

The Core run loop converted every tool-handler exception into a
model-visible tool result. After `ChannelProviderDeliveryError` closed
the effect path, the model could continue and emit text, causing Slack
native rendering to call `slack.stream.start` against a closed delivery.

The native stream also retained that rejection in an unobserved internal
promise until `finish()`, leaving a Node unhandled-rejection window.

## Validation

- `pnpm nx run-many -t test check-types -p
@copilotkit/channels-core,@copilotkit/channels-slack,@copilotkit/channels-intelligence
--skip-nx-cache`
- `pnpm nx run-many -t build publint attw -p
@copilotkit/channels-core,@copilotkit/channels-slack,@copilotkit/channels-intelligence
--skip-nx-cache`
- `pnpm nx run-many -t test check-types build publint attw -p
@copilotkit/channels --skip-nx-cache`
- pre-commit affected-package matrix: 17 projects / 24 tasks
2026-07-31 10:09:55 -05:00
Ben Taylor 468995e8f5 feat(telemetry): inspector opened event and banner surface split (OSS-566/568) (#6203)
Two related Inspector-telemetry tickets: **OSS-566** and **OSS-568**.

## OSS-566 — explicit "Inspector opened" event

There was no event recording that the panel was opened. Opens could only
be inferred from in-panel activity (~1,655/90d, a floor) or from
`banner_clicked` cta=`body` (~511), which misses the common
floating-button path entirely.

Adds `oss.inspector.opened` with:

| property | values |
|---|---|
| `open_source` | `floating_button` \| `announcement_preview` |
| `has_unseen_announcement` | whether an announcement was on screen at
open time |
| `license_status` / `runtime_mode` / `runtime_url_type` | same
segmentation the threads events already carry |
| `package_name` / `package_version` / `inspector_distinct_id` | version
segmentation |

**Restoring a persisted-open panel deliberately does not count.**
Restore assigns `isOpen` directly instead of routing through
`openInspector()`, so page reloads — and every `next dev` hot reload —
stay out of the number.

## OSS-568 — banner surface + first-class dismissal

1. **`surface` on `banner_viewed`** — `collapsed_preview` (bubble on the
collapsed widget) vs `expanded_card` (card inside the opened panel),
stamped at fire time. Dedup is now per `(banner, surface)` instead of
per banner, so opening the panel records the card impression as its own
signal.
2. **`oss.inspector.banner_dismissed`** — emitted **in addition to**
`banner_clicked { cta: "dismiss" }`, not replacing it, so dashboards
reading the `cta` value keep working. Carries `surface` too, separating
"swatted the bubble away" from "dismissed the card after opening".

Both new events clear the sink's `oss.inspector.` prefix gate, so **no
telemetry-sink deploy is needed**.

## Testing

- **`packages/web-inspector` full suite — 112 passed (4 files)**, run
locally in the worktree:
  ```
   ✓ dev/css-raw-import.spec.ts (1 test) 1ms
   ✓ src/__tests__/telemetry-egress-guard.spec.ts (3 tests) 2ms
   ✓ src/lib/__tests__/telemetry.test.ts (28 tests) 8ms
   ✓ src/__tests__/web-inspector.spec.ts (80 tests) 890ms
   Test Files  4 passed (4)
        Tests  112 passed (112)
  ```
- **New coverage**: payload shape for `opened` / `banner_dismissed`,
incl. an allow-list assertion that no content/PII key can be added
accidentally; collapsed→expanded surface sequence on open; per-surface
dedup; open attribution for both sources; no event for an already-open
panel; no event for a restored-open panel; nothing emitted when the
runtime reports `telemetryDisabled`; an open still recorded while the
runtime is disconnected.
- **`tsc --noEmit`** on `@copilotkit/web-inspector`: clean (after
building `core` + `shared` dist in the worktree).
- **`tsdown` build**: succeeds; the test-only egress-guard helper is
**not** present in `dist/`.
- **`oxfmt --check`**: clean. **`oxlint`**: 9 warnings, all
pre-existing.
- `@copilotkit/runtime` (1,760) and `@copilotkit/shared` (199) also
green — both are back on main's own test files in this PR.

## A test-only egress guard rides along

`vitest.setup.ts` installs a fetch guard that swallows requests to the
telemetry sink. This is **not** CI plumbing — it is a prerequisite for
the new events. These tests run in jsdom, where a real `fetch` exists,
and inspector telemetry is fire-and-forget, so any test that drives a
banner / threads / open path without stubbing fetch POSTs a real
`oss.inspector.*` event to the live sink, from developer machines as
well as CI. The announcement-dismissal tests were already doing this;
the new `opened` / `banner_dismissed` tests hit the same send path. No
environment variable can prevent it, because the inspector's opt-out
arrives in the runtime's `/info` response and these tests never boot a
runtime.

## Not in scope

Suppressing telemetry from CI jobs that boot real apps (**OSS-565**) was
explored on this branch and removed. It needs a mechanism that does not
depend on the `/info` handshake — the env → `/info` → core chain is
asynchronous, so an early interaction beats it. That ticket stays open
and unaddressed here.

Closes OSS-566, OSS-568.
2026-07-31 08:33:57 -05:00
Mike Ryan 29d2721775 fix(channels): contain terminal delivery failures 2026-07-30 23:20:48 -07:00
Mike Ryan 552268d47d fix: keep participant metadata out of assistant history 2026-07-30 22:50:27 -07:00
Mike Ryan 8e2d7a5cda test(channels): bind canonical run delivery 2026-07-30 20:05:31 -07:00
Mike Ryan 4680d2f579 fix(channels): normalize remaining provider turns 2026-07-30 19:58:08 -07:00
Mike Ryan 81d192ad22 fix(channels): suppress exact provider self output 2026-07-30 19:51:08 -07:00
Mike Ryan 7259bae438 feat(channels): bound pending delivery capacity 2026-07-30 19:51:08 -07:00
Mike Ryan 9dc343ddf8 feat(channels): confirm managed file delivery 2026-07-30 19:51:08 -07:00
Mike Ryan ddfa6f3453 feat(channels): supersede pre-output runs 2026-07-30 19:51:08 -07:00
Mike Ryan 2a00a5a16a feat(channels): render native Slack status 2026-07-30 19:51:08 -07:00
Mike Ryan bff87d1428 feat(channels): handle transcript failures by surface 2026-07-30 19:51:07 -07:00
Mike Ryan f7437daad3 feat(channels): charge deliveries on first work 2026-07-30 19:51:07 -07:00
Mike Ryan f1eef91fed feat(channels): authorize delivery thread access 2026-07-30 19:51:07 -07:00
Mike Ryan b376c901f6 feat(channels): load delivery-scoped transcripts 2026-07-30 19:51:07 -07:00
Mike Ryan bf8abc3cef feat(channels): add V5 message operation routing 2026-07-30 19:51:07 -07:00
Ben Taylor 5a8a487df3 feat(runtime): auto-start managed Channels on long-running hosts (OSS-641) (#6258)
Resolves [OSS-641](https://linear.app/copilotkit/issue/OSS-641). Mike's
report: *"You have to `await channels.ready()` for it to connect to the
Realtime Gateway. Seems like there's some clunkiness to creating the
runtime and getting it connected."* He then picked the fix: *"I think it
should autostart in the long running wrappers."*

## What changes

**`createCopilotNodeListener` and `createCopilotExpressHandler` start
activation at creation.** A declared Channel connects because it was
declared; `channels.ready()` becomes await-and-observe rather than the
call you must remember. Failure-mode asymmetry is the argument:
forgetting `ready()` today gives you a process that serves HTTP, looks
healthy, and is silently disconnected with **zero output**, while
auto-start's worst case is an activation error in the logs.

**`createCopilotRuntimeHandler` and `createCopilotHonoHandler` stay
lazy.** The generic Fetch handler is the serverless/edge entry point —
isolates freeze and recycle per request, so separate cold starts would
mint competing listeners for the same Channel (the reason activation was
deferred in `fbf35ac59` in the first place). Hono keeps that behavior
because it is our Next.js App Router surface in practice: every route
handler in `examples/showcases/*` (banking, mcp-apps,
generative-ui-playground, oracle-agent-memory) plus the vue/nuxt demo
builds one at module scope. Its TSDoc now states why, loudly, so nobody
"finishes the job" later.

`activateChannels: false` remains the clean opt-out that opens no
socket.

## Consequence for host code: the shutdown boundary moves earlier

Signal handlers must now be registered **before the listener is
created**, not merely before `ready()`. Otherwise a Ctrl-C during the
connect window hits Node's default handler and leaks a live gateway
session. `examples/slack`, `examples/teams`, and the docs snippets are
restructured to wire teardown before the listener exists (a
`stopChannels`/`teardown` binding assigned in the same tick as
creation). **Worth calling out in the changelog** — it is the general
hazard for any user code that registers shutdown after mounting.

## Failure semantics

Fire-and-forget by necessity, since a factory is synchronous. Set-level
failures log at `error`; per-Channel failures keep their existing `warn`
breadcrumbs; an up-front misconfiguration (duplicate/missing Channel
names) now surfaces as a logged error at creation rather than a throw
out of the factory — the factory still never throws. `ready()` stays
idempotent and one-shot, so a host that *does* await it observes this
activation's outcome, including its rejection, rather than triggering a
second one.

## READMEs

Every `channels-*/README.md` quickstart built the *generic* handler and
needed `await handler.channels.ready()` — for a socket-mode Slack bot, a
request handler you construct and never serve, which is likely closer to
what actually felt clunky. All seven now use the Node listener, so they
inherit auto-start and agree with the docs-site quickstarts. No new
public surface: a bot-only `startChannels(runtime)` host was the
alternative and is deliberately not taken here.

## Testing

- **`packages/runtime` unit suite: 1815 passed / 128 files** (`npx
vitest run`), including 9 tests in `endpoints-channels.test.ts`
covering: auto-start on node + express; Hono still lazy;
`activateChannels: false` opens no socket; a failed auto-start logs
instead of leaving an unhandled rejection (asserted via an
`unhandledRejection` listener) and the reason survives to a later
`ready()`; a duplicate-name misconfig logs without throwing; and two
wrappers over one runtime activate once (the per-runtime manager cache
is load-bearing now that *construction* activates).
- **`examples/slack`: 63 passed / 12 files; `examples/teams`: 2 passed /
1 file** (`npm test` in each).
- **Typecheck clean:** `examples/slack` and `examples/teams` (`tsc
--noEmit`), plus a full `@copilotkit/runtime` tsdown build.
- **Lint/format clean:** `oxlint` reports 0 findings in every changed
file (the 21 warnings in that run are pre-existing, all in untouched
example render/tool files), `oxfmt --check` passes on all 9 changed
source files.
- **Docs:** verified no stale lifecycle claims remain (`opens no
connection` / `ready() is required` / `control surface` guards) across
`docs/channels/**` and the Slack + Teams platform guides.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-30 21:26:06 -05:00
Benjamin Taylor eb9643a2f2 fix(channels): assert per-turn isolation by symptom, drop dead concurrency error
Strengthen the shared-instance-factory test to assert the user-visible defect
rather than object identity. Two overlapping turns through
`agent: (id) => shared` both read the one shared `messages` array, so each run
is prompted with the other user's question too. Against main's source the test
now reports exactly that:

  expected [ 'first+second', 'first+second' ] to deeply equal [ 'first', 'second' ]

The symptom is asserted before the mechanism so a regression names the defect
instead of posing an object-identity puzzle.

This also settles what the original "only the first mention gets a reply" report
was: overlapping mentions dropped by the old `onLockConflict: drop` default plus
the managed per-thread exclusive gate, both already fixed in #6256. Both turns
do reply here; what was left was cross-contaminated context, not a dead turn.

Remove `ChannelAgentConcurrencyError`, unthrown since #6256. Its doc claimed it
was kept so older importers would not break, but it was never re-exported from
`channels-intelligence`'s entrypoint — `git log -S` over index.ts confirms it was
never reachable from outside the package, whose only export is `.`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:56:33 -05:00
Benjamin Taylor 9725663035 fix(channels): isolate factory-returned agents per turn
`createChannel` resolved an agent per turn through `agentFactory`, which
cloned the singleton config but returned a factory's result raw. A factory
is free to hand back the same object every call — `agent: (threadId) =>
shared` — which is easy to write by accident and is what a singleton
becomes when someone needs the `threadId`.

Turn concurrency defaults to `"parallel"`, and only the managed adapter
serializes same-thread deliveries, so on a directly connected adapter two
turns in one conversation can run at once. On one shared instance they
corrupt each other: `messages` is a single array both runs append into, so
each run's new-message diff picks up the other's, and `isRunning` /
`activeRunDetach$` / `activeRunCompletionPromise` are single-slot fields
the second run overwrites while the first is still streaming. Managed
delivery instead serializes on object identity, head-of-line blocking two
different conversations that share one instance.

Clone for both shapes so the object a turn runs on is never one the caller
still holds. A fresh factory is unaffected beyond an unused instance.

Because cloning is now mandatory everywhere, add a guard for the failure it
introduces: `AbstractAgent.prototype.clone()` copies a fixed field list, so
a subclass declaring its own state gets it back as `undefined` with no
error — the base method always exists and returns a correctly-typed
instance. Comparing own enumerable keys catches that and names the dropped
fields. Own functions are exempt: assigning a method on the instance is how
spies and instrumentation wrap an agent, and losing that wrapper leaves the
prototype method intact.

Also reset `isRunning` and `abortController` on the clone as hygiene — not a
fix for a dead turn, since `runAgent` assigns a fresh controller before each
run and the run loop passes none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:42:38 -05:00
Benjamin Taylor 4d74bdc5c3 feat(runtime): auto-start managed Channels on long-running hosts (refs OSS-641)
Creating a Node listener or an Express handler now STARTS activation of the
runtime's declared managed Channels, so `channels.ready()` becomes
await-and-observe instead of the thing you must remember to call. A declared
Channel connects because it was declared.

The failure mode this removes: forget `ready()` and you get a process that
serves HTTP, looks healthy, and is silently disconnected with zero output.
Auto-start's worst case is an activation error in the logs.

The generic Fetch handler stays LAZY — it is the serverless/edge entry point,
where isolates freeze and recycle per request and separate cold starts would
mint competing listeners for the same Channel. `createCopilotHonoHandler` stays
lazy for the same reason: it is our Next.js App Router surface in practice
(every `examples/showcases/*` route handler builds one at module scope), and its
TSDoc now says so loudly. `activateChannels: false` remains the opt-out that
opens no socket.

Consequence for host code: the shutdown-handler boundary moves earlier. Signal
handlers must be registered before the listener is CREATED, not merely before
`ready()` — otherwise a Ctrl-C during the connect window hits Node's default
handler and leaks a live gateway session. The slack and teams examples and the
docs snippets are restructured accordingly.

Also migrates the seven channel-package README quickstarts off the generic
handler (a request handler a socket-mode bot constructs and never serves) onto
the Node listener, so they inherit auto-start and agree with the docs site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:37:37 -05:00
Benjamin Taylor e88e857290 test(channels): pin that the default sanitizer keeps graceful abort handling
The PR claims wrapping the transport (rather than replacing the event
transform) preserves the stock AbortError -> RUN_ERROR conversion. That was
read off the @ag-ui/client bundle, not tested. Now it is: a mid-stream
AbortError surfaces as RUN_ERROR{code:'abort'} and the run resolves.
2026-07-30 15:16:30 -05:00
Benjamin Taylor 76662f31b1 feat(channels): apply the event sanitizer by default and drop it from the examples
Completes the previous commit, whose wiring was left out of it by mistake.

createChannel applies sanitizeAgentEventStream at the agentFactory seam, with
sanitizeAgentEvents: false to opt out; HttpAgent is re-exported from
@copilotkit/channels so the examples need no @ag-ui/client dependency; the
Slack + Teams examples and READMEs now wire a plain HttpAgent; and
SanitizingHttpAgent is deprecated (unchanged) in both adapter packages.

Also swaps a stray pair of raw control bytes in the protobuf test fixture for
escapes, so git sees the test file as text.
2026-07-30 15:16:30 -05:00
Benjamin Taylor f427ec573f feat(channels): sanitize agent event streams by default (refs OSS-647)
@ag-ui/langgraph emits a TOOL_CALL_START whose parentMessageId is null --
notably the tool call that triggers an interrupt. The AG-UI schema declares
that field optional but never nullable, so HttpAgent's transform re-validates
the streamed event, Zod rejects it, and one rejected event aborts the whole
run, breaking human-in-the-loop.

Until that is fixed upstream (OSS-691), Channels tolerate it by default:
createChannel coerces the field on the wire, so no call site needs a special
agent class. Opt out with sanitizeAgentEvents: false.

Applied at the transport rather than by replacing the event transform, which
keeps protobuf content-type negotiation, the graceful AbortError -> RUN_ERROR
conversion, and strict validation of every other field -- all of which
SanitizingHttpAgent gives up. It also survives the per-run clone() of a
singleton agent, which an own-property run() override would not.

SanitizingHttpAgent is deprecated but unchanged, so existing code keeps
working. The examples and READMEs now wire up a plain HttpAgent.
2026-07-30 15:16:30 -05:00
Mike Ryan e4a105a1c0 docs(channels): describe gateway delivery 2026-07-30 12:23:35 -07:00
Mike Ryan 7694cbcdd9 fix(channels): complete ordered delivery outcomes 2026-07-30 12:23:22 -07:00
Mike Ryan 02e7eaedb3 feat(channels): prioritize final provider delivery 2026-07-30 11:56:17 -07:00
Mike Ryan 69b23e95b7 fix(channels): name managed post results as assets 2026-07-30 11:56:00 -07:00
Mike Ryan 4d46e57815 feat(channels): persist managed asset history 2026-07-30 11:56:00 -07:00
Mike Ryan 19656b927d fix(channels): bound delivery shutdown 2026-07-30 11:55:29 -07:00
Mike Ryan 14071ec8d6 feat(channels): decline claims before local admission 2026-07-30 11:55:29 -07:00
Mike Ryan a0133c1588 fix(channels): retain first gateway invitation 2026-07-30 11:55:29 -07:00
Mike Ryan 6f63b0de31 feat(channels): retry immutable gateway packets 2026-07-30 11:55:28 -07:00
Mike Ryan 72750c369a feat(channels): make Slack reply-continuation limits configurable (refs OSS-689) (#6255)
## What

PR #6244 taught the Slack renderer to split a long reply across
continuation messages instead of silently truncating it. Its tuning was
hardcoded. Three of those constants are genuinely caller-dependent; this
exposes them through one `replyContinuation` option on both the direct
and managed surfaces.

```ts
// direct
slack({ replyContinuation: { maxMessages: 5 } });

// managed
createChannel({
  name: "support",
  replyContinuation: {
    messageByteLimit: 11_000,
    maxMessages: 20,
    truncationMarker: "\n\n_…réponse tronquée._",
  },
});
```

## Which constants, and why only these

| exposed | why it is a caller's decision |
| --- | --- |
| `messageByteLimit` | Slack's cumulative per-message ceiling is
**undocumented**. 11k is inferred from a single production datapoint
(11,607 bytes observed accepted) and deliberately conservative. If the
real ceiling differs by plan or workspace, an operator needs a knob, not
a release. |
| `maxMessages` | How many messages one reply may occupy is a product
decision, not a platform fact. 20 was chosen to bound a runaway (500k
chars → 46 messages); a support bot and an internal ops bot want
different answers. |
| `truncationMarker` | Hardcoded **English** copy posted into the
customer's channel. The one constant with no correct default. |

Deliberately **not** exposed, because they are correctness rather than
preference:

- `APPEND_CHAR_LIMIT` — a documented Slack per-call limit. A provider
fact; exposing it only invites `msg_too_long`.
- `MIN_MESSAGE_PROGRESS_BYTES` — loop-safety invariant. Exposing it lets
a caller reintroduce the unbounded-message bug #6244 fixed.
- `MAX_FENCE_LANG_CHARS`, `FINISH_DRAIN_ATTEMPTS` — internal heuristics.
If either is wrong that is a bug to fix, not a knob.

## Shape

Grouped under one nested option rather than three flat fields.
`maxMessages` sitting bare on a Channel reads ambiguously (thread
history?), and the group keeps the next continuation knob from adding
another top-level field. The trade-off is that it diverges from
`showToolStatus`'s flat precedent — happy to flatten if reviewers prefer
consistency over disambiguation.

The shared `ReplyContinuationOptions` type lives in `channels-core`, the
common ancestor of all four packages that touch it.

## Plumbing

Both surfaces follow `showToolStatus` exactly:

- **Direct:** `slack({ replyContinuation })` → `adapter.ts` →
`event-renderer.ts` → `NativeMessageStream`, covering both the renderer
path and `adapter.stream()`'s own stream.
- **Managed:** `createChannel({ replyContinuation })` → `Channel` →
`ChannelActivationConfig` → `channel-manager` →
`realtime-gateway-launcher` → `DeliveryAdapter` → the renderer's
`nativeStreaming` block.

**No gateway or Intelligence change is required.** Managed Slack renders
in the SDK process over a gateway live session and only emits
`slack.stream.*` effects; the Elixir `provider_executor` is a dumb
effect applier that owns no message boundaries. Render config therefore
never has to cross into Intelligence.

One non-obvious touchpoint: `channel-manager.ts` keeps a **hand-written
structural mirror** of the launcher's options, so the field has to be
declared there as well or the managed path type-drifts silently.

## Testing

```
channels-core           171 passed
channels-slack          318 passed
channels-intelligence    67 passed
runtime                1809 passed, 3 failed
```

The 3 runtime failures are pre-existing Gemini `AIMessage` filtering
tests, unrelated to this change — confirmed by re-running them with
these changes stashed on `main`. `check-types` passes for all four
packages.

New coverage:
- the marker override at the leaf (`native-stream`);
- the renderer's pass-through — this one fails without the change, since
the 11k/20 defaults would keep that reply in a single message;
- the managed chain end to end via `createChannel` → activation config →
launcher opts, plus the negative case that an unset option adds no
property anywhere.

## Follow-ups (tracked on OSS-689, not in scope here)

- Confirm the byte-vs-char question with a manual >12k
mostly-CJK/Cyrillic reply against a real workspace. It decides whether
`messageByteLimit`'s default is right; the option makes it adjustable
either way.
- Under the managed path's `minIntervalMs: 0` cadence a table row can
still be cut mid-row, so a continuation's re-emitted header is followed
by a malformed row.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-30 10:36:43 -07:00
Alem Tuzlak 10a8c3a302 fix(channels-intelligence): pass channelName in concurrency test
CI typecheck merges with main where DeliveryAdapterOptions requires channelName.
2026-07-30 19:14:07 +02:00
Alem Tuzlak 88bef0e27a feat(channels-core): parallel-by-default turn concurrency
Overlapping turns on the same conversation now run concurrently by default
so multi-user Slack threads get parallel replies. Singleton agents are
isolated via clone() per run; store.concurrency serial/drop remain opt-in.
2026-07-30 19:10:18 +02:00
Benjamin Taylor 13e575ea6f Revert "refactor(runtime): import the launcher options type instead of mirroring it"
This reverts commit 346329aef8.
2026-07-30 11:57:21 -05:00
Benjamin Taylor 346329aef8 refactor(runtime): import the launcher options type instead of mirroring it
`ChannelsIntelligenceModule` re-declared the launcher's options by hand, so
adding `replyContinuation` to the real launcher type-checked clean here while
the managed path silently ignored it — the mirror had to be edited too or the
option was dropped on the floor. That is a trap for every future launcher
option, not just this one.

The mirror existed for a stated CJS/ESM reason, so I checked whether it still
applies rather than assuming. It does not, for a type:

- `import type` is fully erased. The emitted CJS gains no `require` of
  `@copilotkit/channels-intelligence`; the only references in the build output
  remain the pre-existing non-literal specifier constant and the package.json
  dependency entry.
- `ChannelsIntelligenceModule` is not part of the emitted `.d.cts`/`.d.mts`
  surface (it appears only in sourcemaps), so no CJS consumer resolves the
  ESM-only package — which matters because that package's export map has an
  `import` condition and no `require`.

The constraint is real for the *value* import, which is why the dynamic
specifier stays non-literal. The comment now draws that distinction explicitly
so the next reader does not re-mirror it.

Net: 42 lines of duplicated type removed, and the managed path can no longer
drift from the launcher it calls.
2026-07-30 11:55:41 -05:00
github-actions[bot] a85760e06b style: auto-fix formatting 2026-07-30 16:45:15 +00:00
Benjamin Taylor b2fc4c0706 feat(channels): make Slack reply-continuation limits configurable (refs OSS-689)
PR #6244 taught the Slack renderer to split a long reply across continuation
messages, but its tuning was hardcoded. Three of those constants are genuinely
caller-dependent and are now configurable through a single `replyContinuation`
option; the rest stay internal on purpose.

Exposed:
- `messageByteLimit` — Slack's cumulative per-message ceiling is undocumented.
  11k is inferred from one production datapoint and deliberately conservative;
  operators need a knob if the real ceiling differs rather than a release.
- `maxMessages` — how many messages one reply may occupy is a product decision,
  not a platform fact. A support bot and an internal ops bot want different
  answers.
- `truncationMarker` — hardcoded English copy posted into the customer's
  channel. The one constant with no correct default.

Deliberately NOT exposed, because they are correctness rather than preference:
`APPEND_CHAR_LIMIT` (a documented Slack per-call limit),
`MIN_MESSAGE_PROGRESS_BYTES` (loop-safety invariant — exposing it lets a caller
reintroduce the unbounded-message bug #6244 fixed), `MAX_FENCE_LANG_CHARS`, and
`FINISH_DRAIN_ATTEMPTS`.

Grouped under one nested option rather than three flat fields: `maxMessages` on
a Channel reads ambiguously on its own (thread history?), and the group keeps
the next continuation knob from adding another top-level field.

Both surfaces are wired, following `showToolStatus` exactly:
- direct: `slack({ replyContinuation })` → adapter → event-renderer → stream,
  covering both the renderer path and `adapter.stream()`.
- managed: `createChannel({ replyContinuation })` → `Channel` →
  `ChannelActivationConfig` → channel-manager → launcher → `DeliveryAdapter` →
  the renderer's `nativeStreaming` block.

No gateway or Intelligence change is needed. Managed Slack renders in the SDK
process over a gateway live session and only emits `slack.stream.*` effects, so
render config never has to cross into Intelligence — the Elixir provider
executor is a dumb effect applier that owns no message boundaries.

`channel-manager.ts` carries a hand-written structural mirror of the launcher
signature, so the new field is declared there too or the managed path silently
type-drifts.

Tests: the marker override at the leaf, the renderer's pass-through (fails
without it — the defaults would keep that reply in one message), and the managed
chain end to end via `createChannel` → activation config → launcher opts, plus
the negative case that an unset option adds no properties anywhere.
2026-07-30 11:43:04 -05:00
Tyler Slaton 7113c8f932 fix(channels): use channel name for thread lock (#6254)
## Problem

Managed Channel deliveries can request a canonical thread lock with the
inner agent ID, or `default` when that ID is unset. Intelligence owns
the thread under the declared Channel name, so the lock fails with
`THREAD_AGENT_MISMATCH`.

## Why

The SDK treated the agent object identity as the managed Channel
identity. Those values are independent: `createChannel({ name })`
declares the managed thread owner, while an agent ID is optional and may
differ.

## Fix

Pass the declared Channel name into the managed delivery adapter and use
it for canonical runs. Add a public `thread.runAgent()` regression test
that proves an agent with a different ID still runs under the Channel
name.

Validated with the Channels Intelligence test suite, type check, and
build; the runtime Channel manager tests, type check, and build; repo
lint; and the affected-package pre-commit checks.
2026-07-30 08:59:24 -07:00
Tyler Slaton 0c06dbf908 fix(channels): use channel name for thread lock 2026-07-30 10:50:57 -04:00
github-actions[bot] 051833fff8 style: auto-fix formatting 2026-07-30 14:03:24 +00:00
Benjamin Taylor ab53a77c06 test(channels-slack): close native-stream coverage gaps
Coverage on native-stream.ts was 87.5% stmts / 80.45% branches; now 92.7% /
88.5%. The gaps were concentrated in code this branch added, and two of them
were places I had claimed coverage that did not exist.

Genuinely uncovered logic, now tested:
- `renderContextCloser`'s inline-code and emphasis closers, and the
  `hasFenceCodeContent` mirror added in the previous commit — the closer is
  appended at every boundary and none of its three branches were exercised.
- `tableHeaderToReopen`'s "no longer inside the rows" guard, so a boundary landing
  after a table has ended does not inject a stray header.
- The 2-byte UTF-8 width class (Cyrillic). CJK covered 3-byte and emoji 4-byte;
  the class between them was untested.
- The `callEnd < byteEnd` branch. The test that claimed it never reached it: with
  a 40k budget and 30k of ASCII the whole reply fits, so the first branch won and
  the per-call path was never entered. Retargeted with multi-byte text.
- Rollover `stopStream` failure, non-strict (proceeds) and strict-with-a-failed
  closer (reports the closer, not the consequence).
- Truncation marker failure, non-strict (logged) and strict (reported).
- A legacy tail fallback that itself fails.
- Chunk behaviour around a rollover: delivered to the current message, degraded
  when the continuation cannot be opened, and skipped entirely when a strict text
  failure rejects the shared flush queue — the last of which corrected my
  assumption that `flushChunk` still runs in that case. It does not: `.then()` on
  a rejected promise skips its callback.

Four guards are unreachable through the public API and are now commented as
deliberately defensive rather than left looking like missing tests: the
`roomBytes <= 0` and "not even one code point fits" rollover checks (every
filling branch rolls over in the same iteration), `appendSlice`'s empty-delta
stall guard (both boundary helpers return an index strictly greater than
`curPosted`), and `truncate`'s own re-entry check (`appendPending` short-circuits
first). Kept as loop-safety invariants.

The residue is pre-existing or upstream: `append()`'s legacy forwarding, the
zero-interval `scheduleFlush` path, `flushChunk`'s start-failure degradation, and
`flushTextInline`'s strict rethrow.
2026-07-30 09:01:43 -05:00