feat: Slack v1.5 — thread-ownership plugin and message_sending hook#15775
feat: Slack v1.5 — thread-ownership plugin and message_sending hook#15775DarlingtonDeveloper wants to merge 6 commits intoopenclaw:mainfrom
Conversation
Slack thread ownership to prevent agent collisions:
- New extensions/thread-ownership plugin:
- message_sending hook: claims thread via HTTP to slack-forwarder,
cancels send if another agent owns the thread
- message_received hook: tracks @-mentions with 5-min TTL to allow
mentioned agents to bypass ownership
- Fail-open on errors, configurable A/B test channel list
- Wire runMessageSending() in slack outbound sendText/sendMedia
before the actual Slack API call
- Support content modification and cancellation from hooks
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
| const hookResult = await hookRunner.runMessageSending( | ||
| { to, content: text, metadata: { threadTs, channelId: to } }, | ||
| { channelId: "slack", accountId: accountId ?? undefined }, | ||
| ); | ||
| if (hookResult?.cancel) { | ||
| return { channel: "slack", ts: undefined as unknown as string }; | ||
| } |
There was a problem hiding this comment.
Invalid cancel return shape
The cancel path returns { channel: "slack", ts: ... }, but OutboundDeliveryResult requires messageId (and Slack sends normally return { messageId, channelId }). As-is, a cancelled send will return an object missing messageId, which can break downstream code expecting a valid delivery result.
| const hookResult = await hookRunner.runMessageSending( | |
| { to, content: text, metadata: { threadTs, channelId: to } }, | |
| { channelId: "slack", accountId: accountId ?? undefined }, | |
| ); | |
| if (hookResult?.cancel) { | |
| return { channel: "slack", ts: undefined as unknown as string }; | |
| } | |
| return { channel: "slack", messageId: "cancelled" }; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/channels/plugins/outbound/slack.ts
Line: 18:24
Comment:
**Invalid cancel return shape**
The `cancel` path returns `{ channel: "slack", ts: ... }`, but `OutboundDeliveryResult` requires `messageId` (and Slack sends normally return `{ messageId, channelId }`). As-is, a cancelled send will return an object missing `messageId`, which can break downstream code expecting a valid delivery result.
```suggestion
return { channel: "slack", messageId: "cancelled" };
```
How can I resolve this? If you propose a fix, please make it concise.| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | ||
|
|
||
| vi.mock("../../../slack/send.js", () => ({ | ||
| sendMessageSlack: vi.fn().mockResolvedValue({ ts: "1234.5678", channel: "C123" }), | ||
| })); | ||
|
|
There was a problem hiding this comment.
Mock return shape mismatch
This test mocks sendMessageSlack to resolve { ts, channel }, but the real implementation returns { messageId, channelId } (src/slack/send.ts). This mismatch can let the test pass while the real adapter shape (and the cancellation path) is wrong.
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | |
| vi.mock("../../../slack/send.js", () => ({ | |
| sendMessageSlack: vi.fn().mockResolvedValue({ ts: "1234.5678", channel: "C123" }), | |
| })); | |
| vi.mock("../../../slack/send.js", () => ({ | |
| sendMessageSlack: vi.fn().mockResolvedValue({ messageId: "1234.5678", channelId: "C123" }), | |
| })); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/channels/plugins/outbound/slack.test.ts
Line: 1:6
Comment:
**Mock return shape mismatch**
This test mocks `sendMessageSlack` to resolve `{ ts, channel }`, but the real implementation returns `{ messageId, channelId }` (`src/slack/send.ts`). This mismatch can let the test pass while the real adapter shape (and the cancellation path) is wrong.
```suggestion
vi.mock("../../../slack/send.js", () => ({
sendMessageSlack: vi.fn().mockResolvedValue({ messageId: "1234.5678", channelId: "C123" }),
}));
```
How can I resolve this? If you propose a fix, please make it concise.Land PR #15775 by @DarlingtonDeveloper: - add thread-ownership plugin and Slack message_sending hook wiring - include regression tests and changelog update Co-authored-by: Mike <[email protected]>
|
Landed on
What I did:
Key PR commits included in the squash:
Thanks @DarlingtonDeveloper for the contribution. |
…w#15775) Land PR openclaw#15775 by @DarlingtonDeveloper: - add thread-ownership plugin and Slack message_sending hook wiring - include regression tests and changelog update Co-authored-by: Mike <[email protected]>
…w#15775) Land PR openclaw#15775 by @DarlingtonDeveloper: - add thread-ownership plugin and Slack message_sending hook wiring - include regression tests and changelog update Co-authored-by: Mike <[email protected]>
…w#15775) Land PR openclaw#15775 by @DarlingtonDeveloper: - add thread-ownership plugin and Slack message_sending hook wiring - include regression tests and changelog update Co-authored-by: Mike <[email protected]>
…w#15775) Land PR openclaw#15775 by @DarlingtonDeveloper: - add thread-ownership plugin and Slack message_sending hook wiring - include regression tests and changelog update Co-authored-by: Mike <[email protected]>
Summary
extensions/thread-ownership/): Prevents multiple agents from responding in the same Slack thread by coordinating via the slack-forwarder's HTTP ownership API.message_sendinghook: Claims thread ownership before sending. Returns{ cancel: true }if another agent owns the thread.message_receivedhook: Tracks @-mentions with 5-min TTL to allow explicitly mentioned agents to bypass ownership.runMessageSending()is now called inslack.tsoutbound beforesendText/sendMedia, enabling content modification and cancellation.Changes
extensions/thread-ownership/index.tsextensions/thread-ownership/index.test.tsextensions/thread-ownership/openclaw.plugin.jsonsrc/channels/plugins/outbound/slack.tsrunMessageSending()before sendsrc/channels/plugins/outbound/slack.test.tsTest plan
npx vitest run extensions/thread-ownership/index.test.ts src/channels/plugins/outbound/slack.test.ts(14 tests passing)🤖 Generated with Claude Code
Greptile Overview
Greptile Summary
This PR adds a new
thread-ownershipplugin (message_received/message_sending hooks) to coordinate Slack thread ownership via the slack-forwarder HTTP API, and wires the globalmessage_sendinghook runner into the Slack outbound adapter so plugins can cancel/modify outbound messages.Main concern is the new Slack outbound cancellation behavior: the cancel path currently returns an object that doesn’t match the repository’s
OutboundDeliveryResult/Slack send result shape (missingmessageId), and the added unit test mockssendMessageSlackwith a different return shape than the real implementation, which can mask this issue.Confidence Score: 3/5
slackOutboundreturns a result missing the requiredmessageId, and the new unit test mockssendMessageSlackwith an incompatible return shape, which hides the integration/type contract problem.Last reviewed commit: 409aec3
(2/5) Greptile learns from your feedback when you react with thumbs up/down!