fix: scope whatsapp self-chat response prefix

This commit is contained in:
Peter Steinberger
2026-01-16 10:53:32 +00:00
parent f49d0e5476
commit 19bcbf85df
6 changed files with 74 additions and 39 deletions

View File

@@ -1,6 +1,6 @@
# Changelog # Changelog
## 2026.1.15 ## 2026.1.16 (unreleased)
### Highlights ### Highlights
- Plugins: add provider auth registry + `clawdbot models auth login` for plugin-driven OAuth/API key flows. - Plugins: add provider auth registry + `clawdbot models auth login` for plugin-driven OAuth/API key flows.
@@ -47,6 +47,7 @@
- Discord: allow emoji/sticker uploads + channel actions in config defaults. (#870) — thanks @JDIVE. - Discord: allow emoji/sticker uploads + channel actions in config defaults. (#870) — thanks @JDIVE.
### Fixes ### Fixes
- WhatsApp: default response prefix only for self-chat, using identity name when set.
- Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj. - Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj.
- Fix: align OpenAI image-gen defaults with DALL-E 3 standard quality and document output formats. (#880) — thanks @mkbehr. - Fix: align OpenAI image-gen defaults with DALL-E 3 standard quality and document output formats. (#880) — thanks @mkbehr.
- Fix: persist `gateway.mode=local` after selecting Local run mode in `clawdbot configure`, even if no other sections are chosen. - Fix: persist `gateway.mode=local` after selecting Local run mode in `clawdbot configure`, even if no other sections are chosen.

View File

@@ -82,15 +82,13 @@ When the wizard asks for your personal WhatsApp number, enter the phone you will
"selfChatMode": true, "selfChatMode": true,
"dmPolicy": "allowlist", "dmPolicy": "allowlist",
"allowFrom": ["+15551234567"] "allowFrom": ["+15551234567"]
},
"messages": {
"responsePrefix": "[clawdbot]"
} }
} }
``` ```
Tip: set `messages.responsePrefix` explicitly if you want a consistent bot prefix Self-chat replies default to `[{identity.name}]` when set (otherwise `[clawdbot]`)
on outbound replies. if `messages.responsePrefix` is unset. Set it explicitly to customize or disable
the prefix (use `""` to remove it).
### Number sourcing tips ### Number sourcing tips
- **Local eSIM** from your country's mobile carrier (most reliable) - **Local eSIM** from your country's mobile carrier (most reliable)

View File

@@ -1271,7 +1271,9 @@ See [Messages](/concepts/messages) for queueing, sessions, and streaming context
`responsePrefix` is applied to **all outbound replies** (tool summaries, block `responsePrefix` is applied to **all outbound replies** (tool summaries, block
streaming, final replies) across channels unless already present. streaming, final replies) across channels 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. WhatsApp self-chat
replies are the exception: they default to `[{identity.name}]` when set, otherwise
`[clawdbot]`, so same-phone conversations stay legible.
Set it to `"auto"` to derive `[{identity.name}]` for the routed agent (when set). Set it to `"auto"` to derive `[{identity.name}]` for the routed agent (when set).
#### Template variables #### Template variables

View File

@@ -27,16 +27,6 @@ function setWhatsAppAllowFrom(cfg: ClawdbotConfig, allowFrom?: string[]): Clawdb
return mergeWhatsAppConfig(cfg, { allowFrom }, { unsetOnUndefined: ["allowFrom"] }); return mergeWhatsAppConfig(cfg, { allowFrom }, { unsetOnUndefined: ["allowFrom"] });
} }
function setMessagesResponsePrefix(cfg: ClawdbotConfig, responsePrefix?: string): ClawdbotConfig {
return {
...cfg,
messages: {
...cfg.messages,
responsePrefix,
},
};
}
function setWhatsAppSelfChatMode(cfg: ClawdbotConfig, selfChatMode: boolean): ClawdbotConfig { function setWhatsAppSelfChatMode(cfg: ClawdbotConfig, selfChatMode: boolean): ClawdbotConfig {
return mergeWhatsAppConfig(cfg, { selfChatMode }); return mergeWhatsAppConfig(cfg, { selfChatMode });
} }
@@ -65,7 +55,6 @@ async function promptWhatsAppAllowFrom(
const existingPolicy = cfg.channels?.whatsapp?.dmPolicy ?? "pairing"; const existingPolicy = cfg.channels?.whatsapp?.dmPolicy ?? "pairing";
const existingAllowFrom = cfg.channels?.whatsapp?.allowFrom ?? []; const existingAllowFrom = cfg.channels?.whatsapp?.allowFrom ?? [];
const existingLabel = existingAllowFrom.length > 0 ? existingAllowFrom.join(", ") : "unset"; const existingLabel = existingAllowFrom.length > 0 ? existingAllowFrom.join(", ") : "unset";
const existingResponsePrefix = cfg.messages?.responsePrefix;
if (options?.forceAllowlist) { if (options?.forceAllowlist) {
await prompter.note( await prompter.note(
@@ -96,17 +85,8 @@ async function promptWhatsAppAllowFrom(
let next = setWhatsAppSelfChatMode(cfg, true); let next = setWhatsAppSelfChatMode(cfg, true);
next = setWhatsAppDmPolicy(next, "allowlist"); next = setWhatsAppDmPolicy(next, "allowlist");
next = setWhatsAppAllowFrom(next, unique); next = setWhatsAppAllowFrom(next, unique);
if (existingResponsePrefix === undefined) {
next = setMessagesResponsePrefix(next, "[clawdbot]");
}
await prompter.note( await prompter.note(
[ ["Allowlist mode enabled.", `- allowFrom includes ${normalized}`].join("\n"),
"Allowlist mode enabled.",
`- allowFrom includes ${normalized}`,
existingResponsePrefix === undefined
? "- responsePrefix set to [clawdbot]"
: "- responsePrefix left unchanged",
].join("\n"),
"WhatsApp allowlist", "WhatsApp allowlist",
); );
return next; return next;
@@ -163,17 +143,11 @@ async function promptWhatsAppAllowFrom(
let next = setWhatsAppSelfChatMode(cfg, true); let next = setWhatsAppSelfChatMode(cfg, true);
next = setWhatsAppDmPolicy(next, "allowlist"); next = setWhatsAppDmPolicy(next, "allowlist");
next = setWhatsAppAllowFrom(next, unique); next = setWhatsAppAllowFrom(next, unique);
if (existingResponsePrefix === undefined) {
next = setMessagesResponsePrefix(next, "[clawdbot]");
}
await prompter.note( await prompter.note(
[ [
"Personal phone mode enabled.", "Personal phone mode enabled.",
"- dmPolicy set to allowlist (pairing skipped)", "- dmPolicy set to allowlist (pairing skipped)",
`- allowFrom includes ${normalized}`, `- allowFrom includes ${normalized}`,
existingResponsePrefix === undefined
? "- responsePrefix set to [clawdbot]"
: "- responsePrefix left unchanged",
].join("\n"), ].join("\n"),
"WhatsApp personal phone", "WhatsApp personal phone",
); );

View File

@@ -258,6 +258,55 @@ describe("web auto-reply", () => {
expect(reply).toHaveBeenCalledWith("🦞 hello there"); expect(reply).toHaveBeenCalledWith("🦞 hello there");
resetLoadConfigMock(); resetLoadConfigMock();
}); });
it("defaults responsePrefix for self-chat replies when unset", async () => {
setLoadConfigMock(() => ({
agents: {
list: [
{
id: "main",
default: true,
identity: { name: "Mainbot", emoji: "🦞", theme: "space lobster" },
},
],
},
channels: { whatsapp: { allowFrom: ["+1555"] } },
messages: {
messagePrefix: undefined,
responsePrefix: undefined,
},
}));
let capturedOnMessage:
| ((msg: import("./inbound.js").WebInboundMessage) => Promise<void>)
| undefined;
const reply = vi.fn();
const listenerFactory = async (opts: {
onMessage: (msg: import("./inbound.js").WebInboundMessage) => Promise<void>;
}) => {
capturedOnMessage = opts.onMessage;
return { close: vi.fn() };
};
const resolver = vi.fn().mockResolvedValue({ text: "hello there" });
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
body: "hi",
from: "+1555",
to: "+1555",
selfE164: "+1555",
chatType: "direct",
id: "msg1",
sendComposing: vi.fn(),
reply,
sendMedia: vi.fn(),
});
expect(reply).toHaveBeenCalledWith("[Mainbot] hello there");
resetLoadConfigMock();
});
it("does not deliver HEARTBEAT_OK responses", async () => { it("does not deliver HEARTBEAT_OK responses", async () => {
setLoadConfigMock(() => ({ setLoadConfigMock(() => ({
channels: { whatsapp: { allowFrom: ["*"] } }, channels: { whatsapp: { allowFrom: ["*"] } },

View File

@@ -1,4 +1,8 @@
import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../../../agents/identity.js"; import {
resolveEffectiveMessagesConfig,
resolveIdentityName,
resolveIdentityNamePrefix,
} from "../../../agents/identity.js";
import { import {
extractShortModelName, extractShortModelName,
type ResponsePrefixContext, type ResponsePrefixContext,
@@ -172,10 +176,17 @@ export async function processMessage(params: {
const textLimit = params.maxMediaTextChunkLimit ?? resolveTextChunkLimit(params.cfg, "whatsapp"); const textLimit = params.maxMediaTextChunkLimit ?? resolveTextChunkLimit(params.cfg, "whatsapp");
let didLogHeartbeatStrip = false; let didLogHeartbeatStrip = false;
let didSendReply = false; let didSendReply = false;
const responsePrefix = resolveEffectiveMessagesConfig( const configuredResponsePrefix = params.cfg.messages?.responsePrefix;
params.cfg, const resolvedMessages = resolveEffectiveMessagesConfig(params.cfg, params.route.agentId);
params.route.agentId, const isSelfChat =
).responsePrefix; params.msg.chatType !== "group" &&
Boolean(params.msg.selfE164) &&
normalizeE164(params.msg.from) === normalizeE164(params.msg.selfE164 ?? "");
const responsePrefix =
resolvedMessages.responsePrefix ??
(configuredResponsePrefix === undefined && isSelfChat
? resolveIdentityNamePrefix(params.cfg, params.route.agentId) ?? "[clawdbot]"
: undefined);
// Create mutable context for response prefix template interpolation // Create mutable context for response prefix template interpolation
let prefixContext: ResponsePrefixContext = { let prefixContext: ResponsePrefixContext = {