Skip to content

Commit 6570e06

Browse files
chechunhsuclaude
andcommitted
fix(slack): guard Slack API calls against empty threadTs to fix invalid_thread_ts
Preserve the intentional empty threadTs for top-level DMs (added in #39 for openDM() subscription matching) while preventing Slack API errors. Instead of changing the threadTs logic, normalize empty threadTs to undefined at the entry of each method that calls Slack APIs (postMessage, postEphemeral, scheduleMessage, stream). This way: - openDM() subscription matching continues to work (empty threadTs) - Slack API calls receive undefined instead of "" (no invalid_thread_ts) The stream method throws ValidationError on empty threadTs (matching startTyping's early-return pattern) so TypeScript narrows correctly without any `as string` casts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 80a8a34 commit 6570e06

3 files changed

Lines changed: 30 additions & 12 deletions

File tree

.changeset/fix-dm-thread-ts.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@chat-adapter/slack": patch
3+
---
4+
5+
Fix DM messages failing with `invalid_thread_ts` by guarding Slack API calls with `threadTs || undefined`

packages/adapter-slack/src/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2253,7 +2253,7 @@ describe("postMessage", () => {
22532253
expect(client.chat.postMessage).toHaveBeenCalledWith(
22542254
expect.objectContaining({
22552255
channel: "C123",
2256-
thread_ts: "",
2256+
thread_ts: undefined,
22572257
})
22582258
);
22592259
});
@@ -3303,7 +3303,7 @@ describe("postChannelMessage", () => {
33033303
expect(client.chat.postMessage).toHaveBeenCalledWith(
33043304
expect.objectContaining({
33053305
channel: "C123",
3306-
thread_ts: "",
3306+
thread_ts: undefined,
33073307
})
33083308
);
33093309
});

packages/adapter-slack/src/index.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2161,14 +2161,16 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
21612161
_message: AdapterPostableMessage
21622162
): Promise<RawMessage<unknown>> {
21632163
const message = await this.resolveMessageMentions(_message, threadId);
2164-
const { channel, threadTs } = this.decodeThreadId(threadId);
2164+
const { channel, threadTs: rawThreadTs } = this.decodeThreadId(threadId);
2165+
// Normalize empty threadTs to undefined to avoid Slack API "invalid_thread_ts" errors
2166+
const threadTs = rawThreadTs || undefined;
21652167

21662168
try {
21672169
// Check for files to upload
21682170
const files = extractFiles(message);
21692171
if (files.length > 0) {
21702172
// Upload files first (they're shared to the channel automatically)
2171-
await this.uploadFiles(files, channel, threadTs || undefined);
2173+
await this.uploadFiles(files, channel, threadTs);
21722174

21732175
// If message only has files (no text/card), return early
21742176
const hasText =
@@ -2302,7 +2304,8 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
23022304
_message: AdapterPostableMessage
23032305
): Promise<EphemeralMessage> {
23042306
const message = await this.resolveMessageMentions(_message, threadId);
2305-
const { channel, threadTs } = this.decodeThreadId(threadId);
2307+
const { channel, threadTs: rawThreadTs } = this.decodeThreadId(threadId);
2308+
const threadTs = rawThreadTs || undefined;
23062309

23072310
try {
23082311
// Check if message contains a card
@@ -2323,7 +2326,7 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
23232326
const result = await this.client.chat.postEphemeral(
23242327
this.withToken({
23252328
channel,
2326-
thread_ts: threadTs || undefined,
2329+
thread_ts: threadTs,
23272330
user: userId,
23282331
text: fallbackText,
23292332
blocks,
@@ -2356,7 +2359,7 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
23562359
const result = await this.client.chat.postEphemeral(
23572360
this.withToken({
23582361
channel,
2359-
thread_ts: threadTs || undefined,
2362+
thread_ts: threadTs,
23602363
user: userId,
23612364
text: tableResult.text,
23622365
blocks: tableResult.blocks,
@@ -2392,7 +2395,7 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
23922395
const result = await this.client.chat.postEphemeral(
23932396
this.withToken({
23942397
channel,
2395-
thread_ts: threadTs || undefined,
2398+
thread_ts: threadTs,
23962399
user: userId,
23972400
text,
23982401
})
@@ -2420,7 +2423,8 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
24202423
options: { postAt: Date }
24212424
): Promise<ScheduledMessage> {
24222425
const message = await this.resolveMessageMentions(_message, threadId);
2423-
const { channel, threadTs } = this.decodeThreadId(threadId);
2426+
const { channel, threadTs: rawThreadTs } = this.decodeThreadId(threadId);
2427+
const threadTs = rawThreadTs || undefined;
24242428
const postAtUnix = Math.floor(options.postAt.getTime() / 1000);
24252429

24262430
if (postAtUnix <= Math.floor(Date.now() / 1000)) {
@@ -2456,7 +2460,7 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
24562460
const result = await this.client.chat.scheduleMessage({
24572461
token,
24582462
channel,
2459-
thread_ts: threadTs || undefined,
2463+
thread_ts: threadTs,
24602464
post_at: postAtUnix,
24612465
text: fallbackText,
24622466
blocks,
@@ -2498,7 +2502,7 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
24982502
const result = await this.client.chat.scheduleMessage({
24992503
token,
25002504
channel,
2501-
thread_ts: threadTs || undefined,
2505+
thread_ts: threadTs,
25022506
post_at: postAtUnix,
25032507
text,
25042508
unfurl_links: false,
@@ -2929,7 +2933,16 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
29292933
"Slack streaming requires recipientUserId and recipientTeamId in options"
29302934
);
29312935
}
2932-
const { channel, threadTs } = this.decodeThreadId(threadId);
2936+
const { channel, threadTs: rawThreadTs } = this.decodeThreadId(threadId);
2937+
// Normalize empty threadTs to undefined to avoid Slack API "invalid_thread_ts" errors
2938+
const threadTs = rawThreadTs || undefined;
2939+
if (!threadTs) {
2940+
this.logger.debug("Slack: stream skipped - no thread context");
2941+
throw new ValidationError(
2942+
"slack",
2943+
"Slack streaming requires a valid thread context (non-empty threadTs)"
2944+
);
2945+
}
29332946
this.logger.debug("Slack: starting stream", { channel, threadTs });
29342947

29352948
const token = this.getToken();

0 commit comments

Comments
 (0)