Skip to content

Commit 0851cd4

Browse files
author
Kai Liu
committed
fix(slack): resolve workspace token first for synthetic-thread sends
Addresses Codex P2 review on PR #212. When threadIsSynthetic is true, sendMessage degenerates to a top-level channel post (no thread_ts), but the token was still being resolved with getSlackBotToken(channelId, threadId) first. In multi-workspace installs where a scheduled task/cron fires before that channel has been seen by the message router, the registry has no entry for the fake thread_ts and getSlackBotToken can fall back to the first registered token instead of the token for rawChannelId — leading to a follow-on channel_not_found / not_in_channel even though dropping thread_ts was supposed to make the message deliverable. Mirror the token-resolution chain that sendChannelMessage already uses for top-level posts: processor token -> workspace-by-channel token -> getSlackBotToken for the synthetic path. The real-thread path is unchanged so registry bindings for in-flight thread replies still win. Tests: full suite still green (408 pass, 1 skip, 0 fail).
1 parent f09ecb3 commit 0851cd4

1 file changed

Lines changed: 14 additions & 1 deletion

File tree

packages/ims/slack/client.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,6 @@ export async function sendMessage(
309309
const formattedText = markdownToSlack(text);
310310
const chunks = splitForSlack(formattedText);
311311
const workspace = slackAuthRegistry.getChannelWorkspaceName(rawChannelId) || "unknown";
312-
const botToken = getSlackBotTokenForProcessor(processorId) ?? getSlackBotToken(channelId, threadId);
313312

314313
// A "synthetic" thread id (`task:{id}` / `cron-job:{id}:{run}` / `cron:{id}`)
315314
// is an internal placeholder used by Ode's task/cron schedulers before a
@@ -320,6 +319,20 @@ export async function sendMessage(
320319
// the channel instead of being lost + captured as a Sentry error.
321320
const threadIsSynthetic = isSyntheticOwner(threadId);
322321

322+
// Token resolution. For real threads, prefer the registry-bound token for
323+
// this (channel, thread) — that's the token the inbound router observed
324+
// delivering the parent message and is the safest choice for replies. For
325+
// synthetic placeholders the call has degenerated to a top-level channel
326+
// post (see below); the registry has no entry for a fake `thread_ts`, so
327+
// resolve via the channel's workspace first (mirrors `sendChannelMessage`)
328+
// before falling back to `getSlackBotToken` to avoid the multi-workspace
329+
// edge case where `getSlackBotToken` may return the first registered token
330+
// instead of the one for `rawChannelId`.
331+
const botToken = getSlackBotTokenForProcessor(processorId)
332+
?? (threadIsSynthetic
333+
? (getWorkspaceBotTokenForChannel(channelId) ?? getSlackBotToken(channelId))
334+
: getSlackBotToken(channelId, threadId));
335+
323336
if (!botToken) {
324337
log.warn("No Slack bot token available for channel", { channelId });
325338
}

0 commit comments

Comments
 (0)