feat(messages): add whatsapp messagePrefix and responsePrefix auto

This commit is contained in:
Peter Steinberger
2026-01-09 19:28:59 +00:00
parent 0a4cb0d264
commit 2977b296e6
10 changed files with 112 additions and 16 deletions

View File

@@ -77,7 +77,7 @@ Details: [Thinking + reasoning directives](/tools/thinking) and [Token use](/tok
## Prefixes, threading, and replies ## Prefixes, threading, and replies
Outbound message formatting is centralized in `messages`: Outbound message formatting is centralized in `messages`:
- `messages.responsePrefix` and `messages.messagePrefix` - `messages.responsePrefix` (outbound prefix) and `whatsapp.messagePrefix` (WhatsApp inbound prefix)
- Reply threading via `replyToMode` and per-provider defaults - Reply threading via `replyToMode` and per-provider defaults
Details: [Configuration](/gateway/configuration#messages) and provider docs. Details: [Configuration](/gateway/configuration#messages) and provider docs.

View File

@@ -926,8 +926,7 @@ See [Messages](/concepts/messages) for queueing, sessions, and streaming context
```json5 ```json5
{ {
messages: { messages: {
messagePrefix: "[clawdbot]", responsePrefix: "🦞", // or "auto"
responsePrefix: "🦞",
ackReaction: "👀", ackReaction: "👀",
ackReactionScope: "group-mentions" ackReactionScope: "group-mentions"
} }
@@ -938,11 +937,13 @@ See [Messages](/concepts/messages) for queueing, sessions, and streaming context
streaming, final replies) across providers unless already present. streaming, final replies) across providers unless already present.
If `messages.responsePrefix` is unset, no prefix is applied by default. If `messages.responsePrefix` is unset, no prefix is applied by default.
Set it to `"auto"` to derive `[{identity.name}]` for the routed agent (when set).
If `messages.messagePrefix` is unset, the default stays **unchanged**: WhatsApp inbound prefix is configured via `whatsapp.messagePrefix` (deprecated:
`"[clawdbot]"` when `whatsapp.allowFrom` is empty, otherwise `""` (no prefix). `messages.messagePrefix`). Default stays **unchanged**: `"[clawdbot]"` when
When using `"[clawdbot]"`, Clawdbot will instead use `[{identity.name}]` when `whatsapp.allowFrom` is empty, otherwise `""` (no prefix). When using
the routed agent has `identity.name` set. `"[clawdbot]"`, Clawdbot will instead use `[{identity.name}]` when the routed
agent has `identity.name` set.
`ackReaction` sends a best-effort emoji reaction to acknowledge inbound messages `ackReaction` sends a best-effort emoji reaction to acknowledge inbound messages
on providers that support reactions (Slack/Discord/Telegram). Defaults to the on providers that support reactions (Slack/Discord/Telegram). Defaults to the

View File

@@ -197,7 +197,7 @@ Behavior:
- `whatsapp.actions.reactions` (gate WhatsApp tool reactions). - `whatsapp.actions.reactions` (gate WhatsApp tool reactions).
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) - `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`)
- `messages.groupChat.historyLimit` - `messages.groupChat.historyLimit`
- `messages.messagePrefix` (inbound prefix) - `whatsapp.messagePrefix` (inbound prefix; per-account: `whatsapp.accounts.<accountId>.messagePrefix`; deprecated: `messages.messagePrefix`)
- `messages.responsePrefix` (outbound prefix) - `messages.responsePrefix` (outbound prefix)
- `agents.defaults.mediaMaxMb` - `agents.defaults.mediaMaxMb`
- `agents.defaults.heartbeat.every` - `agents.defaults.heartbeat.every`

View File

