refactor: unify peer kind to ChatType, rename dm to direct (#11881)

* fix: use .js extension for ESM imports of RoutePeerKind

The imports incorrectly used .ts extension which doesn't resolve
with moduleResolution: NodeNext. Changed to .js and added 'type'
import modifier.

* fix tsconfig

* refactor: unify peer kind to ChatType, rename dm to direct

- Replace RoutePeerKind with ChatType throughout codebase
- Change 'dm' literal values to 'direct' in routing/session keys
- Keep backward compat: normalizeChatType accepts 'dm' -> 'direct'
- Add ChatType export to plugin-sdk, deprecate RoutePeerKind
- Update session key parsing to accept both 'dm' and 'direct' markers
- Update all channel monitors and extensions to use ChatType

BREAKING CHANGE: Session keys now use 'direct' instead of 'dm'.
Existing 'dm' keys still work via backward compat layer.

* fix tests

* test: update session key expectations for dmdirect migration

- Fix test expectations to expect :direct: in generated output
- Add explicit backward compat test for normalizeChatType('dm')
- Keep input test data with :dm: keys to verify backward compat

* fix: accept legacy 'dm' in session key parsing for backward compat

getDmHistoryLimitFromSessionKey now accepts both :dm: and :direct:
to ensure old session keys continue to work correctly.

* test: add explicit backward compat tests for dmdirect migration

- session-key.test.ts: verify both :dm: and :direct: keys are valid
- getDmHistoryLimitFromSessionKey: verify both formats work

* feat: backward compat for resetByType.dm config key

* test: skip unix-path Nix tests on Windows
This commit is contained in:
max
2026-02-08 16:20:52 -08:00
committed by GitHub
parent 0b07e15b63
commit 223eee0a20
64 changed files with 377 additions and 185 deletions

View File

@@ -1804,7 +1804,7 @@ async function processMessage(
channel: "bluebubbles",
accountId: account.accountId,
peer: {
kind: isGroup ? "group" : "dm",
kind: isGroup ? "group" : "direct",
id: peerId,
},
});
@@ -2442,7 +2442,7 @@ async function processReaction(
channel: "bluebubbles",
accountId: account.accountId,
peer: {
kind: reaction.isGroup ? "group" : "dm",
kind: reaction.isGroup ? "group" : "direct",
id: peerId,
},
});

View File

@@ -652,7 +652,7 @@ export async function handleFeishuMessage(params: {
channel: "feishu",
accountId: account.accountId,
peer: {
kind: isGroup ? "group" : "dm",
kind: isGroup ? "group" : "direct",
id: isGroup ? ctx.chatId : ctx.senderOpenId,
},
});

View File

@@ -615,7 +615,7 @@ async function processMessageWithPipeline(params: {
channel: "googlechat",
accountId: account.accountId,
peer: {
kind: isGroup ? "group" : "dm",
kind: isGroup ? "group" : "direct",
id: spaceId,
},
});

View File

@@ -453,7 +453,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
cfg,
channel: "matrix",
peer: {
kind: isDirectMessage ? "dm" : "channel",
kind: isDirectMessage ? "direct" : "channel",
id: isDirectMessage ? senderId : roomId,
},
});

View File

