mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix(auto-reply): move visible reply warnings to doctor (#75367)
Summary: - The PR removes the auto-reply runtime warning for visible-reply defaults, adds doctor preview warnings and tests for message-tool visibility policy mismatches, and updates the group/channel docs and changelog wording. ClawSweeper fixups: - No separate fixup commits were needed after automerge opt-in. Validation: - ClawSweeper review passed for head1f96b3b568. - Required merge gates passed before the squash merge. Prepared head SHA:1f96b3b568Review: https://github.com/openclaw/openclaw/pull/75367#issuecomment-4357475980 Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
committed by
GitHub
parent
ce833acbdb
commit
8989ceee50
@@ -573,7 +573,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- Group/channel chats (all channels): 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.
|
||||
- Group/channel chats (all channels): keep group/channel replies private by default unless the agent explicitly uses the message tool, fall back to automatic visible replies when the message tool is unavailable, and have `openclaw doctor` warn about that policy mismatch; `messages.groupChat.visibleReplies: "automatic"` restores legacy auto-posting. (#73046) Thanks @scoootscooob.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -43,6 +43,10 @@ otherwise -> reply
|
||||
For group/channel rooms, OpenClaw defaults to `messages.groupChat.visibleReplies: "message_tool"`.
|
||||
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
|
||||
back to automatic visible replies instead of silently suppressing the response.
|
||||
`openclaw doctor` warns about this mismatch.
|
||||
|
||||
For direct chats and any other source turn, use `messages.visibleReplies: "message_tool"` to apply the same tool-only visible-reply behavior globally. `messages.groupChat.visibleReplies` remains the more specific override for group/channel rooms.
|
||||
|
||||
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.
|
||||
|
||||
@@ -772,6 +772,8 @@ Group messages default to **require mention** (metadata mention or safe regex pa
|
||||
|
||||
Visible replies are controlled separately. Group/channel rooms default to `messages.groupChat.visibleReplies: "message_tool"`: OpenClaw still processes the turn, but normal final replies stay private and visible room output requires `message(action=send)`. Set `"automatic"` only when you want the legacy behavior where normal replies are posted back to the room. To apply the same tool-only visible-reply behavior to direct chats too, set `messages.visibleReplies: "message_tool"`.
|
||||
|
||||
If the message tool is unavailable under the active tool policy, OpenClaw falls back to automatic visible replies instead of silently suppressing the response. `openclaw doctor` warns about this mismatch.
|
||||
|
||||
The gateway hot-reloads `messages` config after the file is saved. Restart only when file watching or config reload is disabled in the deployment.
|
||||
|
||||
**Mention types:**
|
||||
|
||||
@@ -1,27 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
|
||||
const loggerMocks = vi.hoisted(() => ({
|
||||
warn: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../logging/subsystem.js", () => ({
|
||||
createSubsystemLogger: () => ({
|
||||
subsystem: "auto-reply",
|
||||
isEnabled: () => false,
|
||||
trace: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: loggerMocks.warn,
|
||||
error: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
raw: vi.fn(),
|
||||
child: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
import {
|
||||
resetVisibleRepliesPrivateDefaultWarningForTest,
|
||||
resolveSourceReplyDeliveryMode,
|
||||
resolveSourceReplyVisibilityPolicy,
|
||||
} from "./source-reply-delivery-mode.js";
|
||||
@@ -40,11 +19,6 @@ const globalToolOnlyReplyConfig = {
|
||||
},
|
||||
} as const satisfies OpenClawConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
loggerMocks.warn.mockClear();
|
||||
resetVisibleRepliesPrivateDefaultWarningForTest();
|
||||
});
|
||||
|
||||
describe("resolveSourceReplyDeliveryMode", () => {
|
||||
it("defaults groups and channels to message-tool-only delivery", () => {
|
||||
expect(resolveSourceReplyDeliveryMode({ cfg: emptyConfig, ctx: { ChatType: "channel" } })).toBe(
|
||||
@@ -56,10 +30,6 @@ describe("resolveSourceReplyDeliveryMode", () => {
|
||||
expect(resolveSourceReplyDeliveryMode({ cfg: emptyConfig, ctx: { ChatType: "direct" } })).toBe(
|
||||
"automatic",
|
||||
);
|
||||
expect(loggerMocks.warn).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMocks.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Group/channel replies are private by default"),
|
||||
);
|
||||
});
|
||||
|
||||
it("honors config and explicit requested mode", () => {
|
||||
@@ -107,7 +77,6 @@ describe("resolveSourceReplyDeliveryMode", () => {
|
||||
ctx: { ChatType: "group", CommandSource: "native" },
|
||||
}),
|
||||
).toBe("automatic");
|
||||
expect(loggerMocks.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to automatic when message tool is unavailable", () => {
|
||||
@@ -133,7 +102,6 @@ describe("resolveSourceReplyDeliveryMode", () => {
|
||||
messageToolAvailable: false,
|
||||
}),
|
||||
).toBe("automatic");
|
||||
expect(loggerMocks.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps message-tool-only delivery when message tool availability is unknown", () => {
|
||||
@@ -150,7 +118,6 @@ describe("resolveSourceReplyDeliveryMode", () => {
|
||||
ctx: { ChatType: "channel" },
|
||||
}),
|
||||
).toBe("message_tool_only");
|
||||
expect(loggerMocks.warn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -294,7 +261,6 @@ describe("resolveSourceReplyVisibilityPolicy", () => {
|
||||
suppressTyping: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps delivery automatic when message-tool-only mode cannot send visibly", () => {
|
||||
expect(
|
||||
resolveSourceReplyVisibilityPolicy({
|
||||
|
||||
@@ -1,58 +1,36 @@
|
||||
import { normalizeChatType } from "../../channels/chat-type.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import type { SessionSendPolicyDecision } from "../../sessions/send-policy.js";
|
||||
import type { SourceReplyDeliveryMode } from "../get-reply-options.types.js";
|
||||
|
||||
const log = createSubsystemLogger("auto-reply");
|
||||
|
||||
let visibleRepliesPrivateDefaultWarned = false;
|
||||
|
||||
export type SourceReplyDeliveryModeContext = {
|
||||
ChatType?: string;
|
||||
CommandSource?: "text" | "native";
|
||||
};
|
||||
|
||||
/** @internal Test-only reset for the process-level one-shot warning. */
|
||||
export function resetVisibleRepliesPrivateDefaultWarningForTest(): void {
|
||||
visibleRepliesPrivateDefaultWarned = false;
|
||||
}
|
||||
|
||||
export function resolveSourceReplyDeliveryMode(params: {
|
||||
cfg: OpenClawConfig;
|
||||
ctx: SourceReplyDeliveryModeContext;
|
||||
requested?: SourceReplyDeliveryMode;
|
||||
messageToolAvailable?: boolean;
|
||||
}): SourceReplyDeliveryMode {
|
||||
let mode: SourceReplyDeliveryMode;
|
||||
if (params.requested) {
|
||||
mode = params.requested;
|
||||
} else if (params.ctx.CommandSource === "native") {
|
||||
mode = "automatic";
|
||||
return params.messageToolAvailable === false && params.requested === "message_tool_only"
|
||||
? "automatic"
|
||||
: params.requested;
|
||||
}
|
||||
if (params.ctx.CommandSource === "native") {
|
||||
return "automatic";
|
||||
}
|
||||
const chatType = normalizeChatType(params.ctx.ChatType);
|
||||
let mode: SourceReplyDeliveryMode;
|
||||
if (chatType === "group" || chatType === "channel") {
|
||||
const configuredMode =
|
||||
params.cfg.messages?.groupChat?.visibleReplies ?? params.cfg.messages?.visibleReplies;
|
||||
mode = configuredMode === "automatic" ? "automatic" : "message_tool_only";
|
||||
} else {
|
||||
const chatType = normalizeChatType(params.ctx.ChatType);
|
||||
if (chatType === "group" || chatType === "channel") {
|
||||
const configuredMode =
|
||||
params.cfg.messages?.groupChat?.visibleReplies ?? params.cfg.messages?.visibleReplies;
|
||||
mode = configuredMode === "automatic" ? "automatic" : "message_tool_only";
|
||||
if (
|
||||
mode === "message_tool_only" &&
|
||||
configuredMode === undefined &&
|
||||
params.messageToolAvailable !== false &&
|
||||
!visibleRepliesPrivateDefaultWarned
|
||||
) {
|
||||
visibleRepliesPrivateDefaultWarned = true;
|
||||
log.warn(
|
||||
`Group/channel replies are private by default since 2026.4.27. ` +
|
||||
`To restore automatic room posting, set messages.groupChat.visibleReplies to "automatic" in openclaw.json and save the config. ` +
|
||||
`The gateway hot-reloads messages config; restart only if file watching/reload is disabled. ` +
|
||||
`Relates to https://github.com/openclaw/openclaw/issues/74876`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
mode =
|
||||
params.cfg.messages?.visibleReplies === "message_tool" ? "message_tool_only" : "automatic";
|
||||
}
|
||||
mode =
|
||||
params.cfg.messages?.visibleReplies === "message_tool" ? "message_tool_only" : "automatic";
|
||||
}
|
||||
if (mode === "message_tool_only" && params.messageToolAvailable === false) {
|
||||
return "automatic";
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { collectDoctorPreviewWarnings } from "./preview-warnings.js";
|
||||
import {
|
||||
collectDoctorPreviewWarnings,
|
||||
collectVisibleReplyToolPolicyWarnings,
|
||||
} from "./preview-warnings.js";
|
||||
|
||||
type TestManifestRecord = {
|
||||
id: string;
|
||||
@@ -392,4 +395,98 @@ describe("doctor preview warnings", () => {
|
||||
]);
|
||||
expect(warnings.join("\n")).not.toContain("stale plugin reference");
|
||||
});
|
||||
|
||||
it("warns softly when default group visible replies need an unavailable message tool", () => {
|
||||
const warnings = collectVisibleReplyToolPolicyWarnings({
|
||||
channels: {
|
||||
slack: {},
|
||||
},
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining('messages.groupChat.visibleReplies defaults to "message_tool"'),
|
||||
]);
|
||||
expect(warnings[0]).toContain("message tool is unavailable");
|
||||
expect(warnings[0]).toContain("falls back to automatic group/channel replies");
|
||||
});
|
||||
|
||||
it("warns strongly when explicit group visible replies require an unavailable message tool", () => {
|
||||
const warnings = collectVisibleReplyToolPolicyWarnings({
|
||||
messages: {
|
||||
groupChat: {
|
||||
visibleReplies: "message_tool",
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
profile: "coding",
|
||||
},
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining('messages.groupChat.visibleReplies is set to "message_tool"'),
|
||||
]);
|
||||
expect(warnings[0]).toContain("normal replies may post to the source chat");
|
||||
expect(warnings[0]).toContain('set messages.groupChat.visibleReplies to "automatic"');
|
||||
});
|
||||
|
||||
it("warns for direct chats when global visible replies are tool-only but groups override automatic", () => {
|
||||
const warnings = collectVisibleReplyToolPolicyWarnings({
|
||||
messages: {
|
||||
visibleReplies: "message_tool",
|
||||
groupChat: {
|
||||
visibleReplies: "automatic",
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining('messages.visibleReplies is set to "message_tool"'),
|
||||
]);
|
||||
expect(warnings[0]).toContain("automatic direct-chat replies");
|
||||
});
|
||||
|
||||
it("warns separately for explicit global and group visible reply policy mismatches", () => {
|
||||
const warnings = collectVisibleReplyToolPolicyWarnings({
|
||||
messages: {
|
||||
visibleReplies: "message_tool",
|
||||
groupChat: {
|
||||
visibleReplies: "message_tool",
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining('messages.groupChat.visibleReplies is set to "message_tool"'),
|
||||
expect.stringContaining('messages.visibleReplies is set to "message_tool"'),
|
||||
]);
|
||||
});
|
||||
|
||||
it("skips visible reply tool warnings when the message tool is available or default groups are unused", () => {
|
||||
expect(
|
||||
collectVisibleReplyToolPolicyWarnings({
|
||||
channels: {
|
||||
slack: {},
|
||||
},
|
||||
tools: {
|
||||
profile: "messaging",
|
||||
},
|
||||
}),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
collectVisibleReplyToolPolicyWarnings({
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
},
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { pickSandboxToolPolicy } from "../../../agents/sandbox-tool-policy.js";
|
||||
import { isToolAllowedByPolicies } from "../../../agents/tool-policy-match.js";
|
||||
import { mergeAlsoAllowPolicy, resolveToolProfilePolicy } from "../../../agents/tool-policy.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import type { AgentToolsConfig, ToolsConfig } from "../../../config/types.tools.js";
|
||||
|
||||
type ChannelDoctorModule = typeof import("./channel-doctor.js");
|
||||
|
||||
@@ -75,6 +79,109 @@ function hasConfiguredSafeBins(cfg: OpenClawConfig): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
type VisibleReplyPolicyProvenance = "default" | "global-explicit" | "group-explicit";
|
||||
|
||||
function resolveMessageToolAvailability(params: {
|
||||
globalTools?: ToolsConfig;
|
||||
agentTools?: AgentToolsConfig;
|
||||
}): boolean {
|
||||
const profile = params.agentTools?.profile ?? params.globalTools?.profile;
|
||||
const profileAlsoAllow = Array.isArray(params.agentTools?.alsoAllow)
|
||||
? params.agentTools.alsoAllow
|
||||
: Array.isArray(params.globalTools?.alsoAllow)
|
||||
? params.globalTools.alsoAllow
|
||||
: undefined;
|
||||
const profilePolicy = mergeAlsoAllowPolicy(resolveToolProfilePolicy(profile), profileAlsoAllow);
|
||||
return isToolAllowedByPolicies("message", [
|
||||
profilePolicy,
|
||||
pickSandboxToolPolicy(params.globalTools),
|
||||
pickSandboxToolPolicy(params.agentTools),
|
||||
]);
|
||||
}
|
||||
|
||||
function collectMessageToolUnavailableTargets(cfg: OpenClawConfig): string[] {
|
||||
const agents = cfg.agents?.list ?? [];
|
||||
if (agents.length === 0) {
|
||||
return resolveMessageToolAvailability({ globalTools: cfg.tools })
|
||||
? []
|
||||
: ["default tool policy"];
|
||||
}
|
||||
return agents.flatMap((agent) =>
|
||||
resolveMessageToolAvailability({ globalTools: cfg.tools, agentTools: agent.tools })
|
||||
? []
|
||||
: [`agent "${agent.id}"`],
|
||||
);
|
||||
}
|
||||
|
||||
function resolveGroupVisibleReplyProvenance(cfg: OpenClawConfig): {
|
||||
path: "messages.groupChat.visibleReplies" | "messages.visibleReplies";
|
||||
provenance: VisibleReplyPolicyProvenance;
|
||||
value: "automatic" | "message_tool";
|
||||
} {
|
||||
const groupVisibleReplies = cfg.messages?.groupChat?.visibleReplies;
|
||||
if (groupVisibleReplies) {
|
||||
return {
|
||||
path: "messages.groupChat.visibleReplies",
|
||||
provenance: "group-explicit",
|
||||
value: groupVisibleReplies,
|
||||
};
|
||||
}
|
||||
const globalVisibleReplies = cfg.messages?.visibleReplies;
|
||||
if (globalVisibleReplies) {
|
||||
return {
|
||||
path: "messages.visibleReplies",
|
||||
provenance: "global-explicit",
|
||||
value: globalVisibleReplies,
|
||||
};
|
||||
}
|
||||
return {
|
||||
path: "messages.groupChat.visibleReplies",
|
||||
provenance: "default",
|
||||
value: "message_tool",
|
||||
};
|
||||
}
|
||||
|
||||
function formatTargets(targets: string[]): string {
|
||||
if (targets.length <= 2) {
|
||||
return targets.join(" and ");
|
||||
}
|
||||
return `${targets.slice(0, 2).join(", ")}, and ${targets.length - 2} more`;
|
||||
}
|
||||
|
||||
export function collectVisibleReplyToolPolicyWarnings(cfg: OpenClawConfig): string[] {
|
||||
const targets = collectMessageToolUnavailableTargets(cfg);
|
||||
if (targets.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const groupPolicy = resolveGroupVisibleReplyProvenance(cfg);
|
||||
const warnings: string[] = [];
|
||||
if (groupPolicy.value === "message_tool") {
|
||||
if (groupPolicy.provenance === "default" && !hasChannels(cfg)) {
|
||||
return warnings;
|
||||
}
|
||||
const targetSummary = formatTargets(targets);
|
||||
if (groupPolicy.provenance === "default") {
|
||||
warnings.push(
|
||||
`- messages.groupChat.visibleReplies defaults to "message_tool", but the message tool is unavailable for ${targetSummary}; OpenClaw falls back to automatic group/channel replies to avoid silent responses. Enable the message tool or set messages.groupChat.visibleReplies explicitly.`,
|
||||
);
|
||||
} else {
|
||||
warnings.push(
|
||||
`- ${groupPolicy.path} is set to "message_tool", but the message tool is unavailable for ${targetSummary}; OpenClaw falls back to automatic visible replies, so normal replies may post to the source chat. Enable the message tool or set ${groupPolicy.path} to "automatic".`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const globalVisibleReplies = cfg.messages?.visibleReplies;
|
||||
if (globalVisibleReplies === "message_tool" && groupPolicy.path !== "messages.visibleReplies") {
|
||||
warnings.push(
|
||||
`- messages.visibleReplies is set to "message_tool", but the message tool is unavailable for ${formatTargets(
|
||||
targets,
|
||||
)}; OpenClaw falls back to automatic direct-chat replies, so normal replies may post to the source chat. Enable the message tool or set messages.visibleReplies to "automatic".`,
|
||||
);
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
export async function collectDoctorPreviewWarnings(params: {
|
||||
cfg: OpenClawConfig;
|
||||
doctorFixCommand: string;
|
||||
@@ -85,6 +192,8 @@ export async function collectDoctorPreviewWarnings(params: {
|
||||
const hasChannelConfig = hasChannels(params.cfg);
|
||||
const hasPluginConfig = hasPlugins(params.cfg);
|
||||
|
||||
warnings.push(...collectVisibleReplyToolPolicyWarnings(params.cfg));
|
||||
|
||||
const channelPluginRuntime =
|
||||
hasChannelConfig && hasExplicitChannelPluginBlockerConfig(params.cfg)
|
||||
? await import("./channel-plugin-blockers.js")
|
||||
|
||||
Reference in New Issue
Block a user