Commit Graph

13191 Commits

Author SHA1 Message Date
Jordan Ritter 883df6951a fix(showcase): close ms-agent-python A2UI outer tool call before RUN_FINISHED
The declarative-gen-ui sales-dashboard hero pill on ms-agent-python was
missing two aimock fixtures for its `generate_a2ui` two-stage flow:

1. the inner `_design_a2ui_surface` secondary-LLM fixture, and
2. the outer-agent narration fixture keyed by
   `toolCallId: call_d6_decl_dash_outer_mspy_001`.

Without the narration fixture, after `generate_a2ui` returned there was no
matching outer turn to close the tool call, so the run looped re-emitting
`generate_a2ui` and eventually tripped the AG-UI protocol guard:
`Cannot send 'RUN_FINISHED' while tool calls are still active:
call_d6_decl_dash_outer_mspy_001` (surfaced as an error banner, turn 1
never completes, runsFinished=0).

Added both fixtures, mirroring the canonical langgraph-python
gen-ui-declarative.json sales-dashboard entries (inner keyed by
`toolName: _design_a2ui_surface`, narration keyed by the outer toolCallId).
After the fix the outer tool call closes, RUN_FINISHED fires cleanly
(runsFinished=1, no error banner), and the narration renders.
2026-07-17 23:29:20 -07:00
Jordan Ritter cfb2061fbe fix(showcase): rewrite gen-ui-agent probe to per-pill content assertion (#6039)
The old gen-ui-agent probe leaned on a cross-pill-difference heuristic —
it fingerprinted the rendered step text and treated inter-pill
differences as a settling signal, then accepted any render that had >=2
rows by the deadline. That let a real regression slip through green:
when pill 3 stale-renders pill 1's card, the ">=2 rows present" check is
still satisfied and the dedup was only ever a wait mechanism, never a
hard gate.

This PR rewrites the assertion to check what each pill should actually
render. Every pill carries a couple of low-brittleness content markers
pulled from its step titles in the d5 fixture (product-launch ->
launch/marketing, team-offsite -> venue/agenda, competitor-research ->
competitor/weakness). The probe polls the swap window until the card
shows >=2 non-empty step rows whose joined text contains all of that
pill's markers, and hard-fails otherwise. Matching is partial and
case-insensitive so it holds up under live-LLM (--direct) nondeterminism
while still proving the right pill's content landed.

Red-green: a dynamic-fake red-green plus three retained false-green
guards — identical-across-pills canned steps, stale non-adjacent content
(pill 3 showing pill 1), and empty/whitespace-only rows all go RED;
distinct-per-pill content passes. 9/9 tests green locally.

Important: this does NOT flip the affected cells green. The corrected
probe EXPOSES a genuine pill-3 stale-card regression on agno, langroid,
and crewai-crews — those cells are legitimately RED now and stay RED
until the underlying backend/frontend bug is fixed (separate follow-up).
The langgraph-python reference passes.

## Follow-ups (not this PR)

CR surfaced a handful of hardening items worth tracking, none of which
block this rework:

- The marker check is present-only, not absence-of-other-pills. A canned
backend reply that happened to contain every pill's tokens at once could
false-green. That's unrealistic for the current regression class (which
this probe does catch), but the check isn't airtight.
- Success latches on the first matching poll — there's no
settle/stability re-check after the content lands.
- Dead `seenStepTextsRef` state is still threaded through but never read
on the prod path; it should be removed.
- The unit-test fakes bypass the real `evaluate`/`.trim()` DOM logic.
That path is covered by the `--direct` value-test, not the unit test.
- Markers are matched against a joined row string, so it doesn't
strictly prove two distinct rows. There's also no drift guard against
`suggestions.ts`/the fixture, and the 15s content-poll deadline is a
magic number decoupled from `responseTimeoutMs`.

