Skip to content

Align message_sent hooks with documented Slack delivery correlation #78046

@bek91

Description

@bek91

Summary

The documented message hook contract says message_sent observes final success/failure and message hook contexts expose stable correlation fields when available (ctx.sessionKey, ctx.runId, ctx.messageId, etc.). In the current implementation, the normal Slack inbound assistant reply path does not appear to emit message_sent, and the generic outbound path emits message_sent without passing available session/run correlation into the canonical hook context.

This makes plugins unable to reliably observe the Slack-delivered assistant message timestamp (response.ts) for normal Slack agent replies. One concrete downstream impact: plugins that need to react to the final assistant Slack message cannot use message hooks as documented and may accidentally fall back to transcript-local message IDs, which are UUIDs and not valid Slack timestamps.

Evidence

Normal Slack inbound reply path appears to bypass generic outbound hook emission:

  • extensions/slack/src/monitor/message-handler/dispatch.ts calls Slack monitor reply delivery (deliverNormally / deliverReplies).
  • extensions/slack/src/monitor/replies.ts calls sendMessageSlack(...).
  • extensions/slack/src/send.ts posts via Slack chat.postMessage.
  • I did not find a message_sent emission on that normal Slack reply path.

Generic outbound does emit message_sent, but drops available correlation:

  • src/infra/outbound/deliver.ts computes sessionKeyForInternalHooks = params.mirror?.sessionKey ?? params.session?.key.
  • createMessageSentEmitter(...) builds canonical sent context with to, content, success, channelId, accountId, conversationId, and messageId, but does not pass sessionKey or runId.
  • src/hooks/message-hook-mappers.ts supports sessionKey and runId in buildCanonicalSentMessageHookContext, and toPluginMessageSentEvent / toPluginMessageContext will expose them if provided.

Slack delivered message ID is available:

  • extensions/slack/src/send.ts returns Slack response.ts as messageId for posted Slack messages.
  • Generic deliverOutboundPayloads passes delivery messageId into emitMessageSent.

Transcript message IDs are not a safe substitute:

  • src/config/sessions/transcript-append.ts generates a random UUID for transcript entries.
  • That UUID is what session transcript update consumers see as the transcript message ID; it is not a channel-delivered Slack timestamp.

Expected behavior

  • Normal Slack assistant reply delivery should emit message_sent after final delivery success/failure.
  • For successful Slack sends, ctx.messageId / event.messageId should be the Slack message timestamp returned by Slack (response.ts).
  • When OpenClaw has correlation fields available, message_sent should include them in first-class fields:
    • ctx.sessionKey / event.sessionKey
    • ctx.runId / event.runId
    • ideally typed threadId / replyToId on message sent/sending surfaces where relevant, matching the docs guidance to prefer typed fields before channel-specific metadata.
  • Generic outbound delivery should pass available params.session?.key and any available run id into buildCanonicalSentMessageHookContext.

Why this matters

Plugins need a stable way to associate delivered channel messages with the agent session/run that produced them. For Slack, the delivered timestamp is required for follow-up actions such as adding reactions, editing, deleting, or threading against a specific assistant message. Without message_sent on normal Slack replies, or without session/run correlation on emitted hooks, plugins have to rely on brittle transcript/session-store inference.

Suggested fix

  1. Emit message_sent from the normal Slack inbound assistant reply path, including the Slack delivered messageId.
  2. Pass available sessionKey and runId through generic outbound createMessageSentEmitter into buildCanonicalSentMessageHookContext.
  3. Add regression tests for:
    • normal Slack agent reply emits message_sent with delivered Slack messageId;
    • generic deliverOutboundPayloads includes sessionKey when params.session.key is present;
    • failure emits message_sent with success: false and a useful error without leaking secrets;
    • hook payload shape aligns with the documented first-class correlation fields.

Metadata

Metadata

Assignees

Labels

maintainerMaintainer-authored PR

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions