The notification bridge shipped last session but couldn’t deliver anything — no resolver existed to map users to destinations. The DestinationResolver SPI was in platform-api, NotificationBridgeStartup wired everything together, but the actual “user-1 gets email at user1@example.com” mapping was absent. Today I closed that gap and two adjacent issues on the same branch.

Config resolver as fallback

The resolver design went through an interesting iteration. My first instinct was a CDI producer creating one resolver bean per channel type. Claude and I discovered that standard CDI can’t dynamically produce N beans from a single @Produces method — each producer defines exactly one bean instance. The design review caught this before implementation started.

The fix was simpler: NotificationBridgeStartup already does the wiring and is the only consumer of resolvers. It scans casehub.notification.destinations.<channel>.<userId> config properties and creates a ConfigDestinationResolver directly as a fallback when no CDI-provided resolver exists. CDI resolvers always take precedence — the config fallback activates only when nothing else handles that channel type. No hardcoded channel list, no producer, no CDI magic.

Digest delivery and the HTML question

ConnectorNotificationDeliverer.deliverDigest() was a hard-coded failure stub. The issue asked for channel-type-aware formatting — HTML for email, short text for SMS, rich text for WhatsApp.

I made DigestFormatter a CDI SPI rather than a static utility. The design review pushed this — a static registry contradicts the CDI-native architecture and prevents downstream applications from overriding formatters. Each formatter is an @ApplicationScoped bean matched by channelId(). The startup indexes them and passes the matching one to each deliverer.

The email formatter writes HTML. But EmailConnector.send() called Mail.withText() unconditionally — the HTML would have been delivered as plain text, angle brackets and all. A format=html attribute on ConnectorMessage signals EmailConnector to use Mail.withHtml() instead. Backward-compatible — default remains plain text.

CalendarPlatform — following the ChatPlatform pattern

Issue #88 is the biggest piece. The casehub-life OpenClaw spec needs agents to interact with calendars. I followed the ChatPlatform module pattern: calendar-spi for the interface, calendar-ref for in-memory testing, calendar-google for the real provider.

The interface is flat — no capability decomposition. ChatPlatform has nine sub-interfaces because chat platforms genuinely vary in what they support. Calendar systems are more uniform. If a future provider needs degradation, we break the interface then. Pre-release, that’s free.

The event timing model was the most interesting design question. My initial draft used boolean allDay with Instant start/end. The design review caught the problem: all-day events are date-scoped, not instant-scoped. Converting “July 27th all day” to an Instant requires picking a timezone and a midnight — lossy in both directions. A sealed EventTiming interface with Timed(Instant, Instant, ZoneId) and AllDay(LocalDate, LocalDate) encodes the distinction at the type level. AllDay.end is exclusive per iCalendar RFC 5545 — a one-day event on July 27 has end July 28.

The Google Calendar provider wraps google-api-services-calendar directly rather than depending on jgccli. jgccli is a CLI tool with its own account management and Picocli infrastructure — baggage we don’t need. The API calls are the same; the provider just needs OAuth2 credentials from config.

The MCP tool layer provides PATCH convenience on top of the SPI’s PUT semantics. Agents call calendar_update_event(summary="New title") without providing every other field. The tool fetches the existing event, merges the provided parameters, and calls the SPI with the complete state.

What’s next

The bridge is functional for dev/test. Production needs a real resolver — SCIM, database, or OIDC-claims-backed. Per-tenant destination deduplication (#90) unblocks Slack and Teams bridging. The calendar SPI is ready for the OpenClaw integration in casehub-life.


<
Previous Post
Tenant Isolation Audit
>
Blog Archive
Archive of all previous blog posts