Extract a PresencePort seam (async setFocus/clearSocket/isViewing) with two
impls: the existing in-memory Map (dev, single instance) and a new
RedisPresenceService (prod). MessageModule provides PRESENCE_PORT via a
REDIS_URL-gated factory — same pattern as the socket.io Redis adapter — so
'who is viewing what' is shared across replicas and the notification projector's
presence gate works at N>1. Gateway + projector inject the port; isViewing is
now awaited. Presence spec updated to async (passing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NotificationProjector now also consumes interaction.normalized and, for kind
EMAIL (external email + app-to-app mail, which land via ingest — not
message.sent), always notifies the non-sender recipient(s), reusing the same
presence + mute gates. Refactors the fan-out into a shared notify() with an
alwaysNotify flag (DM or mail); message.sent keeps its DM/mention/reply policy.
Adds specs: mail notifies in a group thread where a plain message would not, and
a non-EMAIL normalized interaction does not notify.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Web Push (VAPID) env for the already-built notification stack: unset
VAPID_PUBLIC_KEY disables sends (WebPushDelivery returns 'failed'). Generate
with 'npx web-push generate-vapid-keys'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add optional MessagingAdapter.setFocus(threadId) and subscribeActivity(cb)
so hosts can report the foregrounded thread (backend suppresses push for it)
and drive live conversation-list refresh on any thread's activity. Messenger
wires focus/blur presence + activity-driven refetch. Published as 0.1.7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>