mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:20:43 +00:00
fix(auto-reply): surface private group replies
This commit is contained in:
@@ -533,7 +533,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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
c3bcb3a3da46bbbe15a7798869911cab109df950ee51c79fd86c96bb809dfdf1 config-baseline.json
|
||||
d4b34f6fd2c39132bf4feff4be5ddfd226fa52c4596d6bdc438031456dde18d4 config-baseline.json
|
||||
8f573caa7f4cf01ae9d4805d3d14e1ba6772f651f6da182baaf2b469592749a4 config-baseline.core.json
|
||||
92712871defa92eeda8161b516db85574681f2b70678b940508a808b987aeae2 config-baseline.channel.json
|
||||
aca3215b7382af82b5060d73c631a7f82661c6e99193fa5eb1c5b4b499fb657b config-baseline.plugin.json
|
||||
6005cf9f6e8c9f25ef97207b5eee29ae0e506cf910cdeca77fc9894ad1755b1f config-baseline.plugin.json
|
||||
|
||||
@@ -61,6 +61,9 @@ To restore legacy automatic final replies for group/channel rooms:
|
||||
}
|
||||
```
|
||||
|
||||
The gateway hot-reloads `messages` config after the file is saved. Restart only
|
||||
when file watching or config reload is disabled in the deployment.
|
||||
|
||||
To require visible output to go through the message tool for every source chat:
|
||||
|
||||
```json5
|
||||
|
||||
@@ -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"`.
|
||||
|
||||
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:**
|
||||
|
||||
- **Metadata mentions**: Native platform @-mentions. Ignored in WhatsApp self-chat mode.
|
||||
|
||||
@@ -65,10 +65,38 @@ describe("gateway config mutation guard coverage", () => {
|
||||
"agents.list[].id",
|
||||
"agents.list[].model",
|
||||
"channels.*.requireMention",
|
||||
"messages.visibleReplies",
|
||||
"messages.groupChat.visibleReplies",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("allows visible reply delivery mode edits via config.patch", () => {
|
||||
expectAllowed(
|
||||
{},
|
||||
{
|
||||
messages: {
|
||||
visibleReplies: "automatic",
|
||||
groupChat: { visibleReplies: "automatic" },
|
||||
},
|
||||
},
|
||||
);
|
||||
expectAllowed(
|
||||
{
|
||||
messages: {
|
||||
visibleReplies: "automatic",
|
||||
groupChat: { visibleReplies: "message_tool" },
|
||||
},
|
||||
},
|
||||
{
|
||||
messages: {
|
||||
visibleReplies: "message_tool",
|
||||
groupChat: { visibleReplies: "automatic" },
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("blocks disabling sandbox mode via config.patch", () => {
|
||||
expectBlocked(
|
||||
{ agents: { defaults: { sandbox: { mode: "all" } } } },
|
||||
|
||||
@@ -51,6 +51,10 @@ const ALLOWED_GATEWAY_CONFIG_PATHS = [
|
||||
"channels.*.*.*.requireMention",
|
||||
"channels.*.*.*.*.requireMention",
|
||||
"channels.*.*.*.*.*.requireMention",
|
||||
// Visible reply delivery mode is a bounded message UX setting, not a secret
|
||||
// or privilege boundary. Let agents repair silent group/channel rooms.
|
||||
"messages.visibleReplies",
|
||||
"messages.groupChat.visibleReplies",
|
||||
] as const;
|
||||
|
||||
/** @internal Exposed for regression tests only; do not import from runtime code. */
|
||||
|
||||
@@ -4402,6 +4402,31 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () =>
|
||||
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to automatic group/channel delivery when the message tool is unavailable", async () => {
|
||||
setNoAbort();
|
||||
const dispatcher = createDispatcher();
|
||||
const replyResolver = vi.fn(async (_ctx: MsgContext, opts?: GetReplyOptions) => {
|
||||
expect(opts?.sourceReplyDeliveryMode).toBe("automatic");
|
||||
return { text: "visible fallback" } satisfies ReplyPayload;
|
||||
});
|
||||
|
||||
const result = await dispatchReplyFromConfig({
|
||||
ctx: buildTestCtx({
|
||||
ChatType: "channel",
|
||||
SessionKey: "test:discord:channel:C1",
|
||||
}),
|
||||
cfg: { tools: { allow: ["read"] } } as OpenClawConfig,
|
||||
dispatcher,
|
||||
replyResolver,
|
||||
});
|
||||
|
||||
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||
expect(result.queuedFinal).toBe(true);
|
||||
expect(dispatcher.sendFinalReply).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ text: "visible fallback" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps native command replies visible in group/channel turns", async () => {
|
||||
setNoAbort();
|
||||
const dispatcher = createDispatcher();
|
||||
|
||||
@@ -5,6 +5,11 @@ import {
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveSessionAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import {
|
||||
isToolAllowedByPolicies,
|
||||
resolveEffectiveToolPolicy,
|
||||
} from "../../agents/pi-tools.policy.js";
|
||||
import { mergeAlsoAllowPolicy, resolveToolProfilePolicy } from "../../agents/tool-policy.js";
|
||||
import {
|
||||
resolveConversationBindingRecord,
|
||||
touchConversationBindingRecord,
|
||||
@@ -593,6 +598,33 @@ export async function dispatchReplyFromConfig(
|
||||
undefined,
|
||||
chatType: sessionStoreEntry.entry?.chatType,
|
||||
});
|
||||
const {
|
||||
globalPolicy,
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
profile,
|
||||
providerProfile,
|
||||
profileAlsoAllow,
|
||||
providerProfileAlsoAllow,
|
||||
} = resolveEffectiveToolPolicy({
|
||||
config: cfg,
|
||||
sessionKey: acpDispatchSessionKey,
|
||||
agentId: sessionAgentId,
|
||||
});
|
||||
const profilePolicy = mergeAlsoAllowPolicy(resolveToolProfilePolicy(profile), profileAlsoAllow);
|
||||
const providerProfilePolicy = mergeAlsoAllowPolicy(
|
||||
resolveToolProfilePolicy(providerProfile),
|
||||
providerProfileAlsoAllow,
|
||||
);
|
||||
const messageToolAvailable = isToolAllowedByPolicies("message", [
|
||||
profilePolicy,
|
||||
providerProfilePolicy,
|
||||
globalProviderPolicy,
|
||||
agentProviderPolicy,
|
||||
globalPolicy,
|
||||
agentPolicy,
|
||||
]);
|
||||
const sourceReplyPolicy = resolveSourceReplyVisibilityPolicy({
|
||||
cfg,
|
||||
ctx,
|
||||
@@ -601,6 +633,7 @@ export async function dispatchReplyFromConfig(
|
||||
suppressAcpChildUserDelivery,
|
||||
explicitSuppressTyping: params.replyOptions?.suppressTyping === true,
|
||||
shouldSuppressTyping,
|
||||
messageToolAvailable,
|
||||
});
|
||||
const {
|
||||
sourceReplyDeliveryMode,
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } 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";
|
||||
@@ -19,6 +40,11 @@ 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(
|
||||
@@ -30,6 +56,10 @@ 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", () => {
|
||||
@@ -77,6 +107,42 @@ describe("resolveSourceReplyDeliveryMode", () => {
|
||||
ctx: { ChatType: "group", CommandSource: "native" },
|
||||
}),
|
||||
).toBe("automatic");
|
||||
expect(loggerMocks.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to automatic when message tool is unavailable", () => {
|
||||
expect(
|
||||
resolveSourceReplyDeliveryMode({
|
||||
cfg: emptyConfig,
|
||||
ctx: { ChatType: "group" },
|
||||
messageToolAvailable: false,
|
||||
}),
|
||||
).toBe("automatic");
|
||||
expect(
|
||||
resolveSourceReplyDeliveryMode({
|
||||
cfg: globalToolOnlyReplyConfig,
|
||||
ctx: { ChatType: "direct" },
|
||||
messageToolAvailable: false,
|
||||
}),
|
||||
).toBe("automatic");
|
||||
expect(loggerMocks.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps message-tool-only delivery when message tool availability is unknown", () => {
|
||||
expect(
|
||||
resolveSourceReplyDeliveryMode({
|
||||
cfg: emptyConfig,
|
||||
ctx: { ChatType: "group" },
|
||||
messageToolAvailable: true,
|
||||
}),
|
||||
).toBe("message_tool_only");
|
||||
expect(
|
||||
resolveSourceReplyDeliveryMode({
|
||||
cfg: emptyConfig,
|
||||
ctx: { ChatType: "channel" },
|
||||
}),
|
||||
).toBe("message_tool_only");
|
||||
expect(loggerMocks.warn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -220,4 +286,21 @@ describe("resolveSourceReplyVisibilityPolicy", () => {
|
||||
suppressTyping: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps delivery automatic when message-tool-only mode cannot send visibly", () => {
|
||||
expect(
|
||||
resolveSourceReplyVisibilityPolicy({
|
||||
cfg: emptyConfig,
|
||||
ctx: { ChatType: "group" },
|
||||
sendPolicy: "allow",
|
||||
messageToolAvailable: false,
|
||||
}),
|
||||
).toMatchObject({
|
||||
sourceReplyDeliveryMode: "automatic",
|
||||
suppressAutomaticSourceDelivery: false,
|
||||
suppressDelivery: false,
|
||||
suppressHookUserDelivery: false,
|
||||
deliverySuppressionReason: "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
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 {
|
||||
if (params.requested) {
|
||||
return params.requested;
|
||||
@@ -20,12 +31,33 @@ export function resolveSourceReplyDeliveryMode(params: {
|
||||
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;
|
||||
return configuredMode === "automatic" ? "automatic" : "message_tool_only";
|
||||
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";
|
||||
}
|
||||
return params.cfg.messages?.visibleReplies === "message_tool" ? "message_tool_only" : "automatic";
|
||||
if (mode === "message_tool_only" && params.messageToolAvailable === false) {
|
||||
return "automatic";
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
export type SourceReplyVisibilityPolicy = {
|
||||
@@ -47,11 +79,13 @@ export function resolveSourceReplyVisibilityPolicy(params: {
|
||||
suppressAcpChildUserDelivery?: boolean;
|
||||
explicitSuppressTyping?: boolean;
|
||||
shouldSuppressTyping?: boolean;
|
||||
messageToolAvailable?: boolean;
|
||||
}): SourceReplyVisibilityPolicy {
|
||||
const sourceReplyDeliveryMode = resolveSourceReplyDeliveryMode({
|
||||
cfg: params.cfg,
|
||||
ctx: params.ctx,
|
||||
requested: params.requested,
|
||||
messageToolAvailable: params.messageToolAvailable,
|
||||
});
|
||||
const sendPolicyDenied = params.sendPolicy === "deny";
|
||||
const suppressAutomaticSourceDelivery = sourceReplyDeliveryMode === "message_tool_only";
|
||||
|
||||
@@ -28,6 +28,7 @@ export function resolveChannelSourceReplyDeliveryMode(params: {
|
||||
cfg: OpenClawConfig;
|
||||
ctx: SourceReplyDeliveryModeContext;
|
||||
requested?: SourceReplyDeliveryMode;
|
||||
messageToolAvailable?: boolean;
|
||||
}): SourceReplyDeliveryMode {
|
||||
return resolveSourceReplyDeliveryMode(params);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user