diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a324df8599..3c720dd3edb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai - 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. - 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. - 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. +- 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. - 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. - 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. - 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. diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index b2878f1aba8..cebf2cf93e7 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -b9831b7dafd0a7d6d1256ee531b30c0b75c64bf0f494fcc9e68bf2255fdb560a config-baseline.json -b6ebb672410bd1ff148ee6d25fba1a359032686959e28d7b8f0313323f94debf config-baseline.core.json +d9dbaace82aff4445be6ed11e52e69b4548239e3a4e659538f96dfb3ed3c57ac config-baseline.json +9d4d4ab553dadca237d837f71dc7fc13e4ea65d33a564c2ea6775180c413e86a config-baseline.core.json f2a1aad257c570b497865680c331568a6775369528749826dfa35c1f644483fc config-baseline.channel.json -fffe0e74eab92a88c3c57952a70bc932438ce3a7f5f9982688437f2cdaee0bcb config-baseline.plugin.json +858f82733d9828b28bf88bc226e155d8417c494215eb3f808f15799daa42a7d7 config-baseline.plugin.json diff --git a/docs/channels/groups.md b/docs/channels/groups.md index 80215412114..f2045382e1d 100644 --- a/docs/channels/groups.md +++ b/docs/channels/groups.md @@ -41,6 +41,7 @@ otherwise -> reply ## Visible replies For group/channel rooms, OpenClaw defaults to `messages.groupChat.visibleReplies: "message_tool"`. +`openclaw doctor --fix` writes this default into configured-channel configs that omit it. 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)`. If the message tool is unavailable under the active tool policy, OpenClaw falls diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index 80a409a4c67..761c54efd75 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -189,6 +189,7 @@ That stages grounded durable candidates into the short-term dreaming store while - `routing.groupChat.requireMention` → `channels.whatsapp/telegram/imessage.groups."*".requireMention` - `routing.groupChat.historyLimit` → `messages.groupChat.historyLimit` - `routing.groupChat.mentionPatterns` → `messages.groupChat.mentionPatterns` + - configured-channel configs missing visible reply policy → `messages.groupChat.visibleReplies: "message_tool"` - `routing.queue` → `messages.queue` - `routing.bindings` → top-level `bindings` - `routing.agents`/`routing.defaultAgentId` → `agents.list` + `agents.list[].default` diff --git a/src/commands/doctor-legacy-config.migrations.test.ts b/src/commands/doctor-legacy-config.migrations.test.ts index 5cb367575ab..e0d5c4874b3 100644 --- a/src/commands/doctor-legacy-config.migrations.test.ts +++ b/src/commands/doctor-legacy-config.migrations.test.ts @@ -146,6 +146,63 @@ describe("normalizeCompatibilityConfigValues", () => { fs.rmSync(tempOauthDir, { recursive: true, force: true }); }); + it("sets the group visible reply default for configured channels", () => { + const res = normalizeCompatibilityConfigValues({ + channels: { + discord: {}, + }, + messages: { + groupChat: { + mentionPatterns: ["@openclaw"], + }, + }, + }); + + expect(res.config.messages?.groupChat).toEqual({ + mentionPatterns: ["@openclaw"], + visibleReplies: "message_tool", + }); + expect(res.changes).toContain( + 'Set messages.groupChat.visibleReplies to "message_tool" so group/channel replies use the message tool by default.', + ); + }); + + it("does not set group visible replies without channels or when already explicit", () => { + expect( + normalizeCompatibilityConfigValues({ + messages: { + groupChat: { + mentionPatterns: ["@openclaw"], + }, + }, + }).changes, + ).toEqual([]); + + expect( + normalizeCompatibilityConfigValues({ + channels: { + discord: {}, + }, + messages: { + visibleReplies: "automatic", + }, + }).config.messages?.groupChat?.visibleReplies, + ).toBeUndefined(); + + expect( + normalizeCompatibilityConfigValues({ + channels: { + discord: {}, + }, + messages: { + groupChat: { + visibleReplies: "automatic", + }, + }, + }).config.messages?.groupChat?.visibleReplies, + ).toBe("automatic"); + }); + it("does not add whatsapp config when missing and no auth exists", () => { const res = normalizeCompatibilityConfigValues({ messages: { ackReaction: "👀" }, @@ -216,6 +273,11 @@ describe("normalizeCompatibilityConfigValues", () => { it("leaves invalid legacy secretref-env markers for validation to reject", () => { const res = normalizeCompatibilityConfigValues({ + messages: { + groupChat: { + visibleReplies: "message_tool", + }, + }, channels: { discord: { token: "secretref-env:not-valid", diff --git a/src/commands/doctor/shared/legacy-config-compatibility-base.ts b/src/commands/doctor/shared/legacy-config-compatibility-base.ts index c6cd288753d..928ff86a7ba 100644 --- a/src/commands/doctor/shared/legacy-config-compatibility-base.ts +++ b/src/commands/doctor/shared/legacy-config-compatibility-base.ts @@ -8,6 +8,7 @@ import { normalizeLegacyRuntimeModelRefs, normalizeLegacyNanoBananaSkill, normalizeLegacyTalkConfig, + normalizeMissingGroupVisibleRepliesDefault, seedMissingDefaultAccountsFromSingleAccountBase, } from "./legacy-config-core-normalizers.js"; import { migrateLegacyWebFetchConfig } from "./legacy-web-fetch-migrate.js"; @@ -41,6 +42,7 @@ export function normalizeBaseCompatibilityConfigValues( next = normalizeLegacyOpenAIModelProviderApi(next, changes); next = normalizeLegacyRuntimeModelRefs(next, changes); next = normalizeLegacyCrossContextMessageConfig(next, changes); + next = normalizeMissingGroupVisibleRepliesDefault(next, changes); next = normalizeLegacyMediaProviderOptions(next, changes); return normalizeLegacyMistralModelMaxTokens(next, changes); } diff --git a/src/commands/doctor/shared/legacy-config-core-normalizers.ts b/src/commands/doctor/shared/legacy-config-core-normalizers.ts index 04d610656d4..b6238a4e96b 100644 --- a/src/commands/doctor/shared/legacy-config-core-normalizers.ts +++ b/src/commands/doctor/shared/legacy-config-core-normalizers.ts @@ -14,6 +14,42 @@ import { isRecord } from "./legacy-config-record-shared.js"; import { isLegacyModelsAddCodexMetadataModel } from "./legacy-models-add-metadata.js"; export { normalizeLegacyTalkConfig } from "./legacy-talk-config-normalizer.js"; +function hasConfiguredChannels(cfg: OpenClawConfig): boolean { + const channels = cfg.channels; + if (!isRecord(channels)) { + return false; + } + return Object.keys(channels).some((channelId) => channelId !== "defaults"); +} + +export function normalizeMissingGroupVisibleRepliesDefault( + cfg: OpenClawConfig, + changes: string[], +): OpenClawConfig { + const messages = cfg.messages; + if ( + !hasConfiguredChannels(cfg) || + messages?.visibleReplies !== undefined || + messages?.groupChat?.visibleReplies !== undefined + ) { + return cfg; + } + + const nextMessages = messages ? { ...messages } : {}; + nextMessages.groupChat = { + ...messages?.groupChat, + visibleReplies: "message_tool", + }; + changes.push( + 'Set messages.groupChat.visibleReplies to "message_tool" so group/channel replies use the message tool by default.', + ); + + return { + ...cfg, + messages: nextMessages, + }; +} + export function normalizeLegacyCommandsConfig( cfg: OpenClawConfig, changes: string[], diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index 04cd66242fb..350d9a4b89b 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -19056,7 +19056,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { enum: ["automatic", "message_tool"], title: "Group Visible Replies", description: - '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.', + '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.', }, }, additionalProperties: false, @@ -28529,7 +28529,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { }, "messages.groupChat.visibleReplies": { label: "Group Visible Replies", - 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.', + 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.', tags: ["advanced"], }, "messages.queue": { diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index fdf323b5d97..11787e49a0a 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -1642,7 +1642,7 @@ export const FIELD_HELP: Record = { "messages.groupChat.historyLimit": "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.", "messages.groupChat.visibleReplies": - '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.', + '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.', "messages.queue": "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.", "messages.queue.mode":