Skip to content

fix(relay): thread responses under original message in Slack channels - #1848

Merged
PierreLeGuen merged 1 commit into
stagingfrom
fix/slack-thread-replies
Apr 1, 2026
Merged

PierreLeGuen merged 1 commit into
stagingfrom
fix/slack-thread-replies

Conversation

@PierreLeGuen

Copy link
Copy Markdown
Contributor

Summary

  • Channel mention responses now thread under the user's original message instead of posting as top-level messages
  • Root cause: relay channel used event.channel_id (e.g. C088K6C3SQZ) as thread_ts fallback — Slack requires a message timestamp, not a channel ID
  • Fix: use event.id (the Slack message ts) as fallback, which is the correct format for threading

Test plan

  • Regression test: start_uses_message_ts_as_thread_id_for_mentions
  • All 20 relay channel tests pass
  • Zero clippy warnings
  • Deploy and verify @ironclaw mention in channel produces threaded reply

🤖 Generated with Claude Code

For channel mentions, the relay channel used event.channel_id (e.g.
"C088K6C3SQZ") as the thread_id fallback. Slack requires thread_ts to
be a message timestamp, so it silently ignored this and posted a
top-level message instead of threading.

Now uses event.id (the Slack message ts, e.g. "1609459200.000100") as
the fallback, so responses are always threaded under the user's message.
Also fixes metadata["thread_id"] to use the same value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size: M 50-199 changed lines risk: medium Business logic, config, or moderate-risk modules contributor: experienced 6-19 merged PRs labels Apr 1, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the relay channel to use message timestamps as thread IDs for Slack mentions, ensuring responses are correctly threaded. Feedback highlights that the current logic for setting the thread ID may inadvertently overwrite the conversation scope, potentially breaking agent history, and suggests explicitly maintaining the channel ID as the scope. Additionally, it is recommended to handle the thread_id in metadata as an optional value to avoid sending empty strings as invalid timestamps to Slack.

Comment on lines +201 to 211
// Use the original thread_id if present (already in a thread),
// otherwise use the message timestamp (event.id) so that
// responses are threaded under the user's message in channels.
// Fall back to channel_id only if event.id is missing.
let msg = if let Some(ref thread_id) = event.thread_id {
msg.with_thread(thread_id)
} else if !event.id.is_empty() {
msg.with_thread(&event.id)
} else {
msg.with_thread(&event.channel_id)
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There are two issues with this logic:

  1. Conversation Continuity: with_thread overwrites the conversation_scope_id. By using event.id as the thread ID for top-level messages, each message effectively starts a new conversation scope, which breaks the agent's memory/history continuity within the channel. For top-level messages, we should thread under the message but keep the channel_id as the conversation scope.
  2. Invalid Fallback: The fallback to channel_id at line 210 contradicts the PR's goal of avoiding non-timestamp thread IDs. If no valid message ID is available, we should avoid setting a thread ID entirely while still maintaining the channel scope.
                let msg = if let Some(thread_id) = event.thread_id.as_ref() {
                    msg.with_thread(thread_id)
                } else if !event.id.is_empty() {
                    msg.with_thread(&event.id).with_conversation_scope(&event.channel_id)
                } else {
                    msg.with_conversation_scope(&event.channel_id)
                };
References
  1. Use .as_ref().map() on Option fields within a struct to prevent partial moves, making the code more robust against future changes.

"sender_name": event.display_name(),
"event_type": event.event_type,
"thread_id": event.thread_id,
"thread_id": event.thread_id.as_deref().unwrap_or(&event.id),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using unwrap_or(&event.id) forces a string value in the metadata. If event.id is empty, this results in an empty string "" being stored, which causes the respond method to send an invalid thread_ts to Slack. It's better to keep this as an Option so that it serializes to null when no valid ID is available, allowing the response logic to correctly omit the threading parameter.

Suggested change
"thread_id": event.thread_id.as_deref().unwrap_or(&event.id),
"thread_id": event.thread_id.as_ref().map(|s| s.as_str()).or(Some(event.id.as_str()).filter(|s| !s.is_empty())),
References
  1. When coercing an empty string to a value based on a JSON schema, convert it to null only if the schema allows null or does not allow string. Otherwise, preserve the empty string as it may be a valid value.
  2. Use .as_ref().map() on Option fields within a struct to prevent partial moves, making the code more robust against future changes.

@zmanian zmanian left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review -- APPROVE

Clean, correct bug fix. The root cause is right: channel mentions without an existing thread used channel_id as thread_ts, which Slack silently ignores. The fix uses event.id (message timestamp) as the fallback, which is the correct value for Slack's thread_ts parameter.

Three-way cascade is sound: event.thread_id -> event.id -> event.channel_id. Metadata thread_id updated consistently. Good regression test.

Minor note

The channel_id fallback is now only reachable if event.id is empty (shouldn't happen for valid Slack events). Consider logging a warning on that path.

@PierreLeGuen
PierreLeGuen merged commit 510fe19 into staging Apr 1, 2026
14 checks passed
@PierreLeGuen
PierreLeGuen deleted the fix/slack-thread-replies branch April 1, 2026 17:37
serrrfirat pushed a commit that referenced this pull request Apr 5, 2026
…#1848)

For channel mentions, the relay channel used event.channel_id (e.g.
"C088K6C3SQZ") as the thread_id fallback. Slack requires thread_ts to
be a message timestamp, so it silently ignored this and posted a
top-level message instead of threading.

Now uses event.id (the Slack message ts, e.g. "1609459200.000100") as
the fallback, so responses are always threaded under the user's message.
Also fixes metadata["thread_id"] to use the same value.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
drchirag1991 pushed a commit to drchirag1991/ironclaw that referenced this pull request Apr 8, 2026
…nearai#1848)

For channel mentions, the relay channel used event.channel_id (e.g.
"C088K6C3SQZ") as the thread_id fallback. Slack requires thread_ts to
be a message timestamp, so it silently ignored this and posted a
top-level message instead of threading.

Now uses event.id (the Slack message ts, e.g. "1609459200.000100") as
the fallback, so responses are always threaded under the user's message.
Also fixes metadata["thread_id"] to use the same value.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ironclaw-ci ironclaw-ci Bot mentioned this pull request Apr 8, 2026
@ironclaw-ci ironclaw-ci Bot mentioned this pull request Apr 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: experienced 6-19 merged PRs risk: medium Business logic, config, or moderate-risk modules size: M 50-199 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants