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: diff --git a/src/channels/slack.ts b/src/channels/slack.ts index ebbaa7d..1ca4d3e 100644 --- a/src/channels/slack.ts +++ b/src/channels/slack.ts @@ -52,6 +52,7 @@ export class SlackChannel implements Channel { private ownerUserId: string | null; private phantomName: string; private rejectedUsers = new Set(); + private participatedThreads = new Set(); constructor(config: SlackChannelConfig) { if (config.transport && config.transport !== "socket") { @@ -199,6 +200,10 @@ export class SlackChannel implements Channel { return egressRemoveReaction(this.egressContext(), channel, messageTs, emoji); } + 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; @@ -251,7 +256,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}`); diff --git a/src/index.ts b/src/index.ts index e4208a7..f993c49 100644 --- a/src/index.ts +++ b/src/index.ts @@ -626,12 +626,20 @@ async function main(): Promise { if (progressStream) { // Slack: update the progress message with the final response + feedback buttons await progressStream.finish(response.text); + // 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) { // Slack fallback: send direct reply with feedback const thinkingTs = await slackChannel.postThinking(slackChannelId, slackThreadTs); if (thinkingTs) { await slackChannel.updateWithFeedback(slackChannelId, thinkingTs, response.text); } + // 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, {