Complete the snooze/expiry notification lifecycle (re-delivery tick + expiry reporting)#165
Merged
Merged
Conversation
Snooze 24h was a delayed silent decline: the re-delivery tick the code
comments promised ('wired in PR 2') never existed, and expiry was a
blind status flip that reported to no one.
- v037: redeliver_at + redelivery_count columns on notifications
- Snooze stamps redeliver_at and pushes expires_at past it; the 15-min
maintenance loop (renamed from expiry-only) re-fans-out due rows with
a fresh card, capped by notifications.max_redeliveries (default 3)
- Expired questions inject an internal note into the asking session
(dispatched unconditionally; the per-session lock serializes),
expired approvals write an approval-expired audit event; both
broadcast notification_expired and edit the Telegram card best-effort
- Telegram snooze ack shows 'Snoozed until <time>' instead of the
generic answered state; web card shows snoozed/re-delivered state
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.
Problem
The
propose_actionapproval flow shipped with its return leg missing, and plain-question expiry shares the hole:snooze_notificationonly advancedexpires_at, keeping the row pending "so a later re-delivery tick (wired in PR 2) can surface it again" — that tick was never built. Actual lifecycle: snooze → Telegram buttons already consumed → row sits pending and invisible → expiry sweep silently kills it 24h later.expire_notificationsflippedpending → expiredin one blind UPDATE — no web broadcast, no Telegram edit, and no word to the asking session. A cron thatask_user'd something never learns the question died; from the user's side the card just vanished.Changes
Schema (v037) —
redeliver_at+redelivery_countonnotifications. Existing rows untouched; verified against a snapshot of a live DB (852 rows, defaults correct, nothing re-delivered retroactively).Snooze — stamps
redeliver_at = snooze_untiland pushesexpires_atpast it, so the row can't expire before it resurfaces. Re-snooze after re-delivery works — each click buys another cycle.Maintenance tick —
_periodic_notify_expiry→_periodic_notify_maintenance, same 15-min cadence, two ordered passes:_fanoutfor due rows — fresh Telegram card with a fresh keyboard (new message id stored), web broadcast flaggedredelivered: true. Capped bynotifications.max_redeliveries(default 3); at the cap the row expires with reporting instead of re-sending, so an auto-snoozing loop can't nag forever.[Question expired unanswered: <title>]into the origin session (engine.run(..., source="notification:expiry", internal=True), dispatched unconditionally — the per-session lock serializes; external/archived/missing sessions get broadcast only; multiple questions per session batch into one injection). Expired approvals append anapproval-expiredevent to the mechanical-actions audit log. Both broadcastnotification_expired(web grays the card live) and best-effort edit the Telegram message to "⏰ Expired unanswered" (dead keyboard dropped, failures swallowed).notify-kind expiry stays silent as before.Redeliver runs before expire, and re-delivery restarts the expiry window — a row whose
redeliver_atandexpires_atboth passed gets its last chance instead of dying in the same sweep.UX — Telegram snooze ack now edits the card to "💤 Snoozed until — will resurface" instead of the generic answered state. Web: snoozed cards stay actionable with a "Snoozed — returns " line (mirrors the server: the row is still pending), re-delivered cards get a badge and move to the top without double-counting the pending badge.
Config —
notifications.max_redeliveries: 3(only new knob; reusesdefault_expiry_hoursfor the post-redelivery window).Tests
16 new tests in
tests/test_notification_lifecycle.pycovering snooze semantics, the re-delivery cycle (incl. re-snooze and cap-hit), expiry reporting per kind (question injection incl. mid-turn sessions, approval audit, notify silence, external/archived guards, batching), Telegram edit + swallowed failures, migration defaults, and the both-due ordering guarantee. Full suite: 1365 passed. End-to-end simulated: propose → snooze → backdate → tick → fresh card → decide on the re-delivered card.Sibling issue (answers arriving mid-turn get stored-not-injected) is deliberately out of scope — the expiry injection here uses the dispatch-unconditionally pattern that fix needs, factored as
_inject_expiry_notefor it to adopt.