Repository navigation
feat: answer Slack replies and mentions, with Clef deciding when to - #19
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #18.
The bot now listens to Slack as well as posting to it. When someone mentions it or replies under one of its posts in the office channel, it replies, reacts or stays out of it. Clef on Workers AI makes that call for thread replies, and the free banter models write the replies.
Merging deploys, and the new Slack secrets are required, so the Slack side has to be ready first or the bot won't start:
connections:write, bot scopeschat:write,reactions:write,reactions:read,app_mentions:read,channels:history,users:read, and eventsapp_mention,message.channels,reaction_added,reaction_removed. If the channel is private, alsogroups:history/message.groups. Leave Interactivity off. Install it. The steps are in the README under Slack app.SLACK_BOT_TOKEN,SLACK_APP_TOKENandSLACK_CHANNEL_IDGitHub secrets.After merging:
office.slack_postsrow appears.office.members.slack_user_idfor the people we can.UPDATE office.responder_settings SET enabled = true, updated_at = now();SLACK_WEBHOOK_URLsecret and the old webhook.What changed
Posting. Everything now goes through
chat.postMessagewith the bot token instead of the Incoming Webhook.SlackErrorreasons now come from Slack's error codes:ratelimited/429 is retried afterRetry-After;invalid_auth,token_revoked,account_inactive,channel_not_foundandnot_in_channelareAccessRevoked(replacingWebhookRevoked); anything else isInvalidPayload. Retries, timeouts and Sentry severities are unchanged. Every post is recorded inslack_postswith itsts, kind and model. This part is unconditional.Listening.
SlackSocketholds a Socket Mode connection built on Effect'sSocket. It acks every envelope before doing anything else, drops redeliveries, reconnects straight away ondisconnect, and backs off up to 5 minutes when the connection drops or Slack won't open one. It never fails the program.SlackEventdecides what's considered: mentions, and replies in threads under the bot's own posts, both only inSLACK_CHANNEL_ID. Reactions on the bot's posts are recorded raw.Deciding. Clef goes through
effect/ai'sDecisionModelwith a smallCloudflareDecisionModelprovider, called directly on Workers AI. I pinned the response schema against a real call: Clef'sconfidenceis a separate value from the chosen option's probability, and the threshold usesconfidence. The exact request is stored with each decision.Writing. The model fallback, timeouts and daily-limit handling moved out of
HeadlineWriterintoAiWriting, whichHeadlineWriter(unchanged interface) and the newReplyWritershare. Replies try OpenRouter's banter models first, with Workers AI as the fallback, because Clef shares Workers AI's free daily Neurons. The daily-limit check now also recognises Cloudflare's spent allocation (code 4006), for headlines and banter too.Responding.
Responderruns as a fourth fiber next toannounceandrecord. Each thread keeps only its latest waiting message, and mentions wait separately. One lane handles messages one at a time, in the order threads first spoke. It readsresponder_settingsfor every message, checks office hours, staleness and the caps, asks Clef only when it could act, then replies, reacts or ignores, and records why.Settings in the database.
office.responder_settingsis a single typed row withCHECKconstraints, seeded withenabled = false:clef_model,min_confidence(0.7),max_replies_per_day(20),max_reactions_per_day(30),max_replies_per_thread(3),stale_after_seconds(120),clef_thread_messages(10),writer_thread_messages(50) andreaction_emoji. While it's off, nothing is decided, posted or recorded. There are noCLEF_*environment variables.People. A Slack author is linked to an office member by the new
members.slack_user_id, then by their Slack real name againstreal_name. Anyone else keeps their Slack name, and the prompt tells the model not to assume they're anyone in the log.Differences from the issue
RcMapof sliding queues with a fiber per thread. The behaviour is the same (latest message per thread wins, mentions wait separately, threads take turns), with no per-thread fibers to time out mid-reply.mentionreason. I added amentiondecision reason for replies to mentions, sinceclefwould be wrong there.item_userrather than aslack_postslookup, so recording them doesn't need an extra query.Testing
SlackClienterror classification,SlackHistoryand the migration constraints,SlackEventeligibility,SlackSocketagainst a fake WebSocket (acks before hand-off, dropped redeliveries, reconnects),CloudflareDecisionModel,ReplyDecider,ReplyWriter, and 25Responderscenarios: mention → reply, react, ignore, low confidence, each cap, outside hours, off and settings changes, superseded, stale, mentions not pushed out, turn order, and each failure staying quiet.ReplyDecider. "bot, how many days was Sam in last week?" gotreplyat 0.95. Lunch plans between colleagues gotignore. "haha Sam basically lives there" gotreact, but at only 0.18 confidence, so it would be ignored at 0.7.min_confidencemay want tuning once there's data.