@@ -1,5 +1,6 @@
import type {
ChannelAccountSnapshot,
ChatType,
OpenClawConfig,
ReplyPayload,
RuntimeEnv,
@@ -131,13 +132,13 @@ function isSystemPost(post: MattermostPost): boolean {
return Boolean(type);
}
function channelKind(channelType?: string | null): "dm" | "group" | "channel" {
function channelKind(channelType?: string | null): ChatType {
if (!channelType) {
return "channel";
}
const normalized = channelType.trim().toUpperCase();
if (normalized === "D") {
return "dm";
return "direct";
}
if (normalized === "G") {
return "group";
@@ -145,8 +146,8 @@ function channelKind(channelType?: string | null): "dm" | "group" | "channel" {
return "channel";
}
function channelChatType(kind: "dm" | "group" | "channel"): "direct" | "group" | "channel" {
if (kind === "dm") {
function channelChatType(kind: ChatType): "direct" | "group" | "channel" {
if (kind === "direct") {
return "direct";
}
if (kind === "group") {
@@ -469,11 +470,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
hasControlCommand,
});
const commandAuthorized =
kind === "dm"
kind === "direct"
? dmPolicy === "open" || senderAllowedForCommands
: commandGate.commandAuthorized;
if (kind === "dm") {
if (kind === "direct") {
if (dmPolicy === "disabled") {
logVerboseMessage(`mattermost: drop dm (dmPolicy=disabled sender=${senderId})`);
return;
@@ -524,7 +525,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
}
}
if (kind !== "dm" && commandGate.shouldBlock) {
if (kind !== "direct" && commandGate.shouldBlock) {
logInboundDrop({
log: logVerboseMessage,
channel: "mattermost",
@@ -547,7 +548,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
teamId,
peer: {
kind,
id: kind === "dm" ? senderId : channelId,
id: kind === "direct" ? senderId : channelId,
},
});
@@ -559,11 +560,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
parentSessionKey: threadRootId ? baseSessionKey : undefined,
});
const sessionKey = threadKeys.sessionKey;
const historyKey = kind === "dm" ? null : sessionKey;
const historyKey = kind === "direct" ? null : sessionKey;
const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg, route.agentId);
const wasMentioned =
kind !== "dm" &&
kind !== "direct" &&
((botUsername ? rawText.toLowerCase().includes(`@${botUsername.toLowerCase()}`) : false) ||
core.channel.mentions.matchesMentionPatterns(rawText, mentionRegexes));
const pendingBody =
@@ -590,7 +591,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
});
};
const oncharEnabled = account.chatmode === "onchar" && kind !== "dm";
const oncharEnabled = account.chatmode === "onchar" && kind !== "direct";
const oncharPrefixes = oncharEnabled ? resolveOncharPrefixes(account.oncharPrefixes) : [];
const oncharResult = oncharEnabled
? stripOncharPrefix(rawText, oncharPrefixes)
@@ -598,7 +599,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
const oncharTriggered = oncharResult.triggered;
const shouldRequireMention =
kind !== "dm" &&
kind !== "direct" &&
core.channel.groups.resolveRequireMention({
cfg,
channel: "mattermost",
@@ -615,7 +616,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
return;
}
if (kind !== "dm" && shouldRequireMention && canDetectMention) {
if (kind !== "direct" && shouldRequireMention && canDetectMention) {
if (!effectiveWasMentioned) {
recordPendingHistory();
return;
@@ -637,7 +638,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
});
const fromLabel = formatInboundFromLabel({
isGroup: kind !== "dm",
isGroup: kind !== "direct",
groupLabel: channelDisplay || roomLabel,
groupId: channelId,
groupFallback: roomLabel || "Channel",
@@ -647,7 +648,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
const preview = bodyText.replace(/\s+/g, " ").slice(0, 160);
const inboundLabel =
kind === "dm"
kind === "direct"
? `Mattermost DM from ${senderName}`
: `Mattermost message in ${roomLabel} from ${senderName}`;
core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
@@ -685,14 +686,14 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
});
}
const to = kind === "dm" ? `user:${senderId}` : `channel:${channelId}`;
const to = kind === "direct" ? `user:${senderId}` : `channel:${channelId}`;
const mediaPayload = buildMattermostMediaPayload(mediaList);
const ctxPayload = core.channel.reply.finalizeInboundContext({
Body: combinedBody,
RawBody: bodyText,
CommandBody: bodyText,
From:
kind === "dm"
kind === "direct"
? `mattermost:${senderId}`
: kind === "group"
? `mattermost:group:${channelId}`
@@ -703,7 +704,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
AccountId: route.accountId,
ChatType: chatType,
ConversationLabel: fromLabel,
GroupSubject: kind !== "dm" ? channelDisplay || roomLabel : undefined,
GroupSubject: kind !== "direct" ? channelDisplay || roomLabel : undefined,
GroupChannel: channelName ? `#${channelName}` : undefined,
GroupSpace: teamId,
SenderName: senderName,
@@ -718,14 +719,14 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
ReplyToId: threadRootId,
MessageThreadId: threadRootId,
Timestamp: typeof post.create_at === "number" ? post.create_at : undefined,
WasMentioned: kind !== "dm" ? effectiveWasMentioned : undefined,
WasMentioned: kind !== "direct" ? effectiveWasMentioned : undefined,
CommandAuthorized: commandAuthorized,
OriginatingChannel: "mattermost" as const,
OriginatingTo: to,
...mediaPayload,
});
if (kind === "dm") {
if (kind === "direct") {
const sessionCfg = cfg.session;
const storePath = core.channel.session.resolveStorePath(sessionCfg?.store, {
agentId: route.agentId,

View File

@@ -342,7 +342,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
cfg,
channel: "msteams",
peer: {
kind: isDirectMessage ? "dm" : isChannel ? "channel" : "group",
kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
id: isDirectMessage ? senderId : conversationId,
},
});

View File

@@ -228,7 +228,7 @@ export async function handleNextcloudTalkInbound(params: {
channel: CHANNEL_ID,
accountId: account.accountId,
peer: {
kind: isGroup ? "group" : "dm",
kind: isGroup ? "group" : "direct",
id: isGroup ? roomToken : senderId,
},
});

View File

@@ -101,7 +101,7 @@ const tlonOutbound: ChannelOutboundAdapter = {
error: new Error(`Invalid Tlon target. Use ${formatTargetHint()}`),
};
}
if (parsed.kind === "dm") {
if (parsed.kind === "direct") {
return { ok: true, to: parsed.ship };
}
return { ok: true, to: parsed.nest };
@@ -127,7 +127,7 @@ const tlonOutbound: ChannelOutboundAdapter = {
try {
const fromShip = normalizeShip(account.ship);
if (parsed.kind === "dm") {
if (parsed.kind === "direct") {
return await sendDm({
api,
fromShip,
@@ -298,7 +298,7 @@ export const tlonPlugin: ChannelPlugin = {
if (!parsed) {
return target.trim();
}
if (parsed.kind === "dm") {
if (parsed.kind === "direct") {
return parsed.ship;
}
return parsed.nest;

View File

@@ -343,7 +343,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
channel: "tlon",
accountId: opts.accountId ?? undefined,
peer: {
kind: isGroup ? "group" : "dm",
kind: isGroup ? "group" : "direct",
id: isGroup ? (groupChannel ?? senderShip) : senderShip,
},
});

View File

@@ -1,5 +1,5 @@
export type TlonTarget =
| { kind: "dm"; ship: string }
| { kind: "direct"; ship: string }
| { kind: "group"; nest: string; hostShip: string; channelName: string };
const SHIP_RE = /^~?[a-z-]+$/i;
@@ -32,7 +32,7 @@ export function parseTlonTarget(raw?: string | null): TlonTarget | null {
const dmPrefix = withoutPrefix.match(/^dm[/:](.+)$/i);
if (dmPrefix) {
return { kind: "dm", ship: normalizeShip(dmPrefix[1]) };
return { kind: "direct", ship: normalizeShip(dmPrefix[1]) };
}
const groupPrefix = withoutPrefix.match(/^(group|room)[/:](.+)$/i);
@@ -78,7 +78,7 @@ export function parseTlonTarget(raw?: string | null): TlonTarget | null {
}
if (SHIP_RE.test(withoutPrefix)) {
return { kind: "dm", ship: normalizeShip(withoutPrefix) };
return { kind: "direct", ship: normalizeShip(withoutPrefix) };
}
return null;

View File

@@ -515,7 +515,7 @@ async function processMessageWithPipeline(params: {
channel: "zalo",
accountId: account.accountId,
peer: {
kind: isGroup ? "group" : "dm",
kind: isGroup ? "group" : "direct",
id: chatId,
},
});