Skip to content

Deliver notifications through Slack - #405

Open
alex-clickhouse wants to merge 7 commits into
alex/slack-channelfrom
alex/slack-channel-notifications
Open

Deliver notifications through Slack#405
alex-clickhouse wants to merge 7 commits into
alex/slack-channelfrom
alex/slack-channel-notifications

Conversation

@alex-clickhouse

@alex-clickhousealex-clickhouse commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Part 3 of the Slack channel stack. Sends the agent's questions and approval
requests to Slack as cards with buttons, and records the answers, using the
delivery records from #404 and the channel from #403.

Stack

  1. Shared chat-channel foundations #404: shared chat-channel foundations
  2. Slack channel with coherent runtime and access guardrails #403: Slack channel, access guardrails, and runtime ownership
  3. Deliver notifications through Slack #405: Slack notification delivery (this PR)
  4. Add retry-safe live Slack contract tests #406: real-Slack contract tests and diagnostics

What a person sees

When the agent calls notify, ask_user, or propose_action, Slack gets
a Block Kit card. Questions and approvals carry one button per option. Pressing
one records the answer and settles the card in place, naming who pressed it.
When a question expires unanswered, the card is edited to say so and its
buttons are removed.

Slack is in the default delivery list, alongside the web UI and Telegram. A
transport that is not running is skipped, so this costs nothing until Slack is
configured.

Where a card goes

notifications.slack_channel_id names the conversation. Without it, the first
literal channel id in slack.allow_channels is used. Names and globs are not
resolved into ids, because guessing wrong would post agent output somewhere
nobody asked for. With no usable id, delivery is skipped and says why rather
than reporting a card nobody saw as delivered.

How an answer finds its notification

A card is posted at conversation level, never inside a thread, so the delivery
record for it is keyed on the bare conversation. A button press is looked up
the same way.

This matters because Slack fills in thread_ts on any message that has
replies. Reading that field from the press payload would stop matching the
record as soon as somebody replied under the card, and every later press would
report the notification as already answered while the row sat pending until it
expired.

For the same reason, a reaction on a card does not open a session. Shared
channels have no conversation-wide session, only threads, so a reaction on a
channel-level message has no session to join. Opening one would write a
slack:<channel> key that the session pickers deliberately do not list, and
nothing could stop it afterwards.

What is recorded

  • Posts go into Shared chat-channel foundations #404's generic (channel, target, message_id) delivery
    records. No Slack-specific columns.
  • An answer is scoped to the exact delivery target, so a press coming from a
    conversation with no record for that notification is refused.
  • The Slack member who pressed the button is kept in the row metadata, in the
    broadcast to the web UI, and in the approval audit record. answered_by
    alone would only say "slack".
  • Expiry edits the most recent recorded delivery, which stays correct across a
    target change and across re-delivery cycles.

Boundaries

The notification service owns delivery records and answer state. The Slack
adapter owns which conversation a card goes to, the Block Kit, and the Web API
calls.

@alex-clickhouse
alex-clickhouse marked this pull request as ready for review August 24, 2026 07:07
@alex-clickhouse
alex-clickhouseforce-pushed the alex/slack-channel-notifications branch 9 times, most recently from eca2a10 to fa558a9CompareAugust 25, 2026 10:59
@alex-clickhouse
alex-clickhouseforce-pushed the alex/slack-channel-notifications branch from fa558a9 to b04eda5CompareAugust 25, 2026 13:48
Fan out questions and approvals as Block Kit cards, persist Slack delivery identifiers, attribute answers to the workspace member who acted, and keep expiry and re-delivery behavior consistent with the existing channels.
The existing-database upgrade test stamps the schema at the highest non-Slack migration, so it already fails if the Slack migration is not strictly newer. Keep the behavioral upgrade assertion and drop the weaker structural duplicate.
Three faults that between them meant a card either never appeared or
stopped taking answers.
notifications.channels defaulted to web and telegram, so nothing reached
Slack until an operator found a key documented in one sentence with no
default and no example. Add slack to the default, document the key, and
put a notifications block in the example. README said delivery already
covered Slack, which only held once that key was set by hand.
A press carried the message's thread_ts into the delivery target. Slack
fills thread_ts in on any message that has replies, so one reply under a
card made every later press miss the record that post_notification wrote
against the bare conversation, and each one answered "already answered or
expired" while the row stayed pending until it expired. Look the record up
by the conversation, which is where the card is posted.
A reaction on a card routed to slack:<channel>. Shared channels have no
conversation-wide session and the pickers do not list one, so an emoji
opened a session that /nerve stop could never reach. Require a thread
outside DMs, where one conversation is the session.
Slack now being on by default, an absent channel is the ordinary case and
logs at debug; a registered one that cannot take traffic still warns. A
channel name nothing delivers to no longer passes in silence.
@alex-clickhouse
alex-clickhouseforce-pushed the alex/slack-channel-notifications branch from b04eda5 to bb41314CompareAugust 25, 2026 14:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@alex-clickhouse