---
title: meshai Region × Family Routing — Implementation Plan
type: project
tags:
  - mesh
related:
  - [[meshai]]
  - [[meshcore-transport]]
  - [[meshai-fire-alerting-and-persistence]]
updated: 2026-07-07
status: proposed
---

# meshai — Universal Region × Family Routing: Final Implementation Plan

> Produced by a local multi-agent ultraplan (3 planners → 3 judges → synthesize → 3 red-team critics → finalize), verified against prod code at commit ceb95fb.
>
> **Scope principle (Matt, 2026-07-07): build the PLUMBING, not automation.** meshai exposes the mechanism — region-tagged events + a compact `region_routes` matrix the dispatcher honors + a plain editor to set it. **Matt does all configuration by hand:** defines the region boxes, provisions the radio channels, fills each family×region→channel cell himself. meshai auto-creates nothing — no channels, no regions, no routes, no generated rule objects. "No sprawl" comes from the config being ONE compact object he edits, not from automation.

## 1. Verdict
**GO, phased.** Achievable, mostly config/GUI once the keystone code gap is closed: **nothing currently writes `event.region` / `event.regions`** (`notifications/events.py:55-56` define them, the dispatcher *reads* them, zero writers exist). Close that and the existing region-scope machinery comes alive.

Three non-negotiables baked in:
1. **Matrix routing must resolve ABOVE the toggle `min_severity` floor.** Satpass + most road/511 events are `routine`; the toggle floor defaults to `priority` (`config.py:616`) and runs before delivery-plan build. Left naive, "Magic Valley Ham Radio" and road routing silently do nothing.
2. **Meshtastic has a hard 8-channel ceiling** (index 0–7); MeshCore routes by unbounded channel *name*. A 3×3 matrix is 9+ cells → the transports are asymmetric. Confront with an explicit MT-coarse / MC-fine policy.
3. **Everything ships behind default-inert flags on the GUARDED toggle path**, never the unguarded rules path.

Effort: **P1 = S (~½ day), P2 = S (~½ day), P3 = M (~2–3 days).** P1 deploys dark, safe to soak indefinitely.

## 2. Keystone: region tagging (P1)
Region source = `config.coverage.areas[].name` (named bboxes). Reject `mesh_intelligence.regions`/`RegionAnchor` (centroid+radius, wrong for polygon events; its taggers are dead code).

- `config.py:980` add `Coverage.region_tagging: bool = False`. **`coverage` is restart-required** (`config_routes.py:29-40`) — flip needs a container restart.
- `coverage_area.py`: add `MonitoringArea.name: str|None=None`; `areas_from_config` keeps the name; refactor the embedded geometry-extraction (`:193-209`) into a shared `_event_geom_json`; add an **additive** `matching_area_names()` (match-all, no early return — leaves `classify_geom_areas` gate byte-identical) + `event_region_names()`.
- `coverage_filter.py`: add `region_tagging` ctor flag; tag **before** the gate and **only when `not event.regions`** (never clobber satpass); wire in `build_pipeline` (`pipeline/__init__.py:121`).
- **Contract:** `regions` = all intersecting named areas (config order, deduped); `region = regions[0]` (deterministic, keeps cooldown key stable); 0 matches / no geom / parse error ⇒ `None/[]` ⇒ identical to today.

## 3. Routing model: new compact primitive on the guarded toggle path
Add `notifications.region_routes` (a `RegionRouteMatrix`: `enabled: bool=False`, `cells: dict` = family→region→{mt, mc, min_severity}) and resolve it inside `_dispatch_toggles` **above** the severity/region gates.

