From 1e8446324e9c1bba37733bd4d0cab8add5c4e1a3 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 3 Apr 2026 02:41:48 +0000 Subject: [PATCH 1/4] allow phantom to reply to conversations its part of without requiring a specific mention --- src/channels/slack.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/channels/slack.ts b/src/channels/slack.ts index 587a426..73c89ec 100644 --- a/src/channels/slack.ts +++ b/src/channels/slack.ts @@ -43,6 +43,7 @@ export class SlackChannel implements Channel { private ownerUserId: string | null; private phantomName: string; private rejectedUsers = new Set(); + private participatedThreads = new Set(); constructor(config: SlackChannelConfig) { this.app = new App({ @@ -150,6 +151,11 @@ export class SlackChannel implements Channel { lastTs = result.ts ?? ""; } + // Track thread participation so we can respond to replies without @ mention + if (replyThreadTs) { + this.participatedThreads.add(`${channel}:${replyThreadTs}`); + } + return { id: lastTs || randomUUID(), channelId: this.id, @@ -334,7 +340,13 @@ export class SlackChannel implements Channel { if (this.botUserId && userId === this.botUserId) return; const channelType = msg.channel_type as string | undefined; - if (channelType !== "im") return; + if (channelType !== "im") { + // In channels, only respond to thread replies in threads we've participated in + const incomingThreadTs = msg.thread_ts as string | undefined; + if (!incomingThreadTs) return; + const threadKey = `${msg.channel as string}:${incomingThreadTs}`; + if (!this.participatedThreads.has(threadKey)) return; + } if (userId && !this.isOwner(userId)) { console.log(`[slack] Ignoring DM from non-owner: ${userId}`); From c9c46562caa1e6397f9e0b5c85195b7e3c647f03 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 3 Apr 2026 03:21:34 +0000 Subject: [PATCH 2/4] updated to not drop threads without mentions --- src/channels/slack.ts | 4 ++++ src/index.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/channels/slack.ts b/src/channels/slack.ts index 73c89ec..cd4f4c5 100644 --- a/src/channels/slack.ts +++ b/src/channels/slack.ts @@ -288,6 +288,10 @@ export class SlackChannel implements Channel { } } + trackThreadParticipation(channelId: string, threadTs: string): void { + this.participatedThreads.add(`${channelId}:${threadTs}`); + } + private registerEventHandlers(): void { this.app.event("app_mention", async ({ event, client: _client }) => { if (!this.messageHandler) return; diff --git a/src/index.ts b/src/index.ts index a6e0066..f049178 100644 --- a/src/index.ts +++ b/src/index.ts @@ -450,12 +450,16 @@ async function main(): Promise { if (progressStream) { // Slack: update the progress message with the final response + feedback buttons await progressStream.finish(response.text); + if (slackChannel && slackChannelId && slackThreadTs) { + slackChannel.trackThreadParticipation(slackChannelId, slackThreadTs); + } } else if (isSlack && slackChannel && slackChannelId && slackThreadTs) { // Slack fallback: send direct reply with feedback const thinkingTs = await slackChannel.postThinking(slackChannelId, slackThreadTs); if (thinkingTs) { await slackChannel.updateWithFeedback(slackChannelId, thinkingTs, response.text); } + slackChannel.trackThreadParticipation(slackChannelId, slackThreadTs); } else { // All other channels: send via router await router.send(msg.channelId, msg.conversationId, { From 47d96d9197085bbb4f8bd05175543d3f2b2a543b Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 28 Apr 2026 22:08:01 -0400 Subject: [PATCH 3/4] fix: resolve merge conflict and add SlackHttpChannel type guard for thread tracking Resolves the merge conflict in slack.ts between the thread participation feature (PR #33) and upstream's egress refactor (Phase 5b). The send() method now correctly delegates to egressSend() from slack-egress.ts; thread participation tracking continues via the separate trackThreadParticipation() method called from index.ts. Adds instanceof SlackHttpChannel guards at the two trackThreadParticipation call sites in index.ts so the SlackTransport union typechecks cleanly. HTTP mode parity is tracked in rossja/phantom#1. Co-Authored-By: Claude Sonnet 4.6 --- src/channels/slack.ts | 29 ----------------------------- src/index.ts | 8 ++++++-- 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/src/channels/slack.ts b/src/channels/slack.ts index 6d5cf50..1ca4d3e 100644 --- a/src/channels/slack.ts +++ b/src/channels/slack.ts @@ -152,36 +152,7 @@ export class SlackChannel implements Channel { } async send(conversationId: string, message: OutboundMessage): Promise { -<<<<<<< HEAD - const { channel, threadTs } = parseConversationId(conversationId); - const formattedText = toSlackMarkdown(message.text); - const replyThreadTs = message.threadId ?? threadTs; - const chunks = splitMessage(formattedText); - let lastTs = ""; - - for (const chunk of chunks) { - const result = await this.app.client.chat.postMessage({ - channel, - text: chunk, - thread_ts: replyThreadTs, - }); - lastTs = result.ts ?? ""; - } - - // Track thread participation so we can respond to replies without @ mention - if (replyThreadTs) { - this.participatedThreads.add(`${channel}:${replyThreadTs}`); - } - - return { - id: lastTs || randomUUID(), - channelId: this.id, - conversationId, - timestamp: new Date(), - }; -======= return egressSend(this.egressContext(), conversationId, message); ->>>>>>> upstream/main } onMessage(handler: (message: InboundMessage) => Promise): void { diff --git a/src/index.ts b/src/index.ts index abd4aed..f993c49 100644 --- a/src/index.ts +++ b/src/index.ts @@ -626,7 +626,8 @@ async function main(): Promise { if (progressStream) { // Slack: update the progress message with the final response + feedback buttons await progressStream.finish(response.text); - if (slackChannel && slackChannelId && slackThreadTs) { + // Thread participation tracking is Socket Mode only for now (see: rossja/phantom#1) + if (slackChannel && slackChannelId && slackThreadTs && !(slackChannel instanceof SlackHttpChannel)) { slackChannel.trackThreadParticipation(slackChannelId, slackThreadTs); } } else if (isSlack && slackChannel && slackChannelId && slackThreadTs) { @@ -635,7 +636,10 @@ async function main(): Promise { if (thinkingTs) { await slackChannel.updateWithFeedback(slackChannelId, thinkingTs, response.text); } - slackChannel.trackThreadParticipation(slackChannelId, slackThreadTs); + // Thread participation tracking is Socket Mode only for now (see: rossja/phantom#1) + if (!(slackChannel instanceof SlackHttpChannel)) { + slackChannel.trackThreadParticipation(slackChannelId, slackThreadTs); + } } else { // All other channels: send via router await router.send(msg.channelId, msg.conversationId, { From f058d59b5458506fdeb3da2584b2c7912c693935 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 28 Apr 2026 22:26:47 -0400 Subject: [PATCH 4/4] added openrouter provider --- config/phantom.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/phantom.yaml b/config/phantom.yaml index c8481c1..a35b327 100644 --- a/config/phantom.yaml +++ b/config/phantom.yaml @@ -6,6 +6,13 @@ effort: max max_budget_usd: 0 timeout_minutes: 240 +provider: + type: openrouter + api_key_env: OPENROUTER_API_KEY + model_mappings: + opus: moonshotai/kimi-k2.6 + sonnet: deepseek/deepseek-v3.2 + haiku: poolside/laguna-xs.2:free # Peer Phantom connections (other Phantoms this instance can query via MCP) # peers: # swe-phantom: