Skip to content

Dual-Bus Lane Routing

This diagram shows how FCC v1.4.2+ splits the single in-process :class:EventBus surface into two lane-specific buses and a :class:LaneRouter that dispatches events to the correct lane according to an authoritative YAML classification. v1.5.0 carried 85 event types across the paom / ux / both lanes; ADR-006 captures the decision to adopt the dual-bus architecture to keep orchestration traffic (workflow / persona / compliance / plugin / ecosystem / object model) separate from live-view traffic (visualization / demo / tutorial progress / search + KG + RAG query results).

The flowchart below traces every step in the routing pipeline: producer emits EventLaneRouter.classify looks up the lane → publish to the matching bus(es) → subscribers on each lane receive filtered events → transports (WebSocket, SSE, OTel exporter) fan out.

flowchart TB
    subgraph Producers["Event producers"]
        P1[SimulationEngine]
        P2[ActionEngine]
        P3[CompliancePipeline]
        P4[CollaborationEngine]
        P5[MultiUserSession]
        P6[SearchIndex / RAG]
        P7[BidirectionalSync]
        P8[PluginRegistry]
    end

    Producers --> LR

    LR{{"LaneRouter<br/>src/fcc/messaging/lanes.py:132"}}
    MAP[("event_lane_map.yaml<br/>src/fcc/data/messaging/<br/>85 classifications")]
    LR ---|loads at init| MAP

    subgraph PAOMLane["PAOM lane — orchestration / back-office"]
        PAOM_Bus[PAOMBus]
        PAOM_Types["workflow.*<br/>persona.*<br/>simulation.*<br/>governance.*<br/>action.*<br/>collaboration.*<br/>plugin.*<br/>compliance.*<br/>benchmark.*<br/>ecosystem.*<br/>MCP/A2A (non-UX)<br/>object_model.*<br/>ice_ext.model.*"]
        PAOM_Bus --- PAOM_Types
    end

    subgraph UXLane["UX lane — live-view / visualization"]
        UX_Bus[UXBus]
        UX_Types["viz.data.updated<br/>viz.frame.rendered<br/>demo.progress.*<br/>tutorial.progress.*<br/>search.query.result<br/>knowledge_graph.query.result<br/>rag.query.progress<br/>rag.query.result"]
        UX_Bus --- UX_Types
    end

    subgraph BothLane["Cross-cutting — both lanes"]
        Both_Types["PROTOCOL_BRIDGE_ERROR<br/>COLLABORATION_MATURITY_ASSESSED"]
    end

    LR -->|classify == 'paom'<br/>publish to paom_bus| PAOM_Bus
    LR -->|classify == 'ux'<br/>publish to ux_bus| UX_Bus
    LR -->|classify == 'both'| PAOM_Bus
    LR -->|classify == 'both'| UX_Bus
    LR -.unknown type<br/>RuntimeWarning<br/>fallback PAOM.-> PAOM_Bus

    subgraph Subscribers_Paom["PAOM-lane subscribers"]
        S1[CompliancePipeline]
        S2[EventReplay / SessionRecorder]
        S3[OTel span exporter]
        S4[DLQ + circuit breaker]
    end

    subgraph Subscribers_Ux["UX-lane subscribers"]
        S5[WebSocket bridge :8765]
        S6[SSE stream :8901]
        S7[React frontend]
        S8[Streamlit live demo]
    end

    PAOM_Bus --> S1
    PAOM_Bus --> S2
    PAOM_Bus --> S3
    PAOM_Bus --> S4

    UX_Bus --> S5
    UX_Bus --> S6
    UX_Bus --> S7
    UX_Bus --> S8

Three operational notes. First, the classification is loaded once at LaneRouter.__init__ from src/fcc/data/messaging/event_lane_map.yaml; updating the YAML requires a process restart. Second, unknown event types fall back to the PAOM lane (orchestration noise) with a RuntimeWarning — the warning is the signal to add a classification entry for the new type. Third, the both lane invokes a registered handler twice (once per bus); callers that must see each event exactly once use subscribe_paom or subscribe_ux directly instead of subscribe_both.

Reach for the dual-bus design whenever a new event type is added to EventType: decide which lane owns it (does the UX need to react live? or is it orchestration-only?), append a classification entry to the YAML, and the router picks it up automatically. When the decision is genuinely both (e.g. a protocol bridge error that compliance must log AND the UI must surface), use the both lane — it exists for that exact case.

Stage-by-stage data shapes

  • Stage 1 — Producer — Any subsystem constructs an Event(event_type, source, payload, correlation_id) and calls LaneRouter.publish(event).
  • Stage 2 — ClassifyLaneRouter.classify(event_type) returns "paom" / "ux" / "both" by YAML lookup.
  • Stage 3 — Publish — Router dispatches to paom_bus.publish(event), ux_bus.publish(event), or both; delivery counts sum.
  • Stage 4 — Subscriber match — Each bus delivers to global subscribers, type-specific subscribers, and filtered subscribers in that order (see src/fcc/messaging/bus.py:134).
  • Stage 5 — Transport — PAOM lane typically feeds CompliancePipeline / SessionRecorder / OTel exporter / DLQ; UX lane typically feeds WebSocket bridge / SSE stream / React frontend / Streamlit apps.

See also

  • Source: src/fcc/messaging/lanes.py:132 (LaneRouter), src/fcc/messaging/bus.py:54 (EventBus deprecated single-bus), src/fcc/messaging/events.py (EventType enum)
  • Classification: src/fcc/data/messaging/event_lane_map.yaml (85 entries)
  • Related sequence: ../sequence-diagrams/event-bus-pubsub.md
  • Related class diagram: six-pillars-overview.md (Pillar D + Pillar B both flow events here)
  • ADR: docs/decisions/ADR-006_*.md (dual-bus events)
  • Universal services: docs/ecosystem/universal-services.md — UX REST gateway (entry 5) consumes UX lane