router and delivery modules provide the external MCP boundaries.
Delivery flow
1
Resolve
Call
mobkit/routing/resolve with a recipient and optional channel, retry, backoff, and rate-limit values. The runtime calls the router module’s routing.resolve MCP tool, applies any matching runtime-route override, mints a route_id, and retains the resulting RoutingResolution as trusted state.2
Send
Call
mobkit/delivery/send with the complete trusted resolution, a JSON payload, and an optional idempotency_key.3
Validate
The runtime requires the supplied resolution to equal the retained resolution for its
route_id. Unknown or modified resolutions are rejected before dispatch.4
Dispatch and record
Unless keyed idempotency replays an existing record, the runtime applies the resolved rate limit, invokes the delivery module’s
delivery.send MCP tool, and appends a DeliveryRecord to bounded history.Routing contracts
A mutable route override and a resolved route are different types:channel = "notification", retry_max = 1, backoff_ms = 250, and rate_limit_per_minute = 2. Before module and runtime-route overrides, the runtime infers email for recipients containing @, sms for recipients starting with +, and webhook otherwise.
These sink strings are routing defaults, not a promise that MobKit ships an SMTP, SMS, or webhook adapter. The router module can override the sink and return a target_module; a matching runtime route can override both again. The current mobkit/delivery/send contract dispatches only a trusted resolution whose target_module is exactly delivery. Any other target is rejected before the delivery MCP boundary call, so alternate target values are representable for routing inspection but are not dispatchable. The delivery module owns actual dispatch.
Runtime routes can be listed, added, and deleted through:
mobkit/routing/routes/listmobkit/routing/routes/addmobkit/routing/routes/delete
* recipient and channel matches. A matching route is applied after the router MCP response.
Mob-member routes
When the unified runtime reconciles its active roster and the router module is loaded, it manages one route per member:Optional idempotency
DeliverySendRequest.idempotency_key is Option<String>:
{route_id}:{idempotency_key}. A repeat with the same payload and canonical resolution returns the existing DeliveryRecord without another boundary call. Reusing the scoped key with a different payload is rejected.
This protection is conditional and bounded:
- No key means no idempotency lookup.
- The index is local to the runtime instance.
- Delivery history retains at most 200 records.
- Evicting a record also removes its idempotency entries, so a later retry can dispatch again.
Rate limiting
Each resolution carriesrate_limit_per_minute. The implementation uses fixed 60-second windows keyed by:
route_idrecipientsink- the window start timestamp
DeliverySendError::RateLimited.
Delivery records and adapters
DeliveryRecord contains the delivery and route IDs, recipient, sink, target module, original payload, status, attempt records, first and final attempt timestamps, optional idempotency key, and optional sink_adapter.
The sink comes from routing. The optional sink_adapter is recorded only when the delivery module returns a non-empty adapter string. MobKit does not maintain a fixed built-in adapter inventory in this contract.
Delivery dispatch emits a unified module event with:
module: "delivery"event_type: "send"- payload fields
delivery_id,route_id,recipient,sink,status, andattempts
mobkit/delivery/history. The request supports optional recipient and sink filters plus a capped limit.
Clock ordering
Routing and delivery timestamps use a runtime-relative monotonic floor that is also advanced to the latest merged-event timestamp. A route or delivery inserted after an existing merged event therefore does not receive an earlier timestamp.See also
- Gating - evaluate policy before dispatch
- Modules - router and delivery MCP boundaries
- Events - exact
resolvedandsendevent names - JSON-RPC API - routing and delivery methods
