afterhours-shift-manager/docs/sticky-schedule-post.md
seahaven-openswe[bot] 612572bde7 Reuse weekly schedule post instead of reposting
The Monday rollover deleted last week's schedule message and posted a
fresh one every week, which re-notified the channel, changed the
permalink, and dropped any thread/reactions. Roll the two-week window
forward by editing the stored message in place (the same chat_update
path in-week shift changes already use), falling back to a fresh, pinned
post on first run or when the edit fails. A native pin keeps it reachable
from the channel header now that it no longer rises on the weekly repost.

Records the evaluation behind this in docs/sticky-schedule-post.md.

Refs: #135
2026-06-26 19:33:05 +00:00

5.4 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 (a) is implemented in the same PR.

Background

src/weekly-post/app.py (the Monday 7am ET Lambda) currently, every week:

  1. chat_delete the previous week's schedule post (stored ts from get_schedule_post),
  2. chat_postMessage a brand-new two-week schedule message, and
  3. save_schedule_post(channel_id, new_ts, …).

So every Monday the post gets a new ts (new permalink), re-notifies the channel, and loses any thread/reactions. During the week it steadily sinks as people chat.

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. The weekly rollover is the only place that creates a fresh message.

Options considered

(a) Reuse the same post across weeks — recommended

On Monday, chat_update the existing message to roll the two-week window forward instead of delete + repost.

  • Pros: stable permalink; no weekly re-notification; keeps any thread/reactions; trivial change (reuses the _refresh_schedule_post chat_update pattern 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.

(b) True stickybot behavior — keep it pinned to the bottom

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: always visible at the bottom of the channel.
  • Cons: directly conflicts with (a)'s stable permalink (every repost = new ts); requires a new event subscription (message.channels) and the channels:history scope (a reinstall); needs dedupe/debounce plus Slack rate-limit handling; constant repost churn; many more moving parts and a new always-on event path to operate. The benefit (bottom-of-channel visibility) is marginal for a low-traffic on-call channel.

(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 (a) reuse-in-place, complemented by a native Slack pin so the post stays reachable from the channel header even as the channel fills with chatter. Reject (b) — its only advantage over a pin is keeping the post at the literal bottom, which is not worth a new event subscription, an extra scope, and rate-limit/debounce machinery for this channel. The pin recovers most of the discoverability that the weekly bump used to provide, at the cost of one extra API call and one extra scope (pins:write), with no new event path to operate.

What changes (option a)

In src/weekly-post/app.py, on the Monday rollover:

  • Drop the unconditional chat_delete. There is no previous message to remove when we reuse the same one.
  • chat_update the stored post to roll the two-week window forward, keeping the same ts (stable permalink, no re-notification).
  • First-run / recovery fallback: when there is no stored post, or the chat_update fails (e.g. the message was deleted manually), fall back to chat_postMessage and pin the new message.
  • Keep save_schedule_post / get_schedule_post. Still needed — they hold the ts that both the weekly rollover and _refresh_schedule_post edit. On reuse the stored ts is unchanged; on a fresh post it is updated to the new ts and the new week_start.

Interaction with _refresh_schedule_post

No change needed. Both paths now converge on a single long-lived message keyed by the stored ts: the weekly Lambda rolls the window forward each Monday, and in-week shift changes keep editing the same message. They never fight because neither creates a new ts while one already exists.

The "ever-growing edited message" / retention question

A chat_update does not grow channel history — it rewrites one message in place rather than appending. Slack keeps the message's original post time, so an edited-forever post sorts by where it was first posted (hence the pin). There is no retention or history-size concern from editing the same message indefinitely.

Scope / manifest impact

  • pins:write is added to slack-app-manifest.yaml for the native pin. This is a new OAuth scope, so the app must be reinstalled once for the pin to take effect. The pin call is best-effort: until the reinstall happens (or if the bot lacks the scope) it logs and is skipped, and the reuse-in-place behavior still works without it.
  • No new event subscriptions. Option (b)'s message.channels event and channels:history scope are not added.

Follow-ups (not in this PR)

  • After deploy, reinstall the Slack app so pins:write takes effect, then confirm the first rolled-forward post gets pinned.
  • Optional: a one-line threaded note on the rolled-forward post if we later decide we still want a light weekly nudge (kept out here on purpose — the whole point of (a) is to stop the weekly re-notification).