Also worth flagging: this PR EXPOSES a genuine pill-3 stale-state-card
regression on agno/langroid/crewai-crews — it's a backend per-turn
state/history handling problem, tracked separately and NOT fixed here.
The langgraph-python reference passes.
2026-07-17 22:45:33 -07:00
Jordan Ritter 2d7dbecb47 fix(showcase): rewrite gen-ui-agent probe to per-pill content assertion
The previous probe relied on a fragile cross-pill-difference heuristic:
it fingerprinted the rendered step text and used inter-pill differences
as a settling signal, accepting any render with >=2 rows at the deadline.
That heuristic false-greened a real pill-3 stale-card regression, because
a stale render (pill 3 still showing pill 1's content) still satisfied
">=2 rows present" and the dedup was only a wait mechanism, never a hard
gate.

This rewrites the assertion to check EXPECTED CONTENT per pill. Each pill
carries a small set of low-brittleness content markers derived from its
step titles in the d5 fixture (product-launch -> launch/marketing,
team-offsite -> venue/agenda, competitor-research -> competitor/weakness).
The assertion polls the swap window until the card shows >=2 NON-EMPTY
step rows whose joined text contains ALL of that pill's markers; otherwise
it hard-fails. Marker matching is partial and case-insensitive so it stays
robust to live-LLM (--direct) nondeterminism while still proving the RIGHT
pill's content rendered.

Verified with a dynamic-fake red-green plus three retained false-green
guards: identical-across-pills canned steps, stale non-adjacent content
(pill 3 showing pill 1), and empty/whitespace-only rows all turn RED;
distinct-per-pill content passes.

NOTE: this EXPOSES a genuine pill-3 stale-state regression on
agno/langroid/crewai-crews. Those cells are legitimately RED under the
corrected probe until that backend/frontend bug is fixed (separate
follow-up). The langgraph-python reference passes.
2026-07-17 22:37:15 -07:00
Jordan Ritter 34e8aebd7a fix(harness): exclude starter-* and harness-workers from aimock-wiring probe (#6038)
## Root cause

The `aimock_wiring:global` probe
(`showcase/harness/src/probes/aimock-wiring.ts`) was red on six live
Railway services — the "residual-6": `harness-workers` plus
`starter-adk`, `starter-langgraph-js`,
`starter-ms-agent-framework-dotnet`,
`starter-ms-agent-framework-python`, `starter-strands-python`.

This is EXCLUDE naming drift introduced after the egress /
private-networking migration:

- `EXCLUDE_SERVICES` keyed starters as `showcase-starter-<framework>`,
which (via the `showcase-` strip in `isExcluded`) only matches a live
name of `starter-<framework>`. But live Railway starter names are bare
`starter-<framework>[-lang]` — e.g. `starter-strands-python`,
`starter-langgraph-js`, `starter-ms-agent-framework-python`. **No
match** → the starter fell through to being checked → it has no
`*_BASE_URL` aimock override (starters are intentionally not wired) → it
landed in `unwired` → probe red.
- `harness-workers` (the harness background-worker service — pure infra,
no LLM callers) had **no exclude entry at all**, so it was always
counted unwired.

Seven starters coincidentally matched a stale literal and were excluded;
the other five, plus `harness-workers`, kept the probe permanently red.

## Fix

Two changes, both in `aimock-wiring.ts`:

1. **Exclude the whole `starter-*` family by prefix** in `isExcluded`,
instead of per-framework literals. Starters are contributor scaffolds,
categorically not routed through aimock (per product decision), so
matching them by literal name is exactly what drifted on rename. A
prefix rule can't re-drift. This is **safe**: the probe's "checked"
universe is intentionally the `showcase-*` LLM backends, and no
`showcase-*` name begins with `starter-`, so the rule never
over-excludes a real backend (guard test proves `starter-mastra`
excluded while `showcase-mastra` still checked).
2. **Add bare `harness-workers`** to the infra exclude set (`isExcluded`
checks the literal name first, so bare form matches).

The now-superseded `showcase-starter-*` literals are removed; the inert
`showcase-shell-*` legacy literals are retained to keep the diff minimal
and preserve an existing exclusion test. No starter is repointed — this
is a naming/exclusion fix only.

## Context

The 20 `showcase-*` LLM backends were already re-wired via Railway
`AIMOCK_URL` during the egress migration; they are green. This PR fixes
only the exclusion-set drift on the residual-6.

## Red-green (vitest, fakes, no network)

Added to `aimock-wiring.test.ts`:
- a drift-regression test feeding the exact live residual-6 + one wired
backend (`showcase-ag2`), asserting the residual-6 are excluded and only
the backend is checked-and-wired;
- a guard test (`starter-mastra` excluded WHILE `showcase-mastra` still
checked — proves the prefix rule doesn't over-exclude real backends);
- a full-roster test locking "checked universe == exactly the 20
`showcase-*` backends" against the full 41-service production roster.

RED (before fix) — 2 of the new tests fail; residual-6 land in
`unwired`:
```
AssertionError: expected [ 'harness-workers', …(25) ] to deeply equal [ 'showcase-ag2', …(19) ]
+   "harness-workers",
+   "starter-adk",
+   "starter-langgraph-js",
+   "starter-ms-agent-framework-dotnet",
+   "starter-ms-agent-framework-python",
+   "starter-strands-python",
 Test Files  1 failed (1)
      Tests  2 failed | 30 passed (32)
```

GREEN (after fix):
```
 ✓ src/probes/aimock-wiring.test.ts (32 tests)
 Test Files  1 passed (1)
      Tests  32 passed (32)
```

## Quality gates (harness)

- `vitest run` aimock-wiring: 32/32 pass
- `tsc --noEmit`: clean
- `tsc -p tsconfig.build.json` (build): clean
- `oxfmt --check` + `oxlint`: clean (0 warnings/errors)

Two unrelated harness tests (`d5-mapping-drift`, `d0-gone-predicate`)
fail locally because they parse `shell-dashboard` source / a generated
`registry.json` that aren't present in a workspace-scoped install; both
fail identically on clean `origin/main` and are unaffected by this
change. They resolve in CI's full checkout.
2026-07-17 22:21:02 -07:00
Tyler Slaton b7d2fdcc25 fix(channels-intelligence): HTTP-transport robustness cluster (OSS-497) (#6037)
Six pre-existing `@copilotkit/channels-intelligence` HTTP-transport
robustness items surfaced by the pre-merge CR of #5983 (Linear OSS-497).
All confirmed against `main` after #5983 merged; none introduced by it.
Each is an independent, focused commit with a test.

## Fixes

1. **Heartbeat starvation mid-turn** — `HttpDeliverySource.runLoop`
heartbeated only at the top of each iteration, then blocked on
`onDelivery` for up to `turnTimeoutMs` (120s). With a 15s cadence, a
turn longer than the cadence sent no heartbeat, so app-api could mark a
healthily-working runtime stale mid-turn and withhold new deliveries.
Heartbeating now runs on a standalone recurring timer, independent of
the claim loop.
2. **`stop()` shutdown latency** — `stop()` set `running=false` then
awaited the loop, which only rechecked after its current sleep (≤15s
idle) or `onDelivery` (≤120s mid-turn); sleeps were `unref`'d but not
interruptible. Added a `stopWait` promise that the poll-sleep and
turn-wait both race, so shutdown is prompt. A mid-turn stop leaves the
lease for app-api to re-lease and does **not** nack (the turn didn't
fail); the turn's eventual settlement is always handled so a post-stop
rejection never surfaces as unhandled. *(1 & 2 share the runLoop
lifecycle, so they land in one commit.)*
3. **Empty-text `update` silently acked** — the `if (!text) return { ok:
true }` guard ran for both `post` and `update`. An empty POST is a legit
no-op, but an empty UPDATE (e.g. clearing a message body) that this
post-only fallback egress can't express was acked as success. Now
returns `{ ok: false, code: "empty_update" }`; empty posts still no-op.
4. **Static `adapter` on egress** — `emit` posted `this.cfg.adapter`
(default `"slack"`) alongside a possibly-Teams `replyTarget`. Confirmed
app-api's egress route (`sendChannelEgressMessage`) routes on
`replyTarget.adapter` + `channelName` and ignores this field, so it's
latent — but now derived from the delivery's own reply route to avoid
the contradiction. *(Note: the listener heartbeat's
`declaredChannels[].adapter` is intentionally left alone — app-api
genuinely consumes it for per-adapter health/conflict via the
`channel_adapter_configs.provider` join, and declaring the bot's full
adapter set is a separate design change, not a robustness cleanup.)*
5. **`projectId` strict-`number` only** — the realtime scope build
accepted `projectId` only as a JS `number` while org/channel were
`String()`-coerced, so a numeric-string on the untyped wire silently
fell back to the transport-default projectId, defeating per-delivery
scope authority. Extracted a `coerceWireProjectId` helper (number or
numeric-string → positive integer) with fallback.
6. **File/history client ignored injectable `fetch`** —
`fetchFile`/`getHistory`/`uploadFile` hardcoded `globalThis.fetch`, so
an injected `config.fetch` was honored everywhere except
binary/history/upload. The transport's `FetchLike` is text-only and
can't satisfy the binary client, so the file/history client got its
**own** injectable full `fetch` (`typeof fetch`), resolved via a single
helper with a global fallback.

Plus a small `docs` commit fixing a doc-comment placement displaced by
the item-5 export.

## Testing

- `nx run @copilotkit/channels-intelligence:test` — **178 passed** (170
baseline + 8 new).
- `nx run @copilotkit/channels-intelligence:check-types` — clean.
- lefthook (full package test + typecheck) ran on every commit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-17 17:16:32 -07:00
Tyler Slaton 439e763618 docs(channels): clarify read_thread getMessages is text-only (OSS-488) (#6035)
## What

Follow-up nit from [PR
#5969](https://github.com/CopilotKit/CopilotKit/pull/5969) review
([OSS-488](https://linear.app/copilotkit/issue/OSS-488)). Fixes the
`getMessages` JSDoc in
`packages/channels-intelligence/src/intelligence-adapter.ts`, which
oversold image support.

The comment cited *"what was in the image"* as a `read_thread` use case,
but image/file parts contribute **no text** in this mapping —
`read_thread` is text-only by design. Image *content* reaches the model
only via `conversationStore`'s seeding of `agent.messages`. The comment
now says so explicitly instead of implying `read_thread` can see image
content.

Comment-only change — no runtime/API behavior change, so no changeset.

## Scope of OSS-488

The ticket listed three nits; this PR addresses the one still
actionable:

- **#1 (double-space when joining content parts)** — ✅ already resolved
on `main` (the `.filter(Boolean).join(" ")` fix + updated test landed
via the OSS-476 CR round). Nothing to do.
- **#2 (JSDoc oversells image support)** — ✅ **this PR**.
- **#3 (duplicate `getHistory` fetch per turn)** — intentionally
**deferred**. A per-turn memo would need turn-scoped caching with a
clear-on-turn-start hook; without one it risks serving stale history — a
correctness regression on something the ticket itself rates "acceptable
for an on-demand tool." Not worth it here.

The **known limitation** (speakers collapse to `user`/`bot` because
upstream `AgentMessage` carries no names) is unchanged and not
actionable at this layer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-17 17:15:58 -07:00
Tyler Slaton 4b53a4923b Add Angular contributors as code owners of the Angular package (#5388)
## What

Adds `wolfmanfx` (Murat Sari) and `rainerhahnekamp` (Rainer Hahnekamp)
as code owners of the Angular package paths in `.github/CODEOWNERS`:

- `packages/angular/`
- `packages/v2/angular/`

Existing core-dev owners are retained on both lines, so this is purely
additive.

## Notes

- **Access is granted separately.** This CODEOWNERS entry only assigns
review ownership; it does not grant repo access. The two contributors
still need to be added as repo collaborators via GitHub settings for the
entry to take effect (GitHub permissions are repo-level, not
folder-scoped).
- **Branch protection is currently disabled on `main`**, so this assigns
reviewers but does not gate merges until protection requiring code-owner
review is enabled.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-17 15:00:35 -07:00
Jordan Ritter d9ac09f83c fix(harness): exclude starter-* and harness-workers from aimock-wiring probe
The aimock_wiring:global probe went red on the residual-6 live services
(harness-workers + 5 starters). Root cause is EXCLUDE naming drift after
the egress/private-networking migration: EXCLUDE_SERVICES keyed starters
as showcase-starter-<framework> (matching only starter-<framework>), but
live Railway names are bare starter-<framework>[-lang] (e.g.
starter-strands-python, starter-langgraph-js) — no match, so they fell
through to being checked, landed in unwired, and kept the probe red.
harness-workers had no exclude entry at all.

Fix: exclude the whole starter-* family by prefix in isExcluded (starters
are contributor scaffolds, categorically not wired through aimock; safe
because no showcase-* backend name starts with starter-, so it never
over-excludes a real backend), and add bare harness-workers to the infra
exclude set. Superseded showcase-starter-* literals removed; inert
showcase-shell-* legacy literals retained to keep the diff minimal.

The 20 showcase-* LLM backends were already re-wired via Railway
AIMOCK_URL; this is a naming/exclusion fix only (no starter is repointed).
2026-07-17 14:57:36 -07:00
Ben Taylor 54989f31f5 chore(codeowners): drop nonexistent packages/v2/angular path
The repo uses a flat package layout; there is no packages/v2/ directory,
so that CODEOWNERS entry never matches and fails path validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:36:13 -05:00
Benjamin Taylor 15a8ddb0f7 fix(channels-intelligence): address adversarial CR findings (OSS-497)
Follow-up to the OSS-497 robustness cluster after a 3-agent adversarial review.

- Empty-text update: reclassify from a hard {ok:false} failure to a logged
  no-op. The failure path threw up the run and nack'd the WHOLE turn retryably,
  re-running valid work to dead-letter for a condition a retry can't fix (an
  agent clearing a message body the post-only fallback can't express). Now it is
  skipped with a distinct warning log — surfaced, not silent, and not turn-fatal.
- Injectable fetch: plumb it through the transports. Both HttpDeliverySource and
  RealtimeGatewayTransport now accept a binary-capable /
  and forward it to IntelligenceFileHistoryClient, so a consumer injecting a
  fetch has it honored on the download/history/upload paths (previously the
  client-level injectable was reachable only by constructing the client directly).
- coerceWireProjectId: use Number.isSafeInteger so a numeric string beyond
  MAX_SAFE_INTEGER is rejected (was silently coerced to a lossy integer).
- Malformed-present projectId: log before falling back to the transport scope
  (an absent projectId still falls back quietly), mirroring the loud drops in
  the same function instead of silently masking a wire-corruption signal.
- Restart safety: guard the recurring heartbeat with a run generation so a
  stop->start while a heartbeat POST is in flight can't leave two reschedule
  chains doubling the heartbeat rate; bump the generation on stop().
- Remove now-dead lastHeartbeatAt state (only reader was the removed in-loop
  heartbeat check).

Tests: empty-update now asserts log + no-op; added transport fileFetch
wire-through, adapter-from-route fallback, no-unhandled-rejection after a
mid-turn stop, heartbeat-stops-after-stop, elapsed-time bounds on the stop()
promptness tests, and coerceWireProjectId edge cases (leading zeros, > safe int).
nx test (green) + check-types (clean).
2026-07-17 15:22:10 -05:00
Benjamin Taylor 81562f5b24 docs(channels-intelligence): restore assertValidChannelRealtimeScope doc placement
The coerceWireProjectId export (added for OSS-497) was inserted directly above
assertValidChannelRealtimeScope, displacing that function's doc comment onto the
helper. Reorder so each function carries its own docblock.
2026-07-17 14:46:27 -05:00
Benjamin Taylor 0d412f3bb8 fix(channels-intelligence): honor an injectable fetch in the file/history client
IntelligenceFileHistoryConfig's fetchFile/getHistory/uploadFile hardcoded
globalThis.fetch, so a consumer (or test) injecting a fetch had it honored
everywhere EXCEPT the binary download, history, and upload paths. The transport's
FetchLike is text-only ({ ok, status, text() }) and cannot satisfy the binary
client (.body/.arrayBuffer()/.headers/.json()), so add the client's OWN
injectable full `fetch` (typeof fetch) on its config, resolved via a single
resolveFetch() helper that falls back to the global fetch. Each caller keeps its
existing degrade/throw contract for the no-fetch runtime.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:45:34 -05:00
Jordan Ritter 5ebbbd1e2f fix(ci): stop canary dispatches from false-paging the release notifier (#6036)
## What broke

Run
[29605894890](https://github.com/CopilotKit/CopilotKit/actions/runs/29605894890)
— a `workflow_dispatch` canary on branch
`canary/mme-subagents-29605883024-1` — fired a red `🔴 CopilotKit release
notifier failed — a release alert may have been swallowed` page to
#engr. No release was actually missed (`npm latest` was `1.63.1`,
published fine). It was a false page.

## Why it false-paged

The canary build died at `pnpm install --frozen-lockfile`
(`ERR_PNPM_OUTDATED_LOCKFILE` — `@ag-ui/client` lockfile `0.0.57` vs
manifest `0.0.58` on the source branch). That flipped the `notify` job
to `failure()`, which triggered its self-watchdog "best-effort Slack"
post.

That self-alert is gated on release intent (`npm_intended ||
py_intended`), and the `npm_intended` computation treated **any**
`workflow_dispatch` as stable-release intent:

```
NPM_INTENDED="${{ (github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/publish/'))) && 'true' || 'false' }}"
```

But `canary.yml` orchestrates `publish-release.yml` with `-f
mode=prerelease` on a short-lived `canary/*` ref (see `canary.yml` line
~196). A prerelease/canary is explicitly *not* a stable release: it
pushes no tag, cuts no GH Release, and posts nothing to Slack (the
notification builder returns `should_post=false` for it). So a failed
canary build had no business self-paging.

## The fix

**Fix 1 (this PR):** Narrow the `npm_intended` dispatch arm with
`inputs.mode != 'prerelease'`, so a canary dispatch yields
`npm_intended=false` and never self-pages, while a stable dispatch
(default mode + the main-only retry escape hatch) and a merged
`release/publish/*` PR still page correctly on genuine failure. Using
`!= 'prerelease'` (rather than `== 'stable'`) matches the existing `mode
!= 'prerelease'` guards on the publish/tag steps and correctly treats an
empty/default mode as stable.

before:
```
NPM_INTENDED="${{ (github.event_name == 'workflow_dispatch' || (...)) && 'true' || 'false' }}"
```
after:
```
NPM_INTENDED="${{ ((github.event_name == 'workflow_dispatch' && inputs.mode != 'prerelease') || (...)) && 'true' || 'false' }}"
```

**Fix 2 (follow-up, not in this PR — deliberately):** The stale-lockfile
source. `pnpm install --frozen-lockfile` *already is* the
lockfile-consistency check and it failed loudly and correctly — adding
another pre-check to this workflow would be redundant. The lockfile
drift lives on the `canary/mme-subagents-*` source branch, produced by
the `mme-subagents` agent-orchestration automation (it bumped a manifest
without regenerating `pnpm-lock.yaml`). The right place to fix that is
that generator, which is out of scope for this workflow file. Filing as
a follow-up rather than over-engineering a guard here.

## Red-green proof

The failure surface is the intent-gate bash expression. Extracted the
exact `npm_intended` boolean (pre- and post-fix) into isolated scripts
and ran the real canary inputs.

**RED (pre-fix logic):**
```
$ EVENT_NAME=workflow_dispatch INPUT_MODE=prerelease PR_HEAD_REF="canary/mme-subagents-29605883024-1" bash gate_pre.sh
npm_intended=true      # BUG: canary treated as stable-release intent → self-pages
```

**GREEN (post-fix logic):**
```
$ EVENT_NAME=workflow_dispatch INPUT_MODE=prerelease PR_HEAD_REF="canary/mme-subagents-29605883024-1" bash gate_post.sh
npm_intended=false     # canary no longer self-pages

# no regression on genuine-release paths:
$ EVENT_NAME=workflow_dispatch INPUT_MODE=stable                       # stable dispatch (retry hatch)
npm_intended=true
$ EVENT_NAME=workflow_dispatch INPUT_MODE=""                           # stable dispatch, default mode
npm_intended=true
$ EVENT_NAME=pull_request PR_MERGED=true PR_HEAD_REF="release/publish/monorepo/v1.63.1"
npm_intended=true
$ EVENT_NAME=pull_request PR_MERGED=true PR_HEAD_REF="feature/some-branch"   # non-release merge
npm_intended=false
```

## Lint

`actionlint .github/workflows/publish-release.yml` → exit 0 (clean).
This matches the repo's `Lint Release Workflows` CI job (actionlint,
`level=error`, same file in scope).
2026-07-17 12:43:00 -07:00
Benjamin Taylor ba38ee0e5f fix(channels-intelligence): heartbeat during long turns and make stop() prompt
Two coupled HttpDeliverySource runLoop lifecycle robustness fixes:

Heartbeat starvation: the loop heartbeated only at the top of each iteration,
then blocked on onDelivery for up to turnTimeoutMs (120s). With a 15s cadence,
a turn longer than the cadence sent no heartbeat for its whole duration, so
app-api could expire the activation and mark a healthily-working runtime stale
mid-turn, withholding new deliveries. Move heartbeating to a standalone
recurring timer that fires on cadence regardless of what the loop is doing
(rescheduling only after each heartbeat settles, so no overlap).

Shutdown latency: stop() set running=false then awaited the loop, but the loop
only rechecked after its current sleep (up to cadence, idle) or onDelivery (up
to turnTimeoutMs, mid-turn) — the sleeps were unref'd but not interruptible, so
shutdown could take up to 15s idle or 120s mid-turn. Add a stopWait promise that
stop() resolves; the poll-sleep and the turn-wait both race it, so shutdown is
prompt. A mid-turn stop leaves the lease for app-api to re-lease and does NOT
nack (the turn didn't fail); the turn's eventual settlement is always handled so
a post-stop rejection never surfaces as unhandled.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:42:39 -05:00
Benjamin Taylor 787264fa8f fix(channels-intelligence): accept a numeric-string projectId in the realtime scope build
RealtimeGatewayTransport.toIngressEnvelope built the per-delivery scope with
`typeof delivery.projectId === 'number' ? delivery.projectId : this.scope.projectId`,
while organizationId/channelId were String()-coerced. The delivery.available
payload is untyped JSON, so a numeric-string projectId ('9' on the wire) failed
the strict check and silently substituted the transport-default projectId —
defeating the per-delivery scope authority and routing the render-accept under
the wrong project.

Extract a `coerceWireProjectId` helper (number or numeric-string -> positive
integer, else undefined) and use it with a fallback to the transport default.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:35:49 -05:00
Benjamin Taylor 46517eecd9 fix(channels-intelligence): derive egress adapter from the reply route
HttpEgressSink.emit posted the static `this.cfg.adapter` (default "slack")
alongside a possibly-Teams `replyTarget`, so a Teams delivery through the
fallback egress carried a contradictory `adapter: "slack"`. app-api's egress
route (sendChannelEgressMessage) routes on `replyTarget.adapter` + channelName
and ignores this top-level field, so this is latent — but one provider-agnostic
runtime serves every adapter its bot has attached, so posting the config default
is misleading. Derive the posted adapter from the delivery's own reply route,
falling back to the config adapter when the route carries none.

Note: the listener heartbeat's declaredChannels[].adapter is NOT changed here —
app-api genuinely consumes it (the channel_adapter_configs.provider join) for
per-adapter health/conflict reporting, and the runtime only knows its single
configured adapter; declaring the bot's full adapter set is a separate design
change, not this robustness cleanup.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:32:42 -05:00
Jordan Ritter af68620640 fix(ci): stop canary dispatches from false-paging the release notifier
The notify job's intent gate treated ANY workflow_dispatch as stable-release
intent (npm_intended=true), including prerelease/canary dispatches. canary.yml
orchestrates publish-release.yml with mode=prerelease on a short-lived canary/*
ref, so a canary whose build fails (e.g. a stale pnpm-lock.yaml on the source
branch tripping pnpm install --frozen-lockfile) flipped the notify job to
failure() and fired the self-watchdog's best-effort Slack post — a red
'a release alert may have been swallowed' page for a release that was never
going to publish.

Narrow the npm_intended dispatch arm with 'inputs.mode != prerelease' so a
canary yields npm_intended=false and never self-pages, while a stable dispatch
(default mode, and the main-only retry escape hatch) and a merged
release/publish/* PR still page correctly on genuine failure. This mirrors the
existing 'mode != prerelease' guards on the publish/tag steps and the builder's
should_post=false suppression for canaries.
2026-07-17 12:28:51 -07:00
Benjamin Taylor 060cebf304 fix(channels-intelligence): fail loud on empty-text egress update
HttpEgressSink.emit ran the `if (!text) return { ok: true }` no-op guard for
both post and update ops. An empty POST is a legitimate no-op (nothing to say),
but an empty UPDATE — a real intent such as clearing a message body that this
post-only fallback egress cannot express — was acked as success while nothing
was sent, inconsistent with the module's fail-loud posture. Return
`{ ok: false, code: 'empty_update' }` for that case; empty posts still no-op.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:28:25 -05:00
Benjamin Taylor 0b69bb4b4d docs(channels): clarify read_thread getMessages is text-only (OSS-488)
The getMessages JSDoc cited "what was in the image" as a use case, but
image/file parts contribute no text in this mapping — read_thread is
text-only. Image content reaches the model only via conversationStore's
seeding of agent.messages. Tweak the comment so it no longer implies
read_thread can see image content.
2026-07-17 13:52:36 -05:00
Alem Tuzlak e72e9b8f9c chore: release channels v0.2.1 (#6025)
## Release channels v0.2.1

**Scope:** `channels` | **Bump:** `patch`

---

### How this release process works

1. **This PR was created automatically** by the "release / create-pr"
workflow.
   It bumped the `channels` packages to `0.2.1`
   and generated AI-enhanced release notes.

2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
   must pass before merging. This is the review gate.

3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.

4. **When this PR is merged**, the `release / publish` workflow
automatically:
   - Builds all packages
   - Publishes the `channels` packages to npm at version `0.2.1`
   - Creates git tag `channels/v0.2.1`
   - Creates a GitHub Release with the final release notes

### Before merging

- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)

---

> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
channels/v0.2.1
2026-07-17 11:14:53 +02:00
AlemTuzlak 21b9a7138b chore: release channels v0.2.1 2026-07-17 09:13:35 +00:00
Jordan Ritter c51b84a349 fix(showcase): offload sync LLM calls off showcase agent event loops (claude-sdk-python, ag2, llamaindex) (#5997)
## What

The `claude-sdk-python` showcase agent `:8000` wedges under D6/LLM load:
two **synchronous** `anthropic.Anthropic().messages.create()` calls run
directly on the uvicorn asyncio event loop, freezing it for the full LLM
round-trip so `/health` stops answering. The watchdog counts 3
consecutive failures (~90s) and kill-restarts the container, dropping
active sessions. This is the pre-existing root cause behind the
#oss-alerts restart noise that #5987's alerting surfaced (it was never a
#5987 regression).

## Root cause (sync-in-async)

All in `integrations/claude-sdk-python/`:

- **`src/agents/agent.py`** — `_execute_tool`'s `generate_a2ui` branch
builds a sync `anthropic.Anthropic()` and calls
`client.messages.create()` synchronously. `_execute_tool` is a sync
callback invoked on the loop from **two** async callers: `run_agent`'s
agentic loop, and the Claude-Agent-SDK MCP tool handler in
`claude_agent_sdk_adapter.py`.
- **`src/agents/a2ui_dynamic.py`** — `_generate_a2ui`, same sync
pattern, invoked on the loop from the `run_a2ui_dynamic_agent`
generator.

## Fix — approach (a): `await asyncio.to_thread(...)` at the call sites

Chosen over approach (b) (`AsyncAnthropic` + `async def`) because it is
the **lowest blast radius**: the sync functions and the shared
`ExecuteTool` callback type stay unchanged, and wrapping at the call
sites fixes the **whole** tool-dispatch path uniformly (any current or
future sync tool in the dispatcher), not just `generate_a2ui`. Approach
(b) would only fix `generate_a2ui` unless `_execute_tool` were made
fully async — which ripples into the `ExecuteTool` type and both call
sites anyway.

Every occurrence fixed (file:line):
- `src/agents/agent.py:~1355` — `run_agent` call site → `await
asyncio.to_thread(_execute_tool, ...)`
- `src/agents/claude_agent_sdk_adapter.py:~152` — MCP `sdk_tool_handler`
→ `await asyncio.to_thread(execute_tool, ...)`
- `src/agents/a2ui_dynamic.py:~287` — secondary call site → `await
asyncio.to_thread(_generate_a2ui, ...)`

Whole-integration grep for the pattern (`anthropic.Anthropic(`, sync
`.messages.create`, `OpenAI(`, `time.sleep`, blocking I/O): only these
two sites existed. Every other agent in the integration already uses
`AsyncAnthropic`.

## Showcase parity verdict

`a2ui_dynamic.py` is **per-integration**, not shared. Each framework
(langgraph-python, llamaindex, ms-agent-python, pydantic-ai, ag2,
strands, agno, …) ships its own copy that "mirrors" langgraph-python but
uses that framework's own client. Only claude-sdk-python's copy used the
sync `anthropic.Anthropic()` pattern, so **blast radius is
claude-sdk-python only** — no other integration has this wedge. The
`tools` symlink (→ `showcase/shared/python/tools`) was **not** touched
(iron-rule: edit shared source only, never symlink copies; here no
shared change was needed).

## entrypoint.sh alert scoping

- `:8000` agent watchdog branch: **removed** the Slack POST, **kept**
the `kill -9 $AGENT_PID` self-heal. It now self-heals silently (root
cause fixed).
- Public `$PORT` `/api/health` branch: Slack POST to
`$SLACK_WEBHOOK_OSS_ALERTS` **intact** — the public Next.js wedge still
pages.

## Red → green proof

Faithful harness at `showcase/tests/repro/async-wedge/`: a real
`anthropic.Anthropic` sync client (real httpx transport) pointed via
`ANTHROPIC_BASE_URL`/`base_url` at a controllable slow local
Anthropic-compatible endpoint (`slow_anthropic.py`, `SLOW_SECONDS`
latency, serves both `messages.create` JSON and `messages.stream` SSE).
5× concurrent `POST /generate`; poll `/health` 1/s for 10s; assert &
exit non-zero on a false result.

**Minimal replica (`server.py`) — the load-bearing construct:**
```
REPLICA RED (FIXED=0, sync client on loop):   ASSERT_SUMMARY is_fixed=0 ok=6  wedge=4  → PASS RED
REPLICA GREEN (FIXED=1, asyncio.to_thread):   ASSERT_SUMMARY is_fixed=1 ok=10 wedge=0  → PASS GREEN
```

**Real production code (`prod_server.py`):**
```
PROD GENERATOR GREEN (real run_a2ui_dynamic_agent, fix in source):
    ASSERT_SUMMARY expect=green ok=10 wedge=0  → PASS GREEN
    (secondary sync _generate_a2ui fired 5x via to_thread; /health stayed 200)

MUTATION GUARD RED (real _generate_a2ui, sync-on-loop):
    ASSERT_SUMMARY expect=red   ok=5  wedge=5  → PASS RED  (proves the harness fails on the bug)

PROD DIRECT GREEN (real _generate_a2ui via to_thread):
    ASSERT_SUMMARY expect=green ok=10 wedge=0  → PASS GREEN
```

The mutation guard exercises the **real** production `_generate_a2ui`
sync-on-loop and confirms it wedges (`wedge=5`), while the `to_thread`
path stays fast-200 (`wedge=0`) — the harness is not vacuously green.

## Regression check

`pytest tests/python/` → **6 passed**. `ruff format --check` clean on
all 3 edited files (no new lint; the pre-existing origin/main ruff
findings are untouched, out of scope). `bash -n` clean on entrypoint.sh
and all repro scripts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


---

## CR round 2 — repro hardening

Addressed the one mandatory CR finding (M1) plus two optional items on
the `showcase/tests/repro/async-wedge/` harness. **Repro-harness only —
no production source touched** (`src/agents/a2ui_dynamic.py` unchanged).

**M1 (mandatory) — MODE=generator false-green closed.** The GREEN lane
previously asserted only `WEDGE==0`. If the mock's SSE were mis-parsed,
the `generate_a2ui` tool_use dropped, or the generator early-exited,
`_generate_a2ui` (the bug site) would never run, the loop would never
park, and `WEDGE==0` would pass trivially — proving nothing. Fix:
`prod_server.py` now wraps the real `a2ui_dynamic._generate_a2ui` with a
counter (`tool_dispatch_fired`), exposed via `/stats` and the
`/generate` response. `run_prod.sh` reads it after the poll window and
the GREEN lane now **fails (exit 6)** unless `tool_dispatch_fired >= 1`.
The counter tracks genuine execution regardless of call shape
(`asyncio.to_thread` offload or sync-on-loop).

**O1** — `slow_anthropic.py` now uses `await
asyncio.sleep(SLOW_SECONDS)` instead of blocking `time.sleep`, so the
mock's own loop stays free under concurrency.

**O2** — `run.sh` adds a 0.5s gap after firing load, before the first
`/health` poll, so poll 1 isn't wasted on a pre-block fast-200.

Also fixed a latent hang: `run_prod.sh` ended with a bare `wait` that
blocked forever on the long-lived uvicorn server jobs, so the
summary/assertion never printed. It now tracks and reaps only the
load-curl PIDs (bash-3.2 / `set -u` safe).

### Red-green proof (harness assertion)

RED (false-green injected via `REPRO_DROP_TOOL_USE=1` — tool_use
dropped, generator drains but never dispatches; env-only, no source
mutation):
```
ASSERT_SUMMARY expect=green ok=10 wedge=0 tool_dispatch_fired=0
FAIL GREEN: 0 wedges but tool_dispatch_fired=0 — _generate_a2ui never ran; WEDGE==0 is a false green (bug site never exercised)
EXIT=6
```
Old assertion (`WEDGE==0` only) would have PASSED here; hardened
assertion correctly FAILS.

GREEN (normal operation — tool dispatch fires):
```
ASSERT_SUMMARY expect=green ok=10 wedge=0 tool_dispatch_fired=9
PASS GREEN: 0 wedges AND tool_dispatch_fired=9 (>=1) — real production code exercised the bug site and kept /health fast-200 under load
EXIT=0
```

### No regression — deterministic MODE=direct mutation guard
```
MODE=direct EXPECT=red   -> ASSERT_SUMMARY expect=red   ok=8  wedge=2 tool_dispatch_fired=3 -> PASS RED   (exit 0)
MODE=direct EXPECT=green -> ASSERT_SUMMARY expect=green ok=10 wedge=0 tool_dispatch_fired=3 -> PASS GREEN (exit 0)
```


---

## Fold-in: ag2 + llamaindex (fleet-wide sweep)

A CR flagged the same sync-LLM-on-the-event-loop wedge in two more
integrations,
and a fleet-wide sweep of every `integrations/*/src/**` Python file for
a sync
LLM `.create()` running **directly inside an `async def`** on the
uvicorn loop
found a THIRD previously-missed site. All three are now fixed with the
same
lowest-blast-radius approach (extract the blocking round-trip into a
sync
`_generate_a2ui`, and `await asyncio.to_thread(...)` from the async
wrapper):

- `integrations/ag2/src/agents/beautiful_chat.py` — `async def
generate_a2ui` (sync `openai.OpenAI().chat.completions.create`)
- `integrations/llamaindex/src/agents/a2ui_dynamic.py` — `async def
generate_a2ui` (sync `OpenAI().chat.completions.create`)
- `integrations/llamaindex/src/agents/agent.py` — `async def
generate_a2ui` (sync `OpenAI().chat.completions.create`) — **found by
the sweep, not in the original report**

### Fleet-sweep result (what was NOT touched, and why)

- **ag2 `agent.py` + `a2ui_dynamic.py`**: already non-blocking (`await
_async_openai_client.chat.completions.create`, i.e. `AsyncOpenAI`).
Confirmed, not re-touched.
- **agno, ms-agent-python, pydantic-ai, strands, crewai** sync `.create`
sites: all inside plain `def` framework tools (`@tool`-style), which the
frameworks dispatch in their own worker/executor context — never
bare-`await`ed on the loop. No wedge.
- **`.ts` voice routes / `agent_server.ts`**: TypeScript/Node, not the
uvicorn asyncio loop.
- **`llama_index.llms.openai.OpenAI(model=…)`** objects:
framework-managed async LLMs, distinct from the raw `openai` SDK client
at the wedge sites.

So the wedge exists at exactly these three `async def generate_a2ui`
sites (plus the two claude-sdk-python sites fixed above). All three
edited files are per-integration REAL files (not symlinks to
`showcase/shared/`) — verified per the iron rule.

`ag2`/`llamaindex` `entrypoint.sh` were intentionally **not** touched —
the Slack-alert scoping was specific to the claude-sdk-python incident.

### Red → green proof (real production code, deterministic)

New OpenAI-SDK repro harness (dev-only, under
`showcase/tests/repro/async-wedge/`, sibling of the anthropic one): a
real `openai` SDK client (real httpx transport) pointed via
`OPENAI_BASE_URL` at a controllable slow local OpenAI-compatible
endpoint (`slow_openai.py`). `run_prod_openai.sh` drives the **real
production `_generate_a2ui`** for each `TARGET`, 5× concurrent `POST
/generate`, poll `/health` 1/s ×10, with the `tool_dispatch_fired >= 1`
anti-false-green guard.

```
ag2-beautiful-chat  RED:   ASSERT_SUMMARY expect=red   ok=5  wedge=5 tool_dispatch_fired=5  -> PASS RED
ag2-beautiful-chat  GREEN: ASSERT_SUMMARY expect=green ok=10 wedge=0 tool_dispatch_fired=5  -> PASS GREEN
llamaindex-agent    RED:   ASSERT_SUMMARY expect=red   ok=5  wedge=5 tool_dispatch_fired=5  -> PASS RED
llamaindex-agent    GREEN: ASSERT_SUMMARY expect=green ok=10 wedge=0 tool_dispatch_fired=5  -> PASS GREEN
llamaindex-a2ui     RED:   ASSERT_SUMMARY expect=red   ok=5  wedge=5 tool_dispatch_fired=5  -> PASS RED
llamaindex-a2ui     GREEN: ASSERT_SUMMARY expect=green ok=10 wedge=0 tool_dispatch_fired=5  -> PASS GREEN
```

Source-level cross-check driving the **real on-disk `async
generate_a2ui` wrapper** directly: 5 concurrent 3s calls complete in
~3.1s (parallel offload threads, not serialized ~15s) while a bare
asyncio heartbeat keeps ticking (64 ticks through the load window) —
loop stays free. Negative control (ag2 wrapper temporarily reverted to
sync-on-loop): 15.11s serialized, heartbeat starved to 4 ticks — wedge
reproduced; source restored.

Existing claude-sdk-python lanes re-run green (no regression).
2026-07-16 14:38:26 -07:00
github-actions[bot] 8aa763aa15 style: auto-fix formatting 2026-07-16 20:13:44 +00:00
Jordan Ritter bf3f327f8c fix(showcase): offload sync LLM calls in ag2 + llamaindex a2ui generators
A fleet-wide sweep for sync LLM .create() calls running directly inside an
async def on the uvicorn event loop found three more wedge sites (same class
as the claude-sdk-python fix in this PR):

- integrations/ag2/src/agents/beautiful_chat.py
- integrations/llamaindex/src/agents/a2ui_dynamic.py
- integrations/llamaindex/src/agents/agent.py (missed by the original report)

Each extracts the blocking secondary-LLM round-trip into a sync _generate_a2ui
helper and offloads it via await asyncio.to_thread(...) from the async
generate_a2ui wrapper (lowest blast radius; sync body unchanged). ag2's other
agents already use AsyncOpenAI; all other sync .create sites are inside plain
def framework tools dispatched off-loop by their frameworks, so they do not
wedge. entrypoint.sh alert-scoping left untouched (claude-sdk-python-specific).

Adds a dev-only OpenAI-SDK repro harness (slow_openai.py, prod_server_openai.py,
run_prod_openai.sh) that drives the REAL production _generate_a2ui via a slow
local OpenAI-compatible endpoint, with a tool_dispatch_fired>=1 anti-false-green
guard. RED (sync-on-loop) -> GREEN (to_thread) verified for all three sites.
2026-07-16 13:11:57 -07:00
Ben Taylor e49a902741 fix(release): make packed Channels umbrella verify hermetic (#6022)
## Problem

Every **monorepo** release PR is blocked by its own not-yet-published
version. The `test / unit` → **`unit (20.x)` → "Verify packed Channels
umbrella contract"** step (`pnpm run verify:channels-umbrella`, local
mode) fails with:

```
ERR_PNPM_NO_MATCHING_VERSION  No matching version found for @copilotkit/core@^1.63.1
  This error happened while installing the dependencies of @copilotkit/channels@0.2.0
```

### Root cause

`verify:channels-umbrella` (local mode) packs the Channels family into
tarballs and installs a throwaway consumer, pinning **only the family**
to those local tarballs via `pnpm.overrides`. But the family depends on
monorepo-versioned packages — `@copilotkit/core`, `@copilotkit/shared` —
via the `workspace:` protocol. `pnpm pack` rewrites `workspace:^` to the
workspace's *current* version (e.g. `^1.63.1`). On a release PR that's
the freshly-bumped version, which isn't on npm until the release
publishes. Since those packages aren't in the override set, the consumer
install falls through to the public registry and can't find them →
deadlock: the gate can't go green until the very version it's bumping to
is published, and publishing only happens after merge.

This is why `unit (20.x)` was red on **#5992 (v1.63.0)** and **#6019
(v1.63.1)** — both merged through the red manually.

## Fix

In local mode, also `pnpm pack` the family's `workspace:` monorepo
siblings and pin them as consumer overrides, so the install is fully
hermetic and never races the registry against our own in-flight release.
Siblings are discovered **transitively via the `workspace:` protocol**,
so the list never drifts as the family's internal dependencies change
(today it resolves to exactly `@copilotkit/core` + `@copilotkit/shared`;
`@copilotkit/shared`'s `license-verifier ~0.5.0` is correctly excluded
since it's a normal published range, not `workspace:`).

The registry-backed mode (`--registry`, used at publish time in
`publish-release.yml`) is unchanged.

## Testing

- Bumped `packages/shared` + `packages/core` to an unpublished `1.63.99`
to simulate a release PR:
- **Unmodified script** → `ERR_PNPM_NO_MATCHING_VERSION` for
`@copilotkit/core@^1.63.99` (reproduces the CI failure).
- **Fixed script** → `OK: local Channels snapshot is exact, compatible,
singly resolved, and TSX-consumable.`
- `npx vitest run --config scripts/release/vitest.config.mts` →
**109/109 pass**.
- Sibling discovery verified to return exactly `[@copilotkit/core,
@copilotkit/shared]`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-16 14:21:33 -05:00
Benjamin Taylor d221c03f02 fix(release): make packed Channels umbrella verify hermetic
The local `verify:channels-umbrella` check packs the Channels family and
installs a throwaway consumer, overriding only the family tarballs. The
family depends on monorepo-versioned packages (`@copilotkit/core`,
`@copilotkit/shared`) via the `workspace:` protocol, which `pnpm pack`
rewrites to the workspace's current version. On a release PR that version
is the freshly-bumped one that isn't on the registry until the release
publishes, so the consumer install fell through to npm and failed with
ERR_PNPM_NO_MATCHING_VERSION — the unit(20.x) red that blocked every
monorepo release PR (e.g. #5992, #6019) on its own not-yet-published
version.

Pack those workspace siblings locally too and pin them as consumer
overrides, so local mode never races the registry against our own
in-flight release. Siblings are discovered transitively via the
`workspace:` protocol so the list never drifts as the family's internal
dependencies change.

Verified by bumping shared+core to an unpublished version: the old script
fails with ERR_PNPM_NO_MATCHING_VERSION, the fixed script reports OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 13:58:02 -05:00
Ben Taylor 0c9d639b13 chore: release monorepo v1.63.1 (#6019)
## Release monorepo v1.63.1

**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.63.1`
   and generated AI-enhanced release notes.

2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
   must pass before merging. This is the review gate.

3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.

4. **When this PR is merged**, the `release / publish` workflow
automatically:
   - Builds all packages
   - Publishes the `monorepo` packages to npm at version `1.63.1`
   - Creates git tag `monorepo/v1.63.1`
   - Creates a GitHub Release with the final release notes

### Before merging

- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)

---

> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
v1.63.1
2026-07-16 13:43:24 -05:00
tylerslaton a7459f4fb2 chore: release monorepo v1.63.1 2026-07-16 18:23:58 +00:00
Ben Taylor 2296aa5b89 feat(channels): canonical cross-platform reaction normalization (Teams & WhatsApp) (#6011)
## What

Canonical cross-platform reaction normalization for **Teams** and
**WhatsApp**, plus the managed (Intelligence) delivery path.

`onReaction` handlers now receive one canonical `emoji` value regardless
of provider — a 🔄 arrives as `refresh` whether it came from Teams
(`1f504_refresh`), Slack (`arrows_counterclockwise`),
Discord/Telegram/WhatsApp (unicode), or the managed Intelligence path.

## Why

Teams sends emoji reactions as `<unicode-codepoint>_<name>` codes (e.g.
`1f504_refresh`) plus classic bare names (`like`/`heart`/…), and
WhatsApp wasn't a normalized platform — so consumers had to match
provider-specific tokens. Normalization already lived centrally in
`channels-core` for Slack/Discord/Telegram; this extends it to every
adapter we ship.

## Changes

- **channels-ui/emoji**: `EmojiPlatform` → `slack | discord | telegram |
teams | whatsapp`; new canonical entries (`refresh`, `laugh`,
`surprised`, `sad`, `angry`); Teams normalization (codepoint parse +
classic-name map; an out-of-range codepoint degrades to passthrough,
never throws); WhatsApp on the unicode path.
- **channels-core**: `teams`+`whatsapp` added to `EMOJI_PLATFORMS`;
`IncomingReaction` gains an optional source `platform`; `onReaction`
normalizes by `evt.platform ?? adapter.platform`.
- **channels-intelligence**: the managed reaction dispatch forwards the
delivery's source `platform`, so managed-path reactions normalize too.

## Tests

channels-ui (25), channels-core (156), channels-intelligence (170) — all
green; the three packages build clean. Covers Teams codepoint + classic
+ malformed(out-of-range) + unknown-passthrough, WhatsApp unicode, and
managed-path source-platform normalization.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-16 13:23:22 -05:00
renovate[bot] 90720cd188 chore(deps): update github actions (#6002)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/setup-java](https://redirect.github.com/actions/setup-java) |
action | minor | `v5.5.0` → `v5.6.0` |
| [ruby/setup-ruby](https://redirect.github.com/ruby/setup-ruby) |
action | minor | `v1.318.0` → `v1.319.0` |
|
[slackapi/slack-github-action](https://redirect.github.com/slackapi/slack-github-action)
| action | major | `v3.0.5` → `v4.0.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/592) for more information.

---

### Release Notes

<details>
<summary>actions/setup-java (actions/setup-java)</summary>

###
[`v5.6.0`](https://redirect.github.com/actions/setup-java/compare/v5.5.0...v5.6.0)

[Compare
Source](https://redirect.github.com/actions/setup-java/compare/v5.5.0...v5.6.0)

</details>

<details>
<summary>ruby/setup-ruby (ruby/setup-ruby)</summary>

###
[`v1.319.0`](https://redirect.github.com/ruby/setup-ruby/compare/v1.318.0...v1.319.0)

[Compare
Source](https://redirect.github.com/ruby/setup-ruby/compare/v1.318.0...v1.319.0)

</details>

<details>
<summary>slackapi/slack-github-action
(slackapi/slack-github-action)</summary>

###
[`v4.0.0`](https://redirect.github.com/slackapi/slack-github-action/compare/v4.0.0...v4.0.0)

[Compare
Source](https://redirect.github.com/slackapi/slack-github-action/compare/v4.0.0...v4.0.0)

###
[`v4.0`](https://redirect.github.com/slackapi/slack-github-action/compare/v4.0.0...v4.0.0)

[Compare
Source](https://redirect.github.com/slackapi/slack-github-action/compare/v4.0.0...v4.0.0)

###
[`v4`](https://redirect.github.com/slackapi/slack-github-action/compare/v3.0.5...v4.0.0)

[Compare
Source](https://redirect.github.com/slackapi/slack-github-action/compare/v3.0.5...v4.0.0)

</details>

---

### Configuration

📅 **Schedule**: (in timezone America/Los_Angeles)

- Branch creation
  - "before 9am every weekday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/CopilotKit/CopilotKit).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNTkuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI2NS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2026-07-16 18:12:42 +00:00
Benjamin Taylor 1110f18e30 fix(release): exempt first-party publishes from the release-age gate
The packed Channels umbrella (and the runtime-package verify) install
first-party packages straight from the registry, so a just-published
@copilotkit/* version fails pnpm's 24h minimumReleaseAge check — this is
what reddened `unit (20.x)` on channels PRs. The enumerated exclude lists
drift every time an internal dependency is added (main already had to bolt
on @copilotkit/channels-core, then #6012 added @copilotkit/core + shared).

Wildcard the @copilotkit/* scope (org-owned, same publish pipeline — no
outsider can inject a package, and it stops the drift). Keep @ag-ui
enumerated rather than wildcarded: it's a separate upstream org, so we don't
extend immediate-install trust to its entire scope. Third-party deps stay
fully gated. Applied to both the generated consumer workspace and the
repo-root .npmrc.

Verified: `pnpm run verify:channels-umbrella` installs the packed umbrella
with no ERR_PNPM_NO_MATURE_MATCHING_VERSION; umbrella unit tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 13:10:58 -05:00
Benjamin Taylor 052dc0948e fix(channels): address CR — case-insensitive Teams hex + test/comment hygiene
- emoji: match Teams codepoint hex case-insensitively (providers may send
  upper-case) + document single-codepoint-only parsing; cover 1F504_refresh.
- emoji: note the dormant outbound Teams asymmetry in toPlatformEmoji
  (unicode vs `<codepoint>_<name>`) as a TODO for when outbound reactions land.
- channels-slack: split the tool-only status-clear test so one mirrors the
  real event order (RUN_FINISHED before finish()) and one isolates the
  finish() backstop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 12:55:57 -05:00
Alem Tuzlak db65524aad fix(channels-slack): clear the "is thinking" status on finish for tool-only replies
The run renderer only cleared Slack's native "is thinking…" assistant
status from onFirstReply() (streamed-text paths) or the error/interrupt
paths. A turn whose reply streamed NO text — a tool-only / file-only
reply such as a posted chart — never triggered onFirstReply, so the
status indicator lingered forever.

finish() now clears the status as a backstop when statusMode is on and
no reply was posted. The postedReply guard prevents a redundant clear on
the normal streamed-text path (onFirstReply already cleared it).

Adds regression tests covering both the tool-only clear and the
postedReply guard.
2026-07-16 12:55:57 -05:00
Alem Tuzlak a06c8cf1be feat(channels): canonical cross-platform reaction normalization for Teams & WhatsApp
Normalize inbound reaction emoji to a canonical cross-platform name at the
central channels-core ingress, so onReaction handlers match one value
regardless of provider.

- channels-ui/emoji: add "teams" and "whatsapp" to EmojiPlatform and canonical
  entries for refresh/laugh/surprised/sad/angry. Teams normalizes its
  <codepoint>_<name> emoji codes (1f504_refresh -> refresh) and classic bare
  names (like -> thumbs_up); an out-of-range codepoint degrades to passthrough
  instead of throwing. WhatsApp uses the unicode path like Discord/Telegram.
- channels-core: add teams+whatsapp to EMOJI_PLATFORMS; IncomingReaction gains
  an optional source `platform`; onReaction normalizes by
  evt.platform ?? adapter.platform.
- channels-intelligence: the managed reaction dispatch passes the delivery's
  source platform (env.platform) so managed reactions normalize too.
2026-07-16 12:55:57 -05:00
renovate[bot] e4feb44c7a chore(deps): update github actions 2026-07-16 17:55:19 +00:00
Tyler Slaton 3c1b991f7c fix(runtime): install managed Channels activation dependency (#6012)
## Summary

- make @copilotkit/channels-intelligence a direct runtime dependency
- remove the optional peer declaration that left managed Channels
consumers without the activation module
- add a package-metadata regression test

## Root cause

#5963 introduced managed Channels activation through a dynamic import,
then moved @copilotkit/channels-intelligence from an auto-installed
dependency to an optional peer. Package managers therefore omitted the
module while runtime activation still required it, causing Module not
found for consumers on runtime 1.63.0.

## Verification

- regression test fails on main and passes with this change
- packed runtime manifest resolves @copilotkit/channels-intelligence to
0.2.0 as a direct dependency
- affected package tests, publint, and attw passed; Nx marked one
initial Vue failure flaky and its unchanged rerun passed 1,071 tests
2026-07-16 10:54:37 -07:00
Sam Julien a010f33994 docs(shell-docs): add Threads overview (#5947)
## Summary

- turn `/threads` into a product-oriented overview that explains why
developers use CopilotKit Threads and routes them by job to Drawer,
Headless, import, architecture, and deployment docs
- label the overview as `Overview` in the Threads navigation while
retaining `Threads` as the page title
- move the existing custom UI implementation guide to
`/headless-threads` across root, generated, authored, and Built-in
framework surfaces
- present the architecture as a product-to-system story: what users
experience, the UI/runtime/agent pieces in the app, and Enterprise
Intelligence as the cloud-hosted or self-hosted Threads platform
- provide responsive desktop and mobile diagram assets in light and dark
modes, showing durable history, replay to live, realtime sync,
lifecycle, and locking
- move `Threads & Persistence Architecture` into the Threads navigation
group across all framework modes
- replace the standalone migration CTA with contextual prose that leads
naturally to `Import Thread History`
- replace ambiguous linked-card layouts with a comparison table,
explicit action links, and a conventional next-steps list
- migrate implementation-intent links to `/headless-threads` while
keeping product-level links on `/threads`
- correct the ADK Vertex importer project-variable reference
- add regression coverage for route availability, nav labels/order,
page-title separation, shared architecture placement, and
framework-aware link rewriting

## Authoring surfaces

- **Shared/root:** `src/content/docs/{threads,headless-threads}.mdx`,
shared overview and headless snippets, root `meta.json`, and responsive
light/dark diagram assets
- **Authored frameworks:** integration wrappers and navigation metadata;
shared navigation logic inserts the architecture page into each Threads
group
- **Generated frameworks:** shared root routes, snippets, and root
navigation; generated data files are not hand-edited
- **Built-in Agent:** authored wrapper plus the same shared navigation
injection
- **Cross-links/reference:** Drawer, import, CLI, architecture,
tutorials, and `useThreads` reference pages

## Routing and redirects

No redirect is added for the old `/threads` implementation URL because
`/threads` is intentionally reused by the new overview. Existing
external links to `/threads` now land on the product overview, and
internal links that specifically mean the custom `useThreads`
implementation guide have moved to `/headless-threads`. Framework-aware
link rewriting scopes both routes normally.

## Base

This PR targets `main` after #5915 merged. Its diff contains only the
Threads overview follow-up commits.

## Validation

- `npm run lint` (passes with existing repository warnings only)
- `npm run test` (32 files, 170 tests)
- `npm run typecheck`
- `npm run build` (214 static pages generated; existing Turbopack
tracing warning only)
- `git diff --check`
- SVG XML validation for both architecture assets
- live browser checks on root, Mastra, LangGraph Python, and Built-in
Agent routes
- light/dark diagram rendering and narrow/desktop layout passes
2026-07-16 10:52:17 -07:00
Martha Schumann 9e9ce128dd test(runtime): verify packed managed channels dependency 2026-07-16 10:44:10 -07:00
Martha Schumann e84eceffbf fix(release): exempt Channels transitive packages from age gate 2026-07-16 10:35:14 -07:00
Martha Schumann fea464de52 fix(runtime): install managed channels activation dependency 2026-07-16 10:27:31 -07:00
Jordan Ritter 8a0b829732 fix(showcase): harden async-wedge repro generator lane against false-green 2026-07-16 00:06:41 -07:00
github-actions[bot] 518cb42ef3 style: auto-fix formatting 2026-07-16 06:43:01 +00:00
Jordan Ritter c5695baf2d fix(showcase): offload sync anthropic LLM calls off the claude-sdk-python event loop
The claude-sdk-python agent :8000 wedges under D6/LLM load: two synchronous
anthropic.Anthropic().messages.create() calls run directly on the uvicorn
asyncio event loop, freezing it for the full LLM round-trip so /health stops
responding. The watchdog counts 3 consecutive failures (~90s) and kill-restarts
the container, dropping active sessions.

Root cause (sync-in-async), all in integrations/claude-sdk-python/:
- src/agents/agent.py: _execute_tool's generate_a2ui branch builds a sync
  anthropic.Anthropic() and calls messages.create() synchronously; invoked on
  the loop from run_agent's agentic loop AND from the Claude-Agent-SDK MCP tool
  handler in claude_agent_sdk_adapter.py.
- src/agents/a2ui_dynamic.py: _generate_a2ui, same sync pattern, invoked on the
  loop from the run_a2ui_dynamic_agent generator.

Fix: wrap every async call site in `await asyncio.to_thread(...)` (lowest blast
radius — the sync functions and the shared ExecuteTool callback type are
unchanged, and the whole tool-dispatch path is fixed uniformly, not just
generate_a2ui):
- agent.py run_agent call site
- claude_agent_sdk_adapter.py MCP tool handler
- a2ui_dynamic.py secondary call site

Blast radius: claude-sdk-python only. a2ui_dynamic.py is per-integration (each
framework has its own copy); every other claude-sdk-python agent already uses
AsyncAnthropic. The `tools` symlink to shared/python was not touched.

entrypoint.sh: drop the Slack alert from the :8000 agent watchdog branch (keep
the kill-restart — it self-heals silently now that the root cause is fixed);
keep the LOUD #oss-alerts page on the public $PORT /api/health branch.

Adds showcase/tests/repro/async-wedge/ — a faithful RED/GREEN harness driving
the real anthropic sync client against a controllable slow endpoint, plus a
mutation guard on the real _generate_a2ui.
2026-07-15 23:40:48 -07:00
Sam Julien e4db18b718 docs(shell-docs): refine Threads overview screenshot 2026-07-15 17:40:18 -07:00
Jordan Ritter 7abcd216dc fix(showcase): stop the claude-sdk-python stdout-backpressure event-loop wedge (#5987)
## What happened

The `claude-sdk-python` showcase column went fully red on **staging**
(37 cells, `BE ✗` cascading) while prod and the TypeScript sibling
stayed green on the same image. It wasn't 37 bugs — it was one wedged
replica.

**Root cause:** a D6 fan-out pushed the container's combined log volume
past Railway's ~500 logs/sec drain cap. `entrypoint.sh` pipes both
processes' stdout through an `awk` process-substitution, so Next.js's
`fd1` is a **synchronous pipe**; when Railway stopped draining, the pipe
filled and Next.js's next `console.log` blocked in `write(2)`,
**freezing the event loop**. Even the static `/api/health` went 502,
CPU→0, memory flat, process alive — so `restartPolicyType: ON_FAILURE`
never fired and `numReplicas: 1` meant one wedge reds the whole column.
Load-triggered, not a code/env diff.

Full analysis (root cause + mitigation trade-offs): Notion → *Showcase
stdout-backpressure wedge* under Plans / Proposals.

## The fix (two coordinated MUSTs)

Showcase's promise is preserved throughout — real D6 traffic, live page,
and full CVDIAG diagnostics all intact. Nothing is sampled or dropped.

- **MUST-1 — take the flood off stdout, losslessly.** Route CVDIAG's
per-LLM-call breadcrumb off stdout behind a new `CVDIAG_LOG_STDOUT` gate
(**default ON**, so every other integration is byte-for-byte unchanged);
the non-blocking PocketBase sink still receives every envelope at full
fidelity — which is the path `cvdiag classify` and the dashboard already
read from. `emit_cvdiag` now enqueues to PB **before** the stdout write
so a wedged fd1 can't cost the durable breadcrumb. `entrypoint.sh`
self-activates `CVDIAG_LOG_STDOUT=0` only when `CVDIAG_PB_URL` is wired
(safe: never silences stdout when PB isn't receiving). Plus uvicorn
`--no-access-log` to drop the access-log noise.
- **MUST-2 — detect and recover the hang, loudly.** Extend the watchdog
to poll the public `$PORT /api/health` and, on sustained failure, POST a
`#oss-alerts` Slack alert **before** kill-restarting — and add the same
alert to the agent-`:8000` branch. No silent recovery: every wedge
pages.

## Red → green proof

A docker `node:22-slim` repro (`showcase/tests/repro/stdout-wedge/`)
reproduces the real topology (same `awk` pipe, Railway-capped drain
reader, uvicorn+CVDIAG-shaped flood, static no-log `/api/health`
victim):

- **RED** (unmitigated): `/api/health` 200 → 502/timeout the instant the
flood crosses the cap; heartbeat frozen; CPU parked at 0. `200=6 /
WEDGE=11`.
- **GREEN** (flood cut below cap): health stays fast-200 across the
whole window; heartbeat advances. `200=16 / WEDGE=0`.
- `run.sh` asserts the outcome and **exits non-zero on a false result**
(a deliberately-forced false-GREEN exits 5, was exit 0 pre-fix).
- MUST-2: the actual `entrypoint.sh` watchdog loop, run verbatim against
a genuinely wedged port, detects → POSTs the captured alert → `kill -9`s
the real Next.js PID. `watchdog.sh` needle-anchors the public
probe/kill/alert against `entrypoint.sh` (mutating the probe fails the
test).
- Unit: `test_cvdiag_log_stdout_gate.py` — 5/5 incl. a hostile-stdout
durability test (RED: enqueue starved; GREEN: enqueue preserved). Full
`_shared` suite 19 passed / 2 skipped.

## Review

Tier-3 cr-loop (shared source + deploy config + kill path): 7-agent
review → adversarial per-finding verification → 5 mandatory fixes (all
red-green'd, isolated worktrees) → 7-agent confirmation round converged
to **zero mandatory findings** → bucket-(c) promotion audit
`PROMOTE_TO_A: 0`.

## ⚠️ Not merge-ready yet — draft on purpose

- [ ] **Staging branch-deploy validation** — local can't exercise the
one runtime unknown: that the container actually reaches
`showcase-pocketbase.railway.internal:8090` and lands a `cvdiag_events`
row. Must confirm on a staging deploy of this branch before merge.
- [ ] **Railway env wiring (out-of-band, human-gated):** set on
`showcase-claude-sdk-python` (staging + prod) —
`CVDIAG_BACKEND_EMITTER=1`,
`CVDIAG_PB_URL=http://showcase-pocketbase.railway.internal:8090`,
`CVDIAG_WRITER_KEY` (op:// in DevOps `showcase`),
`SLACK_WEBHOOK_OSS_ALERTS`. Without these MUST-1/MUST-2 stay inert
(safe: default is current behavior).
- [ ] Green CI.

## Follow-up (non-blocking, fail-safe)

Repro-harness polish (none false-GREEN-capable): GREEN heartbeat
assertion is timing-fragile under a raised start-delay (false-RED only);
`run.sh` local lane doesn't forward all tunables; `watchdog.sh`
webhook-assert race + helper-death flake; reader has no close handler on
the local lane. Pre-existing (bucket c): `OPENAI_API_KEY` warning is
mislabeled; Next.js has no readiness gate; `_SETUP_DONE` degrade-latch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-15 16:05:21 -07:00
Tyler Slaton 5c36df2099 chore: release monorepo v1.63.0 (#5992)
## Release monorepo v1.63.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.63.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.63.0`
   - Creates git tag `monorepo/v1.63.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.
v1.63.0
2026-07-15 15:27:42 -07:00
tylerslaton 6c354037fc chore: release monorepo v1.63.0 2026-07-15 22:18:07 +00:00
Tyler Slaton 5fa4f2813d fix(release): unblock Channels umbrella publish (#5990)
## Problem

The Channels v0.2.0 release publishes its dependency packages, then
fails before publishing the umbrella package because pnpm rejects the
freshly published dependencies under the 24-hour minimum release age
policy.

## Why

The registry-backed verifier installs the packed umbrella in an isolated
temporary consumer without carrying a Channels-family release-age
exemption into that consumer.

## Fix

Generate a pnpm workspace config for the isolated consumer that exempts
every Channels package while retaining the maturity gate for unrelated
dependencies. Add regression coverage for the complete family. Verified
with all 109 release-script tests, the registry-backed Channels
verifier, explicit lint/typecheck/format checks, and the full Nx package
build.
channels/v0.2.0
2026-07-15 14:26:45 -07:00
Tyler Slaton a5387831c0 fix(release): exempt Channels verifier from maturity gate 2026-07-15 14:11:44 -07:00
Tyler Slaton 45fa204843 chore: release channels v0.2.0 (#5989)
## Release channels v0.2.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.2.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.2.0`
   - Creates git tag `channels/v0.2.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.
2026-07-15 13:38:33 -07:00