mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:10:44 +00:00
fix: slack mention-gating thread participation
This commit is contained in:
@@ -66,6 +66,7 @@ Docs: https://docs.openclaw.ai
|
||||
- WhatsApp/onboarding: canonicalize setup and pairing allowlist entries to WhatsApp's digit-only phone ids while still accepting E.164, JID, and `whatsapp:` inputs, so personal-phone allowlists match WhatsApp Web sender ids after setup. Thanks @vincentkoc.
|
||||
- Gateway/startup: load provider plugins that own explicitly configured image, video, or music generation defaults so generation tools become live after gateway restart instead of remaining catalog-only. Fixes #77244. Thanks @buyuangtampan, @Nikoxx99, and @vincentkoc.
|
||||
- Slack/subagents: keep resumed parent `message.send` calls in the originating Slack thread when ambient session thread context is present, and suppress successful silent child completion rows from follow-up findings. Thanks @bek91.
|
||||
- Slack/mentions: record thread participation for successful visible threaded Slack sends, including message-tool and media delivery paths, so unmentioned replies in bot-participated threads can bypass mention gating as documented. Fixes #77648. Thanks @bek91.
|
||||
- Infra/Windows: skip the POSIX `/tmp/openclaw` preferred path on Windows in `resolvePreferredOpenClawTmpDir` so log files, TTS temp files, and other writes land in `%TEMP%\openclaw-<uid>` instead of `C:\tmp\openclaw`. Fixes #60713. Thanks @juan-flores077.
|
||||
- Gateway/diagnostics: make stuck-session recovery outcome-driven and generation-guarded, add `diagnostics.stuckSessionAbortMs`, and emit structured recovery requested/completed events so stale or skipped recovery no longer looks like a successful abort.
|
||||
- Media/Windows: open saved attachment temp files read/write before fsync so Windows WebChat and `chat.send` media offloads no longer fail with EPERM during durability flush. (#76593) Thanks @qq230849622-a11y.
|
||||
|
||||
@@ -16,7 +16,6 @@ const reactSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
const readSlackMessages = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
const removeOwnSlackReactions = vi.fn(async (..._args: unknown[]) => ["thumbsup"]);
|
||||
const removeSlackReaction = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
const recordSlackThreadParticipation = vi.fn();
|
||||
const sendSlackMessage = vi.fn(async (..._args: unknown[]) => ({ channelId: "C123" }));
|
||||
const unpinSlackMessage = vi.fn(async (..._args: unknown[]) => ({}));
|
||||
|
||||
@@ -103,7 +102,6 @@ describe("handleSlackAction", () => {
|
||||
pinSlackMessage,
|
||||
reactSlackMessage,
|
||||
readSlackMessages,
|
||||
recordSlackThreadParticipation,
|
||||
removeOwnSlackReactions,
|
||||
removeSlackReaction,
|
||||
sendSlackMessage,
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
type OpenClawConfig,
|
||||
withNormalizedTimestamp,
|
||||
} from "./runtime-api.js";
|
||||
import { recordSlackThreadParticipation } from "./sent-thread-cache.js";
|
||||
import { parseSlackTarget, resolveSlackChannelId } from "./targets.js";
|
||||
|
||||
const messagingActions = new Set([
|
||||
@@ -78,7 +77,6 @@ export const slackActionRuntime = {
|
||||
pinSlackMessage: createLazySlackAction("pinSlackMessage"),
|
||||
reactSlackMessage: createLazySlackAction("reactSlackMessage"),
|
||||
readSlackMessages: createLazySlackAction("readSlackMessages"),
|
||||
recordSlackThreadParticipation,
|
||||
removeOwnSlackReactions: createLazySlackAction("removeOwnSlackReactions"),
|
||||
removeSlackReaction: createLazySlackAction("removeSlackReaction"),
|
||||
sendSlackMessage: createLazySlackAction("sendSlackMessage"),
|
||||
@@ -273,14 +271,6 @@ export async function handleSlackAction(
|
||||
blocks,
|
||||
});
|
||||
|
||||
if (threadTs && result.channelId && account.accountId) {
|
||||
slackActionRuntime.recordSlackThreadParticipation(
|
||||
account.accountId,
|
||||
result.channelId,
|
||||
threadTs,
|
||||
);
|
||||
}
|
||||
|
||||
// Keep "first" mode consistent even when the agent explicitly provided
|
||||
// threadTs: once we send a message to the current channel, consider the
|
||||
// first reply "used" so later tool calls don't auto-thread again.
|
||||
@@ -318,14 +308,6 @@ export async function handleSlackAction(
|
||||
...(title ? { uploadTitle: title } : {}),
|
||||
});
|
||||
|
||||
if (threadTs && result.channelId && account.accountId) {
|
||||
slackActionRuntime.recordSlackThreadParticipation(
|
||||
account.accountId,
|
||||
result.channelId,
|
||||
threadTs,
|
||||
);
|
||||
}
|
||||
|
||||
if (context?.hasRepliedRef && context.currentChannelId) {
|
||||
if (sameSlackChannelTarget(to, context.currentChannelId)) {
|
||||
context.hasRepliedRef.value = true;
|
||||
|
||||
@@ -92,6 +92,25 @@ describe("slack outbound shared hook wiring", () => {
|
||||
expect(sendMessageSlackMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("passes replyToId as Slack threadTs for threaded outbound delivery", async () => {
|
||||
await deliverOutboundPayloads({
|
||||
cfg,
|
||||
channel: "slack",
|
||||
to: "C123",
|
||||
payloads: [{ text: "hello" }],
|
||||
accountId: "default",
|
||||
replyToId: "1712000000.000001",
|
||||
});
|
||||
|
||||
expect(sendMessageSlackMock).toHaveBeenCalledWith(
|
||||
"C123",
|
||||
"hello",
|
||||
expect.objectContaining({
|
||||
threadTs: "1712000000.000001",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("respects cancel from the shared hook without a second adapter pass", async () => {
|
||||
const hookRegistry = createEmptyPluginRegistry();
|
||||
const handler = vi.fn().mockResolvedValue({ cancel: true });
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createSlackSendTestClient, installSlackBlockTestMocks } from "./blocks.test-helpers.js";
|
||||
import {
|
||||
clearSlackThreadParticipationCache,
|
||||
hasSlackThreadParticipation,
|
||||
} from "./sent-thread-cache.js";
|
||||
|
||||
installSlackBlockTestMocks();
|
||||
const { sendMessageSlack } = await import("./send.js");
|
||||
@@ -67,6 +71,49 @@ describe("sendMessageSlack NO_REPLY guard", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMessageSlack thread participation", () => {
|
||||
it("records participation after a successful threaded send", async () => {
|
||||
clearSlackThreadParticipationCache();
|
||||
const client = createSlackSendTestClient();
|
||||
|
||||
await sendMessageSlack("channel:C123", "hello thread", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
threadTs: "1712345678.123456",
|
||||
});
|
||||
|
||||
expect(hasSlackThreadParticipation("default", "C123", "1712345678.123456")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not record participation for unthreaded sends", async () => {
|
||||
clearSlackThreadParticipationCache();
|
||||
const client = createSlackSendTestClient();
|
||||
|
||||
await sendMessageSlack("channel:C123", "hello channel", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
});
|
||||
|
||||
expect(hasSlackThreadParticipation("default", "C123", "1712345678.123456")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not record participation for invalid thread ids", async () => {
|
||||
clearSlackThreadParticipationCache();
|
||||
const client = createSlackSendTestClient();
|
||||
|
||||
await sendMessageSlack("channel:C123", "hello invalid thread", {
|
||||
token: "xoxb-test",
|
||||
cfg: SLACK_TEST_CFG,
|
||||
client,
|
||||
threadTs: "not-a-slack-thread",
|
||||
});
|
||||
|
||||
expect(hasSlackThreadParticipation("default", "C123", "not-a-slack-thread")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMessageSlack chunking", () => {
|
||||
it("keeps 4205-character text in a single Slack post by default", async () => {
|
||||
const client = createSlackSendTestClient();
|
||||
|
||||
@@ -24,7 +24,9 @@ import { createSlackTokenCacheKey, getSlackWriteClient } from "./client.js";
|
||||
import { markdownToSlackMrkdwnChunks } from "./format.js";
|
||||
import { SLACK_TEXT_LIMIT } from "./limits.js";
|
||||
import { loadOutboundMediaFromUrl } from "./runtime-api.js";
|
||||
import { recordSlackThreadParticipation } from "./sent-thread-cache.js";
|
||||
import { parseSlackTarget } from "./targets.js";
|
||||
import { normalizeSlackThreadTsCandidate } from "./thread-ts.js";
|
||||
import { resolveSlackBotToken } from "./token.js";
|
||||
import { truncateSlackText } from "./truncate.js";
|
||||
const SLACK_UPLOAD_SSRF_POLICY = {
|
||||
@@ -535,7 +537,7 @@ export async function sendMessageSlack(
|
||||
recipient,
|
||||
threadTs: opts.threadTs,
|
||||
});
|
||||
return await runQueuedSlackSend(queueKey, () =>
|
||||
const result = await runQueuedSlackSend(queueKey, () =>
|
||||
sendMessageSlackQueued({
|
||||
trimmedMessage,
|
||||
opts,
|
||||
@@ -546,6 +548,11 @@ export async function sendMessageSlack(
|
||||
blocks,
|
||||
}),
|
||||
);
|
||||
const threadTs = normalizeSlackThreadTsCandidate(opts.threadTs);
|
||||
if (threadTs && result.channelId && account.accountId) {
|
||||
recordSlackThreadParticipation(account.accountId, result.channelId, threadTs);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function sendMessageSlackQueued(params: {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { WebClient } from "@slack/web-api";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { installSlackBlockTestMocks } from "./blocks.test-helpers.js";
|
||||
import {
|
||||
clearSlackThreadParticipationCache,
|
||||
hasSlackThreadParticipation,
|
||||
} from "./sent-thread-cache.js";
|
||||
|
||||
// --- Module mocks (must precede dynamic import) ---
|
||||
installSlackBlockTestMocks();
|
||||
@@ -96,6 +100,7 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
loadOutboundMediaFromUrlMock.mockClear();
|
||||
clearSlackDmChannelCache();
|
||||
clearSlackSendQueuesForTest();
|
||||
clearSlackThreadParticipationCache();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -297,6 +302,7 @@ describe("sendMessageSlack file upload with user IDs", () => {
|
||||
thread_ts: "171.222",
|
||||
}),
|
||||
);
|
||||
expect(hasSlackThreadParticipation("default", "C123CHAN", "171.222")).toBe(true);
|
||||
});
|
||||
|
||||
it("uses explicit upload filename and title overrides when provided", async () => {
|
||||
|
||||
Reference in New Issue
Block a user