The Monday rollover was chat_update-ing in place, which only refreshes content without moving the message to the bottom. Now it chat_deletes the old post and chat_postMessages a fresh one so the schedule lands at the bottom every Monday, independent of in-week activity. Also added info-level logging to the bump handler silent return paths so skipped bumps are observable at runtime.
7.3 KiB
Evaluation: sticky / reused weekly schedule post
Spike for issue #135 — decide how the Monday two-week schedule post should behave across weeks. This document records the decision and the rationale; the recommended option (b) is implemented in the same PR.
Background
src/weekly-post/app.py (the Monday 7am ET Lambda) originally, every week:
chat_deletethe previous week's schedule post (storedtsfromget_schedule_post),chat_postMessagea brand-new two-week schedule message, andsave_schedule_post(channel_id, new_ts, …).
So every Monday the post got a new ts (new permalink), re-notified the
channel, and lost any thread/reactions. During the week it steadily sank as
people chatted.
The infrastructure to edit in place already exists: in-week shift changes
(pick / drop / swap / admin) call _refresh_schedule_post()
(src/slack-bot/app.py), which does a chat_update against the stored ts.
Options considered
(a) Reuse the same post across weeks — pin for reachability
On Monday, chat_update the existing message to roll the two-week window
forward instead of delete + repost, and add a native Slack pin so the post is
reachable from the channel header.
- Pros: stable permalink; no weekly re-notification; keeps any
thread/reactions; trivial change (reuses the
_refresh_schedule_postchat_updatepattern already in the codebase). - Cons: the message stays where it was first posted — it does not rise to the bottom as the channel gets new activity. A native pin only surfaces the post in the channel's pinned-items panel (the header); it does not hold the message at the bottom of the timeline, which is the actual requirement from #135.
(b) True stickybot behavior — keep it at the bottom — recommended
Slack has no native "sticky" message. Stickybot-style bots subscribe to channel message events and delete + repost the bot message whenever someone else posts, so it is always last.
- Pros: the schedule post is always at the literal bottom of the channel timeline — exactly the behavior #135 asks for.
- Cons: every bump is a new
ts(the permalink changes); the post re-surfaces / re-notifies on activity; requires a newmessage.channelsevent subscription, thechannels:historyscope (a one-time reinstall), and debounce machinery to avoid thrashing on bursts.
(c) Status quo — delete + repost weekly
- Pros: the weekly repost bumps the post to the bottom once a week.
- Cons: new permalink every week; a weekly re-notification; loses thread/reactions; the post still sinks for the rest of the week.
Decision
Adopt (b) true bottom-of-channel stickiness. Adam wants literal bottom-of-channel placement of the schedule post, and explicitly accepts the tradeoffs that come with it:
- The permalink changes on each bump — every delete + repost produces a new
ts, so any saved link to the post goes stale. - The post re-surfaces / re-notifies on activity — re-posting pings the channel (subject to the debounce window below), rather than sitting quietly.
- New machinery — a
message.channelsevent subscription, thechannels:historyscope (one-time reinstall), and a debounce to avoid thrashing on bursts.
The native pin (option a's complement) was rejected: it only gives header reachability — the post shows up in the pinned-items panel — but it does not hold the message at the bottom of the timeline, which is the requirement. A pin is therefore not a substitute for bottom-stickiness.
What changes (option b)
Weekly rollover — src/weekly-post/app.py
- Delete + repost on Monday — the previous week's stored post is
chat_deleted and a fresh schedule ischat_postMessaged so the message lands at the bottom of the channel every Monday regardless of in-week activity. The delete failure is non-fatal; the handler always reposts. - First-run: when there is no stored post to delete, the handler
chat_postMessages a fresh message and stores itsts. - The
pins:writescope and the_pin_schedule_posthelper are removed — the pin is fully replaced by bottom-stickiness.
Activity bump — src/slack-bot/app.py
The bump lives in the slack-bot Lambda, which already owns the Slack events
request URL. On a message.channels event in the schedule channel
(handle_channel_message):
- Debounced so a burst of chatter triggers at most one bump:
SCHEDULE_BUMP_DEBOUNCE_SECONDS = 180(3 minutes). The last bump time is stored aslast_bump_tson the schedule-post record, so the debounce check reads no channel history. - Idempotent / safe — the bump is skipped when:
- the delivery is a Slack retry (
X-Slack-Retry-Numset) — underprocess_before_response=Truethe delete+repost runs before the 200 ack, so a slow run can be retried; we never bump on a retry (the next real message bumps anyway), - the triggering message is a thread reply (
thread_ts) — a threaded reply doesn't push the schedule down the main timeline, - there is no stored post,
- the triggering message is the bot's own (
bot_id) or a non-user message subtype (edits, deletes, joins, …), - the debounce window has not elapsed, or
- the bot isn't in the channel (
not_in_channel).
- the delivery is a Slack retry (
- Otherwise it stamps
last_bump_tsoptimistically before the delete/repost (so a retry fired mid-bump — or after the run dies before the final save — is debounced and can't orphan a duplicate),chat_deletes the stored post,chat_postMessages the currentbuild_week_scheduleblocks, thensave_schedule_posts the newts. If the old message is already gone, it still reposts so the channel always ends with the schedule.
Interaction with _refresh_schedule_post and the rollover
All three paths converge on the single stored ts and never fight:
- Monday rollover —
chat_deletes the old storedts,chat_postMessages a fresh post at the bottom, and saves the newts(clearinglast_bump_tsso the next activity is free to bump). - In-week shift edits —
_refresh_schedule_postchat_updates the current storedts; it does not change thetsor touchlast_bump_ts. - Activity bump — deletes the stored
ts, reposts the same content at the bottom, and saves the newts. Subsequent rollovers and edits then operate on that newts.
Both the rollover and the bump mint a new ts on repost and re-save it
immediately, so _refresh_schedule_post always reads the current ts from the
record.
Scope / manifest impact
channels:history(bot scope) andmessage.channels(bot event) are added toslack-app-manifest.yamlso the slack-bot Lambda receives channel messages and can run the bump. This is a new scope + event, so the app must be reinstalled once for them to take effect.pins:writeis removed — the pin is no longer used.
Follow-ups (not in this PR)
- After deploy, reinstall the Slack app so
channels:history/message.channelstake effect, then confirm a new channel message bumps the schedule post to the bottom (and that bursts are debounced). - If the re-notification on each bump proves noisy, revisit the debounce window or consider posting without a broadcast.