Skip to content

Commit 85e4ec1

Browse files
authored
fix(doctor): persist group visible reply default (#76513)
1 parent de88b37 commit 85e4ec1

9 files changed

Lines changed: 109 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
3131
- Plugins/install: keep managed npm-root security scans from treating earlier plugin `openclaw` peer links as failures, so one external plugin install cannot poison later official npm installs. Thanks @vincentkoc.
3232
- Memory LanceDB: allow installed-but-unconfigured plugin metadata to load so onboarding and setup flows can prompt for embedding config instead of failing the plugin registry first. Thanks @vincentkoc.
3333
- CLI/plugins: keep `plugins enable` and `plugins disable` from creating unconfigured channel config sections, so channel plugins with required setup fields no longer fail validation during lifecycle probes. Thanks @vincentkoc.
34+
- Doctor/config: set `messages.groupChat.visibleReplies: "message_tool"` during compatibility repair for configured-channel configs that omit a visible-reply policy, so upgrades can persist the intended tool-only group/channel reply default. Thanks @kagura-agent.
3435
- Agents/sessions: keep delayed `sessions_send` A2A replies alive after soft wait-window timeouts, while preserving terminal run timeouts and avoiding stale target replies in requester sessions. Fixes #76443. Thanks @ryswork1993 and @vincentkoc.
3536
- CLI/sessions: keep intentional empty agent replies silent after tool-delivered channel output, instead of surfacing a misleading "No reply from agent." fallback. Thanks @vincentkoc.
3637
- Config/doctor: cap `.clobbered.*` forensic snapshots per config path and serialize snapshot writes so repeated `doctor --fix` recovery loops cannot flood the config directory. Fixes #76454; carries forward #65649. Thanks @JUSTICEESSIELP, @rsnow, and @vincentkoc.
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
b9831b7dafd0a7d6d1256ee531b30c0b75c64bf0f494fcc9e68bf2255fdb560a config-baseline.json
2-
b6ebb672410bd1ff148ee6d25fba1a359032686959e28d7b8f0313323f94debf config-baseline.core.json
1+
d9dbaace82aff4445be6ed11e52e69b4548239e3a4e659538f96dfb3ed3c57ac config-baseline.json
2+
9d4d4ab553dadca237d837f71dc7fc13e4ea65d33a564c2ea6775180c413e86a config-baseline.core.json
33
f2a1aad257c570b497865680c331568a6775369528749826dfa35c1f644483fc config-baseline.channel.json
4-
fffe0e74eab92a88c3c57952a70bc932438ce3a7f5f9982688437f2cdaee0bcb config-baseline.plugin.json
4+
858f82733d9828b28bf88bc226e155d8417c494215eb3f808f15799daa42a7d7 config-baseline.plugin.json

docs/channels/groups.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ otherwise -> reply
4141
## Visible replies
4242

4343
For group/channel rooms, OpenClaw defaults to `messages.groupChat.visibleReplies: "message_tool"`.
44+
`openclaw doctor --fix` writes this default into configured-channel configs that omit it.
4445
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)`.
4546

4647
If the message tool is unavailable under the active tool policy, OpenClaw falls

docs/gateway/doctor.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ That stages grounded durable candidates into the short-term dreaming store while
189189
- `routing.groupChat.requireMention` → `channels.whatsapp/telegram/imessage.groups."*".requireMention`
190190
- `routing.groupChat.historyLimit` → `messages.groupChat.historyLimit`
191191
- `routing.groupChat.mentionPatterns` → `messages.groupChat.mentionPatterns`
192+
- configured-channel configs missing visible reply policy → `messages.groupChat.visibleReplies: "message_tool"`
192193
- `routing.queue` → `messages.queue`
193194
- `routing.bindings` → top-level `bindings`
194195
- `routing.agents`/`routing.defaultAgentId` → `agents.list` + `agents.list[].default`

src/commands/doctor-legacy-config.migrations.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,63 @@ describe("normalizeCompatibilityConfigValues", () => {
146146
fs.rmSync(tempOauthDir, { recursive: true, force: true });
147147
});
148148

149+
it("sets the group visible reply default for configured channels", () => {
150+
const res = normalizeCompatibilityConfigValues({
151+
channels: {
152+
discord: {},
153+
},
154+
messages: {
155+
groupChat: {
156+
mentionPatterns: ["@openclaw"],
157+
},
158+
},
159+
});
160+
161+
expect(res.config.messages?.groupChat).toEqual({
162+
mentionPatterns: ["@openclaw"],
163+
visibleReplies: "message_tool",
164+
});
165+
expect(res.changes).toContain(
166+
'Set messages.groupChat.visibleReplies to "message_tool" so group/channel replies use the message tool by default.',
167+
);
168+
});
169+
170+
it("does not set group visible replies without channels or when already explicit", () => {
171+
expect(
172+
normalizeCompatibilityConfigValues({
173+
messages: {
174+
groupChat: {
175+
mentionPatterns: ["@openclaw"],
176+
},
177+
},
178+
}).changes,
179+
).toEqual([]);
180+
181+
expect(
182+
normalizeCompatibilityConfigValues({
183+
channels: {
184+
discord: {},
185+
},
186+
messages: {
187+
visibleReplies: "automatic",
188+
},
189+
}).config.messages?.groupChat?.visibleReplies,
190+
).toBeUndefined();
191+
192+
expect(
193+
normalizeCompatibilityConfigValues({
194+
channels: {
195+
discord: {},
196+
},
197+
messages: {
198+
groupChat: {
199+
visibleReplies: "automatic",
200+
},
201+
},
202+
}).config.messages?.groupChat?.visibleReplies,
203+
).toBe("automatic");
204+
});
205+
149206
it("does not add whatsapp config when missing and no auth exists", () => {
150207
const res = normalizeCompatibilityConfigValues({
151208
messages: { ackReaction: "👀" },
@@ -216,6 +273,11 @@ describe("normalizeCompatibilityConfigValues", () => {
216273

217274
it("leaves invalid legacy secretref-env markers for validation to reject", () => {
218275
const res = normalizeCompatibilityConfigValues({
276+
messages: {
277+
groupChat: {
278+
visibleReplies: "message_tool",
279+
},
280+
},
219281
channels: {
220282
discord: {
221283
token: "secretref-env:not-valid",

src/commands/doctor/shared/legacy-config-compatibility-base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
normalizeLegacyRuntimeModelRefs,
99
normalizeLegacyNanoBananaSkill,
1010
normalizeLegacyTalkConfig,
11+
normalizeMissingGroupVisibleRepliesDefault,
1112
seedMissingDefaultAccountsFromSingleAccountBase,
1213
} from "./legacy-config-core-normalizers.js";
1314
import { migrateLegacyWebFetchConfig } from "./legacy-web-fetch-migrate.js";
@@ -41,6 +42,7 @@ export function normalizeBaseCompatibilityConfigValues(
4142
next = normalizeLegacyOpenAIModelProviderApi(next, changes);
4243
next = normalizeLegacyRuntimeModelRefs(next, changes);
4344
next = normalizeLegacyCrossContextMessageConfig(next, changes);
45+
next = normalizeMissingGroupVisibleRepliesDefault(next, changes);
4446
next = normalizeLegacyMediaProviderOptions(next, changes);
4547
return normalizeLegacyMistralModelMaxTokens(next, changes);
4648
}

src/commands/doctor/shared/legacy-config-core-normalizers.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,42 @@ import { isRecord } from "./legacy-config-record-shared.js";
1414
import { isLegacyModelsAddCodexMetadataModel } from "./legacy-models-add-metadata.js";
1515
export { normalizeLegacyTalkConfig } from "./legacy-talk-config-normalizer.js";
1616

17+
function hasConfiguredChannels(cfg: OpenClawConfig): boolean {
18+
const channels = cfg.channels;
19+
if (!isRecord(channels)) {
20+
return false;
21+
}
22+
return Object.keys(channels).some((channelId) => channelId !== "defaults");
23+
}
24+
25+
export function normalizeMissingGroupVisibleRepliesDefault(
26+
cfg: OpenClawConfig,
27+
changes: string[],
28+
): OpenClawConfig {
29+
const messages = cfg.messages;
30+
if (
31+
!hasConfiguredChannels(cfg) ||
32+
messages?.visibleReplies !== undefined ||
33+
messages?.groupChat?.visibleReplies !== undefined
34+
) {
35+
return cfg;
36+
}
37+
38+
const nextMessages = messages ? { ...messages } : {};
39+
nextMessages.groupChat = {
40+
...messages?.groupChat,
41+
visibleReplies: "message_tool",
42+
};
43+
changes.push(
44+
'Set messages.groupChat.visibleReplies to "message_tool" so group/channel replies use the message tool by default.',
45+
);
46+
47+
return {
48+
...cfg,
49+
messages: nextMessages,
50+
};
51+
}
52+
1753
export function normalizeLegacyCommandsConfig(
1854
cfg: OpenClawConfig,
1955
changes: string[],

src/config/schema.base.generated.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19056,7 +19056,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
1905619056
enum: ["automatic", "message_tool"],
1905719057
title: "Group Visible Replies",
1905819058
description:
19059-
'Overrides visible source replies for group/channel conversations. "message_tool" keeps normal final replies private and requires message(action=send) for room output; "automatic" posts normal replies as before.',
19059+
'Overrides visible source replies for group/channel conversations. Defaults to "message_tool" when no global visible reply policy is set. "message_tool" keeps normal final replies private and requires message(action=send) for room output; "automatic" posts normal replies as before.',
1906019060
},
1906119061
},
1906219062
additionalProperties: false,
@@ -28529,7 +28529,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
2852928529
},
2853028530
"messages.groupChat.visibleReplies": {
2853128531
label: "Group Visible Replies",
28532-
help: 'Overrides visible source replies for group/channel conversations. "message_tool" keeps normal final replies private and requires message(action=send) for room output; "automatic" posts normal replies as before.',
28532+
help: 'Overrides visible source replies for group/channel conversations. Defaults to "message_tool" when no global visible reply policy is set. "message_tool" keeps normal final replies private and requires message(action=send) for room output; "automatic" posts normal replies as before.',
2853328533
tags: ["advanced"],
2853428534
},
2853528535
"messages.queue": {

src/config/schema.help.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1642,7 +1642,7 @@ export const FIELD_HELP: Record<string, string> = {
16421642
"messages.groupChat.historyLimit":
16431643
"Maximum number of prior group messages loaded as context per turn for group sessions. Use higher values for richer continuity, or lower values for faster and cheaper responses.",
16441644
"messages.groupChat.visibleReplies":
1645-
'Overrides visible source replies for group/channel conversations. "message_tool" keeps normal final replies private and requires message(action=send) for room output; "automatic" posts normal replies as before.',
1645+
'Overrides visible source replies for group/channel conversations. Defaults to "message_tool" when no global visible reply policy is set. "message_tool" keeps normal final replies private and requires message(action=send) for room output; "automatic" posts normal replies as before.',
16461646
"messages.queue":
16471647
"Inbound message queue strategy for messages that arrive while a session run is active. Default mode is steer, with followup fallback when steering is unavailable.",
16481648
"messages.queue.mode":

0 commit comments

Comments
 (0)