Skip to content

Commit 5d82c82

Browse files
mudriigumadeiras
andauthored
feat: per-channel responsePrefix override (#9001)
* feat: per-channel responsePrefix override Add responsePrefix field to all channel config types and Zod schemas, enabling per-channel and per-account outbound response prefix overrides. Resolution cascade (most specific wins): L1: channels.<ch>.accounts.<id>.responsePrefix L2: channels.<ch>.responsePrefix L3: (reserved for channels.defaults) L4: messages.responsePrefix (existing global) Semantics: - undefined -> inherit from parent level - empty string -> explicitly no prefix (stops cascade) - "auto" -> derive [identity.name] from routed agent Changes: - Core logic: resolveResponsePrefix() in identity.ts accepts optional channel/accountId and walks the cascade - resolveEffectiveMessagesConfig() passes channel context through - Types: responsePrefix added to WhatsApp, Telegram, Discord, Slack, Signal, iMessage, Google Chat, MS Teams, Feishu, BlueBubbles configs - Zod schemas: responsePrefix added for config validation - All channel handlers wired: telegram, discord, slack, signal, imessage, line, heartbeat runner, route-reply, native commands - 23 new tests covering backward compat, channel/account levels, full cascade, auto keyword, empty string stops, unknown fallthrough Fully backward compatible - no existing config is affected. Fixes #8857 * fix: address CI lint + review feedback - Replace Record<string, any> with proper typed helpers (no-explicit-any) - Add curly braces to single-line if returns (eslint curly) - Fix JSDoc: 'Per-channel' → 'channel/account' on shared config types - Extract getChannelConfig() helper for type-safe dynamic key access * fix: finish responsePrefix overrides (#9001) (thanks @mudrii) * fix: normalize prefix wiring and types (#9001) (thanks @mudrii) --------- Co-authored-by: Gustavo Madeira Santana <[email protected]>
1 parent 43590d8 commit 5d82c82

63 files changed

Lines changed: 838 additions & 120 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
1010
- Onboarding: add Moonshot (.cn) auth choice and keep the China base URL when preserving defaults. (#7180) Thanks @waynelwz.
1111
- Docs: clarify tmux send-keys for TUI by splitting text and Enter. (#7737) Thanks @Wangnov.
1212
- Docs: mirror the landing page revamp for zh-CN (features, quickstart, docs directory, network model, credits). (#8994) Thanks @joshp123.
13+
- Messages: add per-channel and per-account responsePrefix overrides across channels. (#9001) Thanks @mudrii.
1314
- Cron: add announce delivery mode for isolated jobs (CLI + Control UI) and delivery mode config.
1415
- Cron: default isolated jobs to announce delivery; accept ISO 8601 `schedule.at` in tool inputs.
1516
- Cron: hard-migrate isolated jobs to announce/none delivery; drop legacy post-to-main/payload delivery fields and `atMs` inputs.

docs/concepts/messages.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ Details: [Thinking + reasoning directives](/tools/thinking) and [Token use](/tok
148148

149149
Outbound message formatting is centralized in `messages`:
150150

151-
- `messages.responsePrefix` (outbound prefix) and `channels.whatsapp.messagePrefix` (WhatsApp inbound prefix)
151+
- `messages.responsePrefix`, `channels.<channel>.responsePrefix`, and `channels.<channel>.accounts.<id>.responsePrefix` (outbound prefix cascade), plus `channels.whatsapp.messagePrefix` (WhatsApp inbound prefix)
152152
- Reply threading via `replyToMode` and per-channel defaults
153153

154154
Details: [Configuration](/gateway/configuration#messages) and channel docs.

docs/gateway/configuration.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1517,6 +1517,25 @@ See [Messages](/concepts/messages) for queueing, sessions, and streaming context
15171517
`responsePrefix` is applied to **all outbound replies** (tool summaries, block
15181518
streaming, final replies) across channels unless already present.
15191519

1520+
Overrides can be configured per channel and per account:
1521+
1522+
- `channels.<channel>.responsePrefix`
1523+
- `channels.<channel>.accounts.<id>.responsePrefix`
1524+
1525+
Resolution order (most specific wins):
1526+
1527+
1. `channels.<channel>.accounts.<id>.responsePrefix`
1528+
2. `channels.<channel>.responsePrefix`
1529+
3. `messages.responsePrefix`
1530+
1531+
Semantics:
1532+
1533+
- `undefined` falls through to the next level.
1534+
- `""` explicitly disables the prefix and stops the cascade.
1535+
- `"auto"` derives `[{identity.name}]` for the routed agent.
1536+
1537+
Overrides apply to all channels, including extensions, and to every outbound reply kind.
1538+
15201539
If `messages.responsePrefix` is unset, no prefix is applied by default. WhatsApp self-chat
15211540
replies are the exception: they default to `[{identity.name}]` when set, otherwise
15221541
`[openclaw]`, so same-phone conversations stay legible.

extensions/bluebubbles/src/monitor.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { IncomingMessage, ServerResponse } from "node:http";
22
import type { OpenClawConfig } from "openclaw/plugin-sdk";
33
import {
4+
createReplyPrefixOptions,
45
logAckFailure,
56
logInboundDrop,
67
logTypingFailure,
@@ -2173,10 +2174,17 @@ async function processMessage(
21732174
}, typingRestartDelayMs);
21742175
};
21752176
try {
2177+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
2178+
cfg: config,
2179+
agentId: route.agentId,
2180+
channel: "bluebubbles",
2181+
accountId: account.accountId,
2182+
});
21762183
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
21772184
ctx: ctxPayload,
21782185
cfg: config,
21792186
dispatcherOptions: {
2187+
...prefixOptions,
21802188
deliver: async (payload, info) => {
21812189
const rawReplyToId =
21822190
typeof payload.replyToId === "string" ? payload.replyToId.trim() : "";
@@ -2288,6 +2296,7 @@ async function processMessage(
22882296
},
22892297
},
22902298
replyOptions: {
2299+
onModelSelected,
22912300
disableBlockStreaming:
22922301
typeof account.config.blockStreaming === "boolean"
22932302
? !account.config.blockStreaming

extensions/feishu/src/config-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const FeishuAccountSchema = z
3737
blockStreaming: z.boolean().optional(),
3838
streaming: z.boolean().optional(),
3939
mediaMaxMb: z.number().optional(),
40+
responsePrefix: z.string().optional(),
4041
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
4142
})
4243
.strict();

extensions/googlechat/src/monitor.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { IncomingMessage, ServerResponse } from "node:http";
22
import type { OpenClawConfig } from "openclaw/plugin-sdk";
3-
import { resolveMentionGatingWithBypass } from "openclaw/plugin-sdk";
3+
import { createReplyPrefixOptions, resolveMentionGatingWithBypass } from "openclaw/plugin-sdk";
44
import type {
55
GoogleChatAnnotation,
66
GoogleChatAttachment,
@@ -725,10 +725,18 @@ async function processMessageWithPipeline(params: {
725725
}
726726
}
727727

728+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
729+
cfg: config,
730+
agentId: route.agentId,
731+
channel: "googlechat",
732+
accountId: route.accountId,
733+
});
734+
728735
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
729736
ctx: ctxPayload,
730737
cfg: config,
731738
dispatcherOptions: {
739+
...prefixOptions,
732740
deliver: async (payload) => {
733741
await deliverGoogleChatReply({
734742
payload,
@@ -749,6 +757,9 @@ async function processMessageWithPipeline(params: {
749757
);
750758
},
751759
},
760+
replyOptions: {
761+
onModelSelected,
762+
},
752763
});
753764
}
754765

extensions/matrix/src/config-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const MatrixConfigSchema = z.object({
5151
threadReplies: z.enum(["off", "inbound", "always"]).optional(),
5252
textChunkLimit: z.number().optional(),
5353
chunkMode: z.enum(["length", "newline"]).optional(),
54+
responsePrefix: z.string().optional(),
5455
mediaMaxMb: z.number().optional(),
5556
autoJoin: z.enum(["always", "allowlist", "off"]).optional(),
5657
autoJoinAllowlist: z.array(allowFromEntry).optional(),

extensions/matrix/src/matrix/monitor/handler.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { LocationMessageEventContent, MatrixClient } from "@vector-im/matrix-bot-sdk";
22
import {
3-
createReplyPrefixContext,
3+
createReplyPrefixOptions,
44
createTypingCallbacks,
55
formatAllowlistMatchMeta,
66
logInboundDrop,
@@ -579,7 +579,12 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
579579
channel: "matrix",
580580
accountId: route.accountId,
581581
});
582-
const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
582+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
583+
cfg,
584+
agentId: route.agentId,
585+
channel: "matrix",
586+
accountId: route.accountId,
587+
});
583588
const typingCallbacks = createTypingCallbacks({
584589
start: () => sendTypingMatrix(roomId, true, undefined, client),
585590
stop: () => sendTypingMatrix(roomId, false, undefined, client),
@@ -604,8 +609,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
604609
});
605610
const { dispatcher, replyOptions, markDispatchIdle } =
606611
core.channel.reply.createReplyDispatcherWithTyping({
607-
responsePrefix: prefixContext.responsePrefix,
608-
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
612+
...prefixOptions,
609613
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
610614
deliver: async (payload) => {
611615
await deliverMatrixReplies({
@@ -635,7 +639,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
635639
replyOptions: {
636640
...replyOptions,
637641
skillFilter: roomConfig?.skills,
638-
onModelSelected: prefixContext.onModelSelected,
642+
onModelSelected,
639643
},
640644
});
641645
markDispatchIdle();

extensions/matrix/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ export type MatrixConfig = {
7171
textChunkLimit?: number;
7272
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
7373
chunkMode?: "length" | "newline";
74+
/** Outbound response prefix override for this channel/account. */
75+
responsePrefix?: string;
7476
/** Max outbound media size in MB. */
7577
mediaMaxMb?: number;
7678
/** Auto-join invites (always|allowlist|off). Default: always. */

extensions/mattermost/src/channel.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
2+
import { createReplyPrefixOptions } from "openclaw/plugin-sdk";
13
import { describe, expect, it } from "vitest";
24
import { mattermostPlugin } from "./channel.js";
35

@@ -44,5 +46,27 @@ describe("mattermostPlugin", () => {
4446
});
4547
expect(formatted).toEqual(["@alice", "user123", "bot999"]);
4648
});
49+
50+
it("uses account responsePrefix overrides", () => {
51+
const cfg: OpenClawConfig = {
52+
channels: {
53+
mattermost: {
54+
responsePrefix: "[Channel]",
55+
accounts: {
56+
default: { responsePrefix: "[Account]" },
57+
},
58+
},
59+
},
60+
};
61+
62+
const prefixContext = createReplyPrefixOptions({
63+
cfg,
64+
agentId: "main",
65+
channel: "mattermost",
66+
accountId: "default",
67+
});
68+
69+
expect(prefixContext.responsePrefix).toBe("[Account]");
70+
});
4771
});
4872
});

0 commit comments

Comments
 (0)