- **Reject generated per-cell rules:** the rules path is fully unguarded (no cold-start/staleness/dedup; `rule.cooldown_minutes` is dead code) → routing high-frequency families through it floods. It's also the O(R×F) object pile Matt wants gone.
- **Schema must be declared ABOVE `NotificationsConfig`** (no `from __future__ import annotations` → nested-dataclass coercion fires at class-def; else `NameError`). Add an explicit hydration branch mirroring `destinations` (`config.py:1149`).
- **Dispatcher matrix branch** (authoritative-on-match): resolves per event region → cell; per-cell `min_severity` (default `routine`) is the only floor; **bypasses `toggle.regions` and `toggle.min_severity`** (this is what lets routine families route); collapses same-channel overlap to one send but arms cooldown for every contributing region; `_toggle_to_rule` gains optional `broadcast_channel`/`meshcore_channel` overrides. `_audit_route` unchanged (one `mesh_broadcasts_out` row per transport).
- **Cooldown/dedup (matrix branch only; non-matrix path byte-identical):** per-region cooldown key `(toggle, category, region)`; per-`(source, id, transport, channel)` dedup (preserves retry when one region's channel fails + keeps fire growth stages flowing per channel). **Honest caveat:** `toggle.cooldown_seconds` defaults to `0` → out of the box matrix flood protection is **dedup-only**; rollout requires setting non-zero cooldown on high-frequency families (roads/511/wzdx). Keep `fire` cooldown=0 (dedup + growth suffix govern it).

## 4. Manual matrix editor (P3) — plumbing Matt fills in, no generation
**This is a plain editor over the `region_routes` primitive, NOT a generator.** It reads the current matrix, lets Matt assign cells by hand, and saves exactly what he entered. meshai computes/creates nothing — no rules synthesized, no channels/regions invented.

- **Backend** (`dashboard/api/notification_routes.py`): `GET /api/notifications/regions` (lists the region names that exist — coverage area names + satpass observer labels — so the picker offers what Matt already defined; the single routing region source; repoint pickers off the mesh-health `/api/regions`). **MUST read the SAVED (on-disk) coverage, not the in-memory config** — `coverage` is restart-required, so the running config lags disk until restart; but adding/removing a *routing column* must NOT require a bot restart. Reading disk decouples the routing-UI region list (dynamic, instant) from the engine's coverage re-scoping (still restart-gated). Confirmed in preview: create/delete a region on the Coverage page → it appears/disappears in the matrix immediately, no restart; `GET /api/notifications/region-routing` (returns the matrix verbatim); `POST /api/notifications/region-routing` (**plain save via explicit server-side read-modify-write** — persists only what Matt entered; RMW because the section save rebuilds all of `NotificationsConfig` and would wipe toggles/rules/destinations otherwise; golden round-trip test is a release gate); optional read-only preview (shows which categories feed each family + the distinct MT-index count vs the 8 ceiling, so Matt sees what he still has to provision).
- **Frontend** (UNVERIFIED — `dashboard-frontend` not in the code checkout; re-verify `ChannelPicker`/`useFamilies`/`Notifications.tsx` first): `RegionRoutingMatrix.tsx` tab — rows=families (from `useFamilies`), cols=regions (from `/api/notifications/regions`), each cell = MT ChannelPicker + MC ChannelPicker + `min_severity` (default routine) + enable; sparse persistence (only cells Matt fills). Optional **manual** convenience fills (apply-down-a-region, copy-a-region) are just clipboard helpers Matt chooses to click — never automatic. Live MT-budget guard (warn at 8 distinct MT indices).

## 5. MC + MT per cell + the Meshtastic 8-channel ceiling
Each cell carries both `mt` (index) and `mc` (name) in one object; dual transport = one cell. **MT has a hard 8-slot firmware ceiling; MC names are unbounded.** Matt's target (Roads/Fires/Ham × SC-ID/SW-ID/East-ID = 9+ cells) can't get distinct MT channels.

**CHOSEN POLICY (Matt, 2026-07-07): MT axis = per-REGION "Alerts" channels; MC axis = fine per-region×family names.** On Meshtastic, one catch-all channel per region — **SWI Alerts, SCI Alerts, EI Alerts** (+ Magic Valley) — each carrying ALL families for that region, families distinguished by the existing emoji/text prefix (🔥/🚧/🛰️). On MeshCore, keep the full region×family split by name. Keeps region separation on MT (what Matt wants), gives up only per-family separation on MT (MeshCore retains it). Budget: 3–4 region channels + primary + admin, comfortably under 8. **Zero extra code** — it's just how the MT column is filled: set each region's MT index once and use the "apply down the region" fill so every family in that region → that region's Alerts index; the `mc` side stays per-cell.

Channel provisioning is NOT eliminated — meshai only *routes* to channels; every named channel (the 3–4 MT region Alerts channels + each MC region×family name) must still be hand-created on the Meshtastic device + MeshCore companion and subscribers pointed at it. The matrix kills the *rule-object* sprawl, not radio-side setup; the preview endpoint lists exactly what to provision.

## 6. Satpass special-case
Satpass has no geometry → tag it **upstream** so the never-clobber rule protects it. Refactor `env/satpass.py::to_event` to bind-then-return, set `regions=[entry_observer, exit_observer]` (ordered, deduped; friendly labels already at `satpass.py:164-165`). **Add `"satpass"` to `VALID_TOGGLES` (`categories.py:26`)** — hard prerequisite: `categories_for_toggle("satpass")` returns `[]` today only because satpass isn't in the toggle set. Observer friendly names must exactly match `coverage.areas[].name` to line columns up (e.g. "Magic Valley"); unmatched labels render their own column, never dropped. Routine severity routes fine because the matrix resolves above the floor.

## 7. Migration & back-compat
No migration; three default-inert levers (`MonitoringArea.name=None`, `region_tagging=False`, `RegionRouteMatrix.enabled=False`). **"Byte-identical" holds only while `region_tagging=False`.** Flipping it on activates two behaviors with no code change:
1. **Dormant region-scope activation** — every existing non-empty `toggle.regions` / `rule.region_scope` is dead today and activates. **Rules path is unguarded** → activated rules broadcast every sweep, replay backlog on restart.
2. **Cooldown cardinality shift** — the non-matrix cooldown key goes from global `"*"` to per-region → more broadcasts for multi-region feeds on any toggle with `cooldown_seconds>0`.

**Mandatory pre-flip audit (release gate):** dump prod config; list/disable/re-point every non-empty `rule.region_scope` (unguarded) and review every `toggle.regions`; ensure no matrix-routed family is also routed by an enabled rule (exclusivity); confirm per-region cooldown splits are acceptable. This is why P1 ships dark and soaks.

## 8. Test + safe rollout on CT108
Unit: `matching_area_names` (in-one/overlap-ordered/on-edge/outside/null/unnamed), shared geom helper, `coverage_filter` tag+gate + off-parity + satpass-preserve, `RegionRouteMatrix` round-trip golden, region-routing-save golden (toggles/rules/destinations survive), dispatcher matrix cases (routine routes, overlap one-send-both-cooldowns, per-region cooldown independence, per-channel dedup retry, authoritative-on-match, no-cell fall-through, cold-start/staleness still drop). Note: test harness builds NO `CoverageFilter` — matrix tests set `event.region/regions` manually.

Live (pct exec 108 from root@utility, compose in `/home/zvx/meshai/work/`): (1) deploy P1 dark + restart → zero change; (2) run pre-flip audit; (3) flip `region_tagging=True` + restart, matrix disabled → confirm regions populate, soak; (4) enable matrix, ONE cell (roads × SC-ID → throwaway channel, enabled=false first, cooldown>0) → confirm one landing on both + `cooldown_dropped` increments; (5) point real cells one family at a time. Keep `fire` cooldown=0.

## 9. Phases
| Phase | Effort | Ships |
|---|---|---|
| **P1 keystone tagging (dark)** | S ~½d | events carry region/regions; dormant scopes usable; zero routing change; safe soak |
| **P2 region source + one cell e2e (guarded)** | S ~½d | `/api/notifications/regions` + repoint pickers; satpass in VALID_TOGGLES + observer tagging; prove one family×region via `toggle.destinations` on a throwaway channel |
| **P3 compact primitive + manual matrix editor** | M ~2–3d | Matt fills the grid himself (dual MT+MC per cell); one compact object, no rule sprawl, nothing auto-created |

## 10. Open decisions for Matt
1. **Meshtastic 8-channel policy** — DECIDED: MT = per-region Alerts channels (SWI/SCI/EI [+ Magic Valley]), each carrying all families for its region; MC = fine per-region×family. Zero extra code (MT-column fill).
2. **Matrix severity semantics** — accept cells route all events ≥ cell `min_severity` (default routine) to both transports, bypassing per-severity `severity_channels` (chattier, but makes routine routing work).
3. **Cooldown posture** — non-zero on roads/511/wzdx before go-live; fire stays 0.
4. **Observer naming** — satpass observers named to match coverage area names.

**Files:** `config.py`, `coverage_area.py`, `notifications/pipeline/coverage_filter.py`, `notifications/pipeline/__init__.py:121`, `notifications/pipeline/dispatcher.py`, `notifications/categories.py:26`, `env/satpass.py`, `dashboard/api/notification_routes.py`, `dashboard/api/mesh_routes.py:254`, `dashboard-frontend/src/pages/RegionRoutingMatrix.tsx` (new, unverified repo).
