Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
2dcfc25
ci(showcase): guard against dead-on-load demos (runtime-route wiring …
contextablemark Jul 8, 2026
57b6b17
feat(web-inspector): add threads example telemetry
samjulien Jul 9, 2026
962132e
feat(web-inspector): add threads empty-state onboarding
samjulien Jul 9, 2026
ecfc635
style: auto-fix formatting
github-actions[bot] Jul 9, 2026
6db81b8
Merge branch 'main' into mark/oss-451-showcase-route-wiring-guard
contextablemark Jul 10, 2026
8ed045e
ci(showcase): guard against dead-on-load demos (runtime-route wiring …
contextablemark Jul 10, 2026
786d081
feat(channels-intelligence): managed-over-Phoenix launcher + slack ma…
BenTaylorDev Jul 9, 2026
f63578d
style: auto-fix formatting
github-actions[bot] Jul 9, 2026
7207332
fix(examples/slack): managed mode must pass the current message as pr…
BenTaylorDev Jul 9, 2026
f35f282
docs(channels-intelligence): drop deprecated 'coworker' term from lau…
BenTaylorDev Jul 9, 2026
fec701f
ci(showcase): raise shell-script-tests timeout to 10m to stop timeout…
BenTaylorDev Jul 9, 2026
df7d3e3
fix(channels-intelligence): fail-fast bot names + richer activation j…
BenTaylorDev Jul 10, 2026
1441768
style: auto-fix formatting
github-actions[bot] Jul 10, 2026
852bd04
fix(channels-intelligence): single-bot guard + startup/join socket cl…
BenTaylorDev Jul 10, 2026
2851d83
style: auto-fix formatting
github-actions[bot] Jul 10, 2026
3c1b518
feat(web-inspector): add threads onboarding motion cues
samjulien Jul 10, 2026
43fa5be
style: auto-fix formatting
github-actions[bot] Jul 10, 2026
b2dda71
fix(web-inspector): remove threads nav sheen
samjulien Jul 10, 2026
2a16bec
fix(channels-intelligence): claim deliveries provider-agnostically
Jul 10, 2026
bfa9df0
fix(web-inspector): use CDN threads overview video
samjulien Jul 10, 2026
57ddcb9
fix(channels-intelligence): enforce runtimeInstanceId on OnChannel + …
BenTaylorDev Jul 10, 2026
779bb3d
fix(examples/slack): exit nonzero when managed shutdown fails
tylerslaton Jul 10, 2026
4de0057
feat(channels-intelligence): managed-over-Phoenix launcher + slack ma…
tylerslaton Jul 10, 2026
81c1740
fix(web-inspector): add inspector utm attribution
samjulien Jul 10, 2026
feffbac
style: auto-fix formatting
github-actions[bot] Jul 10, 2026
852d41e
fix(web-inspector): scope threads utm links
samjulien Jul 10, 2026
e245a99
fix(web-inspector): use operations sign-in for threads signup
samjulien Jul 10, 2026
b11fd7d
fix(web-inspector): remove threads cta utms
samjulien Jul 10, 2026
556ba9d
style: auto-fix formatting
github-actions[bot] Jul 10, 2026
150164a
fix(channels-intelligence): derive conversationKey per provider (Team…
BenTaylorDev Jul 10, 2026
5c21753
fix(channels-intelligence): claim deliveries provider-agnostically (#…
tylerslaton Jul 10, 2026
67fce71
refactor(channels-intelligence): migrate HTTP contract to channels
tylerslaton Jul 10, 2026
21d3b8c
Merge remote-tracking branch 'origin/main' into update-intelligence-c…
tylerslaton Jul 10, 2026
4dddabd
test(channels-intelligence): align claim test with provider-agnostic …
tylerslaton Jul 10, 2026
ea9910a
refactor(channels-intelligence): introduce realtime gateway abstraction
tylerslaton Jul 10, 2026
b9091c4
fix(web-inspector): address threads onboarding review
samjulien Jul 10, 2026
68e43fe
refactor(channels-intelligence): rename remaining channel APIs
tylerslaton Jul 10, 2026
4970f55
Add Inspector Threads empty-state onboarding (#5909)
BenTaylorDev Jul 10, 2026
001dda5
chore(channels-intelligence): complete channel terminology sweep
tylerslaton Jul 10, 2026
77d43dc
refactor(channels-intelligence): migrate to Channels API (#5916)
BenTaylorDev Jul 10, 2026
ea50654
chore: release channels v0.1.1
tylerslaton Jul 10, 2026
ceae64c
chore: release channels-discord v0.0.3
tylerslaton Jul 10, 2026
f4e19e2
chore: release channels-intelligence v0.1.1
tylerslaton Jul 10, 2026
4ce3124
chore: release channels-slack v0.1.2
tylerslaton Jul 10, 2026
2893fee
chore: release channels-teams v0.1.2
tylerslaton Jul 10, 2026
6281bea
chore: release channels-telegram v0.0.4
tylerslaton Jul 10, 2026
4c04e80
chore: release channels-whatsapp v0.0.2
tylerslaton Jul 10, 2026
5e389aa
chore: release channels v0.1.1 (#5918)
tylerslaton Jul 10, 2026
8427fa1
chore: release channels-discord v0.0.3 (#5919)
tylerslaton Jul 10, 2026
cb1fd90
chore: release channels-slack v0.1.2 (#5922)
tylerslaton Jul 10, 2026
e99e1dc
chore: release channels-teams v0.1.2 (#5921)
tylerslaton Jul 10, 2026
6f03831
chore: release channels-intelligence v0.1.1 (#5920)
tylerslaton Jul 10, 2026
41caca8
chore: release channels-telegram v0.0.4 (#5923)
tylerslaton Jul 10, 2026
87db1b0
chore: release channels-whatsapp v0.0.2 (#5924)
tylerslaton Jul 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/showcase_validate-wiring.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: "Showcase: Runtime-Route Wiring (PR)"

# Pre-merge guard for the OSS-451 failure class: a demo page whose CopilotKit
# `runtimeUrl` points at an `/api/copilotkit-<demo>` route that does not exist,
# so the page 404s on load (runtime_info_fetch_failed) and never mounts.
#
# The existing Showcase Build Check (showcase_build_check.yml) is a Docker
# *build* — it compiles a page that references a non-existent route just fine,
# because `runtimeUrl` is an opaque string with no build-time link to the
# route's existence. That blind spot is exactly how OSS-451 shipped. This
# static check runs alongside the build check and asserts that every SHIPPED
# demo (a demo listed in its integration's manifest `features`) wires its
# `runtimeUrl` to a route that actually exists.
#
# Fast, no Docker, no secrets. Should be added to the branch-protection
# required checks so it blocks merge like the build check does.

on:
pull_request:
paths:
- "showcase/integrations/**"
- "showcase/scripts/validate-runtime-routes.ts"
- "showcase/scripts/validate-runtime-routes.baseline.json"
- "showcase/scripts/package.json"
- ".github/workflows/showcase_validate-wiring.yml"

concurrency:
group: showcase-validate-wiring-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read

jobs:
validate-runtime-routes:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"

- name: Install validator deps
working-directory: showcase/scripts
run: npm ci --no-audit --no-fund --ignore-scripts

- name: Validate runtime-route wiring (OSS-451 guard)
working-directory: showcase/scripts
run: npm run validate-routes
5 changes: 4 additions & 1 deletion .github/workflows/showcase_validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,10 @@ jobs:
# These tests gate the promote-fleet.sh best-effort loop + succeeded_csv
# export that the promote → verify-prod handoff depends on.
runs-on: ubuntu-latest
timeout-minutes: 5
# The bats suite runs ~4.5min and keeps growing; at the old 5min job cap it
# raced the deadline and intermittently got cancelled mid-suite (all steps
# passing) rather than reported. Give headroom so a green suite reports green.
timeout-minutes: 10
permissions:
contents: read
steps:
Expand Down
19 changes: 19 additions & 0 deletions examples/slack/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,25 @@ OPENAI_API_KEY=sk-...
# ANTHROPIC_API_KEY=sk-ant-...
# GOOGLE_API_KEY=...

# ── Intelligence Channel mode — app/managed.ts (`pnpm channel`) ──────────
# The Channel variant runs the SAME bot over the Intelligence realtime gateway
# instead of a native adapter — it holds NO Slack tokens (Intelligence owns the
# Slack edge). Set these to run `pnpm channel`; the native `pnpm dev` ignores
# them. The agent brain reuses AGENT_URL / AGENT_AUTH_HEADER above.
# Gateway runner WebSocket URL (the /runner socket hosting the bot-IO channel).
# INTELLIGENCE_GATEWAY_WS_URL=wss://gateway.intelligence.example/runner
# Project runtime API key (cpk-…) — presented as the socket authToken.
# INTELLIGENCE_API_KEY=cpk-...
# Product organization id (as issued by Intelligence).
# INTELLIGENCE_ORG_ID=org_...
# Numeric project id (the channel topic is channels:project:<id>).
# INTELLIGENCE_PROJECT_ID=123
# Channel id + immutable project-unique channel name (from Intelligence Channel setup).
# INTELLIGENCE_CHANNEL_ID=channel_...
# INTELLIGENCE_CHANNEL_NAME=triage
# Optional: stable runtime instance id; a random rti_… is generated if unset.
# INTELLIGENCE_RUNTIME_INSTANCE_ID=rti_...

# ── Linear MCP ──────────────────────────────────────────────────────────
# The hosted Linear MCP accepts a raw API key as a bearer token (no OAuth
# dance). Create one at linear.app → Settings → API → Personal API keys.
Expand Down
102 changes: 102 additions & 0 deletions examples/slack/app/managed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { afterEach, describe, expect, it, vi } from "vitest";

const fakes = vi.hoisted(() => {
const stop = vi.fn(async () => {
throw new Error("stop failed");
});
return {
stop,
closeBrowser: vi.fn(async () => {}),
startChannelsOverRealtimeGateway: vi.fn(async () => ({ stop })),
bot: {
onMention: vi.fn(),
onModalSubmit: vi.fn(),
onThreadStarted: vi.fn(),
},
};
});

vi.mock("@copilotkit/channels", () => ({
createBot: vi.fn(() => fakes.bot),
}));
vi.mock("@copilotkit/channels-slack", () => ({
defaultSlackTools: [],
defaultSlackContext: [],
SanitizingHttpAgent: function SanitizingHttpAgent() {},
}));
vi.mock("@copilotkit/channels-intelligence", () => ({
startChannelsOverRealtimeGateway: fakes.startChannelsOverRealtimeGateway,
}));
vi.mock("./tools/index.js", () => ({ appTools: [] }));
vi.mock("./context/app-context.js", () => ({ appContext: [] }));
vi.mock("./commands/index.js", () => ({ appCommands: [] }));
vi.mock("./sender-context.js", () => ({ senderContext: vi.fn() }));
vi.mock("./modals/file-issue.js", () => ({
fileIssueSubmit: vi.fn(),
FILE_ISSUE_CALLBACK: "file-issue",
}));
vi.mock("./render/browser.js", () => ({ closeBrowser: fakes.closeBrowser }));

const envKeys = [
"AGENT_URL",
"INTELLIGENCE_PROJECT_ID",
"INTELLIGENCE_CHANNEL_NAME",
"INTELLIGENCE_GATEWAY_WS_URL",
"INTELLIGENCE_API_KEY",
"INTELLIGENCE_ORG_ID",
"INTELLIGENCE_CHANNEL_ID",
] as const;

describe("channel entrypoint shutdown", () => {
const previousEnv = new Map<string, string | undefined>();

afterEach(() => {
vi.restoreAllMocks();
for (const key of envKeys) {
const value = previousEnv.get(key);
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
previousEnv.clear();
});

it("exits nonzero when stopping the channel runtime fails", async () => {
for (const key of envKeys) previousEnv.set(key, process.env[key]);
process.env.AGENT_URL = "http://agent.test/run";
process.env.INTELLIGENCE_PROJECT_ID = "7";
process.env.INTELLIGENCE_CHANNEL_NAME = "opentag";
process.env.INTELLIGENCE_GATEWAY_WS_URL = "wss://gateway.test/runner";
process.env.INTELLIGENCE_API_KEY = "cpk-test";
process.env.INTELLIGENCE_ORG_ID = "org_1";
process.env.INTELLIGENCE_CHANNEL_ID = "channel_1";

let sigterm: (() => void) | undefined;
vi.spyOn(process, "on").mockImplementation(((event, listener) => {
if (event === "SIGTERM") sigterm = listener as () => void;
return process;
}) as typeof process.on);
const exit = vi
.spyOn(process, "exit")
.mockImplementation((() => undefined as never) as typeof process.exit);
vi.spyOn(console, "log").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});

await import("./managed.js");
await vi.waitFor(() => expect(sigterm).toBeTypeOf("function"));
expect(fakes.startChannelsOverRealtimeGateway).toHaveBeenCalledWith(
[fakes.bot],
expect.objectContaining({
scope: expect.objectContaining({
channelId: "channel_1",
channelName: "opentag",
}),
}),
);
sigterm!();
await vi.waitFor(() => expect(exit).toHaveBeenCalled());

expect(fakes.stop).toHaveBeenCalledOnce();
expect(fakes.closeBrowser).toHaveBeenCalledOnce();
expect(exit).toHaveBeenCalledWith(1);
});
});
184 changes: 184 additions & 0 deletions examples/slack/app/managed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* Intelligence Channel entrypoint for the same Slack bot as
* `app/index.ts`.
*
* `index.ts` is the SELF-HOSTED variant: it holds the Slack bot/app tokens and
* talks to Slack directly via the native `slack()` adapter. This file is the
* Channel variant: it holds no Slack credentials and no public endpoint — it
* connects to the Intelligence Realtime Gateway, receives leased
* deliveries, and streams render frames back. Intelligence owns the Slack edge
* (signed ingress → app-api, egress via the Connector Outbox).
*
* The bot itself — the agent, tools, context, commands, and turn handlers — is
* IDENTICAL to the native bot; only the transport changes. `intelligenceAdapter`
* is exclusive, so the Channel Bot is created WITHOUT a native adapter and
* {@link startChannelsOverRealtimeGateway} attaches the Channel transport.
*
* native: createBot({ adapters: [slack({ botToken, appToken }) ] }) // index.ts
* channel: startChannelsOverRealtimeGateway([ createBot({ … }) ], { … }) // this file
*
* Run: `pnpm --filter slack-example channel` with the INTELLIGENCE_* env set
* (see `.env.example`).
*/
import "dotenv/config";
import { randomUUID } from "node:crypto";
import { createBot } from "@copilotkit/channels";
import {
defaultSlackTools,
defaultSlackContext,
SanitizingHttpAgent,
} from "@copilotkit/channels-slack";
import { startChannelsOverRealtimeGateway } from "@copilotkit/channels-intelligence";
import { appTools } from "./tools/index.js";
import { appContext } from "./context/app-context.js";
import { appCommands } from "./commands/index.js";
import { senderContext } from "./sender-context.js";
import { fileIssueSubmit, FILE_ISSUE_CALLBACK } from "./modals/file-issue.js";
import { closeBrowser } from "./render/browser.js";

const required = (name: string): string => {
const v = process.env[name];
if (!v) {
console.error(`Missing required env var: ${name}`);
process.exit(1);
}
return v;
};

async function main() {
const agentUrl = required("AGENT_URL");
const agentHeaders = process.env.AGENT_AUTH_HEADER
? { Authorization: process.env.AGENT_AUTH_HEADER }
: undefined;

const projectId = Number(required("INTELLIGENCE_PROJECT_ID"));
if (!Number.isInteger(projectId) || projectId <= 0) {
console.error(
`Invalid INTELLIGENCE_PROJECT_ID: "${process.env.INTELLIGENCE_PROJECT_ID}"`,
);
process.exit(1);
}
const channelName = required("INTELLIGENCE_CHANNEL_NAME");

// Same Slack Bot as the native example, minus the adapter: the Channel transport is
// attached by startChannelsOverRealtimeGateway. Slack is the only Channel provider
// here, so it always ships the Slack tools/context (the native example adds
// these conditionally per active adapter).
const bot = createBot({
name: channelName,
agent: (threadId) => {
const a = new SanitizingHttpAgent({
url: agentUrl,
headers: agentHeaders,
});
a.threadId = threadId;
return a;
},
tools: [...appTools, ...defaultSlackTools],
context: [...appContext, ...defaultSlackContext],
commands: appCommands,
});

// Turn + feature handlers — identical to the native example (app/index.ts).
bot.onMention(async ({ thread, message }) => {
try {
// Channel history (app-api /api/channels/history) does NOT include the
// in-flight turn (unlike native adapters whose getHistory rebuilds the
// live thread), so pass the current message explicitly as `prompt` —
// otherwise runAgent runs with zero messages. Prefer multimodal parts.
await thread.runAgent({
prompt: message.contentParts?.length
? message.contentParts
: message.text,
context: senderContext(message.user, thread.platform),
});
} catch (err) {
console.error("[channel] agent run failed", err);
await thread
.post("Sorry — I hit an error handling that. Please try again.")
.catch((postErr: unknown) =>
console.error("[channel] failed to post agent error", postErr),
);
}
});
bot.onModalSubmit(FILE_ISSUE_CALLBACK, fileIssueSubmit);
bot.onThreadStarted(async ({ thread, user }) => {
if (!user?.name) return;
await thread.setSuggestedPrompts([
{
title: `Triage ${user.name}'s issues`,
message: "Triage my open issues",
},
{
title: "What shipped this week?",
message: "Summarize what shipped this week",
},
]);
});

const handle = await startChannelsOverRealtimeGateway([bot], {
wsUrl: required("INTELLIGENCE_GATEWAY_WS_URL"),
apiKey: required("INTELLIGENCE_API_KEY"),
scope: {
organizationId: required("INTELLIGENCE_ORG_ID"),
projectId,
channelId: required("INTELLIGENCE_CHANNEL_ID"),
channelName,
},
runtimeInstanceId:
process.env.INTELLIGENCE_RUNTIME_INSTANCE_ID ??
`rti_${randomUUID().replace(/-/g, "")}`,
adapter: "slack",
// DEBUG-ONLY logging. `meta` (and the raw `err` in the onMention catch
// above) can contain message content/payloads — the design says telemetry
// must not include raw message text. In production, drop this `log` or trim
// `meta` to safe fields (ids, counts) before emitting.
log: (msg, meta) => console.log(`[channel] ${msg}`, meta ?? ""),
});
console.log(
`[channel] started over Realtime Gateway as "${channelName}" on project ${projectId}`,
);

const shutdown = async (signal: string) => {
console.log(`\n[channel] received ${signal}, stopping…`);
let exitCode = 0;
try {
await handle.stop();
} catch (err) {
console.error("[channel] error stopping Channel runtime", err);
exitCode = 1;
}
// Browser teardown is best-effort, but still surface a failure rather than
// swallow it silently.
await closeBrowser().catch((err: unknown) =>
console.error(
"[channel] browser cleanup failed (continuing shutdown)",
err,
),
);
process.exit(exitCode);
};
// A failed shutdown must not vanish — log it and exit nonzero.
const runShutdown = (signal: string): void => {
shutdown(signal).catch((err: unknown) => {
console.error(`[channel] fatal during ${signal} shutdown`, err);
process.exit(1);
});
};
process.on("SIGINT", () => runShutdown("SIGINT"));
process.on("SIGTERM", () => runShutdown("SIGTERM"));
}

// Fail loud, not silent: surface any stray async error instead of letting it
// kill the process with no log (mirrors the native entrypoint).
process.on("unhandledRejection", (reason) => {
console.error("[channel] unhandledRejection:", reason);
});
process.on("uncaughtException", (err) => {
console.error("[channel] uncaughtException:", err);
});

main().catch((err: unknown) => {
console.error("[channel] fatal: failed to start Channel runtime", err);
process.exit(1);
});
2 changes: 2 additions & 0 deletions examples/slack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"scripts": {
"dev": "tsx watch app/index.ts",
"start": "tsx app/index.ts",
"channel": "tsx app/managed.ts",
"build": "pnpm exec nx run-many -t build -p \"@copilotkit/channels*\" @copilotkit/runtime",
"runtime": "tsx runtime.ts",
"notion-mcp": "tsx scripts/start-notion-mcp.ts",
Expand All @@ -20,6 +21,7 @@
"dependencies": {
"@copilotkit/channels": "workspace:*",
"@copilotkit/channels-discord": "workspace:*",
"@copilotkit/channels-intelligence": "workspace:*",
"@copilotkit/channels-slack": "workspace:*",
"@copilotkit/channels-telegram": "workspace:*",
"@copilotkit/channels-ui": "workspace:*",
Expand Down
Loading
Loading