fix(heartbeat): block dm targets and internalize blocked prompts

This commit is contained in:
Peter Steinberger
2026-02-25 02:02:26 +00:00
parent e0201c2774
commit a805d6b439
9 changed files with 161 additions and 6 deletions

View File

@@ -301,7 +301,7 @@ describe("resolveSessionDeliveryTarget", () => {
expect(resolved.to).toBe("63448508");
});
it("does not return inherited threadId from resolveHeartbeatDeliveryTarget", () => {
it("blocks heartbeat delivery to Slack DMs and avoids inherited threadId", () => {
const cfg: OpenClawConfig = {};
const resolved = resolveHeartbeatDeliveryTarget({
cfg,
@@ -317,11 +317,49 @@ describe("resolveSessionDeliveryTarget", () => {
},
});
expect(resolved.channel).toBe("slack");
expect(resolved.to).toBe("user:U123");
expect(resolved.channel).toBe("none");
expect(resolved.reason).toBe("dm-blocked");
expect(resolved.threadId).toBeUndefined();
});
it("blocks heartbeat delivery to Discord DMs", () => {
const cfg: OpenClawConfig = {};
const resolved = resolveHeartbeatDeliveryTarget({
cfg,
entry: {
sessionId: "sess-heartbeat-discord-dm",
updatedAt: 1,
lastChannel: "discord",
lastTo: "user:12345",
},
heartbeat: {
target: "last",
},
});
expect(resolved.channel).toBe("none");
expect(resolved.reason).toBe("dm-blocked");
});
it("keeps heartbeat delivery to Discord channels", () => {
const cfg: OpenClawConfig = {};
const resolved = resolveHeartbeatDeliveryTarget({
cfg,
entry: {
sessionId: "sess-heartbeat-discord-channel",
updatedAt: 1,
lastChannel: "discord",
lastTo: "channel:999",
},
heartbeat: {
target: "last",
},
});
expect(resolved.channel).toBe("discord");
expect(resolved.to).toBe("channel:999");
});
it("keeps explicit threadId in heartbeat mode", () => {
const resolved = resolveSessionDeliveryTarget({
entry: {

View File

@@ -1,10 +1,13 @@
import type { ChatType } from "../../channels/chat-type.js";
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.js";
import { formatCliCommand } from "../../cli/command-format.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js";
import type { AgentDefaultsConfig } from "../../config/types.agent-defaults.js";
import { parseDiscordTarget } from "../../discord/targets.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import { parseSlackTarget } from "../../slack/targets.js";
import { parseTelegramTarget } from "../../telegram/targets.js";
import { deliveryContextFromSession } from "../../utils/delivery-context.js";
import type {
@@ -319,6 +322,20 @@ export function resolveHeartbeatDeliveryTarget(params: {
};
}
const deliveryChatType = resolveHeartbeatDeliveryChatType({
channel: resolvedTarget.channel,
to: resolved.to,
});
if (deliveryChatType === "direct") {
return {
channel: "none",
reason: "dm-blocked",
accountId: effectiveAccountId,
lastChannel: resolvedTarget.lastChannel,
lastAccountId: resolvedTarget.lastAccountId,
};
}
let reason: string | undefined;
const plugin = getChannelPlugin(resolvedTarget.channel);
if (plugin?.config.resolveAllowFrom) {
@@ -345,6 +362,59 @@ export function resolveHeartbeatDeliveryTarget(params: {
};
}
function inferChatTypeFromTarget(params: {
channel: DeliverableMessageChannel;
to: string;
}): ChatType | undefined {
const to = params.to.trim();
if (!to) {
return undefined;
}
if (/^user:/i.test(to)) {
return "direct";
}
if (/^(channel:|thread:)/i.test(to)) {
return "channel";
}
if (/^group:/i.test(to)) {
return "group";
}
switch (params.channel) {
case "discord": {
try {
const target = parseDiscordTarget(to, { defaultKind: "channel" });
if (!target) {
return undefined;
}
return target.kind === "user" ? "direct" : "channel";
} catch {
return undefined;
}
}
case "slack": {
const target = parseSlackTarget(to, { defaultKind: "channel" });
if (!target) {
return undefined;
}
return target.kind === "user" ? "direct" : "channel";
}
default:
return undefined;
}
}
function resolveHeartbeatDeliveryChatType(params: {
channel: DeliverableMessageChannel;
to: string;
}): ChatType | undefined {
return inferChatTypeFromTarget({
channel: params.channel,
to: params.to,
});
}
function resolveHeartbeatSenderId(params: {
allowFrom: Array<string | number>;
deliveryTo?: string;