Dispatcher and router semantics
Status: Implemented. See ADR-002.
Registration
Every interaction Router owns message, action, command,
slash_command, quick_command, message_action, added_to_space,
removed_from_space, widget_updated, app_home, form_submit,
unknown_event, event, and error observers. Observers support decorators
and programmatic registration:
@router.message(F.text == "ping")
async def ping(message: MessageEvent) -> str:
return "pong"
router.message.register(other_handler, custom_filter)
router.action("deploy.confirm") is exactly action-name filter sugar. It does
not imply any Google Cards parsing.
slash_command and quick_command route stable typed CommandKind values.
message_action is generally available and routes without preview enrollment. Resource-change notifications use the separate
chattice.workspace_events.EventsRouter and EventsDispatcher runtime.
Hierarchy and traversal
include_router() builds an acyclic single-parent tree. Self-inclusion,
cycles, attaching a dispatcher as a child, and reusing an attached router under
another parent raise RouterConfigurationError.
Traversal is pre-order depth-first: the current router, then descendants in inclusion order. Handler registration order is preserved inside each observer.
Observer precedence
Routing has two global passes:
- Visit the matching specific observer (
message,action, orunknown_event) across the whole tree. - Only if no specific handler handles the event, visit generic
eventobservers across the whole tree.
This ensures a generic handler on an ancestor cannot preempt a specific handler
on a descendant. ErrorEvent uses only error observers and never falls into
ordinary generic handlers.
Propagation
Filters and handlers run deterministically in registration order. The first
candidate whose filters match and whose middleware/handler returns normally is
the result, even when that result is None.
- Raising
SkipHandlerfrom a filter, middleware, or handler skips that candidate and continues. - Raising
StopPropagationstops both specific and generic search and returnsNone. - No match returns
None.
Both control primitives derive from a dedicated BaseException subclass so
ordinary except Exception middleware does not accidentally transform normal
routing control. There is no implicit fan-out.
Errors
An ordinary exception creates an ErrorEvent(source_event, exception) and is
offered once to error observers in router traversal order. A normally returning
error handler handles it and its return value becomes the dispatch result. If
no error handler handles it—or error routing is explicitly stopped—the exact
original exception is re-raised. A failure in an error handler propagates with
the original exception as its cause; error routing does not recurse.