Skip to content

feat: thread participation tracking parity for HTTP mode (SlackHttpChannel) #1

@rossja

Description

@rossja

Background

PR ghostwright#33 added the ability for Phantom to reply naturally in Slack threads it has participated in, without needing an @mention. The implementation tracks which threads the bot has replied to in a participatedThreads set on SlackChannel (Socket Mode), and gates on that set in the message event handler.

This feature is Socket Mode only. SlackHttpChannel (HTTP receiver mode) was not updated.

Current behavior

  • Socket Mode (SlackChannel): After replying in a thread, Phantom will respond to follow-up messages in that thread without being @mentioned. Thread participation is tracked via participatedThreads and called from index.ts via trackThreadParticipation().
  • HTTP Mode (SlackHttpChannel): The message event handler in slack-http-events.ts still uses the old if (channelType !== "im") return guard, meaning Phantom only responds to DMs - not thread replies in channels, regardless of prior participation.

What needs to change

Three files need updates to bring HTTP mode to parity:

1. src/channels/slack-http-receiver.ts

Add participatedThreads and the tracking method:

private participatedThreads = new Set<string>();

trackThreadParticipation(channelId: string, threadTs: string): void {
    this.participatedThreads.add(`${channelId}:${threadTs}`);
}

hasParticipatedInThread(channelId: string, threadTs: string): boolean {
    return this.participatedThreads.has(`${channelId}:${threadTs}`);
}

2. src/channels/slack-http-events.ts

Update EventDispatchHost to expose thread participation check:

export type EventDispatchHost = {
    // ... existing fields ...
    hasParticipatedInThread(channelId: string, threadTs: string): boolean;
};

Then update the message event handler (currently around line 89-90) from:

const channelType = m.channel_type as string | undefined;
if (channelType !== "im") return;

To match the Socket Mode logic in slack.ts:288-294:

const channelType = m.channel_type as string | undefined;
if (channelType !== "im") {
    const incomingThreadTs = m.thread_ts as string | undefined;
    if (!incomingThreadTs) return;
    if (!host.hasParticipatedInThread(m.channel as string, incomingThreadTs)) return;
}

3. src/index.ts

Remove the SlackHttpChannel instanceof guards added as a temporary workaround in PR ghostwright#33's resolution (the two !(slackChannel instanceof SlackHttpChannel) checks around lines 629-639). Once SlackHttpChannel has trackThreadParticipation(), the SlackTransport union will satisfy the call without a guard.

Testing

  • Deploy with HTTP mode transport configured
  • Enable message.channels (and optionally message.groups) event subscriptions in the Slack app's Event Subscriptions settings (same requirement as Socket Mode, documented in PR allow phantom to reply to conversations its part of naturally ghostwright/phantom#33)
  • Reply in a thread Phantom has participated in without @mentioning it - it should respond naturally
  • Post in a channel Phantom has NOT replied in - it should not respond unless @mentioned

Notes

  • The participatedThreads set is in-memory, so it resets on process restart. This is intentional and consistent with Socket Mode behavior.
  • SlackHttpChannel uses registerHttpEventHandlers (delegating to EventDispatchHost) rather than registering handlers directly, so the interface update is the critical path for passing state through.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions