From 63f1f1101c80fcc2b6c204b96fbe069d70b8352a Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 13 Mar 2026 11:58:22 -0500 Subject: [PATCH 1/9] feat: add Slack chat bot integration for Record Label Agent Adds a second Slack bot that runs the same AI chat generation logic as /api/chat/generate and posts responses back to Slack threads. The bot acts as a hardcoded account with conversational memory via roomId persisted in Redis thread state. Co-Authored-By: Claude Opus 4.6 --- app/api/chat/slack/route.ts | 41 +++++++++ lib/slack-chat/bot.ts | 54 ++++++++++++ .../handlers/handleSlackChatMessage.ts | 87 +++++++++++++++++++ lib/slack-chat/handlers/onNewMention.ts | 19 ++++ .../handlers/onSubscribedMessage.ts | 18 ++++ lib/slack-chat/handlers/registerHandlers.ts | 10 +++ lib/slack-chat/types.ts | 10 +++ lib/slack-chat/validateEnv.ts | 20 +++++ 8 files changed, 259 insertions(+) create mode 100644 app/api/chat/slack/route.ts create mode 100644 lib/slack-chat/bot.ts create mode 100644 lib/slack-chat/handlers/handleSlackChatMessage.ts create mode 100644 lib/slack-chat/handlers/onNewMention.ts create mode 100644 lib/slack-chat/handlers/onSubscribedMessage.ts create mode 100644 lib/slack-chat/handlers/registerHandlers.ts create mode 100644 lib/slack-chat/types.ts create mode 100644 lib/slack-chat/validateEnv.ts diff --git a/app/api/chat/slack/route.ts b/app/api/chat/slack/route.ts new file mode 100644 index 00000000..b7a867f0 --- /dev/null +++ b/app/api/chat/slack/route.ts @@ -0,0 +1,41 @@ +import type { NextRequest } from "next/server"; +import { after } from "next/server"; +import { slackChatBot } from "@/lib/slack-chat/bot"; +import "@/lib/slack-chat/handlers/registerHandlers"; + +/** + * GET /api/chat/slack + * + * Handles Slack webhook verification handshake. + * + * @param request - The incoming verification request + * @returns The webhook handler response + */ +export async function GET(request: NextRequest) { + return slackChatBot.webhooks.slack(request, { waitUntil: p => after(() => p) }); +} + +/** + * POST /api/chat/slack + * + * Webhook endpoint for the Record Label Agent Slack bot. + * Handles url_verification challenges and delegates messages to the bot. + * + * @param request - The incoming webhook request + * @returns The webhook handler response or url_verification challenge + */ +export async function POST(request: NextRequest) { + // Handle Slack url_verification challenge before loading the bot. + // This avoids blocking on Redis/adapter initialization during setup. + const body = await request + .clone() + .json() + .catch(() => null); + if (body?.type === "url_verification" && typeof body?.challenge === "string") { + return Response.json({ challenge: body.challenge }); + } + + await slackChatBot.initialize(); + + return slackChatBot.webhooks.slack(request, { waitUntil: p => after(() => p) }); +} diff --git a/lib/slack-chat/bot.ts b/lib/slack-chat/bot.ts new file mode 100644 index 00000000..c7f9e1b8 --- /dev/null +++ b/lib/slack-chat/bot.ts @@ -0,0 +1,54 @@ +import { Chat, ConsoleLogger } from "chat"; +import { SlackAdapter } from "@chat-adapter/slack"; +import { createIoRedisState } from "@chat-adapter/state-ioredis"; +import redis from "@/lib/redis/connection"; +import type { SlackChatThreadState } from "./types"; +import { validateSlackChatEnv } from "./validateEnv"; + +const logger = new ConsoleLogger(); + +type SlackChatAdapters = { + slack: SlackAdapter; +}; + +/** + * Creates a new Chat bot instance configured with a single Slack adapter + * for the Record Label Agent. + * + * @returns A configured Chat instance + */ +export function createSlackChatBot() { + validateSlackChatEnv(); + + if (redis.status === "wait") { + redis.connect().catch(() => { + throw new Error("[slack-chat] Redis failed to connect"); + }); + } + + const state = createIoRedisState({ + client: redis, + keyPrefix: "chat", + logger, + }); + + const slack = new SlackAdapter({ + botToken: process.env.SLACK_CHAT_BOT_TOKEN!, + signingSecret: process.env.SLACK_CHAT_SIGNING_SECRET!, + logger, + }); + + return new Chat({ + userName: "Record Label Agent", + adapters: { slack }, + state, + }); +} + +export type SlackChatBot = ReturnType; + +/** + * Singleton bot instance. + * Does NOT call registerSingleton() to avoid conflicting with the coding-agent bot. + */ +export const slackChatBot = createSlackChatBot(); diff --git a/lib/slack-chat/handlers/handleSlackChatMessage.ts b/lib/slack-chat/handlers/handleSlackChatMessage.ts new file mode 100644 index 00000000..61bfacef --- /dev/null +++ b/lib/slack-chat/handlers/handleSlackChatMessage.ts @@ -0,0 +1,87 @@ +import type { SlackChatThreadState } from "../types"; +import type { ChatRequestBody } from "@/lib/chat/validateChatRequest"; +import { getMessages } from "@/lib/messages/getMessages"; +import convertToUiMessages from "@/lib/messages/convertToUiMessages"; +import { validateMessages } from "@/lib/chat/validateMessages"; +import { setupConversation } from "@/lib/chat/setupConversation"; +import { setupChatRequest } from "@/lib/chat/setupChatRequest"; +import { saveChatCompletion } from "@/lib/chat/saveChatCompletion"; + +const ACCOUNT_ID = "cebcc866-34c3-451c-8cd7-f63309acff0a"; + +/** + * Shared handler for both onNewMention and onSubscribedMessage. + * Generates an AI response and posts it back to the Slack thread. + * + * @param thread - The Chat SDK thread instance + * @param thread.state - Promise resolving to the current thread state + * @param thread.post - Posts a message to the thread + * @param thread.setState - Updates the thread state + * @param text - The user's message text + */ +export async function handleSlackChatMessage( + thread: { + state: Promise; + post: (message: string) => Promise; + setState: (state: SlackChatThreadState) => Promise; + }, + text: string, +) { + const currentState = await thread.state; + + // Prevent concurrent generation in the same thread + if (currentState?.status === "generating") { + await thread.post("I'm still working on a response. Please wait a moment."); + return; + } + + await thread.setState({ + status: "generating", + prompt: text, + roomId: currentState?.roomId, + }); + + await thread.post("Thinking..."); + + // Get or create roomId from thread state + const messages = getMessages(text, "user"); + const uiMessages = convertToUiMessages(messages); + const { lastMessage } = validateMessages(uiMessages); + + const { roomId } = await setupConversation({ + accountId: ACCOUNT_ID, + roomId: currentState?.roomId, + topic: text.slice(0, 100), + promptMessage: lastMessage, + memoryId: lastMessage.id, + }); + + // Build ChatRequestBody (bypasses HTTP validation) + const body: ChatRequestBody = { + messages: uiMessages, + accountId: ACCOUNT_ID, + orgId: null, + roomId, + authToken: process.env.SLACK_CHAT_API_KEY!, + }; + + const chatConfig = await setupChatRequest(body); + const result = await chatConfig.agent.generate(chatConfig); + + // Persist assistant response + try { + await saveChatCompletion({ text: result.text, roomId }); + } catch (error) { + console.error("[slack-chat] Failed to persist assistant message:", error); + } + + // Post response to Slack thread + await thread.post(result.text); + + // Update thread state with roomId for conversational memory + await thread.setState({ + status: "idle", + prompt: text, + roomId, + }); +} diff --git a/lib/slack-chat/handlers/onNewMention.ts b/lib/slack-chat/handlers/onNewMention.ts new file mode 100644 index 00000000..215b9ef2 --- /dev/null +++ b/lib/slack-chat/handlers/onNewMention.ts @@ -0,0 +1,19 @@ +import type { SlackChatBot } from "../bot"; +import { handleSlackChatMessage } from "./handleSlackChatMessage"; + +/** + * Registers the onNewMention handler on the Slack chat bot. + * Subscribes to the thread for follow-up messages, then generates a response. + * + * @param bot - The Slack chat bot instance + */ +export function registerOnNewMention(bot: SlackChatBot) { + bot.onNewMention(async (thread, message) => { + try { + await thread.subscribe(); + await handleSlackChatMessage(thread, message.text); + } catch (error) { + console.error("[slack-chat] onNewMention error:", error); + } + }); +} diff --git a/lib/slack-chat/handlers/onSubscribedMessage.ts b/lib/slack-chat/handlers/onSubscribedMessage.ts new file mode 100644 index 00000000..a084d320 --- /dev/null +++ b/lib/slack-chat/handlers/onSubscribedMessage.ts @@ -0,0 +1,18 @@ +import type { SlackChatBot } from "../bot"; +import { handleSlackChatMessage } from "./handleSlackChatMessage"; + +/** + * Registers the onSubscribedMessage handler on the Slack chat bot. + * Reuses the existing roomId from thread state for conversational memory. + * + * @param bot - The Slack chat bot instance + */ +export function registerOnSubscribedMessage(bot: SlackChatBot) { + bot.onSubscribedMessage(async (thread, message) => { + try { + await handleSlackChatMessage(thread, message.text); + } catch (error) { + console.error("[slack-chat] onSubscribedMessage error:", error); + } + }); +} diff --git a/lib/slack-chat/handlers/registerHandlers.ts b/lib/slack-chat/handlers/registerHandlers.ts new file mode 100644 index 00000000..075d2cfe --- /dev/null +++ b/lib/slack-chat/handlers/registerHandlers.ts @@ -0,0 +1,10 @@ +import { slackChatBot } from "../bot"; +import { registerOnNewMention } from "./onNewMention"; +import { registerOnSubscribedMessage } from "./onSubscribedMessage"; + +/** + * Registers all Slack chat bot event handlers on the bot singleton. + * Import this file once to attach handlers to the bot. + */ +registerOnNewMention(slackChatBot); +registerOnSubscribedMessage(slackChatBot); diff --git a/lib/slack-chat/types.ts b/lib/slack-chat/types.ts new file mode 100644 index 00000000..10f5a28c --- /dev/null +++ b/lib/slack-chat/types.ts @@ -0,0 +1,10 @@ +/** + * Thread state for the Slack chat bot. + * Stored in Redis via Chat SDK's state adapter. + */ +export interface SlackChatThreadState { + status: "idle" | "generating"; + prompt: string; + /** Persists across thread messages so the chat has memory. */ + roomId?: string; +} diff --git a/lib/slack-chat/validateEnv.ts b/lib/slack-chat/validateEnv.ts new file mode 100644 index 00000000..f043cef0 --- /dev/null +++ b/lib/slack-chat/validateEnv.ts @@ -0,0 +1,20 @@ +const REQUIRED_ENV_VARS = [ + "SLACK_CHAT_BOT_TOKEN", + "SLACK_CHAT_SIGNING_SECRET", + "SLACK_CHAT_API_KEY", + "REDIS_URL", +] as const; + +/** + * Validates that all required environment variables for the Slack chat bot are set. + * Throws an error listing all missing variables. + */ +export function validateSlackChatEnv(): void { + const missing: string[] = REQUIRED_ENV_VARS.filter(name => !process.env[name]); + + if (missing.length > 0) { + throw new Error( + `[slack-chat] Missing required environment variables:\n${missing.map(v => ` - ${v}`).join("\n")}`, + ); + } +} From d1e7d3965313734c086182b052877435317d8d45 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 13 Mar 2026 12:34:44 -0500 Subject: [PATCH 2/9] fix: derive accountId from API key instead of hardcoding Let the API key determine the account identity rather than hardcoding an account ID. This follows the existing auth pattern where account_id is always inferred from credentials, never passed explicitly. Co-Authored-By: Claude Opus 4.6 --- .../handlers/handleSlackChatMessage.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/slack-chat/handlers/handleSlackChatMessage.ts b/lib/slack-chat/handlers/handleSlackChatMessage.ts index 61bfacef..d8036566 100644 --- a/lib/slack-chat/handlers/handleSlackChatMessage.ts +++ b/lib/slack-chat/handlers/handleSlackChatMessage.ts @@ -6,8 +6,7 @@ import { validateMessages } from "@/lib/chat/validateMessages"; import { setupConversation } from "@/lib/chat/setupConversation"; import { setupChatRequest } from "@/lib/chat/setupChatRequest"; import { saveChatCompletion } from "@/lib/chat/saveChatCompletion"; - -const ACCOUNT_ID = "cebcc866-34c3-451c-8cd7-f63309acff0a"; +import { getApiKeyDetails } from "@/lib/keys/getApiKeyDetails"; /** * Shared handler for both onNewMention and onSubscribedMessage. @@ -35,6 +34,18 @@ export async function handleSlackChatMessage( return; } + const authToken = process.env.SLACK_CHAT_API_KEY!; + + // Derive account from the API key — no hardcoded account ID + const keyDetails = await getApiKeyDetails(authToken); + if (!keyDetails) { + console.error("[slack-chat] Invalid SLACK_CHAT_API_KEY — could not resolve account"); + await thread.post("Sorry, I'm not configured correctly. Please contact support."); + return; + } + + const { accountId, orgId } = keyDetails; + await thread.setState({ status: "generating", prompt: text, @@ -49,20 +60,20 @@ export async function handleSlackChatMessage( const { lastMessage } = validateMessages(uiMessages); const { roomId } = await setupConversation({ - accountId: ACCOUNT_ID, + accountId, roomId: currentState?.roomId, topic: text.slice(0, 100), promptMessage: lastMessage, memoryId: lastMessage.id, }); - // Build ChatRequestBody (bypasses HTTP validation) + // Build ChatRequestBody — accountId inferred from API key const body: ChatRequestBody = { messages: uiMessages, - accountId: ACCOUNT_ID, - orgId: null, + accountId, + orgId, roomId, - authToken: process.env.SLACK_CHAT_API_KEY!, + authToken, }; const chatConfig = await setupChatRequest(body); From 51c46fc132a4cd4d2abdfb9f141e1664758e464f Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 13 Mar 2026 12:47:44 -0500 Subject: [PATCH 3/9] fix: log Redis connection error instead of throwing unhandled rejection The throw inside .catch() creates an unhandled rejection that crashes the Node process. Log the error instead since the shared Redis client may already be connected by the coding-agent bot. Co-Authored-By: Claude Opus 4.6 --- lib/slack-chat/bot.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/slack-chat/bot.ts b/lib/slack-chat/bot.ts index c7f9e1b8..9ea0e54d 100644 --- a/lib/slack-chat/bot.ts +++ b/lib/slack-chat/bot.ts @@ -21,8 +21,8 @@ export function createSlackChatBot() { validateSlackChatEnv(); if (redis.status === "wait") { - redis.connect().catch(() => { - throw new Error("[slack-chat] Redis failed to connect"); + redis.connect().catch(err => { + console.error("[slack-chat] Redis failed to connect:", err); }); } From 84af9104ba91897546e9458facc1932f62209216 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 13 Mar 2026 12:48:38 -0500 Subject: [PATCH 4/9] revert: match coding-agent Redis connect pattern exactly Keep the throw inside .catch() to match lib/coding-agent/bot.ts. The Redis timeout is a pre-existing pattern, not specific to slack-chat. Co-Authored-By: Claude Opus 4.6 --- lib/slack-chat/bot.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/slack-chat/bot.ts b/lib/slack-chat/bot.ts index 9ea0e54d..ff7017e9 100644 --- a/lib/slack-chat/bot.ts +++ b/lib/slack-chat/bot.ts @@ -20,9 +20,11 @@ type SlackChatAdapters = { export function createSlackChatBot() { validateSlackChatEnv(); + // ioredis is configured with lazyConnect: true, so we must + // explicitly connect before the state adapter listens for "ready". if (redis.status === "wait") { - redis.connect().catch(err => { - console.error("[slack-chat] Redis failed to connect:", err); + redis.connect().catch(() => { + throw new Error("[slack-chat] Redis failed to connect"); }); } From df7f5684867c831f3fde1a26932d907e58e906a8 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 13 Mar 2026 12:59:41 -0500 Subject: [PATCH 5/9] fix: load full conversation history for Slack thread memory Each Slack message was only sending the current message to the AI, with no prior context. Now loads all memories from the room before generating, giving the bot conversational memory within a thread. Co-Authored-By: Claude Opus 4.6 --- .../handlers/handleSlackChatMessage.ts | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/slack-chat/handlers/handleSlackChatMessage.ts b/lib/slack-chat/handlers/handleSlackChatMessage.ts index d8036566..26d7ba85 100644 --- a/lib/slack-chat/handlers/handleSlackChatMessage.ts +++ b/lib/slack-chat/handlers/handleSlackChatMessage.ts @@ -7,6 +7,7 @@ import { setupConversation } from "@/lib/chat/setupConversation"; import { setupChatRequest } from "@/lib/chat/setupChatRequest"; import { saveChatCompletion } from "@/lib/chat/saveChatCompletion"; import { getApiKeyDetails } from "@/lib/keys/getApiKeyDetails"; +import selectMemories from "@/lib/supabase/memories/selectMemories"; /** * Shared handler for both onNewMention and onSubscribedMessage. @@ -54,11 +55,12 @@ export async function handleSlackChatMessage( await thread.post("Thinking..."); - // Get or create roomId from thread state - const messages = getMessages(text, "user"); - const uiMessages = convertToUiMessages(messages); - const { lastMessage } = validateMessages(uiMessages); + // Build current message as UIMessage + const newMessages = getMessages(text, "user"); + const newUiMessages = convertToUiMessages(newMessages); + const { lastMessage } = validateMessages(newUiMessages); + // Setup conversation: create room if needed, persist user message const { roomId } = await setupConversation({ accountId, roomId: currentState?.roomId, @@ -67,9 +69,32 @@ export async function handleSlackChatMessage( memoryId: lastMessage.id, }); + // Load full conversation history from the room (includes the message we just saved) + const memories = await selectMemories(roomId, { ascending: true }); + const historyMessages = (memories ?? []) + .filter(m => { + const content = m.content as unknown as { role?: string; parts?: unknown[] }; + return content?.role && content?.parts; + }) + .map(m => { + const content = m.content as unknown as { + role: string; + parts: { type: string; text?: string }[]; + }; + return { + id: m.id, + role: content.role as "user" | "assistant" | "system", + parts: content.parts, + }; + }); + + const allUiMessages = convertToUiMessages( + historyMessages.length > 0 ? historyMessages : newUiMessages, + ); + // Build ChatRequestBody — accountId inferred from API key const body: ChatRequestBody = { - messages: uiMessages, + messages: allUiMessages, accountId, orgId, roomId, From 732d01de2988e8a29323d7550edaeee5a9e235ef Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 13 Mar 2026 13:02:36 -0500 Subject: [PATCH 6/9] fix: type error in conversation history mapping Cast memory content as UIMessage to satisfy strict parts type checking. Co-Authored-By: Claude Opus 4.6 --- lib/slack-chat/handlers/handleSlackChatMessage.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/slack-chat/handlers/handleSlackChatMessage.ts b/lib/slack-chat/handlers/handleSlackChatMessage.ts index 26d7ba85..9d665a52 100644 --- a/lib/slack-chat/handlers/handleSlackChatMessage.ts +++ b/lib/slack-chat/handlers/handleSlackChatMessage.ts @@ -1,3 +1,4 @@ +import type { UIMessage } from "ai"; import type { SlackChatThreadState } from "../types"; import type { ChatRequestBody } from "@/lib/chat/validateChatRequest"; import { getMessages } from "@/lib/messages/getMessages"; @@ -71,19 +72,16 @@ export async function handleSlackChatMessage( // Load full conversation history from the room (includes the message we just saved) const memories = await selectMemories(roomId, { ascending: true }); - const historyMessages = (memories ?? []) + const historyMessages: UIMessage[] = (memories ?? []) .filter(m => { const content = m.content as unknown as { role?: string; parts?: unknown[] }; return content?.role && content?.parts; }) .map(m => { - const content = m.content as unknown as { - role: string; - parts: { type: string; text?: string }[]; - }; + const content = m.content as unknown as UIMessage; return { id: m.id, - role: content.role as "user" | "assistant" | "system", + role: content.role, parts: content.parts, }; }); From 5911f72a1f9a5ad76455618dd4d552d08c0809f7 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 13 Mar 2026 13:19:06 -0500 Subject: [PATCH 7/9] refactor: extract shared Slack url_verification handler (DRY) Both the coding-agent and slack-chat routes had identical url_verification challenge logic. Extract to lib/slack/handleUrlVerification.ts so both routes share a single implementation. Co-Authored-By: Claude Opus 4.6 --- app/api/chat/slack/route.ts | 12 +++--------- app/api/coding-agent/[platform]/route.ts | 9 +++------ lib/slack/handleUrlVerification.ts | 21 +++++++++++++++++++++ 3 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 lib/slack/handleUrlVerification.ts diff --git a/app/api/chat/slack/route.ts b/app/api/chat/slack/route.ts index b7a867f0..7c9977a5 100644 --- a/app/api/chat/slack/route.ts +++ b/app/api/chat/slack/route.ts @@ -1,6 +1,7 @@ import type { NextRequest } from "next/server"; import { after } from "next/server"; import { slackChatBot } from "@/lib/slack-chat/bot"; +import { handleUrlVerification } from "@/lib/slack/handleUrlVerification"; import "@/lib/slack-chat/handlers/registerHandlers"; /** @@ -25,15 +26,8 @@ export async function GET(request: NextRequest) { * @returns The webhook handler response or url_verification challenge */ export async function POST(request: NextRequest) { - // Handle Slack url_verification challenge before loading the bot. - // This avoids blocking on Redis/adapter initialization during setup. - const body = await request - .clone() - .json() - .catch(() => null); - if (body?.type === "url_verification" && typeof body?.challenge === "string") { - return Response.json({ challenge: body.challenge }); - } + const verification = await handleUrlVerification(request); + if (verification) return verification; await slackChatBot.initialize(); diff --git a/app/api/coding-agent/[platform]/route.ts b/app/api/coding-agent/[platform]/route.ts index 996a0a4d..a51a2104 100644 --- a/app/api/coding-agent/[platform]/route.ts +++ b/app/api/coding-agent/[platform]/route.ts @@ -1,6 +1,7 @@ import type { NextRequest } from "next/server"; import { after } from "next/server"; import { codingAgentBot } from "@/lib/coding-agent/bot"; +import { handleUrlVerification } from "@/lib/slack/handleUrlVerification"; import "@/lib/coding-agent/handlers/registerHandlers"; /** @@ -41,13 +42,9 @@ export async function POST( ) { const { platform } = await params; - // Handle Slack url_verification challenge before loading the bot. - // This avoids blocking on Redis/adapter initialization during setup. if (platform === "slack") { - const body = await request.clone().json().catch(() => null); - if (body?.type === "url_verification" && typeof body?.challenge === "string") { - return Response.json({ challenge: body.challenge }); - } + const verification = await handleUrlVerification(request); + if (verification) return verification; } await codingAgentBot.initialize(); diff --git a/lib/slack/handleUrlVerification.ts b/lib/slack/handleUrlVerification.ts new file mode 100644 index 00000000..5c29b3a9 --- /dev/null +++ b/lib/slack/handleUrlVerification.ts @@ -0,0 +1,21 @@ +import type { NextRequest } from "next/server"; + +/** + * Handles Slack url_verification challenge before bot initialization. + * This avoids blocking on Redis/adapter initialization during setup. + * + * @param request - The incoming webhook request + * @returns A Response with the challenge if verified, or null to continue processing + */ +export async function handleUrlVerification(request: NextRequest): Promise { + const body = await request + .clone() + .json() + .catch(() => null); + + if (body?.type === "url_verification" && typeof body?.challenge === "string") { + return Response.json({ challenge: body.challenge }); + } + + return null; +} From 9740d1d71f431320159c49d06ee3dfda24c36b33 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 13 Mar 2026 13:21:34 -0500 Subject: [PATCH 8/9] refactor: move lib/slack-chat/ to lib/slack/chat/ Consolidate all Slack-related libs under lib/slack/ for simpler organization. Shared utils like handleUrlVerification live at lib/slack/, bot-specific code lives at lib/slack/chat/. Co-Authored-By: Claude Opus 4.6 --- app/api/chat/slack/route.ts | 4 ++-- lib/{slack-chat => slack/chat}/bot.ts | 0 .../chat}/handlers/handleSlackChatMessage.ts | 0 lib/{slack-chat => slack/chat}/handlers/onNewMention.ts | 0 .../chat}/handlers/onSubscribedMessage.ts | 0 lib/{slack-chat => slack/chat}/handlers/registerHandlers.ts | 0 lib/{slack-chat => slack/chat}/types.ts | 0 lib/{slack-chat => slack/chat}/validateEnv.ts | 0 8 files changed, 2 insertions(+), 2 deletions(-) rename lib/{slack-chat => slack/chat}/bot.ts (100%) rename lib/{slack-chat => slack/chat}/handlers/handleSlackChatMessage.ts (100%) rename lib/{slack-chat => slack/chat}/handlers/onNewMention.ts (100%) rename lib/{slack-chat => slack/chat}/handlers/onSubscribedMessage.ts (100%) rename lib/{slack-chat => slack/chat}/handlers/registerHandlers.ts (100%) rename lib/{slack-chat => slack/chat}/types.ts (100%) rename lib/{slack-chat => slack/chat}/validateEnv.ts (100%) diff --git a/app/api/chat/slack/route.ts b/app/api/chat/slack/route.ts index 7c9977a5..9b86f10a 100644 --- a/app/api/chat/slack/route.ts +++ b/app/api/chat/slack/route.ts @@ -1,8 +1,8 @@ import type { NextRequest } from "next/server"; import { after } from "next/server"; -import { slackChatBot } from "@/lib/slack-chat/bot"; +import { slackChatBot } from "@/lib/slack/chat/bot"; import { handleUrlVerification } from "@/lib/slack/handleUrlVerification"; -import "@/lib/slack-chat/handlers/registerHandlers"; +import "@/lib/slack/chat/handlers/registerHandlers"; /** * GET /api/chat/slack diff --git a/lib/slack-chat/bot.ts b/lib/slack/chat/bot.ts similarity index 100% rename from lib/slack-chat/bot.ts rename to lib/slack/chat/bot.ts diff --git a/lib/slack-chat/handlers/handleSlackChatMessage.ts b/lib/slack/chat/handlers/handleSlackChatMessage.ts similarity index 100% rename from lib/slack-chat/handlers/handleSlackChatMessage.ts rename to lib/slack/chat/handlers/handleSlackChatMessage.ts diff --git a/lib/slack-chat/handlers/onNewMention.ts b/lib/slack/chat/handlers/onNewMention.ts similarity index 100% rename from lib/slack-chat/handlers/onNewMention.ts rename to lib/slack/chat/handlers/onNewMention.ts diff --git a/lib/slack-chat/handlers/onSubscribedMessage.ts b/lib/slack/chat/handlers/onSubscribedMessage.ts similarity index 100% rename from lib/slack-chat/handlers/onSubscribedMessage.ts rename to lib/slack/chat/handlers/onSubscribedMessage.ts diff --git a/lib/slack-chat/handlers/registerHandlers.ts b/lib/slack/chat/handlers/registerHandlers.ts similarity index 100% rename from lib/slack-chat/handlers/registerHandlers.ts rename to lib/slack/chat/handlers/registerHandlers.ts diff --git a/lib/slack-chat/types.ts b/lib/slack/chat/types.ts similarity index 100% rename from lib/slack-chat/types.ts rename to lib/slack/chat/types.ts diff --git a/lib/slack-chat/validateEnv.ts b/lib/slack/chat/validateEnv.ts similarity index 100% rename from lib/slack-chat/validateEnv.ts rename to lib/slack/chat/validateEnv.ts From 47279666906c79f91949e3521cd3239879b32b48 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 13 Mar 2026 13:25:22 -0500 Subject: [PATCH 9/9] fix: prevent thread state stuck in "generating" on error Wrap generation logic in try/catch/finally so thread state always resets to "idle", even if an exception occurs mid-flow. Co-Authored-By: Claude Opus 4.6 --- .../chat/handlers/handleSlackChatMessage.ts | 122 ++++++++++-------- 1 file changed, 65 insertions(+), 57 deletions(-) diff --git a/lib/slack/chat/handlers/handleSlackChatMessage.ts b/lib/slack/chat/handlers/handleSlackChatMessage.ts index 9d665a52..5838d84a 100644 --- a/lib/slack/chat/handlers/handleSlackChatMessage.ts +++ b/lib/slack/chat/handlers/handleSlackChatMessage.ts @@ -56,66 +56,74 @@ export async function handleSlackChatMessage( await thread.post("Thinking..."); - // Build current message as UIMessage - const newMessages = getMessages(text, "user"); - const newUiMessages = convertToUiMessages(newMessages); - const { lastMessage } = validateMessages(newUiMessages); - - // Setup conversation: create room if needed, persist user message - const { roomId } = await setupConversation({ - accountId, - roomId: currentState?.roomId, - topic: text.slice(0, 100), - promptMessage: lastMessage, - memoryId: lastMessage.id, - }); + let finalRoomId = currentState?.roomId; - // Load full conversation history from the room (includes the message we just saved) - const memories = await selectMemories(roomId, { ascending: true }); - const historyMessages: UIMessage[] = (memories ?? []) - .filter(m => { - const content = m.content as unknown as { role?: string; parts?: unknown[] }; - return content?.role && content?.parts; - }) - .map(m => { - const content = m.content as unknown as UIMessage; - return { - id: m.id, - role: content.role, - parts: content.parts, - }; + try { + // Build current message as UIMessage + const newMessages = getMessages(text, "user"); + const newUiMessages = convertToUiMessages(newMessages); + const { lastMessage } = validateMessages(newUiMessages); + + // Setup conversation: create room if needed, persist user message + const { roomId } = await setupConversation({ + accountId, + roomId: currentState?.roomId, + topic: text.slice(0, 100), + promptMessage: lastMessage, + memoryId: lastMessage.id, }); - const allUiMessages = convertToUiMessages( - historyMessages.length > 0 ? historyMessages : newUiMessages, - ); - - // Build ChatRequestBody — accountId inferred from API key - const body: ChatRequestBody = { - messages: allUiMessages, - accountId, - orgId, - roomId, - authToken, - }; - - const chatConfig = await setupChatRequest(body); - const result = await chatConfig.agent.generate(chatConfig); - - // Persist assistant response - try { - await saveChatCompletion({ text: result.text, roomId }); + finalRoomId = roomId; + + // Load full conversation history from the room (includes the message we just saved) + const memories = await selectMemories(roomId, { ascending: true }); + const historyMessages: UIMessage[] = (memories ?? []) + .filter(m => { + const content = m.content as unknown as { role?: string; parts?: unknown[] }; + return content?.role && content?.parts; + }) + .map(m => { + const content = m.content as unknown as UIMessage; + return { + id: m.id, + role: content.role, + parts: content.parts, + }; + }); + + const allUiMessages = convertToUiMessages( + historyMessages.length > 0 ? historyMessages : newUiMessages, + ); + + // Build ChatRequestBody — accountId inferred from API key + const body: ChatRequestBody = { + messages: allUiMessages, + accountId, + orgId, + roomId, + authToken, + }; + + const chatConfig = await setupChatRequest(body); + const result = await chatConfig.agent.generate(chatConfig); + + // Persist assistant response + try { + await saveChatCompletion({ text: result.text, roomId }); + } catch (error) { + console.error("[slack-chat] Failed to persist assistant message:", error); + } + + // Post response to Slack thread + await thread.post(result.text); } catch (error) { - console.error("[slack-chat] Failed to persist assistant message:", error); + console.error("[slack-chat] Generation failed:", error); + await thread.post("Sorry, something went wrong. Please try again."); + } finally { + await thread.setState({ + status: "idle", + prompt: text, + roomId: finalRoomId, + }); } - - // Post response to Slack thread - await thread.post(result.text); - - // Update thread state with roomId for conversational memory - await thread.setState({ - status: "idle", - prompt: text, - roomId, - }); }