mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:00:42 +00:00
fix(slack): suppress verbose progress in rooms
This commit is contained in:
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Codex harness: route native `request_user_input` prompts back to the originating chat, preserve queued follow-up answers, and honor newer app-server command approval amendment decisions.
|
||||
- Slack/groups: classify MPIM group DMs as group chat context and suppress verbose tool/plan progress on Slack non-DM surfaces, so internal "Working…" traces no longer leak into rooms. Fixes #70912.
|
||||
- Agents/replay: stop OpenAI/Codex transcript replay from synthesizing missing tool results while still preserving synthetic repair on Anthropic, Gemini, and Bedrock transport-owned sessions. (#61556) Thanks @VictorJeon and @vincentkoc.
|
||||
- Telegram/media replies: parse remote markdown image syntax into outbound media payloads on the final reply path, so Telegram group chats stop falling back to plain-text image URLs when the model or a tool emits `` instead of a `MEDIA:` token. (#66191) Thanks @apezam and @vincentkoc.
|
||||
- Agents/WebChat: surface non-retryable provider failures such as billing, auth, and rate-limit errors from the embedded runner instead of logging `surface_error` and leaving webchat with no rendered error. Fixes #70124. (#70848) Thanks @truffle-dev.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { SlackMessageEvent } from "../types.js";
|
||||
|
||||
export type SlackChatType = "direct" | "group" | "channel";
|
||||
|
||||
export function inferSlackChannelType(
|
||||
channelId?: string | null,
|
||||
): SlackMessageEvent["channel_type"] | undefined {
|
||||
@@ -40,3 +42,15 @@ export function normalizeSlackChannelType(
|
||||
}
|
||||
return inferred ?? "channel";
|
||||
}
|
||||
|
||||
export function resolveSlackChatType(
|
||||
channelType: SlackMessageEvent["channel_type"],
|
||||
): SlackChatType {
|
||||
if (channelType === "im") {
|
||||
return "direct";
|
||||
}
|
||||
if (channelType === "mpim") {
|
||||
return "group";
|
||||
}
|
||||
return "channel";
|
||||
}
|
||||
|
||||
@@ -25,7 +25,11 @@ import { normalizeSlackChannelType } from "./channel-type.js";
|
||||
import { resolveSessionKey } from "./config.runtime.js";
|
||||
import { isSlackChannelAllowedByPolicy } from "./policy.js";
|
||||
|
||||
export { inferSlackChannelType, normalizeSlackChannelType } from "./channel-type.js";
|
||||
export {
|
||||
inferSlackChannelType,
|
||||
normalizeSlackChannelType,
|
||||
resolveSlackChatType,
|
||||
} from "./channel-type.js";
|
||||
|
||||
export type SlackMonitorContext = {
|
||||
cfg: OpenClawConfig;
|
||||
|
||||
@@ -405,6 +405,22 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
expect(prepared!.ctxPayload.MessageThreadId).toBe("1.000");
|
||||
});
|
||||
|
||||
it("classifies MPIM group DMs as group chat context", async () => {
|
||||
const prepared = await prepareMessageWith(
|
||||
createReplyToAllSlackCtx(),
|
||||
createSlackAccount({ replyToMode: "all" }),
|
||||
createSlackMessage({
|
||||
channel: "G123",
|
||||
channel_type: "mpim",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(prepared).toBeTruthy();
|
||||
expect(prepared!.isRoomish).toBe(true);
|
||||
expect(prepared!.ctxPayload.ChatType).toBe("group");
|
||||
expect(prepared!.ctxPayload.From).toBe("slack:group:G123");
|
||||
});
|
||||
|
||||
it("respects replyToModeByChatType.direct override for DMs", async () => {
|
||||
const prepared = await prepareMessageWith(
|
||||
createReplyToAllSlackCtx(),
|
||||
|
||||
@@ -47,7 +47,11 @@ import {
|
||||
resolveChannelContextVisibilityMode,
|
||||
resolveStorePath,
|
||||
} from "../config.runtime.js";
|
||||
import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js";
|
||||
import {
|
||||
normalizeSlackChannelType,
|
||||
resolveSlackChatType,
|
||||
type SlackMonitorContext,
|
||||
} from "../context.js";
|
||||
import { recordInboundSession, resolveConversationLabel } from "../conversation.runtime.js";
|
||||
import { authorizeSlackDirectMessage } from "../dm-auth.js";
|
||||
import { resolveSlackThreadStarter } from "../media.js";
|
||||
@@ -541,6 +545,7 @@ export async function prepareSlackMessage(params: {
|
||||
|
||||
const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`;
|
||||
const senderName = await resolveSenderName();
|
||||
const chatType = resolveSlackChatType(conversation.resolvedChannelType);
|
||||
const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
|
||||
const inboundLabel = isDirectMessage
|
||||
? `Slack DM from ${senderName}`
|
||||
@@ -558,7 +563,7 @@ export async function prepareSlackMessage(params: {
|
||||
|
||||
const envelopeFrom =
|
||||
resolveConversationLabel({
|
||||
ChatType: isDirectMessage ? "direct" : "channel",
|
||||
ChatType: chatType,
|
||||
SenderName: senderName,
|
||||
GroupSubject: isRoomish ? roomLabel : undefined,
|
||||
From: slackFrom,
|
||||
@@ -581,7 +586,7 @@ export async function prepareSlackMessage(params: {
|
||||
from: envelopeFrom,
|
||||
timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
|
||||
body: textWithId,
|
||||
chatType: isDirectMessage ? "direct" : "channel",
|
||||
chatType,
|
||||
sender: { name: senderName, id: senderId },
|
||||
previousTimestamp,
|
||||
envelope: envelopeOptions,
|
||||
@@ -665,7 +670,7 @@ export async function prepareSlackMessage(params: {
|
||||
To: slackTo,
|
||||
SessionKey: sessionKey,
|
||||
AccountId: route.accountId,
|
||||
ChatType: isDirectMessage ? "direct" : "channel",
|
||||
ChatType: chatType,
|
||||
ConversationLabel: envelopeFrom,
|
||||
GroupSubject: isRoomish ? roomLabel : undefined,
|
||||
GroupSpace: ctx.teamId || undefined,
|
||||
|
||||
@@ -1023,6 +1023,22 @@ describe("slack slash commands access groups", () => {
|
||||
expect(dispatchArg?.ctx?.CommandAuthorized).toBe(false);
|
||||
});
|
||||
|
||||
it("classifies MPIM slash commands as group chat context", async () => {
|
||||
const harness = createPolicyHarness({
|
||||
channelId: "G_MPIM",
|
||||
channelName: "group-dm",
|
||||
resolveChannelName: async () => ({ name: "group-dm", type: "mpim" }),
|
||||
});
|
||||
await registerAndRunPolicySlash({ harness });
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
const dispatchArg = dispatchMock.mock.calls[0]?.[0] as {
|
||||
ctx?: { ChatType?: string; From?: string };
|
||||
};
|
||||
expect(dispatchArg?.ctx?.ChatType).toBe("group");
|
||||
expect(dispatchArg?.ctx?.From).toBe("slack:group:G_MPIM");
|
||||
});
|
||||
|
||||
it("enforces access-group gating when lookup fails for private channels", async () => {
|
||||
const harness = createPolicyHarness({
|
||||
allowFrom: [],
|
||||
|
||||
@@ -21,7 +21,7 @@ import { resolveSlackEffectiveAllowFrom } from "./auth.js";
|
||||
import { resolveSlackChannelConfig, type SlackChannelConfigResolved } from "./channel-config.js";
|
||||
import { buildSlackSlashCommandMatcher, resolveSlackSlashCommandConfig } from "./commands.js";
|
||||
import type { SlackMonitorContext } from "./context.js";
|
||||
import { normalizeSlackChannelType } from "./context.js";
|
||||
import { normalizeSlackChannelType, resolveSlackChatType } from "./context.js";
|
||||
import { authorizeSlackDirectMessage } from "./dm-auth.js";
|
||||
import {
|
||||
createSlackExternalArgMenuStore,
|
||||
@@ -340,6 +340,7 @@ export async function registerSlackMonitorSlashCommands(params: {
|
||||
const rawChannelType =
|
||||
channelInfo?.type ?? (command.channel_name === "directmessage" ? "im" : undefined);
|
||||
const channelType = normalizeSlackChannelType(rawChannelType, command.channel_id);
|
||||
const chatType = resolveSlackChatType(channelType);
|
||||
const isDirectMessage = channelType === "im";
|
||||
const isGroupDm = channelType === "mpim";
|
||||
const isRoom = channelType === "channel" || channelType === "group";
|
||||
@@ -574,10 +575,10 @@ export async function registerSlackMonitorSlashCommands(params: {
|
||||
? `slack:channel:${command.channel_id}`
|
||||
: `slack:group:${command.channel_id}`,
|
||||
To: `slash:${command.user_id}`,
|
||||
ChatType: isDirectMessage ? "direct" : "channel",
|
||||
ChatType: chatType,
|
||||
ConversationLabel:
|
||||
resolveConversationLabel({
|
||||
ChatType: isDirectMessage ? "direct" : "channel",
|
||||
ChatType: chatType,
|
||||
SenderName: senderName,
|
||||
GroupSubject: isRoomish ? roomLabel : undefined,
|
||||
From: isDirectMessage
|
||||
|
||||
@@ -1451,6 +1451,47 @@ describe("dispatchReplyFromConfig", () => {
|
||||
expect(dispatcher.sendFinalReply).toHaveBeenCalledWith({ text: "done" });
|
||||
});
|
||||
|
||||
it("suppresses Slack non-DM verbose progress even when verbose is enabled", async () => {
|
||||
setNoAbort();
|
||||
const cfg = {
|
||||
...emptyConfig,
|
||||
agents: {
|
||||
defaults: {
|
||||
verboseDefault: "on",
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
const dispatcher = createDispatcher();
|
||||
const ctx = buildTestCtx({
|
||||
Provider: "slack",
|
||||
Surface: "slack",
|
||||
ChatType: "channel",
|
||||
});
|
||||
|
||||
const replyResolver = async (
|
||||
_ctx: MsgContext,
|
||||
opts?: GetReplyOptions,
|
||||
_cfg?: OpenClawConfig,
|
||||
) => {
|
||||
await opts?.onPlanUpdate?.({
|
||||
phase: "update",
|
||||
explanation: "Inspect code, patch it, run tests.",
|
||||
steps: ["Inspect code", "Patch code", "Run tests"],
|
||||
});
|
||||
await opts?.onPatchSummary?.({
|
||||
phase: "end",
|
||||
title: "apply patch",
|
||||
summary: "1 added, 2 modified",
|
||||
});
|
||||
return { text: "done" } satisfies ReplyPayload;
|
||||
};
|
||||
|
||||
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
|
||||
|
||||
expect(dispatcher.sendToolResult).not.toHaveBeenCalled();
|
||||
expect(dispatcher.sendFinalReply).toHaveBeenCalledWith({ text: "done" });
|
||||
});
|
||||
|
||||
it("suppresses plan and working-status progress when session verbose is off", async () => {
|
||||
setNoAbort();
|
||||
sessionStoreMocks.currentEntry = {
|
||||
|
||||
@@ -643,8 +643,12 @@ export async function dispatchReplyFromConfig(
|
||||
return { queuedFinal, counts };
|
||||
}
|
||||
|
||||
const shouldSendToolSummaries = ctx.ChatType !== "group" || ctx.IsForum === true;
|
||||
const shouldSendToolStartStatuses = ctx.ChatType !== "group" || ctx.IsForum === true;
|
||||
const isSlackNonDirectSurface =
|
||||
(ctx.Surface === "slack" || ctx.Provider === "slack") && ctx.ChatType !== "direct";
|
||||
const shouldSendVerboseProgressMessages =
|
||||
!isSlackNonDirectSurface && (ctx.ChatType !== "group" || ctx.IsForum === true);
|
||||
const shouldSendToolSummaries = shouldSendVerboseProgressMessages;
|
||||
const shouldSendToolStartStatuses = shouldSendVerboseProgressMessages;
|
||||
const sendFinalPayload = async (
|
||||
payload: ReplyPayload,
|
||||
): Promise<{ queuedFinal: boolean; routedFinalCount: number }> => {
|
||||
@@ -806,7 +810,7 @@ export async function dispatchReplyFromConfig(
|
||||
explanation?: string;
|
||||
steps?: string[];
|
||||
}): Promise<void> => {
|
||||
if (suppressDelivery || !shouldEmitVerboseProgress()) {
|
||||
if (suppressDelivery || !shouldEmitVerboseProgress() || !shouldSendVerboseProgressMessages) {
|
||||
return;
|
||||
}
|
||||
const replyPayload: ReplyPayload = {
|
||||
|
||||
Reference in New Issue
Block a user