MCPcopy
hub / github.com/CopilotKit/CopilotKit / SlackConversationStore

Class SlackConversationStore

packages/bot-slack/src/conversation-store.ts:36–230  ·  view source on GitHub ↗

Source from the content-addressed store, hash-verified

34 * Slack lookup that already produces the answer.
35 */
36export class SlackConversationStore {
37 private readonly client: WebClient;
38 private readonly botUserId: string;
39 /** Bot token used to download uploaded files from their `url_private`. */
40 private readonly botToken: string;
41 private readonly filesConfig: FileDeliveryConfig;
42 /** Stable threadIds → conversation keys ("channelId::scope"). */
43 private readonly participated = new Set<string>();
44
45 constructor(args: {
46 client: WebClient;
47 botUserId: string;
48 botToken: string;
49 files?: FileDeliveryConfig;
50 }) {
51 this.client = args.client;
52 this.botUserId = args.botUserId;
53 this.botToken = args.botToken;
54 this.filesConfig = args.files ?? {};
55 }
56
57 private keyOf(k: ConversationKey): string {
58 return `${k.channelId}::${k.scope}`;
59 }
60
61 /**
62 * A *fresh* AG-UI threadId per turn.
63 *
64 * We deliberately do NOT reuse a stable per-conversation threadId.
65 * Slack is our durable history (every turn is rebuilt from it via
66 * {@link fetchHistory}), so the LangGraph thread only needs to live for
67 * the duration of one turn. Reusing a stable threadId across turns lets
68 * the server-side thread accumulate the agent's *internal* messages
69 * (tool calls/results that never round-trip through Slack); on the next
70 * turn `@ag-ui/langgraph` regenerates state and the now-larger existing
71 * thread no longer matches the incoming history, surfacing as a
72 * "Message not found" failure. A unique thread per turn sidesteps that
73 * entirely. Restart-recovery for interrupts still works because the
74 * picker carries its turn's threadId in Slack message metadata (see
75 * `recoverFromStaleClick`).
76 */
77 private newThreadId(k: ConversationKey): string {
78 return `slack-${k.channelId}-${k.scope}-${randomUUID()}`;
79 }
80
81 /**
82 * Does the bot own this thread? "Ownership" means the bot has at least
83 * one prior reply in it (which is the natural definition since the bot
84 * only ever replies when @-mentioned or in a thread it already owns).
85 *
86 * Cached in-process; the first call after a restart is one Slack API
87 * round-trip, subsequent calls are O(1).
88 */
89 async has(key: ConversationKey): Promise<boolean> {
90 if (this.participated.has(this.keyOf(key))) return true;
91 // DM "scope" is a sentinel — no Slack thread to query. We treat the
92 // listener-level DM gate as authoritative; DMs always go through.
93 if (key.scope === DM_SCOPE) return false;

Callers

nothing calls this directly

Calls

no outgoing calls

Tested by

no test coverage detected

Used in the wild real call sites across dependent graphs

searching dependent graphs…