Commit Graph

72 Commits

Author SHA1 Message Date
Mike Ryan db88826432 chore: rename Enterprise Intelligence product copy 2026-08-24 09:38:15 -07:00
Benjamin Taylor 6f58b2c6a4 fix(runtime): unify the Intelligence key name and publish the wiring (refs OSS-881)
Three names for one value were live in CopilotKit's own documentation, and
following the wrong one with a CLI-provisioned project yields an undefined
key:

- `INTELLIGENCE_API_KEY` — what `copilotkit project select` writes, used by
  all 34 integration examples and the docs site.
- `COPILOTKIT_INTELLIGENCE_API_KEY` — the seven Channels package READMEs and
  the packaged skills. Nothing ever read it.
- `COPILOTKIT_API_KEY` — the Slack and Teams examples, and the TSDoc on
  `CopilotKitIntelligence` itself, which is what an IDE shows on hover.

`INTELLIGENCE_API_KEY` wins, because it is the name the CLI provisions and
changing it would break every scaffolded project in the wild.
`COPILOTKIT_INTELLIGENCE_API_KEY` is retired outright — no code read it.
`COPILOTKIT_API_KEY` stays readable as a deprecated alias in the two
examples that consume it, so an existing `.env` keeps working, and is
documented as deprecated everywhere it appears.

The skills reference also documented `organizationId`, sourced from a fourth
and fifth env name, as a `CopilotKitIntelligence` option. It is not one:
`CopilotKitIntelligenceConfig` has no such field, so the copy-pasteable
sample it appeared in would not compile. Removed from the samples, and the
prose that told readers to fetch a value for it corrected.

The Intelligence wiring itself was published only inside
`node_modules/@copilotkit/runtime/skills/`, and the only docs pages showing
`CopilotKitIntelligence` were the two Channels frontends — so a developer on
the plain web path had no page to reach it from. Adds
`/premium/connect-your-runtime`, which covers the wiring, how to confirm the
credential is actually consumed, and the self-hosted two-URL rule.

`scripts/validate-intelligence-env-names.ts` keeps this from drifting back.
It runs unfiltered in CI on purpose: the two workflows that would otherwise
cover it filter paths, and static/quality ignores `examples/**` — exactly
where the deprecated alias lives.
2026-08-19 17:50:09 -05:00
tylerslaton f8cb4d2447 chore: release channels v0.9.0 2026-08-14 20:42:01 +00:00
tylerslaton a7378e70eb chore: release channels v0.8.1 2026-08-10 18:53:10 +00:00
Atai Barkai 6e0e1de049 docs(channels): state lifecycle positively across READMEs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 22:56:58 -07:00
Atai Barkai 049bd2bdcc docs(channels-core): drop the phantom-negation phrasing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 22:55:32 -07:00
Atai Barkai adbdf65263 docs(channels): state the free plan plainly
Drop the defensive parentheticals around Intelligence pricing; say
"available on a free plan" and move on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 22:52:33 -07:00
Atai Barkai cb4439b241 docs(channels): clarify the custom channel runner path
Note in the Channels overview and package READMEs that building your own
channel runner on the open-source SDK primitives is a supported path with
no CopilotKit Intelligence dependency; teams choosing it own their state,
persistence, concurrency, locking, retries, and race-condition handling.
Intelligence remains the managed runner, with analytics, learning, and
governance in addition.

Also updates the production self-hosting note: Enterprise Intelligence can
be fully self-hosted today, onboarding guides are still to come.

