diff --git a/extensions/matrix/src/group-mentions.ts b/extensions/matrix/src/group-mentions.ts index d548bc6f4..e2696d3c5 100644 --- a/extensions/matrix/src/group-mentions.ts +++ b/extensions/matrix/src/group-mentions.ts @@ -16,14 +16,14 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b if (roomId.toLowerCase().startsWith("room:")) { roomId = roomId.slice("room:".length).trim(); } - const groupRoom = params.groupRoom?.trim() ?? ""; - const aliases = groupRoom ? [groupRoom] : []; + const groupChannel = params.groupChannel?.trim() ?? ""; + const aliases = groupChannel ? [groupChannel] : []; const cfg = params.cfg as CoreConfig; const resolved = resolveMatrixRoomConfig({ rooms: cfg.channels?.matrix?.rooms, roomId, aliases, - name: groupRoom || undefined, + name: groupChannel || undefined, }).config; if (resolved) { if (resolved.autoReply === true) return false; diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index dc97c2f5f..5e2192bc9 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -379,7 +379,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi SenderId: senderId, SenderUsername: senderId.split(":")[0]?.replace(/^@/, ""), GroupSubject: isRoom ? (roomName ?? roomId) : undefined, - GroupRoom: isRoom ? (room.getCanonicalAlias?.() ?? roomId) : undefined, + GroupChannel: isRoom ? (room.getCanonicalAlias?.() ?? roomId) : undefined, GroupSystemPrompt: isRoom ? groupSystemPrompt : undefined, Provider: "matrix" as const, Surface: "matrix" as const, diff --git a/src/auto-reply/reply/groups.test.ts b/src/auto-reply/reply/groups.test.ts index 831a4ac4e..5db0601e1 100644 --- a/src/auto-reply/reply/groups.test.ts +++ b/src/auto-reply/reply/groups.test.ts @@ -23,7 +23,7 @@ describe("resolveGroupRequireMention", () => { const ctx: TemplateContext = { Provider: "discord", From: "group:123", - GroupRoom: "#general", + GroupChannel: "#general", GroupSpace: "145", }; const groupResolution: GroupKeyResolution = { diff --git a/src/auto-reply/reply/groups.ts b/src/auto-reply/reply/groups.ts index eb0302dcf..0897f32dd 100644 --- a/src/auto-reply/reply/groups.ts +++ b/src/auto-reply/reply/groups.ts @@ -16,12 +16,12 @@ export function resolveGroupRequireMention(params: { const channel = normalizeChannelId(rawChannel); if (!channel) return true; const groupId = groupResolution?.id ?? ctx.From?.replace(/^group:/, ""); - const groupRoom = ctx.GroupRoom?.trim() ?? ctx.GroupSubject?.trim(); + const groupChannel = ctx.GroupChannel?.trim() ?? ctx.GroupSubject?.trim(); const groupSpace = ctx.GroupSpace?.trim(); const requireMention = getChannelDock(channel)?.groups?.resolveRequireMention?.({ cfg, groupId, - groupRoom, + groupChannel, groupSpace, accountId: ctx.AccountId, }); @@ -62,13 +62,13 @@ export function buildGroupIntro(params: { ? "Activation: always-on (you receive every group message)." : "Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included)."; const groupId = params.sessionCtx.From?.replace(/^group:/, ""); - const groupRoom = params.sessionCtx.GroupRoom?.trim() ?? subject; + const groupChannel = params.sessionCtx.GroupChannel?.trim() ?? subject; const groupSpace = params.sessionCtx.GroupSpace?.trim(); const providerIdsLine = providerId ? getChannelDock(providerId)?.groups?.resolveGroupIntroHint?.({ cfg: params.cfg, groupId, - groupRoom, + groupChannel, groupSpace, accountId: params.sessionCtx.AccountId, }) diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index bd5accd88..2d42f0f6d 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -242,14 +242,19 @@ export async function initSessionState(params: { const channel = groupResolution.channel; const subject = ctx.GroupSubject?.trim(); const space = ctx.GroupSpace?.trim(); - const explicitRoom = ctx.GroupRoom?.trim(); + const explicitChannel = ctx.GroupChannel?.trim(); const normalizedChannel = normalizeChannelId(channel); - const isRoomProvider = Boolean( + const isChannelProvider = Boolean( normalizedChannel && getChannelDock(normalizedChannel)?.capabilities.chatTypes.includes("channel"), ); const nextRoom = - explicitRoom ?? (isRoomProvider && subject && subject.startsWith("#") ? subject : undefined); + explicitChannel ?? + ((groupResolution.chatType === "channel" || isChannelProvider) && + subject && + subject.startsWith("#") + ? subject + : undefined); const nextSubject = nextRoom ? undefined : subject; sessionEntry.chatType = groupResolution.chatType ?? "group"; sessionEntry.channel = channel; diff --git a/src/auto-reply/templating.ts b/src/auto-reply/templating.ts index 74146c250..c77dedd2c 100644 --- a/src/auto-reply/templating.ts +++ b/src/auto-reply/templating.ts @@ -63,7 +63,8 @@ export type MsgContext = { /** Human label for envelope headers (conversation label, not sender). */ ConversationLabel?: string; GroupSubject?: string; - GroupRoom?: string; + /** Human label for channel-like group conversations (e.g. #general, #support). */ + GroupChannel?: string; GroupSpace?: string; GroupMembers?: string; GroupSystemPrompt?: string; diff --git a/src/channels/conversation-label.ts b/src/channels/conversation-label.ts index 97434fa89..fb4719779 100644 --- a/src/channels/conversation-label.ts +++ b/src/channels/conversation-label.ts @@ -27,7 +27,7 @@ export function resolveConversationLabel(ctx: MsgContext): string | undefined { } const base = - ctx.GroupRoom?.trim() || + ctx.GroupChannel?.trim() || ctx.GroupSubject?.trim() || ctx.GroupSpace?.trim() || ctx.From?.trim() || diff --git a/src/channels/plugins/group-mentions.ts b/src/channels/plugins/group-mentions.ts index 24f167a22..ec38ca85d 100644 --- a/src/channels/plugins/group-mentions.ts +++ b/src/channels/plugins/group-mentions.ts @@ -6,7 +6,7 @@ import { resolveSlackAccount } from "../../slack/accounts.js"; type GroupMentionParams = { cfg: ClawdbotConfig; groupId?: string | null; - groupRoom?: string | null; + groupChannel?: string | null; groupSpace?: string | null; accountId?: string | null; }; @@ -133,13 +133,14 @@ export function resolveDiscordGroupRequireMention(params: GroupMentionParams): b ); const channelEntries = guildEntry?.channels; if (channelEntries && Object.keys(channelEntries).length > 0) { - const channelSlug = normalizeDiscordSlug(params.groupRoom); + const groupChannel = params.groupChannel; + const channelSlug = normalizeDiscordSlug(groupChannel); const entry = (params.groupId ? channelEntries[params.groupId] : undefined) ?? (channelSlug ? (channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`]) : undefined) ?? - (params.groupRoom ? channelEntries[normalizeDiscordSlug(params.groupRoom)] : undefined); + (groupChannel ? channelEntries[normalizeDiscordSlug(groupChannel)] : undefined); if (entry && typeof entry.requireMention === "boolean") { return entry.requireMention; } @@ -159,7 +160,8 @@ export function resolveSlackGroupRequireMention(params: GroupMentionParams): boo const keys = Object.keys(channels); if (keys.length === 0) return true; const channelId = params.groupId?.trim(); - const channelName = params.groupRoom?.replace(/^#/, ""); + const groupChannel = params.groupChannel; + const channelName = groupChannel?.replace(/^#/, ""); const normalizedName = normalizeSlackSlug(channelName); const candidates = [ channelId ?? "", diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index dad18d3cf..e8b912922 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -133,7 +133,8 @@ export type ChannelLogSink = { export type ChannelGroupContext = { cfg: ClawdbotConfig; groupId?: string | null; - groupRoom?: string | null; + /** Human label for channel-like group conversations (e.g. #general). */ + groupChannel?: string | null; groupSpace?: string | null; accountId?: string | null; }; diff --git a/src/discord/monitor/message-handler.process.ts b/src/discord/monitor/message-handler.process.ts index d669cf4dd..164bd41c3 100644 --- a/src/discord/monitor/message-handler.process.ts +++ b/src/discord/monitor/message-handler.process.ts @@ -124,8 +124,8 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) senderDisplay && senderTag && senderDisplay !== senderTag ? `${senderDisplay} (${senderTag})` : (senderDisplay ?? senderTag ?? author.id); - const groupRoom = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined; - const groupSubject = isDirectMessage ? undefined : groupRoom; + const groupChannel = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined; + const groupSubject = isDirectMessage ? undefined : groupChannel; const channelDescription = channelInfo?.topic?.trim(); const systemPromptParts = [ channelDescription ? `Channel topic: ${channelDescription}` : null, @@ -245,7 +245,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) SenderUsername: author.username, SenderTag: formatDiscordUserTag(author), GroupSubject: groupSubject, - GroupRoom: groupRoom, + GroupChannel: groupChannel, GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined, GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined, Provider: "discord" as const, diff --git a/src/slack/monitor.test.ts b/src/slack/monitor.test.ts index 65eb998e7..e4aa58192 100644 --- a/src/slack/monitor.test.ts +++ b/src/slack/monitor.test.ts @@ -2,14 +2,14 @@ import { describe, expect, it } from "vitest"; import { buildSlackSlashCommandMatcher, - isSlackRoomAllowedByPolicy, + isSlackChannelAllowedByPolicy, resolveSlackThreadTs, } from "./monitor.js"; describe("slack groupPolicy gating", () => { it("allows when policy is open", () => { expect( - isSlackRoomAllowedByPolicy({ + isSlackChannelAllowedByPolicy({ groupPolicy: "open", channelAllowlistConfigured: false, channelAllowed: false, @@ -19,7 +19,7 @@ describe("slack groupPolicy gating", () => { it("blocks when policy is disabled", () => { expect( - isSlackRoomAllowedByPolicy({ + isSlackChannelAllowedByPolicy({ groupPolicy: "disabled", channelAllowlistConfigured: true, channelAllowed: true, @@ -29,7 +29,7 @@ describe("slack groupPolicy gating", () => { it("blocks allowlist when no channel allowlist configured", () => { expect( - isSlackRoomAllowedByPolicy({ + isSlackChannelAllowedByPolicy({ groupPolicy: "allowlist", channelAllowlistConfigured: false, channelAllowed: true, @@ -39,7 +39,7 @@ describe("slack groupPolicy gating", () => { it("allows allowlist when channel is allowed", () => { expect( - isSlackRoomAllowedByPolicy({ + isSlackChannelAllowedByPolicy({ groupPolicy: "allowlist", channelAllowlistConfigured: true, channelAllowed: true, @@ -49,7 +49,7 @@ describe("slack groupPolicy gating", () => { it("blocks allowlist when channel is not allowed", () => { expect( - isSlackRoomAllowedByPolicy({ + isSlackChannelAllowedByPolicy({ groupPolicy: "allowlist", channelAllowlistConfigured: true, channelAllowed: false, diff --git a/src/slack/monitor.ts b/src/slack/monitor.ts index 8462a2ea4..95b584eb3 100644 --- a/src/slack/monitor.ts +++ b/src/slack/monitor.ts @@ -1,5 +1,5 @@ export { buildSlackSlashCommandMatcher } from "./monitor/commands.js"; -export { isSlackRoomAllowedByPolicy } from "./monitor/policy.js"; +export { isSlackChannelAllowedByPolicy } from "./monitor/policy.js"; export { monitorSlackProvider } from "./monitor/provider.js"; export { resolveSlackThreadTs } from "./monitor/replies.js"; export type { MonitorSlackOpts } from "./monitor/types.js"; diff --git a/src/slack/monitor/context.ts b/src/slack/monitor/context.ts index 4120a068e..855908a5e 100644 --- a/src/slack/monitor/context.ts +++ b/src/slack/monitor/context.ts @@ -11,7 +11,7 @@ import type { SlackMessageEvent } from "../types.js"; import { normalizeAllowList, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js"; import { resolveSlackChannelConfig } from "./channel-config.js"; -import { isSlackRoomAllowedByPolicy } from "./policy.js"; +import { isSlackChannelAllowedByPolicy } from "./policy.js"; export function inferSlackChannelType( channelId?: string | null, @@ -314,7 +314,7 @@ export function createSlackMonitorContext(params: { const channelAllowlistConfigured = Boolean(params.channelsConfig) && Object.keys(params.channelsConfig ?? {}).length > 0; if ( - !isSlackRoomAllowedByPolicy({ + !isSlackChannelAllowedByPolicy({ groupPolicy: params.groupPolicy, channelAllowlistConfigured, channelAllowed, diff --git a/src/slack/monitor/policy.ts b/src/slack/monitor/policy.ts index 9832199ea..a4ac37917 100644 --- a/src/slack/monitor/policy.ts +++ b/src/slack/monitor/policy.ts @@ -1,4 +1,4 @@ -export function isSlackRoomAllowedByPolicy(params: { +export function isSlackChannelAllowedByPolicy(params: { groupPolicy: "open" | "disabled" | "allowlist"; channelAllowlistConfigured: boolean; channelAllowed: boolean; diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index b84da205d..6e1e0f684 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -33,7 +33,7 @@ import { import { resolveSlackChannelConfig, type SlackChannelConfigResolved } from "./channel-config.js"; import { buildSlackSlashCommandMatcher, resolveSlackSlashCommandConfig } from "./commands.js"; import type { SlackMonitorContext } from "./context.js"; -import { isSlackRoomAllowedByPolicy } from "./policy.js"; +import { isSlackChannelAllowedByPolicy } from "./policy.js"; import { deliverSlackSlashReplies } from "./replies.js"; type SlackBlock = { type: string; [key: string]: unknown }; @@ -247,7 +247,7 @@ export function registerSlackMonitorSlashCommands(params: { Boolean(ctx.channelsConfig) && Object.keys(ctx.channelsConfig ?? {}).length > 0; const channelAllowed = channelConfig?.allowed !== false; if ( - !isSlackRoomAllowedByPolicy({ + !isSlackChannelAllowedByPolicy({ groupPolicy: ctx.groupPolicy, channelAllowlistConfigured, channelAllowed,