MalloryEstate Briefing
M·023Approved
← All briefings

Notifications & Nudges

Notification Voice & Source Handoff

Haiku-composed push teasers plus a server-supplied native deep-link so "Open in Slack/Gmail/Calendar" scales without an Android release.

Dated 2026-06-14Programme Notifications & Nudges

Situation

Two related fixes to Mallory's push notifications: (a) make every teaser sound like him, composed Worker-side by Haiku instead of using the raw title/body, and (b) hand off correctly to the native app (Slack/Gmail/Calendar) when a source is tapped from Updates, in a way that scales as integrations grow — new sources add a deep-link server-side, never an Android release.

This builds directly on already-shipped dependencies: the Updates surface, notification-tap-to-Updates routing, data-only FCM (a hard prerequisite for any tap-through), and the priority soft-cap — none of which are rebuilt here.

Key decisions

Haiku composes every teaser

composeTeaser(env, {...}) makes one small, low-temp Haiku call at nudge creation (using MALLORY_SYSTEM); on error/timeout it falls back to the existing title/body, never blocking the nudge or its Updates record.

Server-supplied deep-link + JS bridge

Chosen over App-Links auto-routing or a client-side host-map as the most reliable, server-scalable mechanism — the Hub emits native_url + app per item, Android's bridge just opens it.

Label: "Open in {app}"

Falls back to "Go to source" when app is absent but a url exists; no button at all when neither is present (e.g. briefing/EOD).

Notification tap still goes to Updates, not direct-to-source

Already shipped and verified — this design only changes what happens after the user taps "Open in {app}" inside Updates, not the push tap itself.

Fallback to browser on ActivityNotFoundException

Android's openSource bridge tries the native scheme first and catches the failure to open the https URL instead, so an app that isn't installed never dead-ends the tap.

Architecture

A Worker-side Haiku pass writes a voiced teaser at nudge creation; the Hub attaches a native deep-link per source; the web and Android layer only decide whether to hand off natively or fall back to the browser.

composeTeaser (Worker)
Small Haiku helper called in handleNudge and by briefing/EOD; writes the teaser column, falls back to title/body on failure.
Migration (native_url, app columns)
Two optional, validated string columns on the nudge/update row, supplied by the Hub where it knows the source.
GET /updates toItem()
Serves nativeUrl + app per item to the web client.
UpdatesView (web)
"Open in {app}" action; uses the Android bridge when present and nativeUrl is set, else opens url in a new tab.
openSource JS bridge (Android)
startActivity(ACTION_VIEW, nativeUrl) with an ActivityNotFoundException catch that falls back to the web URL; mirrors the existing openSettings/openNotificationAccess bridge pattern.
Hub wiring (post_nudge, slack-email-monitor, calendar-sync)
post_nudge gains native_url/app/priority params; mention nudges emit a slack:// deep-link; calendar-sync emits Calendar + high priority for imminent (≤2h) meetings.

Risks & guardrails

Haiku cost/latency per nudge

One cheap Haiku call per nudge is accepted as fine for now; if batch mention runs make it noticeable, lazy generation at flush/send time is an explicitly deferred optimisation.

App-not-installed dead end

Guarded by the ActivityNotFoundException-to-browser fallback in the Android bridge, verified manually on-device (only Alex can confirm real app handoff).

Bridge-detection must degrade cleanly

The web checks for JS-bridge presence before calling openSource, so the identical UpdatesView code works unchanged in a plain browser and in the Android shell.

Delivery

Worker: migration, data layer, teaser composition

Migration 012 adds native_url/app; db.ts, parseNudgePayload, and handleNudge gain the fields and the Haiku compose step; briefing/EOD callers fixed and /updates serves the new fields.

Web + Android handoff

UpdatesView gets the "Open in {app}" label and bridge-vs-browser branching; Android gets the openSource bridge method.

Hub wiring

post_nudge threads native_url/app/priority through; slack-email-monitor and calendar-sync emit them for their sources.

Verify + deploy

Held for Alex — Worker/web/Hub PRs plus an on-device Android verification of the real app handoff.

Out of scope

Chooser/disambiguation UI for multiple matching appsDeep-links for sources beyond Slack/Gmail/CalendarTeaser cost/latency optimisation (lazy/at-send generation)Push tapping directly to the native source (tap still routes to Updates by design)
spec · docs/superpowers/specs/2026-06-14-notification-voice-and-source-handoff-design.md
plan · docs/superpowers/plans/2026-06-14-notification-voice-and-source-handoff.md
Mallory · Estate briefing