Refs FAC-155

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 22:35:13 -07:00
Austin Merrick b32b5539cc feat: add Inspector navigation, usage, and locked Threads (refs ENT-1173) (#6275)
## What does this PR do?

Adds the CopilotKit consumer side of ENT-1173 across Shared, Runtime,
Core, Web Inspector, and the existing Shell Docs pages.

- Defines and parses optional trusted Inspector metadata for identity,
plan, license, action, usage, and expiry. Runtime proxies it through a
private, failure-isolated route, and Core refreshes it without changing
connection state.
- Groups Inspector navigation into Threads, Agents, and Learning.
Threads renders finite, unlimited, unknown, overage, and expiring usage
states plus matching trusted plan or license actions.
- Keeps explicit `threadEndpoints` as the only authority for Thread
requests. Locked or absent capability states make no list, subscription,
detail, message, event, or state calls.
- Keeps the zero-thread video, three example Threads, detail tabs, and
guided tour in empty and locked states. General Intelligence remains the
default onboarding path; only trusted `team_self_hosted` metadata uses
self-hosted onboarding.
- Gives an active license with missing Runtime routes a short **Finish
setting up Rich Threads** state. Users can copy a safe coding-agent
prompt or open the public Runtime setup guide. The same copy control
appears in that guide, and raw Markdown/LLM views include the full
prompt.
- Keeps finite usage green below 90%, orange from 90% to the limit, and
red at or above the limit. At 90%, a trusted plan action changes from
**Manage Your Plan** to a purple **Upgrade Your Plan** without changing
its trusted URL, action kind, or telemetry contract.
- Adds a deterministic 33-state loopback lab for CopilotKit developers.
It has no production route or export, is absent from public docs and
package metadata, and is excluded from the npm tarball.

`Expiring Soon` is display-only; this PR does not enable the thread
culler. Managed Enterprise receives no manage-plan action, and Team
Self-Hosted receives no hosted plan action. Optional metadata and the
additive expiry field remain compatible across mixed producer, Runtime,
Core, and Inspector versions.

A small Channels test-only change updates fetch mocks for current
TypeScript types. It changes no Slack or Teams docs or runtime behavior.

## Related PRs and issues

- Refs
[ENT-1173](https://linear.app/copilotkit/issue/ENT-1173/ship-plg-ready-inspector-navigation-metadata-and-locked-threads)
- Producer:
[CopilotKit/Intelligence#696](https://github.com/CopilotKit/Intelligence/pull/696)

## Validation

- `@copilotkit/web-inspector`: 20 files and 372 tests passed; typecheck
and production build passed.
- Shell Docs: 57 files and 383 tests passed; lint, typecheck, and
production build passed. The build generated all 222 static pages.
- Browser checks cover the copy-prompt flow, unchanged white **Manage
Your Plan**, purple **Upgrade Your Plan**, orange 4,500/5,000 usage, and
red 5,000/5,000 usage.
- Independent review found no Critical or Important issues.
- The broader Runtime, React Native, Channels, package-quality,
compatibility, and Node-version checks from the prior pushed head remain
green.

## Checklist

- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] I updated the relevant documentation
- [ ] "Allow edits by maintainers" is checked
2026-08-07 14:08:23 -07:00
tylerslaton 289ae4a539 chore: release channels v0.8.0 2026-08-06 09:26:34 -07:00
Austin Merrick 9b604378aa test(channels): update fetch mocks for current types 2026-08-05 11:55:50 -07:00
Tyler Slaton 7f2a7a9638 fix(channels): prefer inbound prompt over the welcome default
Review follow-up: resolve the default prompt after the implicit inbound
prompt, so real user input outranks the welcome default and the
implicit-inbound-consumed flag can never mark a turn consumed that was
never injected. Pin the seeded-store welcome path (all shipping
adapters) and the inbound-over-default precedence with tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 11:53:00 -07:00
Tyler Slaton 96e2f064b8 fix(channels): default welcome agent prompt 2026-08-05 11:39:09 -07:00
tylerslaton b95a43e254 chore: release channels v0.7.3 2026-08-04 21:51:00 +00:00
Mike Ryan 80370b5ecb fix(channels): preserve provider diagnostics 2026-08-04 12:33:17 -07:00
tylerslaton 1f5c70da2e chore: release channels v0.7.2 2026-08-04 18:42:33 +00:00
tylerslaton f8f9e2a721 chore: release channels v0.7.1 2026-08-04 14:48:21 +00:00
tylerslaton 53bf978904 chore: release channels v0.7.0 2026-08-03 19:52:48 +00:00
Mike Ryan b88f9b7e4f feat(channels): complete native JSX contracts 2026-08-03 09:23:15 -07:00
Mike Ryan fa92ebcf44 feat(channels): recover actions by stable JSX key 2026-08-03 09:23:15 -07:00
Mike Ryan 8707d8ce12 feat(channels): add agent-rendered component tools 2026-08-03 09:23:15 -07:00
BenTaylorDev bdf054f8bd chore: release channels v0.6.1 2026-08-03 14:06:07 +00:00
Benjamin Taylor 768a69c99d fix(channels): report a clone that drops subclass state, do not refuse the turn
isolateAgentInstance threw when an agent's clone() did not carry the subclass's
own fields. That rejected every turn on a Channel built with LangGraphAgent --
its clone() drops emittedToolCallStartIds and eventsStreamActive -- so the
reference starter could not answer a single message.

The refusal was wrong because whether a dropped field matters depends on what it
HOLDS, and the check cannot see that:

- Config read during the run and never rewritten (an auth client, a URL) does
  gut the agent when it is lost.
- Per-run scratch state is re-initialized at the start of every run, so losing it
  changes nothing. LangGraphAgent's two fields are exactly this: both are reset
  when a run binds its subscriber, before anything reads them.

The tell is that the identical clone happens on every ordinary runtime request --
agent-utils.ts clones per request for SSE and Intelligence alike -- and has never
caused a problem. Channels differed only in asserting at clone time, before the
run that would have repopulated the fields. Confirmed against a real Slack round
trip: with the throw downgraded, the same Channel that could not take a turn ran
the agent and replied.

So it warns and continues, naming the fields and both readings. The check still
earns its place: A2AMiddlewareAgent's base clone() drops orchestrationAgent,
agentClients and agentCards, which are config, and that is worth seeing.

Deliberately not done: copying the dropped fields onto the clone. That shares one
mutable object across concurrent turns, the exact hazard the isolation exists to
prevent.

Upstream fix to follow in @ag-ui/langgraph, whose clone() should carry them.
2026-08-02 21:45:23 -05:00
BenTaylorDev 63c15ce445 chore: release channels v0.6.0 2026-08-02 20:24:17 +00:00
Mike Ryan 17e7f33876 fix(channels): reconcile identity stack integration 2026-08-01 09:26:39 -07:00
Mike Ryan 561bf19fa6 feat(channels): add explicit identity and memory grants 2026-08-01 09:19:13 -07:00
Tyler Slaton 5ceb53799b fix(channels): complete managed provider parity 2026-08-01 11:10:28 -04:00
Tyler Slaton 1712be5ae9 feat(channels): complete managed Teams SDK parity 2026-07-31 23:11:06 -04:00
Tyler Slaton 871cd20f2a feat(channels): add managed welcome lifecycle 2026-07-31 18:26:41 -04:00
tylerslaton db8ebf5f09 chore: release channels v0.5.0 2026-07-31 12:46:39 -07:00
Mike Ryan 29d2721775 fix(channels): contain terminal delivery failures 2026-07-30 23:20:48 -07:00
Mike Ryan ddfa6f3453 feat(channels): supersede pre-output runs 2026-07-30 19:51:08 -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 7694cbcdd9 fix(channels): complete ordered delivery outcomes 2026-07-30 12:23:22 -07:00
Mike Ryan 69b23e95b7 fix(channels): name managed post results as assets 2026-07-30 11:56:00 -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 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 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
Alem Tuzlak 9250f22d88 fix(channels): stream cleanup, digest parity, postFile fail-loud
CR r5 bucket (a): always stop native Slack streams on failure (thread
finish + NativeMessageStream queue drain), advance append/replace text
only after apply, rethrow permanent postFile gateway errors, exclude
stream.stop from provider-output tracking, classify errors by message,
validate prepared turn fields per kind, and stop unit tests from hitting
live lock cleanup HTTP.
2026-07-30 12:55:03 +02:00
Mike Ryan d545eba10e fix(channels): preserve live-session run semantics 2026-07-29 17:22:56 -07:00
Mike Ryan d5beb8e5de fix(channels): enforce live-session delivery boundaries 2026-07-29 16:42:17 -07:00
Mike Ryan 74b38ecdfb fix(channels): await delivery thread operations 2026-07-29 16:42:04 -07:00