Skip to content

feat: Slack v1.5 — thread-ownership plugin and message_sending hook#15775

Closed
DarlingtonDeveloper wants to merge 6 commits intoopenclaw:mainfrom
DarlingtonDeveloper:feat/slack-v1.5-thread-ownership
Closed

feat: Slack v1.5 — thread-ownership plugin and message_sending hook#15775
DarlingtonDeveloper wants to merge 6 commits intoopenclaw:mainfrom
DarlingtonDeveloper:feat/slack-v1.5-thread-ownership

Conversation

@DarlingtonDeveloper
Copy link
Contributor

@DarlingtonDeveloper DarlingtonDeveloper commented Feb 13, 2026

Summary

  • Thread-ownership plugin (extensions/thread-ownership/): Prevents multiple agents from responding in the same Slack thread by coordinating via the slack-forwarder's HTTP ownership API.
    • message_sending hook: Claims thread ownership before sending. Returns { cancel: true } if another agent owns the thread.
    • message_received hook: Tracks @-mentions with 5-min TTL to allow explicitly mentioned agents to bypass ownership.
    • Fail-open on network errors. Configurable A/B test channel list.
  • message_sending hook wiring: runMessageSending() is now called in slack.ts outbound before sendText/sendMedia, enabling content modification and cancellation.

Changes

File Change
extensions/thread-ownership/index.ts New: thread-ownership plugin
extensions/thread-ownership/index.test.ts New: 9 unit tests
extensions/thread-ownership/openclaw.plugin.json New: plugin manifest
src/channels/plugins/outbound/slack.ts Wire runMessageSending() before send
src/channels/plugins/outbound/slack.test.ts New: 5 unit tests

Test plan

  • Unit tests: npx vitest run extensions/thread-ownership/index.test.ts src/channels/plugins/outbound/slack.test.ts (14 tests passing)
  • Deploy slack-forwarder, enable plugin, verify thread claim/reject flow
  • Test @-mention bypass: agent mentioned in thread should respond even if not owner

🤖 Generated with Claude Code

Greptile Overview

Greptile Summary

This PR adds a new thread-ownership plugin (message_received/message_sending hooks) to coordinate Slack thread ownership via the slack-forwarder HTTP API, and wires the global message_sending hook 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 (missing messageId), and the added unit test mocks sendMessageSlack with a different return shape than the real implementation, which can mask this issue.

Confidence Score: 3/5

  • This PR has a concrete correctness issue in the Slack outbound cancel return value that should be fixed before merging.
  • The new hook wiring is straightforward, but the cancel path in slackOutbound returns a result missing the required messageId, and the new unit test mocks sendMessageSlack with an incompatible return shape, which hides the integration/type contract problem.
  • src/channels/plugins/outbound/slack.ts, src/channels/plugins/outbound/slack.test.ts

Last reviewed commit: 409aec3

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

DarlingtonDeveloper and others added 2 commits February 13, 2026 21:33
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]>
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 18 to 24
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 };
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

Comment on lines +1 to +6
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("../../../slack/send.js", () => ({
sendMessageSlack: vi.fn().mockResolvedValue({ ts: "1234.5678", channel: "C123" }),
}));

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

steipete added a commit that referenced this pull request Feb 13, 2026
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]>
@steipete
Copy link
Contributor

Landed on main via squash commit:

  • 51296e770c70c69a39cf11b234776c41798212a5

What I did:

  • Created temp landing branch from latest main
  • Applied PR changes, kept final fixes/tests, and added changelog entry (CHANGELOG.md)
  • Ran full gate before commit: pnpm lint && pnpm build && pnpm test (pass)
  • Landed with contributor/PR mention in commit message and Co-authored-by trailer for @DarlingtonDeveloper

Key PR commits included in the squash:

  • 5f6c15cfb7669ee52a3a36facd8f52aa1e4e6af1
  • 72873b80405f2a33a76ce529b3d08cb981e781f5
  • 02fecd0580247ce6aad732f5e8ad1539d79678b6

Thanks @DarlingtonDeveloper for the contribution.

@steipete steipete closed this Feb 13, 2026
steipete added a commit to azade-c/openclaw that referenced this pull request Feb 14, 2026
…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]>
mverrilli pushed a commit to mverrilli/openclaw that referenced this pull request Feb 14, 2026
…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]>
GwonHyeok pushed a commit to learners-superpumped/openclaw that referenced this pull request Feb 15, 2026
…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]>
cloud-neutral pushed a commit to cloud-neutral-toolkit/openclawbot.svc.plus that referenced this pull request Feb 15, 2026
…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]>
jiulingyun added a commit to jiulingyun/openclaw-cn that referenced this pull request Feb 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments