> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiroleague.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Channel Manager

> The Hiro Server component that manages and operates communication channels.

export const DiagramViewer = ({children, title = "Diagram"}) => {
  const dialogRef = useRef(null);
  const viewportRef = useRef(null);
  const contentRef = useRef(null);
  const [isOpen, setIsOpen] = useState(false);
  const [zoom, setZoom] = useState(1);
  const [pan, setPan] = useState({
    x: 0,
    y: 0
  });
  const zoomRef = useRef(1);
  const panRef = useRef({
    x: 0,
    y: 0
  });
  const dragRef = useRef(null);
  const open = () => {
    setZoom(1);
    setPan({
      x: 0,
      y: 0
    });
    setIsOpen(true);
  };
  const close = () => {
    setZoom(1);
    setPan({
      x: 0,
      y: 0
    });
    setIsOpen(false);
  };
  const resetView = () => {
    fitDiagramToViewport();
    setPan({
      x: 0,
      y: 0
    });
  };
  const setView = (nextZoom, nextPan) => {
    zoomRef.current = nextZoom;
    panRef.current = nextPan;
    setZoom(nextZoom);
    setPan(nextPan);
  };
  const MaximizeIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5" />
    </svg>;
  const ResetIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
      <path d="M3 3v5h5" />
    </svg>;
  const CloseIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M18 6 6 18M6 6l12 12" />
    </svg>;
  const fitDiagramToViewport = () => {
    const viewport = viewportRef.current;
    const content = contentRef.current;
    const svg = content?.querySelector(".mermaid svg[viewBox]");
    if (!viewport || !svg) return;
    const viewBox = svg.getAttribute("viewBox")?.split(/\s+/).map(Number);
    if (!viewBox || viewBox.length !== 4 || !viewBox[2] || !viewBox[3]) return;
    const [, , width, height] = viewBox;
    svg.style.width = `${width}px`;
    svg.style.maxWidth = "none";
    const rect = viewport.getBoundingClientRect();
    const nextZoom = Math.min(2.2, Math.max(0.75, Number((Math.min((rect.width - 96) / width, (rect.height - 96) / height) * 0.98).toFixed(3))));
    setView(nextZoom, {
      x: 0,
      y: 0
    });
  };
  const handleWheel = event => {
    if (!isOpen) return;
    event.preventDefault();
    event.stopPropagation();
    const viewport = viewportRef.current;
    if (!viewport) return;
    const rect = viewport.getBoundingClientRect();
    const pointerX = event.clientX - rect.left - rect.width / 2;
    const pointerY = event.clientY - rect.top - rect.height / 2;
    const currentZoom = zoomRef.current;
    const currentPan = panRef.current;
    const nextZoom = Math.max(0.25, Math.min(5, Number((currentZoom * (event.deltaY < 0 ? 1.12 : 0.88)).toFixed(3))));
    const ratio = nextZoom / currentZoom;
    setView(nextZoom, {
      x: pointerX - (pointerX - currentPan.x) * ratio,
      y: pointerY - (pointerY - currentPan.y) * ratio
    });
  };
  const onPointerDown = event => {
    if (!isOpen || event.button !== 0) return;
    event.preventDefault();
    dragRef.current = {
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      pan: panRef.current
    };
    try {
      event.currentTarget.setPointerCapture?.(event.pointerId);
    } catch {}
  };
  const onPointerMove = event => {
    const drag = dragRef.current;
    if (!isOpen || !drag || drag.pointerId !== event.pointerId) return;
    event.preventDefault();
    const nextPan = {
      x: drag.pan.x + event.clientX - drag.startX,
      y: drag.pan.y + event.clientY - drag.startY
    };
    panRef.current = nextPan;
    setPan(nextPan);
  };
  const endDrag = event => {
    if (dragRef.current?.pointerId === event.pointerId) {
      dragRef.current = null;
    }
  };
  useEffect(() => {
    const dialog = dialogRef.current;
    if (isOpen && dialog && !dialog.open) {
      dialog.showModal();
      requestAnimationFrame(() => {
        requestAnimationFrame(fitDiagramToViewport);
      });
    }
    if (!isOpen && dialog?.open) {
      dialog.close();
    }
  }, [isOpen]);
  useEffect(() => {
    if (!isOpen) return;
    const onKeyDown = event => {
      if (event.key === "Escape") close();
    };
    const onWheel = event => {
      event.preventDefault();
      event.stopPropagation();
      if (viewportRef.current?.contains(event.target)) {
        handleWheel(event);
      }
    };
    const previousOverflow = document.body.style.overflow;
    const previousHtmlOverflow = document.documentElement.style.overflow;
    document.body.style.overflow = "hidden";
    document.documentElement.style.overflow = "hidden";
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("wheel", onWheel, {
      capture: true,
      passive: false
    });
    return () => {
      document.body.style.overflow = previousOverflow;
      document.documentElement.style.overflow = previousHtmlOverflow;
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("wheel", onWheel, {
        capture: true
      });
    };
  }, [isOpen]);
  const toolbar = <div className="diagram-viewer__bar">
      <span className="diagram-viewer__title">{title}</span>
      <div className="diagram-viewer__actions">
        {isOpen && <span className="diagram-viewer__zoom" aria-live="polite">
            {Math.round(zoom * 100)}%
          </span>}
        {isOpen && <button className="diagram-viewer__button" type="button" onClick={resetView} aria-label="Reset diagram">
            <ResetIcon />
          </button>}
        <button className="diagram-viewer__button" type="button" onClick={isOpen ? close : open} aria-label={isOpen ? "Close" : "Maximize"}>
          {isOpen ? <CloseIcon /> : <MaximizeIcon />}
        </button>
      </div>
    </div>;
  const diagram = <div ref={viewportRef} className="diagram-viewer__viewport" onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={endDrag} onPointerCancel={endDrag}>
      <div className="diagram-viewer__stage" style={{
    transform: isOpen ? `translate(${pan.x}px, ${pan.y}px)` : undefined
  }}>
        <div className="diagram-viewer__centerer">
          <div ref={contentRef} className="diagram-viewer__content" style={{
    transform: isOpen ? `scale(${zoom})` : "none"
  }}>
            {children}
          </div>
        </div>
      </div>
    </div>;
  return <>
      {!isOpen && <div className="diagram-viewer not-prose">
          {toolbar}
          {diagram}
        </div>}

      <dialog ref={dialogRef} className="diagram-viewer-dialog" onWheel={event => {
    if (event.target === event.currentTarget) {
      event.preventDefault();
      event.stopPropagation();
    }
  }} onCancel={event => {
    event.preventDefault();
    close();
  }} onClose={() => setIsOpen(false)}>
        {isOpen && <div className="diagram-viewer diagram-viewer--open not-prose">
            {toolbar}
            {diagram}
          </div>}
      </dialog>
    </>;
};

Channel Manager is the link between Hiro Server and the outside communication surfaces: device nodes through Hiro Gate, and other channel integrations through channel plugins. The Channel Manager is the Hiro Server component that owns channel plugins. It starts enabled plugins, accepts their local WebSocket connections, and moves messages between plugins and the rest of the server.

Channel Plugins include WhatsApp, Telegram, and other channel integrations. Learn more about channel plugins in the [Channel Plugins](/architecture/concepts/channel-plugins) section.

The Channel Manager handles two types of messages, Messages and Events.

* `channel.receive` — inbound `UnifiedMessage`s from plugins
* `channel.send` — outbound `UnifiedMessage`s to plugins
* `channel.event` — infrastructure events from plugins

<DiagramViewer title="Channel Manager Architecture">
  ```mermaid actions={false} theme={null}
  flowchart TB
   subgraph HiroServer["<b>Hiro Server</b>"]
      direction TB
          plugins["plugins"]
          ChannelManager["ChannelManager"]
          CommunicationManager["RightSide"]
          Handlers[" "]
    end
   subgraph plugins["Plugins"]
          DevicesPlugin["devices channel-plugin"]
          OtherPlugin["Telegram channel-plugin"]
    end
   subgraph ChannelManager["Channel Manager"]
      direction TB
          UnifiedMessage["Messages"]
          Events["Events"]
    end
   subgraph CommunicationManager["Communication Manager"]
      direction TB
          InboundPipeline["Inbound Pipeline"]
          OutboundPipeline["Outbound Pipeline"]
    end
    subgraph Handlers["Handlers"]
        EventHandler["Event Handler"]
        InfraHandler["Infra Handler"]
    end
      DeviceNode["Device"] <-- websocket --> DevicesPlugin
      TelegramNode["Telegram"] <--> OtherPlugin
      plugins --> ChannelManager
      UnifiedMessage -- "channel.send" --> InboundPipeline
      OutboundPipeline -- "channel.receive" --> UnifiedMessage
      Events --channel.event--> EventHandler
      InfraHandler --> Events

      style plugins stroke-dasharray: 5 5,fill:transparent
      style HiroServer stroke-dasharray: 5 1,fill:transparent,stroke:#ffffff
      style ChannelManager fill:#ef4444

  ```
</DiagramViewer>

## Responsibilities

**Plugin spawning** — On startup, the Channel Manager reads all enabled `ChannelConfig` entries from the workspace database and spawns one subprocess per channel.

**Connection handling** — Plugins connect back over WebSocket using JSON-RPC 2.0. The Channel Manager accepts these connections and dispatches each incoming message by its `method` field.

**Inbound routing** — `channel.receive` notifications from plugins are forwarded to the `on_message` callback, which is the Communication Manager's `receive()` method.

**Event routing** — `channel.event` notifications (such as `gateway_connected` or `pairing_request`) are forwarded to the `on_event` callback, which is handled directly in the server process for pairing logic.

**Outbound dispatch** — The Communication Manager calls `send_to_channel()` or `broadcast()` to push messages back to one or all connected plugins.

**Graceful shutdown** — On stop, the Channel Manager sends a `channel.stop` notification to each connected plugin, waits one second, then terminates any subprocesses that are still running.

***

## Event handlers

### **`ChannelEventHandler`**

A small dispatcher that maps channel-event names (e.g. `pairing_request`, `gateway_connected`, `device_connected`) to registered async handlers. Channel Manager forwards every inbound `channel.event` to it; unknown events are logged and dropped.

### **`InfraEventHandlers`**

The concrete handlers registered on `ChannelEventHandler`. They implement the infrastructure-level reactions to plugin events: validating pairing requests and minting device attestations, tracking gateway up/down state, and updating the live recipient list when devices connect or disconnect. These responsibilities live here — not in Channel Manager — because they are security and state-management concerns rather than transport.

**Inbound infrastructure events**

These are `channel.event` notifications emitted by channel plugins and dispatched by `ChannelEventHandler` to `InfraEventHandlers`.

| Event name             | Event meaning                                                                                 | What the handler does                                                                                                                                                                                                               |
| ---------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pairing_request`      | A device is asking to pair with this Hiro Server through the `devices` channel and Hiro Gate. | Validates the request, checks the active pairing session/code, creates a device ID and attestation if approved, saves the approved device, clears the pairing session, then sends `pairing_response` back to the `devices` channel. |
| `gateway_connected`    | The `devices` channel successfully connected to Hiro Gate.                                    | Marks workspace runtime state as gateway-connected and records the gateway URL.                                                                                                                                                     |
| `gateway_disconnected` | The `devices` channel lost its connection to Hiro Gate.                                       | Marks workspace runtime state as disconnected and clears connected devices from `ResourceChangeBroadcaster`.                                                                                                                        |
| `device_connected`     | A device connected to Hiro Gate and is reachable through the `devices` channel.               | Reads `device_id` and adds it to `ResourceChangeBroadcaster`'s connected recipient set.                                                                                                                                             |
| `device_disconnected`  | A device disconnected from Hiro Gate and should no longer be treated as reachable.            | Reads `device_id` and removes it from `ResourceChangeBroadcaster`'s connected recipient set.                                                                                                                                        |

**Outbound infrastructure events**

These are server-to-plugin `channel.event` notifications sent through `ChannelManager`. They go directly from `InfraEventHandlers` to the `devices` channel plugin.

| Event name         | Meaning                                               | Receiver                                                                                   |
| ------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `pairing_response` | Approves or rejects a pending device pairing request. | `devices` channel plugin, which forwards the response to Hiro Gate and then to the device. |

## Related components

### **`CommunicationManager`**

The message router that sits between Channel Manager and the application core. It validates inbound messages, applies permissions, runs the adapter pipeline (transcription, persistence, …), and decides where each message goes (chat, agent, transcript, outbound queue). Channel Manager is its inbound source and its outbound sink, but every routing and policy decision is made here. See [Communication Manager](/architecture/concepts/communication-manager).

### **Channel plugins**

The subprocesses Channel Manager spawns and talks to. Each plugin is a standalone Python package implementing the contract from `hiro-channel-sdk`. The built-in `devices` plugin is the bridge to Hiro Gate; optional plugins such as Telegram and WhatsApp follow the same shape. See [Channel Plugins](/architecture/concepts/channel-plugins).

### **Workspace DB (`channel_plugins` table)**

The source of truth for which channels are enabled, how to launch them, and what configuration to push after registration. Channel Manager reads this on startup; it does not write it.

### **Read-only consumers**

`MetricsCollector`, the Admin API, and the HTTP surface query Channel Manager for connected-plugin metadata and child-process info. They observe state; they do not drive the component.

***

## See also

<CardGroup cols={2}>
  <Card title="Channel Plugins" icon="plug" href="/architecture/concepts/channel-plugins">
    Wire protocol, JSON-RPC methods, plugin lifecycle, and the SDK contract.
  </Card>

  <Card title="Communication Manager" icon="arrows-left-right" href="/architecture/concepts/communication-manager">
    The router that makes inbound and outbound decisions across channels and the core.
  </Card>

  <Card title="UnifiedMessage reference" icon="envelope" href="/architecture/protocol/unified-message">
    The shared wire contract for UnifiedMessage payloads and gateway frames.
  </Card>
</CardGroup>
