refactor!: rename chat providers to channels

This commit is contained in:
Peter Steinberger
2026-01-13 06:16:43 +00:00
parent 0cd632ba84
commit 90342a4f3a
393 changed files with 8004 additions and 6737 deletions

View File

@@ -21,6 +21,9 @@ import {
resolveSandboxRuntimeStatus,
} from "../../agents/sandbox.js";
import { hasNonzeroUsage, type NormalizedUsage } from "../../agents/usage.js";
import { getChannelDock } from "../../channels/dock.js";
import type { ChannelThreadingToolContext } from "../../channels/plugins/types.js";
import { normalizeChannelId } from "../../channels/registry.js";
import type { ClawdbotConfig } from "../../config/config.js";
import {
loadSessionStore,
@@ -37,9 +40,6 @@ import {
registerAgentRunContext,
} from "../../infra/agent-events.js";
import { isAudioFileName } from "../../media/mime.js";
import { getProviderDock } from "../../providers/dock.js";
import type { ProviderThreadingToolContext } from "../../providers/plugins/types.js";
import { normalizeProviderId } from "../../providers/registry.js";
import { defaultRuntime } from "../../runtime.js";
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
import {
@@ -96,19 +96,19 @@ function buildThreadingToolContext(params: {
sessionCtx: TemplateContext;
config: ClawdbotConfig | undefined;
hasRepliedRef: { value: boolean } | undefined;
}): ProviderThreadingToolContext {
}): ChannelThreadingToolContext {
const { sessionCtx, config, hasRepliedRef } = params;
if (!config) return {};
const provider = normalizeProviderId(sessionCtx.Provider);
const provider = normalizeChannelId(sessionCtx.Provider);
if (!provider) return {};
const dock = getProviderDock(provider);
const dock = getChannelDock(provider);
if (!dock?.threading?.buildToolContext) return {};
return (
dock.threading.buildToolContext({
cfg: config,
accountId: sessionCtx.AccountId,
context: {
Provider: sessionCtx.Provider,
Channel: sessionCtx.Provider,
To: sessionCtx.To,
ReplyToId: sessionCtx.ReplyToId,
ThreadLabel: sessionCtx.ThreadLabel,

View File

@@ -1,17 +1,17 @@
import { getChannelDock } from "../../channels/dock.js";
import { CHANNEL_IDS, normalizeChannelId } from "../../channels/registry.js";
import type { ClawdbotConfig } from "../../config/config.js";
import type { BlockStreamingCoalesceConfig } from "../../config/types.js";
import { getProviderDock } from "../../providers/dock.js";
import { normalizeProviderId, PROVIDER_IDS } from "../../providers/registry.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import { INTERNAL_MESSAGE_PROVIDER } from "../../utils/message-provider.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
import { resolveTextChunkLimit, type TextChunkProvider } from "../chunk.js";
const DEFAULT_BLOCK_STREAM_MIN = 800;
const DEFAULT_BLOCK_STREAM_MAX = 1200;
const DEFAULT_BLOCK_STREAM_COALESCE_IDLE_MS = 1000;
const BLOCK_CHUNK_PROVIDERS = new Set<TextChunkProvider>([
...PROVIDER_IDS,
INTERNAL_MESSAGE_PROVIDER,
...CHANNEL_IDS,
INTERNAL_MESSAGE_CHANNEL,
]);
function normalizeChunkProvider(
@@ -64,9 +64,9 @@ export function resolveBlockStreamingChunking(
breakPreference: "paragraph" | "newline" | "sentence";
} {
const providerKey = normalizeChunkProvider(provider);
const providerId = providerKey ? normalizeProviderId(providerKey) : null;
const providerId = providerKey ? normalizeChannelId(providerKey) : null;
const providerChunkLimit = providerId
? getProviderDock(providerId)?.outbound?.textChunkLimit
? getChannelDock(providerId)?.outbound?.textChunkLimit
: undefined;
const textLimit = resolveTextChunkLimit(cfg, providerKey, accountId, {
fallbackLimit: providerChunkLimit,
@@ -102,15 +102,15 @@ export function resolveBlockStreamingCoalescing(
},
): BlockStreamingCoalescing | undefined {
const providerKey = normalizeChunkProvider(provider);
const providerId = providerKey ? normalizeProviderId(providerKey) : null;
const providerId = providerKey ? normalizeChannelId(providerKey) : null;
const providerChunkLimit = providerId
? getProviderDock(providerId)?.outbound?.textChunkLimit
? getChannelDock(providerId)?.outbound?.textChunkLimit
: undefined;
const textLimit = resolveTextChunkLimit(cfg, providerKey, accountId, {
fallbackLimit: providerChunkLimit,
});
const providerDefaults = providerId
? getProviderDock(providerId)?.streaming?.blockStreamingCoalesceDefaults
? getChannelDock(providerId)?.streaming?.blockStreamingCoalesceDefaults
: undefined;
const providerCfg = resolveProviderBlockStreamingCoalesce({
cfg,

View File

@@ -83,7 +83,7 @@ describe("handleCommands gating", () => {
it("blocks /config when disabled", async () => {
const cfg = {
commands: { config: false, debug: false, text: true },
whatsapp: { allowFrom: ["*"] },
channels: { whatsapp: { allowFrom: ["*"] } },
} as ClawdbotConfig;
const params = buildParams("/config show", cfg);
const result = await handleCommands(params);
@@ -94,7 +94,7 @@ describe("handleCommands gating", () => {
it("blocks /debug when disabled", async () => {
const cfg = {
commands: { config: false, debug: false, text: true },
whatsapp: { allowFrom: ["*"] },
channels: { whatsapp: { allowFrom: ["*"] } },
} as ClawdbotConfig;
const params = buildParams("/debug show", cfg);
const result = await handleCommands(params);
@@ -133,7 +133,7 @@ describe("handleCommands identity", () => {
it("returns sender details for /whoami", async () => {
const cfg = {
commands: { text: true },
whatsapp: { allowFrom: ["*"] },
channels: { whatsapp: { allowFrom: ["*"] } },
} as ClawdbotConfig;
const params = buildParams("/whoami", cfg, {
SenderId: "12345",
@@ -142,7 +142,7 @@ describe("handleCommands identity", () => {
});
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Provider: whatsapp");
expect(result.reply?.text).toContain("Channel: whatsapp");
expect(result.reply?.text).toContain("User id: 12345");
expect(result.reply?.text).toContain("Username: @TestUser");
expect(result.reply?.text).toContain("AllowFrom: 12345");

View File

@@ -19,6 +19,7 @@ import {
isEmbeddedPiRunActive,
waitForEmbeddedPiRunEnd,
} from "../../agents/pi-embedded.js";
import type { ChannelId } from "../../channels/plugins/types.js";
import type { ClawdbotConfig } from "../../config/config.js";
import {
readConfigFileSnapshot,
@@ -54,7 +55,6 @@ import {
triggerClawdbotRestart,
} from "../../infra/restart.js";
import { enqueueSystemEvent } from "../../infra/system-events.js";
import type { ProviderId } from "../../providers/plugins/types.js";
import { parseAgentSessionKey } from "../../routing/session-key.js";
import { resolveSendPolicy } from "../../sessions/send-policy.js";
import { resolveCommandAuthorization } from "../command-auth.js";
@@ -108,8 +108,8 @@ function resolveSessionEntryForKey(
export type CommandContext = {
surface: string;
provider: string;
providerId?: ProviderId;
channel: string;
channelId?: ChannelId;
ownerList: string[];
isAuthorizedSender: boolean;
senderId?: string;
@@ -189,7 +189,7 @@ export async function buildStatusReply(params: {
}
const queueSettings = resolveQueueSettings({
cfg,
provider: command.provider,
channel: command.channel,
sessionEntry,
});
const queueKey = sessionKey ?? sessionEntry?.sessionId;
@@ -347,7 +347,7 @@ export function buildCommandContext(params: {
commandAuthorized: params.commandAuthorized,
});
const surface = (ctx.Surface ?? ctx.Provider ?? "").trim().toLowerCase();
const provider = (ctx.Provider ?? surface).trim().toLowerCase();
const channel = (ctx.Provider ?? surface).trim().toLowerCase();
const abortKey =
sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
const rawBodyNormalized = triggerBodyNormalized;
@@ -359,8 +359,8 @@ export function buildCommandContext(params: {
return {
surface,
provider,
providerId: auth.providerId,
channel,
channelId: auth.providerId,
ownerList: auth.ownerList,
isAuthorizedSender: auth.isAuthorizedSender,
senderId: auth.senderId,
@@ -677,7 +677,7 @@ export async function handleCommands(params: {
}
const senderId = ctx.SenderId ?? "";
const senderUsername = ctx.SenderUsername ?? "";
const lines = ["🧭 Identity", `Provider: ${command.provider}`];
const lines = ["🧭 Identity", `Channel: ${command.channel}`];
if (senderId) lines.push(`User id: ${senderId}`);
if (senderUsername) {
const handle = senderUsername.startsWith("@")
@@ -980,7 +980,7 @@ export async function handleCommands(params: {
const result = await compactEmbeddedPiSession({
sessionId,
sessionKey,
messageProvider: command.provider,
messageChannel: command.channel,
sessionFile: resolveSessionFilePath(sessionId, sessionEntry),
workspaceDir,
config: cfg,
@@ -1056,7 +1056,7 @@ export async function handleCommands(params: {
cfg,
entry: sessionEntry,
sessionKey,
provider: sessionEntry?.provider ?? command.provider,
channel: sessionEntry?.channel ?? command.channel,
chatType: sessionEntry?.chatType,
});
if (sendPolicy === "deny") {

View File

@@ -1150,7 +1150,7 @@ export async function handleDirectiveOnly(params: {
) {
const settings = resolveQueueSettings({
cfg: params.cfg,
provider,
channel: provider,
sessionEntry,
});
const debounceLabel =

View File

@@ -7,12 +7,14 @@ import { resolveGroupRequireMention } from "./groups.js";
describe("resolveGroupRequireMention", () => {
it("respects Discord guild/channel requireMention settings", () => {
const cfg: ClawdbotConfig = {
discord: {
guilds: {
"145": {
requireMention: false,
channels: {
general: { allow: true },
channels: {
discord: {
guilds: {
"145": {
requireMention: false,
channels: {
general: { allow: true },
},
},
},
},
@@ -25,7 +27,7 @@ describe("resolveGroupRequireMention", () => {
GroupSpace: "145",
};
const groupResolution: GroupKeyResolution = {
provider: "discord",
channel: "discord",
id: "123",
chatType: "group",
};
@@ -37,9 +39,11 @@ describe("resolveGroupRequireMention", () => {
it("respects Slack channel requireMention settings", () => {
const cfg: ClawdbotConfig = {
slack: {
channels: {
C123: { requireMention: false },
channels: {
slack: {
channels: {
C123: { requireMention: false },
},
},
},
};
@@ -49,7 +53,7 @@ describe("resolveGroupRequireMention", () => {
GroupSubject: "#general",
};
const groupResolution: GroupKeyResolution = {
provider: "slack",
channel: "slack",
id: "C123",
chatType: "group",
};

View File

@@ -1,14 +1,14 @@
import { getChannelDock } from "../../channels/dock.js";
import {
getChatChannelMeta,
normalizeChannelId,
} from "../../channels/registry.js";
import type { ClawdbotConfig } from "../../config/config.js";
import type {
GroupKeyResolution,
SessionEntry,
} from "../../config/sessions.js";
import { getProviderDock } from "../../providers/dock.js";
import {
getChatProviderMeta,
normalizeProviderId,
} from "../../providers/registry.js";
import { isInternalMessageProvider } from "../../utils/message-provider.js";
import { isInternalMessageChannel } from "../../utils/message-channel.js";
import { normalizeGroupActivation } from "../group-activation.js";
import type { TemplateContext } from "../templating.js";
@@ -18,14 +18,14 @@ export function resolveGroupRequireMention(params: {
groupResolution?: GroupKeyResolution;
}): boolean {
const { cfg, ctx, groupResolution } = params;
const rawProvider = groupResolution?.provider ?? ctx.Provider?.trim();
const provider = normalizeProviderId(rawProvider);
if (!provider) return true;
const rawChannel = groupResolution?.channel ?? ctx.Provider?.trim();
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 groupSpace = ctx.GroupSpace?.trim();
const requireMention = getProviderDock(
provider,
const requireMention = getChannelDock(
channel,
)?.groups?.resolveRequireMention?.({
cfg,
groupId,
@@ -57,11 +57,11 @@ export function buildGroupIntro(params: {
const members = params.sessionCtx.GroupMembers?.trim();
const rawProvider = params.sessionCtx.Provider?.trim();
const providerKey = rawProvider?.toLowerCase() ?? "";
const providerId = normalizeProviderId(rawProvider);
const providerId = normalizeChannelId(rawProvider);
const providerLabel = (() => {
if (!providerKey) return "chat";
if (isInternalMessageProvider(providerKey)) return "WebChat";
if (providerId) return getChatProviderMeta(providerId).label;
if (isInternalMessageChannel(providerKey)) return "WebChat";
if (providerId) return getChatChannelMeta(providerId).label;
return `${providerKey.at(0)?.toUpperCase() ?? ""}${providerKey.slice(1)}`;
})();
const subjectLine = subject
@@ -76,7 +76,7 @@ export function buildGroupIntro(params: {
const groupRoom = params.sessionCtx.GroupRoom?.trim() ?? subject;
const groupSpace = params.sessionCtx.GroupSpace?.trim();
const providerIdsLine = providerId
? getProviderDock(providerId)?.groups?.resolveGroupIntroHint?.({
? getChannelDock(providerId)?.groups?.resolveGroupIntroHint?.({
cfg: params.cfg,
groupId,
groupRoom,

View File

@@ -1,7 +1,7 @@
import { resolveAgentConfig } from "../../agents/agent-scope.js";
import { getChannelDock } from "../../channels/dock.js";
import { normalizeChannelId } from "../../channels/registry.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { getProviderDock } from "../../providers/dock.js";
import { normalizeProviderId } from "../../providers/registry.js";
import type { MsgContext } from "../templating.js";
function escapeRegExp(text: string): string {
@@ -114,9 +114,9 @@ export function stripMentions(
agentId?: string,
): string {
let result = text;
const providerId = ctx.Provider ? normalizeProviderId(ctx.Provider) : null;
const providerId = ctx.Provider ? normalizeChannelId(ctx.Provider) : null;
const providerMentions = providerId
? getProviderDock(providerId)?.mentions
? getChannelDock(providerId)?.mentions
: undefined;
const patterns = normalizeMentionPatterns([
...resolveMentionPatterns(cfg, agentId),

View File

@@ -577,28 +577,28 @@ export function scheduleFollowupDrain(
}
})();
}
function defaultQueueModeForProvider(_provider?: string): QueueMode {
function defaultQueueModeForChannel(_channel?: string): QueueMode {
return "collect";
}
export function resolveQueueSettings(params: {
cfg: ClawdbotConfig;
provider?: string;
channel?: string;
sessionEntry?: SessionEntry;
inlineMode?: QueueMode;
inlineOptions?: Partial<QueueSettings>;
}): QueueSettings {
const providerKey = params.provider?.trim().toLowerCase();
const channelKey = params.channel?.trim().toLowerCase();
const queueCfg = params.cfg.messages?.queue;
const providerModeRaw =
providerKey && queueCfg?.byProvider
? (queueCfg.byProvider as Record<string, string | undefined>)[providerKey]
channelKey && queueCfg?.byChannel
? (queueCfg.byChannel as Record<string, string | undefined>)[channelKey]
: undefined;
const resolvedMode =
params.inlineMode ??
normalizeQueueMode(params.sessionEntry?.queueMode) ??
normalizeQueueMode(providerModeRaw) ??
normalizeQueueMode(queueCfg?.mode) ??
defaultQueueModeForProvider(providerKey);
defaultQueueModeForChannel(channelKey);
const debounceRaw =
params.inlineOptions?.debounceMs ??
params.sessionEntry?.queueDebounceMs ??

View File

@@ -24,9 +24,11 @@ describe("resolveReplyToMode", () => {
it("uses configured value when present", () => {
const cfg = {
telegram: { replyToMode: "all" },
discord: { replyToMode: "first" },
slack: { replyToMode: "all" },
channels: {
telegram: { replyToMode: "all" },
discord: { replyToMode: "first" },
slack: { replyToMode: "all" },
},
} as ClawdbotConfig;
expect(resolveReplyToMode(cfg, "telegram")).toBe("all");
expect(resolveReplyToMode(cfg, "discord")).toBe("first");

View File

@@ -1,7 +1,7 @@
import { getChannelDock } from "../../channels/dock.js";
import { normalizeChannelId } from "../../channels/registry.js";
import type { ClawdbotConfig } from "../../config/config.js";
import type { ReplyToMode } from "../../config/types.js";
import { getProviderDock } from "../../providers/dock.js";
import { normalizeProviderId } from "../../providers/registry.js";
import type { OriginatingChannelType } from "../templating.js";
import type { ReplyPayload } from "../types.js";
@@ -10,9 +10,9 @@ export function resolveReplyToMode(
channel?: OriginatingChannelType,
accountId?: string | null,
): ReplyToMode {
const provider = normalizeProviderId(channel);
const provider = normalizeChannelId(channel);
if (!provider) return "all";
const resolved = getProviderDock(provider)?.threading?.resolveReplyToMode?.({
const resolved = getChannelDock(provider)?.threading?.resolveReplyToMode?.({
cfg,
accountId,
});
@@ -43,9 +43,9 @@ export function createReplyToModeFilterForChannel(
mode: ReplyToMode,
channel?: OriginatingChannelType,
) {
const provider = normalizeProviderId(channel);
const provider = normalizeChannelId(channel);
const allowTagsWhenOff = provider
? Boolean(getProviderDock(provider)?.threading?.allowTagsWhenOff)
? Boolean(getChannelDock(provider)?.threading?.allowTagsWhenOff)
: false;
return createReplyToModeFilter(mode, {
allowTagsWhenOff,

View File

@@ -226,8 +226,10 @@ describe("routeReply", () => {
it("routes MS Teams via proactive sender", async () => {
mocks.sendMessageMSTeams.mockClear();
const cfg = {
msteams: {
enabled: true,
channels: {
msteams: {
enabled: true,
},
},
} as unknown as ClawdbotConfig;
await routeReply({

View File

@@ -9,9 +9,9 @@
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
import { resolveEffectiveMessagesConfig } from "../../agents/identity.js";
import { normalizeChannelId } from "../../channels/registry.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { normalizeProviderId } from "../../providers/registry.js";
import { INTERNAL_MESSAGE_PROVIDER } from "../../utils/message-provider.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
import type { OriginatingChannelType } from "../templating.js";
import type { ReplyPayload } from "../types.js";
import { normalizeReplyPayload } from "./normalize-reply.js";
@@ -88,15 +88,15 @@ export async function routeReply(
return { ok: true };
}
if (channel === INTERNAL_MESSAGE_PROVIDER) {
if (channel === INTERNAL_MESSAGE_CHANNEL) {
return {
ok: false,
error: "Webchat routing not supported for queued replies",
};
}
const provider = normalizeProviderId(channel) ?? null;
if (!provider) {
const channelId = normalizeChannelId(channel) ?? null;
if (!channelId) {
return { ok: false, error: `Unknown channel: ${String(channel)}` };
}
if (abortSignal?.aborted) {
@@ -111,7 +111,7 @@ export async function routeReply(
);
const results = await deliverOutboundPayloads({
cfg,
provider,
channel: channelId,
to,
accountId: accountId ?? undefined,
payloads: [normalized],
@@ -138,10 +138,7 @@ export async function routeReply(
*/
export function isRoutableChannel(
channel: OriginatingChannelType | undefined,
): channel is Exclude<
OriginatingChannelType,
typeof INTERNAL_MESSAGE_PROVIDER
> {
if (!channel || channel === INTERNAL_MESSAGE_PROVIDER) return false;
return normalizeProviderId(channel) !== null;
): channel is Exclude<OriginatingChannelType, typeof INTERNAL_MESSAGE_CHANNEL> {
if (!channel || channel === INTERNAL_MESSAGE_CHANNEL) return false;
return normalizeChannelId(channel) !== null;
}

View File

@@ -3,8 +3,8 @@ import crypto from "node:crypto";
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
import { buildProviderSummary } from "../../infra/provider-summary.js";
import { drainSystemEventEntries } from "../../infra/system-events.js";
import { buildChannelSummary } from "../../infra/channel-summary.js";
export async function prependSystemEvents(params: {
cfg: ClawdbotConfig;
@@ -48,7 +48,7 @@ export async function prependSystemEvents(params: {
.filter((v): v is string => Boolean(v)),
);
if (params.isMainSession && params.isNewSession) {
const summary = await buildProviderSummary(params.cfg);
const summary = await buildChannelSummary(params.cfg);
if (summary.length > 0) systemLines.unshift(...summary);
}
if (systemLines.length === 0) return params.prefixedBodyBase;

View File

@@ -7,6 +7,8 @@ import {
SessionManager,
} from "@mariozechner/pi-coding-agent";
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
import { getChannelDock } from "../../channels/dock.js";
import { normalizeChannelId } from "../../channels/registry.js";
import type { ClawdbotConfig } from "../../config/config.js";
import {
buildGroupDisplayName,
@@ -23,8 +25,6 @@ import {
type SessionScope,
saveSessionStore,
} from "../../config/sessions.js";
import { getProviderDock } from "../../providers/dock.js";
import { normalizeProviderId } from "../../providers/registry.js";
import { normalizeMainKey } from "../../routing/session-key.js";
import { resolveCommandAuthorization } from "../command-auth.js";
import type { MsgContext, TemplateContext } from "../templating.js";
@@ -228,20 +228,20 @@ export async function initSessionState(params: {
queueDrop: baseEntry?.queueDrop,
displayName: baseEntry?.displayName,
chatType: baseEntry?.chatType,
provider: baseEntry?.provider,
channel: baseEntry?.channel,
subject: baseEntry?.subject,
room: baseEntry?.room,
space: baseEntry?.space,
};
if (groupResolution?.provider) {
const provider = groupResolution.provider;
if (groupResolution?.channel) {
const channel = groupResolution.channel;
const subject = ctx.GroupSubject?.trim();
const space = ctx.GroupSpace?.trim();
const explicitRoom = ctx.GroupRoom?.trim();
const normalizedProvider = normalizeProviderId(provider);
const normalizedChannel = normalizeChannelId(channel);
const isRoomProvider = Boolean(
normalizedProvider &&
getProviderDock(normalizedProvider)?.capabilities.chatTypes.includes(
normalizedChannel &&
getChannelDock(normalizedChannel)?.capabilities.chatTypes.includes(
"channel",
),
);
@@ -252,12 +252,12 @@ export async function initSessionState(params: {
: undefined);
const nextSubject = nextRoom ? undefined : subject;
sessionEntry.chatType = groupResolution.chatType ?? "group";
sessionEntry.provider = provider;
sessionEntry.channel = channel;
if (nextSubject) sessionEntry.subject = nextSubject;
if (nextRoom) sessionEntry.room = nextRoom;
if (space) sessionEntry.space = space;
sessionEntry.displayName = buildGroupDisplayName({
provider: sessionEntry.provider,
provider: sessionEntry.channel,
subject: sessionEntry.subject,
room: sessionEntry.room,
space: sessionEntry.space,