Skip to content

Commit 3c63620

Browse files
authored
fix(messages): keep group replies tool-only by default
Rewrites the always-on reply handling so group/channel rooms default to message-tool-visible output, while `messages.groupChat.visibleReplies: \"automatic\"` preserves legacy auto-posting.\n\nThanks @scoootscooob.
1 parent e388f28 commit 3c63620

46 files changed

Lines changed: 684 additions & 63 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
@@ -113,6 +113,7 @@ Docs: https://docs.openclaw.ai
113113
- Doctor/channels: suppress disabled bundled-plugin blocker warnings when a trusted external plugin owns the configured channel, so Lark/Feishu installs no longer get Feishu repair noise after switching to `openclaw-lark`. Fixes #56794. Thanks @wuji-tech-dev.
114114
- CLI/status: show skipped fast-path memory checks as `not checked` and report active custom memory plugin runtime status from `status --json --all` without requiring built-in `agents.defaults.memorySearch`, so plugins such as memory-lancedb-pro and memory-cms no longer look unavailable when their own runtime is healthy. Fixes #56968. Thanks @Tony-ooo and @aderius.
115115
- Gateway/channels: record and log unexpected clean channel monitor exits so channels that return without throwing no longer appear stopped with no error. Fixes #73099. Thanks @balaji1968-kingler.
116+
- Discord/group chats: keep group/channel replies private by default unless the agent explicitly uses the message tool, so always-on rooms can lurk without leaking automatic final, block, preview, or status-reaction output; `messages.groupChat.visibleReplies: "automatic"` restores legacy auto-posting. (#73046) Thanks @scoootscooob.
116117
- Plugins/package: force nested bundled-plugin runtime dependency installs out of inherited npm dry-run mode during prepack and package smoke checks, so packed installs materialize required plugin modules instead of reporting missing bundled files. Refs #73128. Thanks @Adam-Researchh.
117118
- Discord: skip reaction events before REST channel fetch when notifications are off, guild reactions are disabled, or allowlist mode cannot match without channel overrides, reducing reconnect bursts that caused slow listener warnings. Fixes #73133. Thanks @isaacsummers.
118119
- Channels/Telegram: centralize polling update tracking so accepted offsets remain durable across restarts, same-process handler failures can still retry, and slow offset writes cannot overwrite newer accepted watermarks. Refs #73115. Thanks @vdruts.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
9caccd04afca25d18cfcc4a66bdc30c995f5ec51eaa764c076ce58c9af11a7bf config-baseline.json
2-
8530c8fd54e04a2ab7f6704195f9959311e289ae122ebd8e27af236de435fef9 config-baseline.core.json
1+
4fd357ae137b920586ce5760d461be586f4f9a94e49b73cad1f81110167cd9da config-baseline.json
2+
f874cddd0744be277af58ef14261af7994aba669c642f613be10f92b095998ba config-baseline.core.json
33
a9f058ee9616e189dab7fc223e1207a49ae52b8490b8028935c9d0a2b16f81b2 config-baseline.channel.json
44
1f5592bfd141ba1e982ce31763a253c10afb080ab4ea2b6538299b114e29cee1 config-baseline.plugin.json

docs/channels/discord.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ Once DMs are working, you can set up your Discord server as a full workspace whe
216216
<Step title="Allow responses without @mention">
217217
By default, your agent only responds in guild channels when @mentioned. For a private server, you probably want it to respond to every message.
218218

219+
In guild channels, normal assistant final replies stay private by default. Visible Discord output must be sent explicitly with the `message` tool, so the agent can lurk by default and only post when it decides a channel reply is useful.
220+
219221
<Tabs>
220222
<Tab title="Ask your agent">
221223
> "Allow my agent to respond on this server without having to be @mentioned"
@@ -237,6 +239,8 @@ Once DMs are working, you can set up your Discord server as a full workspace whe
237239
}
238240
```
239241

242+
To restore legacy automatic final replies for group/channel rooms, set `messages.groupChat.visibleReplies: "automatic"`.
243+
240244
</Tab>
241245
</Tabs>
242246

docs/channels/groups.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Default behavior:
1616

1717
- Groups are restricted (`groupPolicy: "allowlist"`).
1818
- Replies require a mention unless you explicitly disable mention gating.
19+
- Normal final replies in groups/channels are private by default. Visible room output uses the `message` tool.
1920

2021
Translation: allowlisted senders can trigger OpenClaw by mentioning it.
2122

@@ -36,6 +37,25 @@ requireMention? yes -> mentioned? no -> store for context only
3637
otherwise -> reply
3738
```
3839

