feat(channel): migrate Intelligence channel host to CopilotKit v2 managed-channels (channels 0.2) - #10
feat(channel): migrate Intelligence channel host to CopilotKit v2 managed-channels (channels 0.2)#10jerelvelarde wants to merge 12 commits into
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…API) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nel rename set) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…te-opentag) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…managed-channels runtime
- app/index.ts: self-hosted createBot->createChannel (behavior unchanged)
- app/managed.ts: rewrite to createChannel + CopilotRuntime({channels}) + createCopilotNodeListener; drop startChannelsOverRealtimeGateway + org/project/channel-id env
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…doc/comment cleanup Now transitive via @copilotkit/channels + @copilotkit/runtime; v2 managed.ts no longer imports it directly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
managed.ts: await channels.ready() + exit-1 on activation reject; server 'error' handler; blank INTELLIGENCE_CHANNEL_NAME normalized; extension-agnostic entry guard. IaC/docs: INTELLIGENCE_API_URL/GATEWAY_WS_URL now preserve() (drift + dev-leak); .railway added to tsconfig. commands: shared safely()/safePost() guards for bare postEphemeral/openModal/post. HITL: confirm_write detail capped; file-issue log prefix. Slack char-budget clamps: issue-list count, showcase fields/links, render-table monospace fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…essions showcase-tools: clampWithinBudget keeps >=1 item (show_links empty-Section crash on a single over-budget link); show_incident summary .min(1) + Section guarded; MAX_LINKS budget 3000->2800 (margin parity + mrkdwn escape-expansion). render-table: sanitize embedded newlines in monospace fallback (fixes column alignment + split-based shownRows accounting). Comment accuracy: managed.ts brain framing (agent/ ships), tools/index tool-order, HITL 'later wave' marker, .railway port-pinning invariant scope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t/diagram rendering The channel host runs the Playwright-based render_chart/render_diagram tools; the IaC never installed Chromium, so those failed on the one-click deploy. Add a build step (playwright install --with-deps chromium), PLAYWRIGHT_BROWSERS_PATH=0 so the browser ships in node_modules, and RAILPACK_DEPLOY_APT_PACKAGES for the runtime libs — mirrors the kite reference deploy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewing PR #8 (feat/intelligence-adapter) alongside this branch surfaced three defects on the managed path. All three are things the un-run live gateway check would have caught. 1. Dropped instruction on attachment turns. `promptFromMessage` returned `contentParts ?? text`, but channels-core's `msgFromTurn` keeps the turn's text and its attachment parts in SEPARATE fields — parts never fold in the instruction. So "chart this" + a CSV reached the model as a bare data dump and came back "what would you like me to do?". Lead with the instruction, then the parts. PR #8 hit and fixed this on the sibling transport; the fix is ported from there. A test asserted the broken shape — replaced. 2. Unregistered interactive components. Nothing was passed to `createChannel({ components })`, so on the managed path — where a click arrives as a fresh delivery, with no in-process handler cache — a click cannot be resolved and dead-letters. That silently strands `ConfirmWrite`, the HITL gate on every write. Register it plus `IncidentCard`, and add a source-scan test so a future interactive card cannot be forgotten. 3. Unauthenticated HTTP surface. `createCopilotNodeListener` publishes the whole v2 route set (agent/:id/run, threads/*, memories/*, transcribe) with no auth, and this host registers `agents.kite` pointed at AGENT_URL — an open proxy to the brain and to thread history for anyone who can reach the port. Gate it via `hooks.onRequest`: closed (404) unless CHANNEL_HTTP_TOKEN is set, bearer-gated (401) when it is, constant-time compare. Verified against a real listener: 404/404/404 closed, 401/401/pass gated. pnpm check-types: 0 errors. pnpm test: 122 passing (was 116). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`.migration-discovery.md` was a scratch artifact sitting at the repo root, ungitignored, and contradicted by the PR that carried it: it records the runtime floor staying at ^1.62.3 and channels-intelligence as a direct dep, while the shipped branch raises the floor to ^1.63.2 and drops that dep. Its 571 lines of verified SDK signatures are worth keeping, so move it beside the design doc and plan under docs/superpowers/specs/ (the convention PRs #6 and #7 established) and head it with a note marking it a point-in-time record plus the two places the implementation deliberately diverged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review — v2 managed-channels migrationScope: the PR diff only (38 files, +2659/−442). I verified the SDK contracts this PR depends on against the published OverviewReplaces the deprecated The migration itself is sound. Confirmed against the SDK types that Blocking: the success log can still lie
Confirmed in await listener.channels?.ready?.({ timeoutMs: 30_000 });
const health = listener.channels?.status();
if (health && health.overall !== "online") {
console.error(`[channel] channel not live: ${JSON.stringify(health.channels)}`);
process.exit(1);
}Should fix
Nits
What's goodThe HTTP auth gate is the strongest part of the PR: default-404 (not 403) is the right posture for a surface nobody should be probing, the On the #8 overlap: that is a coordination call, not a code question — but this is the branch based on current |
Summary
Migrates OpenTag's Intelligence channel host (
app/managed.ts,pnpm channel) from the deprecated@copilotkit/channels-intelligencegateway launcher to the v2 managed-channels runtime, so it connects to a current CopilotKit Intelligence project (opentag-dev/ channelkite-opentag).The live Intelligence dashboard documents a runtime shape OpenTag didn't ship:
@copilotkit/runtime/v2(CopilotRuntime,CopilotKitIntelligence) +@copilotkit/channels(createChannel) +@copilotkit/runtime/v2/node(createCopilotNodeListener), with the channel declared by name and env reduced toINTELLIGENCE_API_URL+INTELLIGENCE_GATEWAY_WS_URL+INTELLIGENCE_API_KEY— no org/project/channel IDs (the runtime derives them from the credentials + channel name).The SDK typings back this shape:
createChannel({ provider })is documented as "the managed delivery provider a no-adapter Channel targets when activated via CopilotKit Intelligence." This PR builds exactly that — a channel with noadapters, activated when the node listener mounts.#8 (
feat/intelligence-adapter) is a second, independent implementation of this same migration. Both bump@copilotkit/channels*to0.2.1and rewriteapp/managed.ts. They conflict in 22 files with each other; #8 conflicts in ~15 withmain.#8 branches from
df93bc0, which predates both merged PRs #6 (Intelligence gateway + Pythonagent/) and #7 (Railway IaC) — so it has no knowledge ofagent/,.railway/, or thepnpm channel/pnpm agentscripts, and itspackage.jsondeletes the latter two.createChannel({ adapters: [intelligenceAdapter()] })CopilotRuntime({ channels })channel.start()COPILOTKIT_INTELLIGENCE_URL,COPILOTKIT_API_KEYINTELLIGENCE_API_URL/_GATEWAY_WS_URL/_API_KEYproviderdefaults to slack)df93bc0(pre-#6, pre-#7)mainBoth APIs still exist in 0.2.1, so neither is wrong — but #8's own
MANAGED.mddocuments it against a feature-flagged Intelligence branch (codex/hosted-bots-infra-vertical-slice,FF_MANAGED_BOTS=true), and the SDK calls that path "V1 Intelligence Channel exclusivity."#8 has real end-to-end Teams + Slack verification that this PR does not, and that's the one thing that can't be cherry-picked. Worth a conversation with @AlemTuzlak before either lands. Suggested split: land this as the base, then port #8's genuinely better pieces (listed at the bottom).
What changed
Dependencies
@copilotkit/channels*^0.1.1→^0.2.1;@copilotkit/runtimefloor^1.62.3→^1.63.2(where the v2channelsAPI +createCopilotNodeListenership).@copilotkit/channels-intelligencedep (still present transitively).Code
app/**migrated to the 0.2 API — mechanical rename:createBot→createChannel,BotTool→ChannelTool,BotCommand→ChannelCommand,defineBotTool→defineChannelTool,defineBotCommand→defineChannelCommand,Bot→Channel,BotNode→ChannelNode. Behavior unchanged.app/index.ts(self-hosted) migrated tocreateChannel; per-platform gating / handlers / shutdown preserved.app/managed.tsrewritten to the v2 managed-channels runtime:createChannel({ name: "kite-opentag" })→new CopilotRuntime({ agents, intelligence, identifyUser, channels })→createCopilotNodeListener; awaitslistener.channels.ready()(fail-loud, exit non-zero on activation error), adds a bind-errorhandler, and normalizes blank env.Config / docs
.env.example,.railway/railway.ts,README.md,setup.mdupdated to the v2 var set + runtime model..railway/railway.ts:INTELLIGENCE_API_URL/_GATEWAY_WS_URLare nowpreserve()(deployer-set, not clobbered byrailway config apply); thechannelservice installs Chromium (playwright install --with-deps chromium+PLAYWRIGHT_BROWSERS_PATH=0+ runtime apt libs) so inline chart/diagram rendering works on deploy;.railway/**added tocheck-types.Suggested review order
app/managed.ts— the only genuinely new file. Everything else is either mechanical rename or config.app/managed.test.ts— covers the three defects below..railway/railway.ts+.env.example— the deploy contract.git log -pper commit is cleaner than the squashed diff.Review history
Two rounds of unbiased 7-agent review with fix cycles, then a cross-review against #8 that caught three more defects this branch had on its own — all with covering red-green tests.
Caught by the earlier rounds:
managed.tsfail-loud regression — success was logged even when gateway activation failed.<Section>crashes (show_linkssingle over-budget link;show_incidentempty summary) and a monospace-fallback mis-count when a table cell contains a newline.Caught by the cross-review against #8 (commit
7f40b1d) — all three are things the un-run live check would have surfaced:promptFromMessagereturnedcontentParts ?? text, but channels-core'smsgFromTurnkeeps the turn's text and its attachment parts in separate fields — parts never fold in the instruction. "Chart this" + a CSV reached the model as a bare data dump. Fix ported from feat: managed (Intelligence-hosted) channel bot — Slack + Teams via @copilotkit/channels 0.2.1 #8, which hit this first on the sibling transport. A test asserted the broken shape; replaced.createChannel({ components }), so on the managed path — where a click arrives as a fresh delivery with no in-process handler cache — clicks can't resolve and dead-letter. That silently strandedConfirmWrite, the HITL gate on every write. Now registers it plusIncidentCard, with a source-scan test so a future interactive card can't be forgotten.createCopilotNodeListenerpublishes the whole v2 route set —agent/:id/run,threads/list,threads/messages,memories/*,transcribe— with no auth, and this host registersagents.kitepointed atAGENT_URL: an open proxy to the brain and to thread history for anyone who can reach the port. Now gated viahooks.onRequest— 404 whenCHANNEL_HTTP_TOKENis unset (default), 401 on missing/wrong bearer when it is, constant-time compare. Verified against a real listener:404/404/404closed,401/401/passgated.Testing
pnpm check-types: 0 errors (now includes.railway/).pnpm test: 122 passing (12 files), incl. red-green tests for every behavioral fix.INTELLIGENCE_*, runpnpm agent+pnpm channelagainstopentag-dev, confirmkite-opentagflips Waiting for runtime → live, then upload a CSV with "chart this" (exercises fix 1) and click through aconfirm_writegate (exercises fix 2). This PR does not deploy the channel to Railway.Deferred — pre-existing, unrelated to this migration
Kept out to preserve reviewability; filed as follow-ups.
app/index.ts: partial platform creds silently skipped + misleading "No platform secrets found"; whitespace-onlyPORTcrashes (vsmanaged.ts, hardened here).confirm-write-tool.tsx:awaitChoicerejection unwrapped (gate stays safe; error opaque).page-list.tsx: uncapped title/snippet (silent-truncation risk).render-chart/render-diagram:(e as Error).message→ "undefined" on non-Error throw; caption uses*x*(italic) not**x**(bold); misleading status when only the upload throws.issue-card/issue-list:.slice()can split an astral-plane character..env.example: presence-gated secrets ship non-blank placeholders → a copy-paste.envtreats them as configured..railway/railway.ts: builder pinning inconsistent across services.vitest.config.ts: dead esbuild JSX block — confirmed live, the run printsBoth esbuild and oxc options were set… esbuild options will be ignored.confirm_writebut docstring/setup imply a universal gate;NOTION_MCP_HOSTundocumented;render-tools.test.tsmisnamed; deadfindByTypehelper; imprecise "≤5 inputs" title.Worth porting from #8 afterwards
process.exit(0)immediately afterprocess.stdout.write(png)can truncate the PNG on a pipe, and the parent resolves that buffer with no length check; and theexecFiletimeout SIGTERMs the worker without a handler, sobrowser.close()never runs and Chromium orphans.render_chartschema coercions (numeric labels, quoted numbers, tolerantoptions).emoji: "refresh"on every provider).runner: new InMemoryAgentRunner({ onConcurrentRun: "supersede" })inruntime.ts.createChannel({ provider: "teams" }).Design + plan + discovery:
docs/superpowers/specs/2026-07-24-opentag-intelligence-v2-migration-design.md,docs/superpowers/plans/2026-07-24-opentag-intelligence-v2-migration.md,docs/superpowers/specs/2026-07-24-opentag-intelligence-v2-migration-discovery.md.🤖 Generated with Claude Code