@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import type { ClawdbotConfig } from "../config/config.js";
import { resolveMessagePrefix, resolveResponsePrefix } from "./identity.js";
describe("message prefix resolution", () => {
it("returns configured messagePrefix override", () => {
const cfg: ClawdbotConfig = {};
expect(
resolveMessagePrefix(cfg, "main", {
configured: "[x]",
hasAllowFrom: true,
}),
).toBe("[x]");
expect(
resolveMessagePrefix(cfg, "main", {
configured: "",
hasAllowFrom: false,
}),
).toBe("");
});
it("defaults messagePrefix based on allowFrom + identity", () => {
const cfg: ClawdbotConfig = {
agents: { list: [{ id: "main", identity: { name: "Richbot" } }] },
};
expect(resolveMessagePrefix(cfg, "main", { hasAllowFrom: true })).toBe("");
expect(resolveMessagePrefix(cfg, "main", { hasAllowFrom: false })).toBe(
"[Richbot]",
);
});
it("falls back to [clawdbot] when identity is missing", () => {
const cfg: ClawdbotConfig = {};
expect(resolveMessagePrefix(cfg, "main", { hasAllowFrom: false })).toBe(
"[clawdbot]",
);
});
});
describe("response prefix resolution", () => {
it("does not apply any default when unset", () => {
const cfg: ClawdbotConfig = {
agents: { list: [{ id: "main", identity: { name: "Richbot" } }] },
};
expect(resolveResponsePrefix(cfg, "main")).toBeUndefined();
});
it("returns explicit responsePrefix when set", () => {
const cfg: ClawdbotConfig = { messages: { responsePrefix: "PFX" } };
expect(resolveResponsePrefix(cfg, "main")).toBe("PFX");
});
it("supports responsePrefix: auto (identity-derived opt-in)", () => {
const withIdentity: ClawdbotConfig = {
agents: { list: [{ id: "main", identity: { name: "Richbot" } }] },
messages: { responsePrefix: "auto" },
};
expect(resolveResponsePrefix(withIdentity, "main")).toBe("[Richbot]");
const withoutIdentity: ClawdbotConfig = {
messages: { responsePrefix: "auto" },
};
expect(resolveResponsePrefix(withoutIdentity, "main")).toBeUndefined();
});
});

View File

