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
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
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
{
messages: {
messagePrefix: "[clawdbot]",
responsePrefix: "🦞",
responsePrefix: "🦞", // or "auto"
ackReaction: "👀",
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.
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**:
`"[clawdbot]"` when `whatsapp.allowFrom` is empty, otherwise `""` (no prefix).
When using `"[clawdbot]"`, Clawdbot will instead use `[{identity.name}]` when
the routed agent has `identity.name` set.
WhatsApp inbound prefix is configured via `whatsapp.messagePrefix` (deprecated:
`messages.messagePrefix`). Default stays **unchanged**: `"[clawdbot]"` when
`whatsapp.allowFrom` is empty, otherwise `""` (no prefix). When using
`"[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
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).
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`)
- `messages.groupChat.historyLimit`
- `messages.messagePrefix` (inbound prefix)
- `whatsapp.messagePrefix` (inbound prefix; per-account: `whatsapp.accounts.<accountId>.messagePrefix`; deprecated: `messages.messagePrefix`)
- `messages.responsePrefix` (outbound prefix)
- `agents.defaults.mediaMaxMb`
- `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(
cfg: ClawdbotConfig,
agentId: string,
opts?: { hasAllowFrom?: boolean; fallback?: string },
opts?: { configured?: string; hasAllowFrom?: boolean; fallback?: string },
): string {
const configured = cfg.messages?.messagePrefix;
const configured = opts?.configured ?? cfg.messages?.messagePrefix;
if (configured !== undefined) return configured;
const hasAllowFrom = opts?.hasAllowFrom === true;
@@ -47,10 +47,15 @@ export function resolveMessagePrefix(
export function resolveResponsePrefix(
cfg: ClawdbotConfig,
_agentId: string,
agentId: string,
): string | undefined {
const configured = cfg.messages?.responsePrefix;
if (configured !== undefined) return configured;
if (configured !== undefined) {
if (configured === "auto") {
return resolveIdentityNamePrefix(cfg, agentId);
}
return configured;
}
return undefined;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -784,6 +784,7 @@ export async function monitorWebProvider(
...baseCfg,
whatsapp: {
...baseCfg.whatsapp,
messagePrefix: account.messagePrefix,
allowFrom: account.allowFrom,
groupAllowFrom: account.groupAllowFrom,
groupPolicy: account.groupPolicy,
@@ -1039,8 +1040,9 @@ export async function monitorWebProvider(
};
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, {
configured: cfg.whatsapp?.messagePrefix,
hasAllowFrom: (cfg.whatsapp?.allowFrom?.length ?? 0) > 0,
});
const prefixStr = messagePrefix ? `${messagePrefix} ` : "";