Files
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
..
2026-07-17 09:13:35 +00:00
2026-07-16 18:23:58 +00:00
2026-07-16 18:23:58 +00:00
2026-07-16 18:23:58 +00:00
2026-07-16 18:23:58 +00:00
2026-07-16 18:23:58 +00:00
2026-07-16 18:23:58 +00:00
2026-07-16 18:23:58 +00:00