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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

### Fixed

- Route bot message and reaction triggers from Discord threads through the same request handling as regular text channels.
- Prevent mixed reaction replies from narrating the bot's internal choice to react while preserving natural reaction-plus-text responses.
- Run Discord-initiated Claude login in a pseudo-terminal so the CLI accepts submitted OAuth codes.
- Isolate saved history and summaries by Discord channel ID in dedicated storage namespaces so same-named channels do not share automatic context.
Expand Down
9 changes: 9 additions & 0 deletions src/discord/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { TextChannel, ThreadChannel } from "discord.js";

export type BotMessageChannel = TextChannel | ThreadChannel;

export function isBotMessageChannel(
channel: unknown,
): channel is BotMessageChannel {
return channel instanceof TextChannel || channel instanceof ThreadChannel;
}
25 changes: 13 additions & 12 deletions src/discord/handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Message, TextChannel, MessageReaction, User, PartialMessageReaction, PartialUser } from "discord.js";
import { Message, MessageReaction, User, PartialMessageReaction, PartialUser } from "discord.js";
import { REQUIRED_ROLE_ID, COOLDOWN_MS, LIVE_CONTEXT_LIMIT, DEEP_LIVE_CONTEXT_LIMIT } from "../config.js";
import { client } from "./client.js";
import { normalizeBotMentions } from "./mentions.js";
Expand All @@ -17,6 +17,7 @@ import { downloadAttachment } from "../storage/images.js";
import { backgroundProfileUpdate, backgroundServerMemoryUpdate } from "../storage/profiles.js";
import { ensureYesterdaySummaries } from "../storage/summaries.js";
import { smartSplit } from "./split.js";
import { BotMessageChannel, isBotMessageChannel } from "./channel.js";

// Consistent display name for a user — used in logs, prompts, and history
function authorLabel(user: { displayName?: string; globalName?: string | null; username: string; id: string }): string {
Expand Down Expand Up @@ -61,7 +62,7 @@ function formatMessageForContext(msg: Message): string {
return `[${time}] ${label}: ${messageContentForMemory(msg) || "[no text]"}`;
}

async function fetchChannelMessages(channel: TextChannel, limit: number): Promise<Message[]> {
async function fetchChannelMessages(channel: BotMessageChannel, limit: number): Promise<Message[]> {
const collected: Message[] = [];
let before: string | undefined;

Expand All @@ -87,7 +88,7 @@ async function fetchChannelMessages(channel: TextChannel, limit: number): Promis
}

async function buildLiveMessagesContext(
channel: TextChannel,
channel: BotMessageChannel,
question: string,
): Promise<{ text: string; messages: Message[] }> {
const limit = isDeepHistoryRequest(question)
Expand All @@ -114,7 +115,7 @@ function logIncomingMessage(msg: Message): void {
authorLabel(msg.author),
content,
msg.channel.id,
msg.channel instanceof TextChannel ? msg.channel.name : "unknown",
isBotMessageChannel(msg.channel) ? msg.channel.name : "unknown",
msg.createdAt,
);
}
Expand Down Expand Up @@ -205,7 +206,7 @@ export function registerHandler() {
try {
if (msg.author.bot) return;
if (await handleAuthTextMessage(msg)) return;
if (!(msg.channel instanceof TextChannel)) return;
if (!isBotMessageChannel(msg.channel)) return;

logIncomingMessage(msg);

Expand Down Expand Up @@ -307,7 +308,7 @@ export function registerHandler() {
if (user.bot) return;

const msg = reaction.message as Message;
if (!(msg.channel instanceof TextChannel)) return;
if (!isBotMessageChannel(msg.channel)) return;
if (!msg.guild) return;

// Don't respond to reactions on bot's own messages
Expand Down Expand Up @@ -372,7 +373,7 @@ export function registerHandler() {

await msg.channel.sendTyping();
const typingInterval = setInterval(() => {
(msg.channel as TextChannel).sendTyping().catch(() => {});
(msg.channel as BotMessageChannel).sendTyping().catch(() => {});
}, 8000);

let response: string;
Expand Down Expand Up @@ -464,7 +465,7 @@ async function processMessage(msg: Message): Promise<void> {
userProcessing.add(userId);

try {
if (!(msg.channel instanceof TextChannel)) return;
if (!isBotMessageChannel(msg.channel)) return;

const askQuestion = parseAskCommand(msg.content);
console.error(
Expand Down Expand Up @@ -572,14 +573,14 @@ async function processMessage(msg: Message): Promise<void> {
// Show typing indicator
await msg.channel.sendTyping();
const typingInterval = setInterval(() => {
(msg.channel as TextChannel).sendTyping().catch(() => {});
(msg.channel as BotMessageChannel).sendTyping().catch(() => {});
}, 8000);

// Fetch live channel messages for context (reused later for participant collection)
let liveMessages = "";
let recentMessages: Message[] = [];
try {
const liveContext = await buildLiveMessagesContext(msg.channel as TextChannel, question);
const liveContext = await buildLiveMessagesContext(msg.channel as BotMessageChannel, question);
recentMessages = liveContext.messages;
liveMessages = liveContext.text;
console.error(`[Bot] Added ${recentMessages.length} live messages to context`);
Expand Down Expand Up @@ -634,10 +635,10 @@ async function processMessage(msg: Message): Promise<void> {
try {
await msg.reply(text);
} catch {
await (msg.channel as TextChannel).send(text);
await (msg.channel as BotMessageChannel).send(text);
}
} else {
await (msg.channel as TextChannel).send(text);
await (msg.channel as BotMessageChannel).send(text);
}
};

Expand Down
18 changes: 18 additions & 0 deletions tests/discordChannelRouting.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import assert from "node:assert/strict";
import test from "node:test";
import { TextChannel, ThreadChannel } from "discord.js";

import { isBotMessageChannel } from "../build/discord/channel.js";

test("accepts guild text channels and threads for bot request routing", () => {
const textChannel = Object.create(TextChannel.prototype);
const threadChannel = Object.create(ThreadChannel.prototype);

assert.equal(isBotMessageChannel(textChannel), true);
assert.equal(isBotMessageChannel(threadChannel), true);
});

test("rejects non-message channels for bot request routing", () => {
assert.equal(isBotMessageChannel(null), false);
assert.equal(isBotMessageChannel({ isTextBased: () => true }), false);
});