Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config/phantom.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 12 additions & 1 deletion src/channels/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class SlackChannel implements Channel {
private ownerUserId: string | null;
private phantomName: string;
private rejectedUsers = new Set<string>();
private participatedThreads = new Set<string>();

constructor(config: SlackChannelConfig) {
if (config.transport && config.transport !== "socket") {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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}`);
Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,12 +626,20 @@ async function main(): Promise<void> {
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, {
Expand Down
Loading