@@ -32,9 +32,9 @@ export function resolveIdentityNamePrefix(
export function resolveMessagePrefix( export function resolveMessagePrefix(
cfg: ClawdbotConfig, cfg: ClawdbotConfig,
agentId: string, agentId: string,
opts?: { hasAllowFrom?: boolean; fallback?: string }, opts?: { configured?: string; hasAllowFrom?: boolean; fallback?: string },
): string { ): string {
const configured = cfg.messages?.messagePrefix; const configured = opts?.configured ?? cfg.messages?.messagePrefix;
if (configured !== undefined) return configured; if (configured !== undefined) return configured;
const hasAllowFrom = opts?.hasAllowFrom === true; const hasAllowFrom = opts?.hasAllowFrom === true;
@@ -47,10 +47,15 @@ export function resolveMessagePrefix(
export function resolveResponsePrefix( export function resolveResponsePrefix(
cfg: ClawdbotConfig, cfg: ClawdbotConfig,
_agentId: string, agentId: string,
): string | undefined { ): string | undefined {
const configured = cfg.messages?.responsePrefix; const configured = cfg.messages?.responsePrefix;
if (configured !== undefined) return configured; if (configured !== undefined) {
if (configured === "auto") {
return resolveIdentityNamePrefix(cfg, agentId);
}
return configured;
}
return undefined; return undefined;
} }

View File

@@ -69,7 +69,9 @@ export async function routeReply(
cfg, cfg,
resolveAgentIdFromSessionKey(params.sessionKey), resolveAgentIdFromSessionKey(params.sessionKey),
).responsePrefix ).responsePrefix
: cfg.messages?.responsePrefix; : cfg.messages?.responsePrefix === "auto"
? undefined
: cfg.messages?.responsePrefix;
const normalized = normalizeReplyPayload(payload, { const normalized = normalizeReplyPayload(payload, {
responsePrefix, responsePrefix,
}); });

View File

@@ -111,6 +111,11 @@ export type WhatsAppActionConfig = {
export type WhatsAppConfig = { export type WhatsAppConfig = {
/** Optional per-account WhatsApp configuration (multi-account). */ /** Optional per-account WhatsApp configuration (multi-account). */
accounts?: Record<string, WhatsAppAccountConfig>; accounts?: Record<string, WhatsAppAccountConfig>;
/**
* Inbound message prefix (WhatsApp only).
* Default: `[{agents.list[].identity.name}]` (or `[clawdbot]`) when allowFrom is empty, else `""`.
*/
messagePrefix?: string;
/** Direct message access policy (default: pairing). */ /** Direct message access policy (default: pairing). */
dmPolicy?: DmPolicy; dmPolicy?: DmPolicy;
/** /**
@@ -152,6 +157,8 @@ export type WhatsAppAccountConfig = {
name?: string; name?: string;
/** If false, do not start this WhatsApp account provider. Default: true. */ /** If false, do not start this WhatsApp account provider. Default: true. */
enabled?: boolean; enabled?: boolean;
/** Inbound message prefix override for this account (WhatsApp only). */
messagePrefix?: string;
/** Override auth directory (Baileys multi-file auth state). */ /** Override auth directory (Baileys multi-file auth state). */
authDir?: string; authDir?: string;
/** Direct message access policy (default: pairing). */ /** Direct message access policy (default: pairing). */
@@ -931,8 +938,15 @@ export type AudioConfig = {
}; };
export type MessagesConfig = { export type MessagesConfig = {
messagePrefix?: string; // Prefix added to all inbound messages (default: "[{agents.list[].identity.name}]" or "[clawdbot]" if no allowFrom, else "") /** @deprecated Use `whatsapp.messagePrefix` (WhatsApp-only inbound prefix). */
responsePrefix?: string; // Prefix auto-added to all outbound replies (default: none) messagePrefix?: string;
/**
* Prefix auto-added to all outbound replies.
* - string: explicit prefix
* - special value: `"auto"` derives `[{agents.list[].identity.name}]` for the routed agent (when set)
* Default: none
*/
responsePrefix?: string;
groupChat?: GroupChatConfig; groupChat?: GroupChatConfig;
queue?: QueueConfig; queue?: QueueConfig;
/** Emoji reaction used to acknowledge inbound messages (empty disables). */ /** Emoji reaction used to acknowledge inbound messages (empty disables). */

View File

@@ -1229,6 +1229,7 @@ export const ClawdbotSchema = z.object({
.object({ .object({
name: z.string().optional(), name: z.string().optional(),
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
messagePrefix: z.string().optional(),
/** Override auth directory for this WhatsApp account (Baileys multi-file auth state). */ /** Override auth directory for this WhatsApp account (Baileys multi-file auth state). */
authDir: z.string().optional(), authDir: z.string().optional(),
dmPolicy: DmPolicySchema.optional().default("pairing"), dmPolicy: DmPolicySchema.optional().default("pairing"),
@@ -1268,6 +1269,7 @@ export const ClawdbotSchema = z.object({
) )
.optional(), .optional(),
dmPolicy: DmPolicySchema.optional().default("pairing"), dmPolicy: DmPolicySchema.optional().default("pairing"),
messagePrefix: z.string().optional(),
selfChatMode: z.boolean().optional(), selfChatMode: z.boolean().optional(),
allowFrom: z.array(z.string()).optional(), allowFrom: z.array(z.string()).optional(),
groupAllowFrom: z.array(z.string()).optional(), groupAllowFrom: z.array(z.string()).optional(),

View File

@@ -15,6 +15,7 @@ export type ResolvedWhatsAppAccount = {
accountId: string; accountId: string;
name?: string; name?: string;
enabled: boolean; enabled: boolean;
messagePrefix?: string;
authDir: string; authDir: string;
isLegacyAuthDir: boolean; isLegacyAuthDir: boolean;
selfChatMode?: boolean; selfChatMode?: boolean;
@@ -111,6 +112,10 @@ export function resolveWhatsAppAccount(params: {
accountId, accountId,
name: accountCfg?.name?.trim() || undefined, name: accountCfg?.name?.trim() || undefined,
enabled, enabled,
messagePrefix:
accountCfg?.messagePrefix ??
params.cfg.whatsapp?.messagePrefix ??
params.cfg.messages?.messagePrefix,
authDir, authDir,
isLegacyAuthDir: isLegacy, isLegacyAuthDir: isLegacy,
selfChatMode: accountCfg?.selfChatMode ?? params.cfg.whatsapp?.selfChatMode, selfChatMode: accountCfg?.selfChatMode ?? params.cfg.whatsapp?.selfChatMode,

View File

@@ -784,6 +784,7 @@ export async function monitorWebProvider(
...baseCfg, ...baseCfg,
whatsapp: { whatsapp: {
...baseCfg.whatsapp, ...baseCfg.whatsapp,
messagePrefix: account.messagePrefix,
allowFrom: account.allowFrom, allowFrom: account.allowFrom,
groupAllowFrom: account.groupAllowFrom, groupAllowFrom: account.groupAllowFrom,
groupPolicy: account.groupPolicy, groupPolicy: account.groupPolicy,
@@ -1039,8 +1040,9 @@ export async function monitorWebProvider(
}; };
const buildLine = (msg: WebInboundMsg, agentId: string) => { const buildLine = (msg: WebInboundMsg, agentId: string) => {
// Build message prefix: explicit config > identity name > default based on allowFrom // WhatsApp inbound prefix: whatsapp.messagePrefix > legacy messages.messagePrefix > identity/defaults
const messagePrefix = resolveMessagePrefix(cfg, agentId, { const messagePrefix = resolveMessagePrefix(cfg, agentId, {
configured: cfg.whatsapp?.messagePrefix,
hasAllowFrom: (cfg.whatsapp?.allowFrom?.length ?? 0) > 0, hasAllowFrom: (cfg.whatsapp?.allowFrom?.length ?? 0) > 0,
}); });
const prefixStr = messagePrefix ? `${messagePrefix} ` : ""; const prefixStr = messagePrefix ? `${messagePrefix} ` : "";