7.2 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
- Drop the unconditional
chat_delete+ always-repost. On Monday,chat_updatethe stored post to roll the two-week window forward, keeping the samets(the activity bump is what moves it to the bottom; the rollover just refreshes content). - First-run / recovery fallback: when there is no stored post or the
chat_updatefails (e.g. the message was deleted manually),chat_postMessagea fresh message and store 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_updatethe storedts(content refresh; same message), clearinglast_bump_tsso the next activity is free to bump. - In-week shift edits —
_refresh_schedule_postchat_updates the same 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.
Because only the bump ever mints a new ts (and it always re-saves it
immediately), the other two paths always read 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.