40+
## Visible replies
41+
42+
For group/channel rooms, OpenClaw defaults to `messages.groupChat.visibleReplies: "message_tool"`.
43+
That means the agent still processes the turn and can update memory/session state, but its normal final answer is not automatically posted back into the room. To speak visibly, the agent uses `message(action=send)`.
44+
45+
This replaces the old pattern of forcing the model to answer `NO_REPLY` for most lurk-mode turns. In tool-only mode, doing nothing visible simply means not calling the message tool.
46+
47+
To restore legacy automatic final replies for group/channel rooms:
48+
49+
```json5
50+
{
51+
messages: {
52+
groupChat: {
53+
visibleReplies: "automatic",
54+
},
55+
},
56+
}
57+
```
58+
3959
## Context visibility and allowlists
4060

4161
Two different controls are involved in group safety:

extensions/discord/src/actions/handle-action.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,52 @@ describe("handleDiscordMessageAction", () => {
9999
);
100100
});
101101

102+
it("falls back to Discord toolContext.currentChannelId for sends", async () => {
103+
await handleDiscordMessageAction({
104+
action: "send",
105+
params: {
106+
message: "hello",
107+
},
108+
cfg: {
109+
channels: { discord: { token: "tok" } },
110+
} as OpenClawConfig,
111+
toolContext: {
112+
currentChannelProvider: "discord",
113+
currentChannelId: "channel:123",
114+
},
115+
});
116+
117+
expect(handleDiscordActionMock).toHaveBeenCalledWith(
118+
expect.objectContaining({
119+
action: "sendMessage",
120+
to: "channel:123",
121+
content: "hello",
122+
}),
123+
expect.any(Object),
124+
expect.any(Object),
125+
);
126+
});
127+
128+
it("does not use another provider's current target for Discord sends", async () => {
129+
await expect(
130+
handleDiscordMessageAction({
131+
action: "send",
132+
params: {
133+
message: "hello",
134+
},
135+
cfg: {
136+
channels: { discord: { token: "tok" } },
137+
} as OpenClawConfig,
138+
toolContext: {
139+
currentChannelProvider: "telegram",
140+
currentChannelId: "channel:123",
141+
},
142+
}),
143+
).rejects.toThrow(/channel target is required/i);
144+
145+
expect(handleDiscordActionMock).not.toHaveBeenCalled();
146+
});
147+
102148
it("does not use another provider's current target for Discord reactions", async () => {
103149
await expect(
104150
handleDiscordMessageAction({

extensions/discord/src/actions/handle-action.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,13 @@ export async function handleDiscordMessageAction(
6868
const resolveChannelId = () => resolveDiscordChannelId(readTarget());
6969

7070
if (action === "send") {
71-
const to = readStringParam(params, "to", { required: true });
71+
const to =
72+
readStringParam(params, "to") ??
73+
readStringParam(params, "target") ??
74+
readCurrentDiscordTarget(ctx.toolContext);
75+
if (!to) {
76+
throw new Error("Discord channel target is required (use channel:<id>).");
77+
}
7278
const asVoice = readBooleanParam(params, "asVoice") === true;
7379
const rawComponents =
7480
buildDiscordPresentationComponents(normalizeMessagePresentation(params.presentation)) ??

0 commit comments

Comments
 (0)