fix: unify inbound sender labels
parent
572e04d5fb
commit
f7089cde54
|
|
@ -36,6 +36,7 @@
|
||||||
- Gateway: honor explicit delivery targets without implicit accountId fallback; preserve lastAccountId for implicit routing.
|
- Gateway: honor explicit delivery targets without implicit accountId fallback; preserve lastAccountId for implicit routing.
|
||||||
- Repo: fix oxlint config filename and move ignore pattern into config. (#1064) — thanks @connorshea.
|
- Repo: fix oxlint config filename and move ignore pattern into config. (#1064) — thanks @connorshea.
|
||||||
- Messages: `/stop` now hard-aborts queued followups and sub-agent runs; suppress zero-count stop notes.
|
- Messages: `/stop` now hard-aborts queued followups and sub-agent runs; suppress zero-count stop notes.
|
||||||
|
- Messages: include sender labels for live group messages across channels, matching queued/history formatting. (#1059)
|
||||||
- Sessions: reset `compactionCount` on `/new` and `/reset`, and preserve `sessions.json` file mode (0600).
|
- Sessions: reset `compactionCount` on `/new` and `/reset`, and preserve `sessions.json` file mode (0600).
|
||||||
- Sessions: repair orphaned user turns before embedded prompts.
|
- Sessions: repair orphaned user turns before embedded prompts.
|
||||||
- Channels: treat replies to the bot as implicit mentions across supported channels.
|
- Channels: treat replies to the bot as implicit mentions across supported channels.
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,10 @@ When a channel supplies history, it uses a shared wrapper:
|
||||||
- `[Chat messages since your last reply - for context]`
|
- `[Chat messages since your last reply - for context]`
|
||||||
- `[Current message - respond to this]`
|
- `[Current message - respond to this]`
|
||||||
|
|
||||||
|
For **non-direct chats** (groups/channels/rooms), the **current message body** is prefixed with the
|
||||||
|
sender label (same style used for history entries). This keeps real-time and queued/history
|
||||||
|
messages consistent in the agent prompt.
|
||||||
|
|
||||||
History buffers are **pending-only**: they include group messages that did *not*
|
History buffers are **pending-only**: they include group messages that did *not*
|
||||||
trigger a run (for example, mention-gated messages) and **exclude** messages
|
trigger a run (for example, mention-gated messages) and **exclude** messages
|
||||||
already in the session transcript.
|
already in the session transcript.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { formatAgentEnvelope } from "./envelope.js";
|
import { formatAgentEnvelope, formatInboundEnvelope } from "./envelope.js";
|
||||||
|
|
||||||
describe("formatAgentEnvelope", () => {
|
describe("formatAgentEnvelope", () => {
|
||||||
it("includes channel, from, ip, host, and timestamp", () => {
|
it("includes channel, from, ip, host, and timestamp", () => {
|
||||||
|
|
@ -43,3 +43,38 @@ describe("formatAgentEnvelope", () => {
|
||||||
expect(body).toBe("[Telegram] hi");
|
expect(body).toBe("[Telegram] hi");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("formatInboundEnvelope", () => {
|
||||||
|
it("prefixes sender for non-direct chats", () => {
|
||||||
|
const body = formatInboundEnvelope({
|
||||||
|
channel: "Discord",
|
||||||
|
from: "Guild #general",
|
||||||
|
body: "hi",
|
||||||
|
chatType: "channel",
|
||||||
|
senderLabel: "Alice",
|
||||||
|
});
|
||||||
|
expect(body).toBe("[Discord Guild #general] Alice: hi");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses sender fields when senderLabel is missing", () => {
|
||||||
|
const body = formatInboundEnvelope({
|
||||||
|
channel: "Signal",
|
||||||
|
from: "Signal Group id:123",
|
||||||
|
body: "ping",
|
||||||
|
chatType: "group",
|
||||||
|
sender: { name: "Bob", id: "42" },
|
||||||
|
});
|
||||||
|
expect(body).toBe("[Signal Signal Group id:123] Bob (42): ping");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps direct messages unprefixed", () => {
|
||||||
|
const body = formatInboundEnvelope({
|
||||||
|
channel: "iMessage",
|
||||||
|
from: "+1555",
|
||||||
|
body: "hello",
|
||||||
|
chatType: "direct",
|
||||||
|
senderLabel: "Alice",
|
||||||
|
});
|
||||||
|
expect(body).toBe("[iMessage +1555] hello");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { normalizeChatType } from "../channels/chat-type.js";
|
||||||
|
import { resolveSenderLabel, type SenderLabelParams } from "../channels/sender-label.js";
|
||||||
|
|
||||||
export type AgentEnvelopeParams = {
|
export type AgentEnvelopeParams = {
|
||||||
channel: string;
|
channel: string;
|
||||||
from?: string;
|
from?: string;
|
||||||
|
|
@ -35,6 +38,27 @@ export function formatAgentEnvelope(params: AgentEnvelopeParams): string {
|
||||||
return `${header} ${params.body}`;
|
return `${header} ${params.body}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatInboundEnvelope(params: {
|
||||||
|
channel: string;
|
||||||
|
from: string;
|
||||||
|
body: string;
|
||||||
|
timestamp?: number | Date;
|
||||||
|
chatType?: string;
|
||||||
|
senderLabel?: string;
|
||||||
|
sender?: SenderLabelParams;
|
||||||
|
}): string {
|
||||||
|
const chatType = normalizeChatType(params.chatType);
|
||||||
|
const isDirect = !chatType || chatType === "direct";
|
||||||
|
const resolvedSender = params.senderLabel?.trim() || resolveSenderLabel(params.sender ?? {});
|
||||||
|
const body = !isDirect && resolvedSender ? `${resolvedSender}: ${params.body}` : params.body;
|
||||||
|
return formatAgentEnvelope({
|
||||||
|
channel: params.channel,
|
||||||
|
from: params.from,
|
||||||
|
timestamp: params.timestamp,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function formatThreadStarterEnvelope(params: {
|
export function formatThreadStarterEnvelope(params: {
|
||||||
channel: string;
|
channel: string;
|
||||||
author?: string;
|
author?: string;
|
||||||
|
|
|
||||||
|
|
@ -41,4 +41,11 @@ describe("formatInboundBodyWithSenderMeta", () => {
|
||||||
"[X] hi\n[from: Alice (A1)]",
|
"[X] hi\n[from: Alice (A1)]",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not append when the body already includes a sender prefix", () => {
|
||||||
|
const ctx: MsgContext = { ChatType: "group", SenderName: "Alice", SenderId: "A1" };
|
||||||
|
expect(formatInboundBodyWithSenderMeta({ ctx, body: "Alice (A1): hi" })).toBe(
|
||||||
|
"Alice (A1): hi",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,43 @@
|
||||||
import type { MsgContext } from "../templating.js";
|
import type { MsgContext } from "../templating.js";
|
||||||
import { normalizeChatType } from "../../channels/chat-type.js";
|
import { normalizeChatType } from "../../channels/chat-type.js";
|
||||||
|
import { listSenderLabelCandidates, resolveSenderLabel } from "../../channels/sender-label.js";
|
||||||
|
|
||||||
export function formatInboundBodyWithSenderMeta(params: { body: string; ctx: MsgContext }): string {
|
export function formatInboundBodyWithSenderMeta(params: { body: string; ctx: MsgContext }): string {
|
||||||
const body = params.body;
|
const body = params.body;
|
||||||
if (!body.trim()) return body;
|
if (!body.trim()) return body;
|
||||||
const chatType = normalizeChatType(params.ctx.ChatType);
|
const chatType = normalizeChatType(params.ctx.ChatType);
|
||||||
if (!chatType || chatType === "direct") return body;
|
if (!chatType || chatType === "direct") return body;
|
||||||
if (hasSenderMetaLine(body)) return body;
|
if (hasSenderMetaLine(body, params.ctx)) return body;
|
||||||
|
|
||||||
const senderLabel = formatSenderLabel(params.ctx);
|
const senderLabel = resolveSenderLabel({
|
||||||
|
name: params.ctx.SenderName,
|
||||||
|
username: params.ctx.SenderUsername,
|
||||||
|
tag: params.ctx.SenderTag,
|
||||||
|
e164: params.ctx.SenderE164,
|
||||||
|
id: params.ctx.SenderId,
|
||||||
|
});
|
||||||
if (!senderLabel) return body;
|
if (!senderLabel) return body;
|
||||||
|
|
||||||
return `${body}\n[from: ${senderLabel}]`;
|
return `${body}\n[from: ${senderLabel}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasSenderMetaLine(body: string): boolean {
|
function hasSenderMetaLine(body: string, ctx: MsgContext): boolean {
|
||||||
return /(^|\n)\[from:/i.test(body);
|
if (/(^|\n)\[from:/i.test(body)) return true;
|
||||||
|
const candidates = listSenderLabelCandidates({
|
||||||
|
name: ctx.SenderName,
|
||||||
|
username: ctx.SenderUsername,
|
||||||
|
tag: ctx.SenderTag,
|
||||||
|
e164: ctx.SenderE164,
|
||||||
|
id: ctx.SenderId,
|
||||||
|
});
|
||||||
|
if (candidates.length === 0) return false;
|
||||||
|
return candidates.some((candidate) => {
|
||||||
|
const escaped = escapeRegExp(candidate);
|
||||||
|
const pattern = new RegExp(`(^|\\n)${escaped}:\\s`, "i");
|
||||||
|
return pattern.test(body);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSenderLabel(ctx: MsgContext): string | null {
|
function escapeRegExp(value: string): string {
|
||||||
const senderName = ctx.SenderName?.trim();
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const senderId = (ctx.SenderE164?.trim() || ctx.SenderId?.trim()) ?? "";
|
|
||||||
if (senderName && senderId && senderName !== senderId) {
|
|
||||||
return `${senderName} (${senderId})`;
|
|
||||||
}
|
|
||||||
return senderName ?? (senderId || null);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
export type SenderLabelParams = {
|
||||||
|
name?: string;
|
||||||
|
username?: string;
|
||||||
|
tag?: string;
|
||||||
|
e164?: string;
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalize(value?: string): string | undefined {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
return trimmed ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSenderLabel(params: SenderLabelParams): string | null {
|
||||||
|
const name = normalize(params.name);
|
||||||
|
const username = normalize(params.username);
|
||||||
|
const tag = normalize(params.tag);
|
||||||
|
const e164 = normalize(params.e164);
|
||||||
|
const id = normalize(params.id);
|
||||||
|
|
||||||
|
const display = name ?? username ?? tag ?? "";
|
||||||
|
const idPart = e164 ?? id ?? "";
|
||||||
|
if (display && idPart && display !== idPart) return `${display} (${idPart})`;
|
||||||
|
return display || idPart || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listSenderLabelCandidates(params: SenderLabelParams): string[] {
|
||||||
|
const candidates = new Set<string>();
|
||||||
|
const name = normalize(params.name);
|
||||||
|
const username = normalize(params.username);
|
||||||
|
const tag = normalize(params.tag);
|
||||||
|
const e164 = normalize(params.e164);
|
||||||
|
const id = normalize(params.id);
|
||||||
|
|
||||||
|
if (name) candidates.add(name);
|
||||||
|
if (username) candidates.add(username);
|
||||||
|
if (tag) candidates.add(tag);
|
||||||
|
if (e164) candidates.add(e164);
|
||||||
|
if (id) candidates.add(id);
|
||||||
|
const resolved = resolveSenderLabel(params);
|
||||||
|
if (resolved) candidates.add(resolved);
|
||||||
|
return Array.from(candidates);
|
||||||
|
}
|
||||||
|
|
@ -369,6 +369,97 @@ describe("discord tool result dispatch", () => {
|
||||||
expect(capturedCtx?.SessionKey).toBe("agent:main:discord:channel:c1");
|
expect(capturedCtx?.SessionKey).toBe("agent:main:discord:channel:c1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefixes group bodies with sender label", async () => {
|
||||||
|
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||||
|
let capturedBody = "";
|
||||||
|
dispatchMock.mockImplementationOnce(async ({ ctx, dispatcher }) => {
|
||||||
|
capturedBody = ctx.Body ?? "";
|
||||||
|
dispatcher.sendFinalReply({ text: "ok" });
|
||||||
|
return { queuedFinal: true, counts: { final: 1 } };
|
||||||
|
});
|
||||||
|
|
||||||
|
const cfg = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: "/tmp/clawd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||||
|
channels: {
|
||||||
|
discord: {
|
||||||
|
dm: { enabled: true, policy: "open" },
|
||||||
|
guilds: {
|
||||||
|
"*": {
|
||||||
|
requireMention: false,
|
||||||
|
channels: { c1: { allow: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routing: { allowFrom: [] },
|
||||||
|
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||||
|
|
||||||
|
const handler = createDiscordMessageHandler({
|
||||||
|
cfg,
|
||||||
|
discordConfig: cfg.channels.discord,
|
||||||
|
accountId: "default",
|
||||||
|
token: "token",
|
||||||
|
runtime: {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: (code: number): never => {
|
||||||
|
throw new Error(`exit ${code}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
botUserId: "bot-id",
|
||||||
|
guildHistories: new Map(),
|
||||||
|
historyLimit: 0,
|
||||||
|
mediaMaxBytes: 10_000,
|
||||||
|
textLimit: 2000,
|
||||||
|
replyToMode: "off",
|
||||||
|
dmEnabled: true,
|
||||||
|
groupDmEnabled: false,
|
||||||
|
guildEntries: {
|
||||||
|
"*": { requireMention: false, channels: { c1: { allow: true } } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = {
|
||||||
|
fetchChannel: vi.fn().mockResolvedValue({
|
||||||
|
type: ChannelType.GuildText,
|
||||||
|
name: "general",
|
||||||
|
parentId: "category-1",
|
||||||
|
}),
|
||||||
|
rest: { get: vi.fn() },
|
||||||
|
} as unknown as Client;
|
||||||
|
|
||||||
|
await handler(
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
id: "m-prefix",
|
||||||
|
content: "hello",
|
||||||
|
channelId: "c1",
|
||||||
|
timestamp: new Date("2026-01-17T00:00:00Z").toISOString(),
|
||||||
|
type: MessageType.Default,
|
||||||
|
attachments: [],
|
||||||
|
embeds: [],
|
||||||
|
mentionedEveryone: false,
|
||||||
|
mentionedUsers: [],
|
||||||
|
mentionedRoles: [],
|
||||||
|
author: { id: "u1", bot: false, username: "Ada", discriminator: "1234" },
|
||||||
|
},
|
||||||
|
author: { id: "u1", bot: false, username: "Ada", discriminator: "1234" },
|
||||||
|
member: { displayName: "Ada" },
|
||||||
|
guild: { id: "g1", name: "Guild" },
|
||||||
|
guild_id: "g1",
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(capturedBody).toContain("Ada (Ada#1234): hello");
|
||||||
|
});
|
||||||
|
|
||||||
it("replies with pairing code and sender id when dmPolicy is pairing", async () => {
|
it("replies with pairing code and sender id when dmPolicy is pairing", async () => {
|
||||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||||
const cfg = {
|
const cfg = {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@ import {
|
||||||
extractShortModelName,
|
extractShortModelName,
|
||||||
type ResponsePrefixContext,
|
type ResponsePrefixContext,
|
||||||
} from "../../auto-reply/reply/response-prefix-template.js";
|
} from "../../auto-reply/reply/response-prefix-template.js";
|
||||||
import { formatAgentEnvelope, formatThreadStarterEnvelope } from "../../auto-reply/envelope.js";
|
import {
|
||||||
|
formatInboundEnvelope,
|
||||||
|
formatThreadStarterEnvelope,
|
||||||
|
} from "../../auto-reply/envelope.js";
|
||||||
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
|
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
|
||||||
import {
|
import {
|
||||||
buildPendingHistoryContextFromMap,
|
buildPendingHistoryContextFromMap,
|
||||||
|
|
@ -118,6 +121,12 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||||
channelName: channelName ?? message.channelId,
|
channelName: channelName ?? message.channelId,
|
||||||
channelId: message.channelId,
|
channelId: message.channelId,
|
||||||
});
|
});
|
||||||
|
const senderTag = formatDiscordUserTag(author);
|
||||||
|
const senderDisplay = data.member?.nickname ?? author.globalName ?? author.username;
|
||||||
|
const senderLabel =
|
||||||
|
senderDisplay && senderTag && senderDisplay !== senderTag
|
||||||
|
? `${senderDisplay} (${senderTag})`
|
||||||
|
: senderDisplay ?? senderTag ?? author.id;
|
||||||
const groupRoom = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined;
|
const groupRoom = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined;
|
||||||
const groupSubject = isDirectMessage ? undefined : groupRoom;
|
const groupSubject = isDirectMessage ? undefined : groupRoom;
|
||||||
const channelDescription = channelInfo?.topic?.trim();
|
const channelDescription = channelInfo?.topic?.trim();
|
||||||
|
|
@ -127,11 +136,13 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||||
].filter((entry): entry is string => Boolean(entry));
|
].filter((entry): entry is string => Boolean(entry));
|
||||||
const groupSystemPrompt =
|
const groupSystemPrompt =
|
||||||
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
|
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
|
||||||
let combinedBody = formatAgentEnvelope({
|
let combinedBody = formatInboundEnvelope({
|
||||||
channel: "Discord",
|
channel: "Discord",
|
||||||
from: fromLabel,
|
from: fromLabel,
|
||||||
timestamp: resolveTimestampMs(message.timestamp),
|
timestamp: resolveTimestampMs(message.timestamp),
|
||||||
body: text,
|
body: text,
|
||||||
|
chatType: isDirectMessage ? "direct" : "channel",
|
||||||
|
senderLabel,
|
||||||
});
|
});
|
||||||
const shouldIncludeChannelHistory =
|
const shouldIncludeChannelHistory =
|
||||||
!isDirectMessage && !(isGuildMessage && channelConfig?.autoThread && !threadChannel);
|
!isDirectMessage && !(isGuildMessage && channelConfig?.autoThread && !threadChannel);
|
||||||
|
|
@ -142,11 +153,13 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||||
limit: historyLimit,
|
limit: historyLimit,
|
||||||
currentMessage: combinedBody,
|
currentMessage: combinedBody,
|
||||||
formatEntry: (entry) =>
|
formatEntry: (entry) =>
|
||||||
formatAgentEnvelope({
|
formatInboundEnvelope({
|
||||||
channel: "Discord",
|
channel: "Discord",
|
||||||
from: fromLabel,
|
from: fromLabel,
|
||||||
timestamp: entry.timestamp,
|
timestamp: entry.timestamp,
|
||||||
body: `${entry.sender}: ${entry.body} [id:${entry.messageId ?? "unknown"} channel:${message.channelId}]`,
|
body: `${entry.body} [id:${entry.messageId ?? "unknown"} channel:${message.channelId}]`,
|
||||||
|
chatType: "channel",
|
||||||
|
senderLabel: entry.sender,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -461,4 +461,35 @@ describe("monitorIMessageProvider", () => {
|
||||||
|
|
||||||
expect(replyMock).not.toHaveBeenCalled();
|
expect(replyMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefixes group message bodies with sender", async () => {
|
||||||
|
const run = monitorIMessageProvider();
|
||||||
|
await waitForSubscribe();
|
||||||
|
|
||||||
|
notificationHandler?.({
|
||||||
|
method: "message",
|
||||||
|
params: {
|
||||||
|
message: {
|
||||||
|
id: 11,
|
||||||
|
chat_id: 99,
|
||||||
|
chat_name: "Test Group",
|
||||||
|
sender: "+15550001111",
|
||||||
|
is_from_me: false,
|
||||||
|
text: "@clawd hi",
|
||||||
|
is_group: true,
|
||||||
|
created_at: "2026-01-17T00:00:00Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flush();
|
||||||
|
closeResolve?.();
|
||||||
|
await run;
|
||||||
|
|
||||||
|
expect(replyMock).toHaveBeenCalled();
|
||||||
|
const ctx = replyMock.mock.calls[0]?.[0];
|
||||||
|
const body = ctx?.Body ?? "";
|
||||||
|
expect(body).toContain("Test Group id:99");
|
||||||
|
expect(body).toContain("+15550001111: @clawd hi");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from "../../auto-reply/reply/response-prefix-template.js";
|
} from "../../auto-reply/reply/response-prefix-template.js";
|
||||||
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
||||||
import { hasControlCommand } from "../../auto-reply/command-detection.js";
|
import { hasControlCommand } from "../../auto-reply/command-detection.js";
|
||||||
import { formatAgentEnvelope } from "../../auto-reply/envelope.js";
|
import { formatInboundEnvelope } from "../../auto-reply/envelope.js";
|
||||||
import {
|
import {
|
||||||
createInboundDebouncer,
|
createInboundDebouncer,
|
||||||
resolveInboundDebounceMs,
|
resolveInboundDebounceMs,
|
||||||
|
|
@ -363,11 +363,13 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||||
const fromLabel = isGroup
|
const fromLabel = isGroup
|
||||||
? `${message.chat_name || "iMessage Group"} id:${chatId ?? "unknown"}`
|
? `${message.chat_name || "iMessage Group"} id:${chatId ?? "unknown"}`
|
||||||
: `${senderNormalized} id:${sender}`;
|
: `${senderNormalized} id:${sender}`;
|
||||||
const body = formatAgentEnvelope({
|
const body = formatInboundEnvelope({
|
||||||
channel: "iMessage",
|
channel: "iMessage",
|
||||||
from: fromLabel,
|
from: fromLabel,
|
||||||
timestamp: createdAt,
|
timestamp: createdAt,
|
||||||
body: bodyText,
|
body: bodyText,
|
||||||
|
chatType: isGroup ? "group" : "direct",
|
||||||
|
sender: { name: senderNormalized, id: sender },
|
||||||
});
|
});
|
||||||
let combinedBody = body;
|
let combinedBody = body;
|
||||||
if (isGroup && historyKey && historyLimit > 0) {
|
if (isGroup && historyKey && historyLimit > 0) {
|
||||||
|
|
@ -377,13 +379,13 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||||
limit: historyLimit,
|
limit: historyLimit,
|
||||||
currentMessage: combinedBody,
|
currentMessage: combinedBody,
|
||||||
formatEntry: (entry) =>
|
formatEntry: (entry) =>
|
||||||
formatAgentEnvelope({
|
formatInboundEnvelope({
|
||||||
channel: "iMessage",
|
channel: "iMessage",
|
||||||
from: fromLabel,
|
from: fromLabel,
|
||||||
timestamp: entry.timestamp,
|
timestamp: entry.timestamp,
|
||||||
body: `${entry.sender}: ${entry.body}${
|
body: `${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`,
|
||||||
entry.messageId ? ` [id:${entry.messageId}]` : ""
|
chatType: "group",
|
||||||
}`,
|
senderLabel: entry.sender,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
|
||||||
|
const dispatchMock = vi.fn();
|
||||||
|
const readAllowFromMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("../pairing/pairing-store.js", () => ({
|
||||||
|
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromMock(...args),
|
||||||
|
upsertChannelPairingRequest: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
describe("signal event handler sender prefix", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
dispatchMock.mockReset().mockImplementation(async ({ dispatcher, ctx }) => {
|
||||||
|
dispatcher.sendFinalReply({ text: "ok" });
|
||||||
|
return { queuedFinal: true, counts: { final: 1 }, ctx };
|
||||||
|
});
|
||||||
|
readAllowFromMock.mockReset().mockResolvedValue([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefixes group bodies with sender label", async () => {
|
||||||
|
let capturedBody = "";
|
||||||
|
const dispatchModule = await import("../auto-reply/reply/dispatch-from-config.js");
|
||||||
|
vi.spyOn(dispatchModule, "dispatchReplyFromConfig").mockImplementation(
|
||||||
|
async (...args: unknown[]) => dispatchMock(...args),
|
||||||
|
);
|
||||||
|
dispatchMock.mockImplementationOnce(async ({ dispatcher, ctx }) => {
|
||||||
|
capturedBody = ctx.Body ?? "";
|
||||||
|
dispatcher.sendFinalReply({ text: "ok" });
|
||||||
|
return { queuedFinal: true, counts: { final: 1 } };
|
||||||
|
});
|
||||||
|
|
||||||
|
const { createSignalEventHandler } = await import("./monitor/event-handler.js");
|
||||||
|
const handler = createSignalEventHandler({
|
||||||
|
runtime: {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: (code: number): never => {
|
||||||
|
throw new Error(`exit ${code}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cfg: {
|
||||||
|
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" } },
|
||||||
|
channels: { signal: {} },
|
||||||
|
} as never,
|
||||||
|
baseUrl: "http://localhost",
|
||||||
|
account: "+15550009999",
|
||||||
|
accountId: "default",
|
||||||
|
blockStreaming: false,
|
||||||
|
historyLimit: 0,
|
||||||
|
groupHistories: new Map(),
|
||||||
|
textLimit: 4000,
|
||||||
|
dmPolicy: "open",
|
||||||
|
allowFrom: [],
|
||||||
|
groupAllowFrom: [],
|
||||||
|
groupPolicy: "open",
|
||||||
|
reactionMode: "off",
|
||||||
|
reactionAllowlist: [],
|
||||||
|
mediaMaxBytes: 1000,
|
||||||
|
ignoreAttachments: true,
|
||||||
|
fetchAttachment: async () => null,
|
||||||
|
deliverReplies: async () => undefined,
|
||||||
|
resolveSignalReactionTargets: () => [],
|
||||||
|
isSignalReactionMessage: () => false,
|
||||||
|
shouldEmitSignalReactionNotification: () => false,
|
||||||
|
buildSignalReactionSystemEventText: () => "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
envelope: {
|
||||||
|
sourceNumber: "+15550002222",
|
||||||
|
sourceName: "Alice",
|
||||||
|
timestamp: 1700000000000,
|
||||||
|
dataMessage: {
|
||||||
|
message: "hello",
|
||||||
|
groupInfo: { groupId: "group-1", groupName: "Test Group" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await handler({ event: "receive", data: JSON.stringify(payload) });
|
||||||
|
|
||||||
|
expect(dispatchMock).toHaveBeenCalled();
|
||||||
|
expect(capturedBody).toContain("Alice (+15550002222): hello");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
type ResponsePrefixContext,
|
type ResponsePrefixContext,
|
||||||
} from "../../auto-reply/reply/response-prefix-template.js";
|
} from "../../auto-reply/reply/response-prefix-template.js";
|
||||||
import { hasControlCommand } from "../../auto-reply/command-detection.js";
|
import { hasControlCommand } from "../../auto-reply/command-detection.js";
|
||||||
import { formatAgentEnvelope } from "../../auto-reply/envelope.js";
|
import { formatInboundEnvelope } from "../../auto-reply/envelope.js";
|
||||||
import {
|
import {
|
||||||
createInboundDebouncer,
|
createInboundDebouncer,
|
||||||
resolveInboundDebounceMs,
|
resolveInboundDebounceMs,
|
||||||
|
|
@ -68,11 +68,13 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||||
const fromLabel = entry.isGroup
|
const fromLabel = entry.isGroup
|
||||||
? `${entry.groupName ?? "Signal Group"} id:${entry.groupId}`
|
? `${entry.groupName ?? "Signal Group"} id:${entry.groupId}`
|
||||||
: `${entry.senderName} id:${entry.senderDisplay}`;
|
: `${entry.senderName} id:${entry.senderDisplay}`;
|
||||||
const body = formatAgentEnvelope({
|
const body = formatInboundEnvelope({
|
||||||
channel: "Signal",
|
channel: "Signal",
|
||||||
from: fromLabel,
|
from: fromLabel,
|
||||||
timestamp: entry.timestamp ?? undefined,
|
timestamp: entry.timestamp ?? undefined,
|
||||||
body: entry.bodyText,
|
body: entry.bodyText,
|
||||||
|
chatType: entry.isGroup ? "group" : "direct",
|
||||||
|
sender: { name: entry.senderName, id: entry.senderDisplay },
|
||||||
});
|
});
|
||||||
let combinedBody = body;
|
let combinedBody = body;
|
||||||
const historyKey = entry.isGroup ? String(entry.groupId ?? "unknown") : undefined;
|
const historyKey = entry.isGroup ? String(entry.groupId ?? "unknown") : undefined;
|
||||||
|
|
@ -83,13 +85,15 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||||
limit: deps.historyLimit,
|
limit: deps.historyLimit,
|
||||||
currentMessage: combinedBody,
|
currentMessage: combinedBody,
|
||||||
formatEntry: (historyEntry) =>
|
formatEntry: (historyEntry) =>
|
||||||
formatAgentEnvelope({
|
formatInboundEnvelope({
|
||||||
channel: "Signal",
|
channel: "Signal",
|
||||||
from: fromLabel,
|
from: fromLabel,
|
||||||
timestamp: historyEntry.timestamp,
|
timestamp: historyEntry.timestamp,
|
||||||
body: `${historyEntry.sender}: ${historyEntry.body}${
|
body: `${historyEntry.body}${
|
||||||
historyEntry.messageId ? ` [id:${historyEntry.messageId}]` : ""
|
historyEntry.messageId ? ` [id:${historyEntry.messageId}]` : ""
|
||||||
}`,
|
}`,
|
||||||
|
chatType: "group",
|
||||||
|
senderLabel: historyEntry.sender,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { SlackMonitorContext } from "../context.js";
|
||||||
|
import { prepareSlackMessage } from "./prepare.js";
|
||||||
|
|
||||||
|
describe("prepareSlackMessage sender prefix", () => {
|
||||||
|
it("prefixes channel bodies with sender label", async () => {
|
||||||
|
const ctx = {
|
||||||
|
cfg: {
|
||||||
|
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" } },
|
||||||
|
channels: { slack: {} },
|
||||||
|
},
|
||||||
|
accountId: "default",
|
||||||
|
botToken: "xoxb",
|
||||||
|
app: { client: {} },
|
||||||
|
runtime: {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: (code: number): never => {
|
||||||
|
throw new Error(`exit ${code}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
botUserId: "BOT",
|
||||||
|
teamId: "T1",
|
||||||
|
apiAppId: "A1",
|
||||||
|
historyLimit: 0,
|
||||||
|
channelHistories: new Map(),
|
||||||
|
sessionScope: "per-sender",
|
||||||
|
mainKey: "agent:main:main",
|
||||||
|
dmEnabled: true,
|
||||||
|
dmPolicy: "open",
|
||||||
|
allowFrom: [],
|
||||||
|
groupDmEnabled: false,
|
||||||
|
groupDmChannels: [],
|
||||||
|
defaultRequireMention: true,
|
||||||
|
groupPolicy: "open",
|
||||||
|
useAccessGroups: false,
|
||||||
|
reactionMode: "off",
|
||||||
|
reactionAllowlist: [],
|
||||||
|
replyToMode: "off",
|
||||||
|
threadHistoryScope: "channel",
|
||||||
|
threadInheritParent: false,
|
||||||
|
slashCommand: { command: "/clawd", enabled: true },
|
||||||
|
textLimit: 2000,
|
||||||
|
ackReactionScope: "off",
|
||||||
|
mediaMaxBytes: 1000,
|
||||||
|
removeAckAfterReply: false,
|
||||||
|
logger: { info: vi.fn() },
|
||||||
|
markMessageSeen: () => false,
|
||||||
|
shouldDropMismatchedSlackEvent: () => false,
|
||||||
|
resolveSlackSystemEventSessionKey: () => "agent:main:slack:channel:C1",
|
||||||
|
isChannelAllowed: () => true,
|
||||||
|
resolveChannelName: async () => ({
|
||||||
|
name: "general",
|
||||||
|
type: "channel",
|
||||||
|
}),
|
||||||
|
resolveUserName: async () => ({ name: "Alice" }),
|
||||||
|
setSlackThreadStatus: async () => undefined,
|
||||||
|
} satisfies SlackMonitorContext;
|
||||||
|
|
||||||
|
const result = await prepareSlackMessage({
|
||||||
|
ctx,
|
||||||
|
account: { accountId: "default", config: {} } as never,
|
||||||
|
message: {
|
||||||
|
type: "message",
|
||||||
|
channel: "C1",
|
||||||
|
channel_type: "channel",
|
||||||
|
text: "<@BOT> hello",
|
||||||
|
user: "U1",
|
||||||
|
ts: "1700000000.0001",
|
||||||
|
event_ts: "1700000000.0001",
|
||||||
|
} as never,
|
||||||
|
opts: { source: "message", wasMentioned: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
const body = result?.ctxPayload.Body ?? "";
|
||||||
|
expect(body).toContain("Alice (U1): <@BOT> hello");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { resolveAckReaction } from "../../../agents/identity.js";
|
import { resolveAckReaction } from "../../../agents/identity.js";
|
||||||
import { hasControlCommand } from "../../../auto-reply/command-detection.js";
|
import { hasControlCommand } from "../../../auto-reply/command-detection.js";
|
||||||
import { shouldHandleTextCommands } from "../../../auto-reply/commands-registry.js";
|
import { shouldHandleTextCommands } from "../../../auto-reply/commands-registry.js";
|
||||||
import { formatAgentEnvelope, formatThreadStarterEnvelope } from "../../../auto-reply/envelope.js";
|
import {
|
||||||
|
formatInboundEnvelope,
|
||||||
|
formatThreadStarterEnvelope,
|
||||||
|
} from "../../../auto-reply/envelope.js";
|
||||||
import {
|
import {
|
||||||
buildPendingHistoryContextFromMap,
|
buildPendingHistoryContextFromMap,
|
||||||
recordPendingHistoryEntry,
|
recordPendingHistoryEntry,
|
||||||
|
|
@ -340,11 +343,13 @@ export async function prepareSlackMessage(params: {
|
||||||
From: slackFrom,
|
From: slackFrom,
|
||||||
}) ?? (isDirectMessage ? senderName : roomLabel);
|
}) ?? (isDirectMessage ? senderName : roomLabel);
|
||||||
const textWithId = `${rawBody}\n[slack message id: ${message.ts} channel: ${message.channel}]`;
|
const textWithId = `${rawBody}\n[slack message id: ${message.ts} channel: ${message.channel}]`;
|
||||||
const body = formatAgentEnvelope({
|
const body = formatInboundEnvelope({
|
||||||
channel: "Slack",
|
channel: "Slack",
|
||||||
from: envelopeFrom,
|
from: envelopeFrom,
|
||||||
timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
|
timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
|
||||||
body: textWithId,
|
body: textWithId,
|
||||||
|
chatType: isDirectMessage ? "direct" : "channel",
|
||||||
|
sender: { name: senderName, id: senderId },
|
||||||
});
|
});
|
||||||
|
|
||||||
let combinedBody = body;
|
let combinedBody = body;
|
||||||
|
|
@ -355,13 +360,15 @@ export async function prepareSlackMessage(params: {
|
||||||
limit: ctx.historyLimit,
|
limit: ctx.historyLimit,
|
||||||
currentMessage: combinedBody,
|
currentMessage: combinedBody,
|
||||||
formatEntry: (entry) =>
|
formatEntry: (entry) =>
|
||||||
formatAgentEnvelope({
|
formatInboundEnvelope({
|
||||||
channel: "Slack",
|
channel: "Slack",
|
||||||
from: roomLabel,
|
from: roomLabel,
|
||||||
timestamp: entry.timestamp,
|
timestamp: entry.timestamp,
|
||||||
body: `${entry.sender}: ${entry.body}${
|
body: `${entry.body}${
|
||||||
entry.messageId ? ` [id:${entry.messageId} channel:${message.channel}]` : ""
|
entry.messageId ? ` [id:${entry.messageId} channel:${message.channel}]` : ""
|
||||||
}`,
|
}`,
|
||||||
|
chatType: "channel",
|
||||||
|
senderLabel: entry.sender,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { buildTelegramMessageContext } from "./bot-message-context.js";
|
||||||
|
|
||||||
|
describe("buildTelegramMessageContext sender prefix", () => {
|
||||||
|
it("prefixes group bodies with sender label", async () => {
|
||||||
|
const ctx = await buildTelegramMessageContext({
|
||||||
|
primaryCtx: {
|
||||||
|
message: {
|
||||||
|
message_id: 1,
|
||||||
|
chat: { id: -99, type: "supergroup", title: "Dev Chat" },
|
||||||
|
date: 1700000000,
|
||||||
|
text: "hello",
|
||||||
|
from: { id: 42, first_name: "Alice" },
|
||||||
|
},
|
||||||
|
me: { id: 7, username: "bot" },
|
||||||
|
} as never,
|
||||||
|
allMedia: [],
|
||||||
|
storeAllowFrom: [],
|
||||||
|
options: {},
|
||||||
|
bot: {
|
||||||
|
api: {
|
||||||
|
sendChatAction: vi.fn(),
|
||||||
|
setMessageReaction: vi.fn(),
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
cfg: {
|
||||||
|
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" } },
|
||||||
|
channels: { telegram: {} },
|
||||||
|
messages: { groupChat: { mentionPatterns: [] } },
|
||||||
|
} as never,
|
||||||
|
account: { accountId: "default" } as never,
|
||||||
|
historyLimit: 0,
|
||||||
|
groupHistories: new Map(),
|
||||||
|
dmPolicy: "open",
|
||||||
|
allowFrom: [],
|
||||||
|
groupAllowFrom: [],
|
||||||
|
ackReactionScope: "off",
|
||||||
|
logger: { info: vi.fn() },
|
||||||
|
resolveGroupActivation: () => undefined,
|
||||||
|
resolveGroupRequireMention: () => false,
|
||||||
|
resolveTelegramGroupConfig: () => ({
|
||||||
|
groupConfig: { requireMention: false },
|
||||||
|
topicConfig: undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ctx).not.toBeNull();
|
||||||
|
const body = ctx?.ctxPayload?.Body ?? "";
|
||||||
|
expect(body).toContain("Alice (42): hello");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { resolveAckReaction } from "../agents/identity.js";
|
import { resolveAckReaction } from "../agents/identity.js";
|
||||||
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
||||||
import { normalizeCommandBody } from "../auto-reply/commands-registry.js";
|
import { normalizeCommandBody } from "../auto-reply/commands-registry.js";
|
||||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
import { formatInboundEnvelope } from "../auto-reply/envelope.js";
|
||||||
import {
|
import {
|
||||||
buildPendingHistoryContextFromMap,
|
buildPendingHistoryContextFromMap,
|
||||||
recordPendingHistoryEntry,
|
recordPendingHistoryEntry,
|
||||||
|
|
@ -325,14 +325,21 @@ export const buildTelegramMessageContext = async ({
|
||||||
}]\n${replyTarget.body}\n[/Replying]`
|
}]\n${replyTarget.body}\n[/Replying]`
|
||||||
: "";
|
: "";
|
||||||
const groupLabel = isGroup ? buildGroupLabel(msg, chatId, resolvedThreadId) : undefined;
|
const groupLabel = isGroup ? buildGroupLabel(msg, chatId, resolvedThreadId) : undefined;
|
||||||
|
const senderName = buildSenderName(msg);
|
||||||
const conversationLabel = isGroup
|
const conversationLabel = isGroup
|
||||||
? (groupLabel ?? `group:${chatId}`)
|
? (groupLabel ?? `group:${chatId}`)
|
||||||
: buildSenderLabel(msg, senderId || chatId);
|
: buildSenderLabel(msg, senderId || chatId);
|
||||||
const body = formatAgentEnvelope({
|
const body = formatInboundEnvelope({
|
||||||
channel: "Telegram",
|
channel: "Telegram",
|
||||||
from: conversationLabel,
|
from: conversationLabel,
|
||||||
timestamp: msg.date ? msg.date * 1000 : undefined,
|
timestamp: msg.date ? msg.date * 1000 : undefined,
|
||||||
body: `${bodyText}${replySuffix}`,
|
body: `${bodyText}${replySuffix}`,
|
||||||
|
chatType: isGroup ? "group" : "direct",
|
||||||
|
sender: {
|
||||||
|
name: senderName,
|
||||||
|
username: senderUsername || undefined,
|
||||||
|
id: senderId || undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let combinedBody = body;
|
let combinedBody = body;
|
||||||
if (isGroup && historyKey && historyLimit > 0) {
|
if (isGroup && historyKey && historyLimit > 0) {
|
||||||
|
|
@ -342,11 +349,13 @@ export const buildTelegramMessageContext = async ({
|
||||||
limit: historyLimit,
|
limit: historyLimit,
|
||||||
currentMessage: combinedBody,
|
currentMessage: combinedBody,
|
||||||
formatEntry: (entry) =>
|
formatEntry: (entry) =>
|
||||||
formatAgentEnvelope({
|
formatInboundEnvelope({
|
||||||
channel: "Telegram",
|
channel: "Telegram",
|
||||||
from: groupLabel ?? `group:${chatId}`,
|
from: groupLabel ?? `group:${chatId}`,
|
||||||
timestamp: entry.timestamp,
|
timestamp: entry.timestamp,
|
||||||
body: `${entry.sender}: ${entry.body} [id:${entry.messageId ?? "unknown"} chat:${chatId}]`,
|
body: `${entry.body} [id:${entry.messageId ?? "unknown"} chat:${chatId}]`,
|
||||||
|
chatType: "group",
|
||||||
|
senderLabel: entry.sender,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -371,7 +380,7 @@ export const buildTelegramMessageContext = async ({
|
||||||
ConversationLabel: conversationLabel,
|
ConversationLabel: conversationLabel,
|
||||||
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
|
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
|
||||||
GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
|
GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
|
||||||
SenderName: buildSenderName(msg),
|
SenderName: senderName,
|
||||||
SenderId: senderId || undefined,
|
SenderId: senderId || undefined,
|
||||||
SenderUsername: senderUsername || undefined,
|
SenderUsername: senderUsername || undefined,
|
||||||
Provider: "telegram",
|
Provider: "telegram",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { buildInboundLine } from "./message-line.js";
|
||||||
|
|
||||||
|
describe("buildInboundLine", () => {
|
||||||
|
it("prefixes group messages with sender", () => {
|
||||||
|
const line = buildInboundLine({
|
||||||
|
cfg: {
|
||||||
|
agents: { defaults: { workspace: "/tmp/clawd" } },
|
||||||
|
channels: { whatsapp: { messagePrefix: "" } },
|
||||||
|
} as never,
|
||||||
|
agentId: "main",
|
||||||
|
msg: {
|
||||||
|
from: "123@g.us",
|
||||||
|
conversationId: "123@g.us",
|
||||||
|
to: "+15550009999",
|
||||||
|
accountId: "default",
|
||||||
|
body: "ping",
|
||||||
|
timestamp: 1700000000000,
|
||||||
|
chatType: "group",
|
||||||
|
chatId: "123@g.us",
|
||||||
|
senderJid: "111@s.whatsapp.net",
|
||||||
|
senderE164: "+15550001111",
|
||||||
|
senderName: "Bob",
|
||||||
|
sendComposing: async () => undefined,
|
||||||
|
reply: async () => undefined,
|
||||||
|
sendMedia: async () => undefined,
|
||||||
|
} as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(line).toContain("Bob (+15550001111):");
|
||||||
|
expect(line).toContain("ping");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { resolveMessagePrefix } from "../../../agents/identity.js";
|
import { resolveMessagePrefix } from "../../../agents/identity.js";
|
||||||
import { formatAgentEnvelope } from "../../../auto-reply/envelope.js";
|
import { formatInboundEnvelope } from "../../../auto-reply/envelope.js";
|
||||||
import type { loadConfig } from "../../../config/config.js";
|
import type { loadConfig } from "../../../config/config.js";
|
||||||
import type { WebInboundMsg } from "../types.js";
|
import type { WebInboundMsg } from "../types.js";
|
||||||
|
|
||||||
|
|
@ -26,10 +26,16 @@ export function buildInboundLine(params: {
|
||||||
const baseLine = `${prefixStr}${msg.body}${replyContext ? `\n\n${replyContext}` : ""}`;
|
const baseLine = `${prefixStr}${msg.body}${replyContext ? `\n\n${replyContext}` : ""}`;
|
||||||
|
|
||||||
// Wrap with standardized envelope for the agent.
|
// Wrap with standardized envelope for the agent.
|
||||||
return formatAgentEnvelope({
|
return formatInboundEnvelope({
|
||||||
channel: "WhatsApp",
|
channel: "WhatsApp",
|
||||||
from: msg.chatType === "group" ? msg.from : msg.from?.replace(/^whatsapp:/, ""),
|
from: msg.chatType === "group" ? msg.from : msg.from?.replace(/^whatsapp:/, ""),
|
||||||
timestamp: msg.timestamp,
|
timestamp: msg.timestamp,
|
||||||
body: baseLine,
|
body: baseLine,
|
||||||
|
chatType: msg.chatType,
|
||||||
|
sender: {
|
||||||
|
name: msg.senderName,
|
||||||
|
e164: msg.senderE164,
|
||||||
|
id: msg.senderJid,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
type ResponsePrefixContext,
|
type ResponsePrefixContext,
|
||||||
} from "../../../auto-reply/reply/response-prefix-template.js";
|
} from "../../../auto-reply/reply/response-prefix-template.js";
|
||||||
import { resolveTextChunkLimit } from "../../../auto-reply/chunk.js";
|
import { resolveTextChunkLimit } from "../../../auto-reply/chunk.js";
|
||||||
import { formatAgentEnvelope } from "../../../auto-reply/envelope.js";
|
import { formatInboundEnvelope } from "../../../auto-reply/envelope.js";
|
||||||
import {
|
import {
|
||||||
buildHistoryContextFromEntries,
|
buildHistoryContextFromEntries,
|
||||||
type HistoryEntry,
|
type HistoryEntry,
|
||||||
|
|
@ -95,11 +95,13 @@ export async function processMessage(params: {
|
||||||
const bodyWithId = entry.messageId
|
const bodyWithId = entry.messageId
|
||||||
? `${entry.body}\n[message_id: ${entry.messageId}]`
|
? `${entry.body}\n[message_id: ${entry.messageId}]`
|
||||||
: entry.body;
|
: entry.body;
|
||||||
return formatAgentEnvelope({
|
return formatInboundEnvelope({
|
||||||
channel: "WhatsApp",
|
channel: "WhatsApp",
|
||||||
from: conversationId,
|
from: conversationId,
|
||||||
timestamp: entry.timestamp,
|
timestamp: entry.timestamp,
|
||||||
body: `${entry.sender}: ${bodyWithId}`,
|
body: bodyWithId,
|
||||||
|
chatType: "group",
|
||||||
|
senderLabel: entry.sender,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue