Skip to content

Commit b5f25de

Browse files
authored
bluebubbles: forward per-group systemPrompt into GroupSystemPrompt (#69198)
Forward per-group systemPrompt config into inbound context GroupSystemPrompt so configured group-specific behavioral instructions (for example threaded-reply and tapback conventions) are injected on every turn. Supports "*" wildcard fallback matching the existing requireMention pattern. Closes #60665. Co-authored-by: Omar Shahine <omarshahine@users.noreply.github.com>
1 parent d1f7f69 commit b5f25de

8 files changed

Lines changed: 165 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
1212
- Agents/compaction: send opt-in start and completion notices during context compaction. (#67830) Thanks @feniix.
1313
- Moonshot/Kimi: default bundled Moonshot setup, web search, and media-understanding surfaces to `kimi-k2.6` while keeping `kimi-k2.5` available for compatibility. (#69477) Thanks @scoootscooob.
1414
- Moonshot/Kimi: allow `thinking.keep = "all"` on `moonshot/kimi-k2.6`, and strip it for other Moonshot models or requests where pinned `tool_choice` disables thinking. (#68816) Thanks @aniaan.
15+
- BlueBubbles/groups: forward per-group `systemPrompt` config into inbound context `GroupSystemPrompt` so configured group-specific behavioral instructions (for example threaded-reply and tapback conventions) are injected on every turn. Supports `"*"` wildcard fallback matching the existing `requireMention` pattern. Closes #60665. (#69198) Thanks @omarshahine.
1516

1617
### Fixes
1718

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ab40431597e9f7c09a9f010f267bab250c7f9c570c4a100776de98869f589a92 config-baseline.json
1+
580abc79677d84fa66cb55e42ea093bfa9681655861166c02dfaa5a313d44310 config-baseline.json
22
04a82c2208bf69e0a195e7712e3a518a8255c1bb002c31f712cb95003325635b config-baseline.core.json
33
e239cc20f20f8d0172812bc0ad3ee6df52da88e2e2702e3d03a47e01561132ae config-baseline.channel.json
4-
b695cb31b4c0cf1d31f842f2892e99cc3ff8d84263ae72b72977cae844b81d6e config-baseline.plugin.json
4+
8fb3a1cf5fe56ab8fc2cb46341c3403aed32b0d1f0aaeac0e96cd3599db4f06e config-baseline.plugin.json

docs/channels/bluebubbles.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,54 @@ Per-group configuration:
217217
- Uses `allowFrom` and `groupAllowFrom` to determine command authorization.
218218
- Authorized senders can run control commands even without mentioning in groups.
219219

220+
### Per-group system prompt
221+
222+
Each entry under `channels.bluebubbles.groups.*` accepts an optional `systemPrompt` string. The value is injected into the agent's system prompt on every turn that handles a message in that group, so you can set per-group persona or behavioral rules without editing agent prompts:
223+
224+
```json5
225+
{
226+
channels: {
227+
bluebubbles: {
228+
groups: {
229+
"iMessage;-;chat123": {
230+
systemPrompt: "Keep responses under 3 sentences. Mirror the group's casual tone.",
231+
},
232+
},
233+
},
234+
},
235+
}
236+
```
237+
238+
The key matches whatever BlueBubbles reports as `chatGuid` / `chatIdentifier` / numeric `chatId` for the group, and a `"*"` wildcard entry provides a default for every group without an exact match (same pattern used by `requireMention` and per-group tool policies). Exact matches always win over the wildcard. DMs ignore this field; use agent-level or account-level prompt customization instead.
239+
240+
#### Worked example: threaded replies and tapback reactions (Private API)
241+
242+
With the BlueBubbles Private API enabled, inbound messages arrive with short message IDs (for example `[[reply_to:5]]`) and the agent can call `action=reply` to thread into a specific message or `action=react` to drop a tapback. A per-group `systemPrompt` is a reliable way to keep the agent choosing the right tool:
243+
244+
```json5
245+
{
246+
channels: {
247+
bluebubbles: {
248+
groups: {
249+
"iMessage;+;chat-family": {
250+
systemPrompt: [
251+
"When replying in this group, always call action=reply with the",
252+
"[[reply_to:N]] messageId from context so your response threads",
253+
"under the triggering message. Never send a new unlinked message.",
254+
"",
255+
"For short acknowledgements ('ok', 'got it', 'on it'), use",
256+
"action=react with an appropriate tapback emoji (❤️, 👍, 😂, ‼️, ❓)",
257+
"instead of sending a text reply.",
258+
].join(" "),
259+
},
260+
},
261+
},
262+
},
263+
}
264+
```
265+
266+
Tapback reactions and threaded replies both require the BlueBubbles Private API; see [Advanced actions](#advanced-actions) and [Message IDs](#message-ids-short-vs-full) for the underlying mechanics.
267+
220268
## ACP conversation bindings
221269

222270
BlueBubbles chats can be turned into durable ACP workspaces without changing the transport layer.

extensions/bluebubbles/src/config-schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ const bluebubblesActionSchema = z
3030
const bluebubblesGroupConfigSchema = z.object({
3131
requireMention: z.boolean().optional(),
3232
tools: ToolPolicySchema,
33+
/**
34+
* Free-form directive appended to the system prompt for every turn that
35+
* handles a message in this group. Use it for per-group persona tweaks or
36+
* behavioral rules (reply-threading, tapback conventions, etc.).
37+
*/
38+
systemPrompt: z.string().optional(),
3339
});
3440

3541
const bluebubblesNetworkSchema = z

extensions/bluebubbles/src/monitor-processing.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,6 +1537,14 @@ async function processMessageAfterDedupe(
15371537
OriginatingTo: `bluebubbles:${outboundTarget}`,
15381538
WasMentioned: effectiveWasMentioned,
15391539
CommandAuthorized: commandAuthorized,
1540+
// Exact group match wins over the "*" wildcard fallback, matching the
1541+
// pattern used by resolveChannelGroupRequireMention/toolsPolicy.
1542+
GroupSystemPrompt: isGroup
1543+
? normalizeOptionalString(
1544+
account.config.groups?.[peerId]?.systemPrompt ??
1545+
account.config.groups?.["*"]?.systemPrompt,
1546+
)
1547+
: undefined,
15401548
});
15411549

15421550
let sentMessage = false;

extensions/bluebubbles/src/monitor.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,100 @@ describe("BlueBubbles webhook monitor", () => {
593593
expect(callArgs.ctx.GroupMembers).toBe("Alice (+15551234567), Bob (+15557654321)");
594594
});
595595

596+
it("threads per-group systemPrompt into ctx for group messages", async () => {
597+
setupWebhookTarget({
598+
account: createMockAccount({
599+
groups: {
600+
"iMessage;+;chat123456": {
601+
systemPrompt: "Reply in thread with action=reply; ack via action=react.",
602+
},
603+
},
604+
}),
605+
});
606+
607+
const payload = createTimestampedNewMessagePayloadForTest({
608+
text: "hello group",
609+
isGroup: true,
610+
chatGuid: "iMessage;+;chat123456",
611+
chatName: "Family",
612+
participants: [{ address: "+15551234567", displayName: "Alice" }],
613+
});
614+
615+
await dispatchWebhookPayload(payload);
616+
617+
const callArgs = getFirstDispatchCall();
618+
expect(callArgs.ctx.GroupSystemPrompt).toBe(
619+
"Reply in thread with action=reply; ack via action=react.",
620+
);
621+
});
622+
623+
it("falls back to the '*' wildcard systemPrompt when no exact group match", async () => {
624+
setupWebhookTarget({
625+
account: createMockAccount({
626+
groups: {
627+
"*": { systemPrompt: "Default group rule: keep it short." },
628+
},
629+
}),
630+
});
631+
632+
const payload = createTimestampedNewMessagePayloadForTest({
633+
text: "hi group",
634+
isGroup: true,
635+
chatGuid: "iMessage;+;chat-unmapped",
636+
chatName: "Family",
637+
participants: [{ address: "+15551234567", displayName: "Alice" }],
638+
});
639+
640+
await dispatchWebhookPayload(payload);
641+
642+
const callArgs = getFirstDispatchCall();
643+
expect(callArgs.ctx.GroupSystemPrompt).toBe("Default group rule: keep it short.");
644+
});
645+
646+
it("prefers an exact group systemPrompt over the '*' wildcard", async () => {
647+
setupWebhookTarget({
648+
account: createMockAccount({
649+
groups: {
650+
"*": { systemPrompt: "wildcard value" },
651+
"iMessage;+;chat123456": { systemPrompt: "exact value" },
652+
},
653+
}),
654+
});
655+
656+
const payload = createTimestampedNewMessagePayloadForTest({
657+
text: "hi group",
658+
isGroup: true,
659+
chatGuid: "iMessage;+;chat123456",
660+
chatName: "Family",
661+
participants: [{ address: "+15551234567", displayName: "Alice" }],
662+
});
663+
664+
await dispatchWebhookPayload(payload);
665+
666+
const callArgs = getFirstDispatchCall();
667+
expect(callArgs.ctx.GroupSystemPrompt).toBe("exact value");
668+
});
669+
670+
it("omits GroupSystemPrompt for DMs even when the group config would match", async () => {
671+
setupWebhookTarget({
672+
account: createMockAccount({
673+
groups: {
674+
"+15551234567": { systemPrompt: "unused in DM" },
675+
},
676+
}),
677+
});
678+
679+
const payload = createTimestampedNewMessagePayloadForTest({
680+
text: "hi",
681+
isGroup: false,
682+
});
683+
684+
await dispatchWebhookPayload(payload);
685+
686+
const callArgs = getFirstDispatchCall();
687+
expect(callArgs.ctx.GroupSystemPrompt).toBeUndefined();
688+
});
689+
596690
it("does not enrich group participants when the config flag is disabled", async () => {
597691
const resolvePhoneNames = vi.fn(async () => new Map([["5551234567", "Alice Contact"]]));
598692
setupWebhookTarget({

extensions/bluebubbles/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export type BlueBubblesGroupConfig = {
1010
requireMention?: boolean;
1111
/** Optional tool policy overrides for this group. */
1212
tools?: { allow?: string[]; deny?: string[] };
13+
/**
14+
* Free-form directive appended to the system prompt on every turn that
15+
* handles a message in this group.
16+
*/
17+
systemPrompt?: string;
1318
};
1419

1520
export type BlueBubblesActionConfig = {

scripts/check-no-raw-channel-fetch.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const sourceRoots = ["src/channels", "src/routing", "src/line", "extensions"];
1515
// code should be rejected and migrated to fetchWithSsrFGuard/shared channel helpers.
1616
const allowedRawFetchCallsites = new Set([
1717
bundledPluginCallsite("bluebubbles", "src/test-harness.ts", 132),
18-
bundledPluginCallsite("bluebubbles", "src/types.ts", 189),
18+
bundledPluginCallsite("bluebubbles", "src/types.ts", 194),
1919
bundledPluginCallsite("browser", "src/browser/cdp.helpers.ts", 268),
2020
bundledPluginCallsite("browser", "src/browser/client-fetch.ts", 192),
2121
bundledPluginCallsite("browser", "src/browser/test-fetch.ts", 24),

0 commit comments

Comments
 (0)