Concluded·2 Aug 2026 · 6 min read

I gave Claude a Unified Namespace. It knew everything and understood nothing.

UMH Core 0.44 · Redpanda · Benthos-UMH · MQTT · Modbus TCP · TimescaleDB · MCP streamable HTTP · Docker Compose

The last lab put an MCP server over two Modbus devices and found that a model reasoning from bare register values produces confident, well-argued nonsense. The fix there was a tag layer: names, units, scaling, written by hand per device.

Which raises the obvious question. A Unified Namespace is supposed to be exactly that layer, done properly, for a whole plant. So what happens if you put a model on one?

UMH Core is a good place to test it because it ships with no AI surface of any kind — no MCP, no LLM, nothing through v0.44. (What UMH is and why it exists, if the name is new — this lab assumes it.) The namespace is the vendor's, the AI layer is mine, and nobody has published the combination.

One licensing note, since this gets repeated carelessly. UMH's own code is Apache-2, but the broker at the heart of UMH Core is Redpanda, which is BSL 1.1: source-available, converting to Apache-2 four years after each merge, and barred from being resold as a streaming service. Free to run on your own plant, and not the same thing as an open-source stack.

The rig

ONE FEED, TWO STACKS

THE ESTATE
83 energy transfer stations · one MQTT feed · 9 metrics each
↓ both subscribe ↓
A — THE COPILOT
Profiles generated from an asset database
17 domain tools
knows what a delta-T should be
blind to anything not in the register
B — THE NAMESPACE
Nothing declared: the tree is read off the stream
13 generic tools
knows nothing about cooling
any protocol, any asset, no code
Same estate, same instant, same questions. The only difference is where meaning is kept.

On top of the estate, side B carries two Modbus devices on completely different register maps, because a namespace that only ever sees one protocol proves nothing.

The tree assembles itself. Nobody declares a tag anywhere:

umh.v1
└── northwind
    ├── site-a
    │   ├── ETS-01   comms delta_t energy_kwh flow_m3h power_kw
    │   │            return_bar return_c supply_bar supply_c
    │   ├── ETS-02   …
    ├── utilities
    │   ├── pump01      bearing_temperature discharge_pressure fault
    │   │               motor_current running speed_setpoint suction_pressure
    │   └── chiller01   alarm delta_temperature load_percent motor_current …
 
86 assets · 1,014 tags · Modbus and MQTT in one tree

That is the part that genuinely impressed me. A Modbus pump and a district cooling station, two protocols with nothing in common, land in the same hierarchy and one substring query crosses both. The copilot needed an asset database and a profile generator to know that anything existed at all.

Seven questions, both stacks

Each side gets its own best tool for each question, identical sixty-minute windows, same instant.

SEVEN QUESTIONS, IDENTICAL WINDOWS

COPILOT
UNS
BEFORE
UNS
AFTER
Live value right now
Below its design figure
Where the number came from
Is the reading current
Average over the last hour
Which stations are silent
Estate shortfall vs design
answered
7/7
4/7
7/7
Read the middle column. Everything the namespace could reach, it answered instantly; everything it could not, it could not reach at all.

Both return delta_t = 8.80 on the first question, which is how I know they are genuinely reading the same estate. Then it comes apart.

The three the namespace failed are not three problems. They are one problem three times: every one of them needs to know what a number is supposed to be. Which stations are below design. Which stations are silent. How far off the estate is. None of that is in a measurement, and a namespace is made of measurements.

The silence one is worth sitting with. The namespace knew 764 tags and exactly how stale each was. It could not tell me how many stations should have been reporting, because a station that never published is indistinguishable from a station that does not exist.

The number that makes it concrete

ONE FIELD, TWO READINGS

design_dt_f = 16  (Fahrenheit)
measured = 8.80  (Celsius)
CONVERTED AS A DIFFERENCE
16 × 5/9 = 8.89 °C

0.09 °C under design. Marginal drift, worth a look at some point.

TAKEN AS CELSIUS
16 − 8.80 = 7.2 °C

An estate-wide emergency that is not happening. Someone gets paged.

Same number, same direction, wrong by eighty times. This is not the kind of error that looks wrong in a sentence.

What makes this one nasty is that the correct conversion is the unusual one. A temperature difference scales by ratio alone; the 32-degree offset everybody remembers applies to absolute temperatures and, used here, produces a negative design figure. So the wrong arithmetic is the arithmetic most people were taught, and the result it produces is not absurd enough to trip anyone up.

The namespace carried that 16 with complete fidelity and could not prevent any of it. At this point in the afternoon I had written the verdict in my head: a UNS gives you reach, and reach without meaning is how you get confident wrong answers at scale.

That verdict was wrong.

The namespace was never the problem

The failure was not that a namespace cannot carry meaning. It was that nobody had put any meaning into this one. So I published it.

ONE ASSET, TWO CONTRACTS

_historianwhat is happening
delta_t = 8.80supply_c = 4.12flow_m3h = 247.0
_contextwhat is supposed to happen
design_min_delta_t_c = 8.89design_cooling_load_tr = 1000registered = 1

The pairing rule is design_min_x or design_max_x against measurement x. That is the whole of it. The direction has to be in the name, because a delta-T should be at least its design figure and a bearing temperature at most.

Measurements and targets, side by side on the same asset, in the same namespace, from two different sources.

Two more flows, both inside UMH, no new services. One reads the engineering columns of the asset register and publishes them under a _context contract next to the measurements. The other does the same for the two Modbus devices from a static source.

Three details carry the whole idea:

Converting on the way in changes who has to remember. Publishing the Celsius figure means no consumer can repeat the mistake, instead of every consumer having to know not to make it. The raw Fahrenheit value still travels, as design_delta_t_f_raw, so the conversion stays auditable.

The direction lives in the name. My first attempt published design_bearing_temperature_c = 70 and promptly reported a pump at 57.8 °C as failing. A delta-T should be at least its design figure; a bearing temperature at most. Targets became design_min_* and design_max_*, and a generic consumer stopped having to guess.

The register itself is context. Publishing registered = 1 for every asset in the database means a station that exists but never speaks now appears on the namespace as context with no measurements. Silence and absence became distinguishable, which closed the last question.

Same seven questions, same tools, nothing added to the MCP server except the ability to pair design_min_x with x:

answered:  copilot 7/7      namespace 7/7
 
  below design      43 of 82 assets carrying context
  not reporting     85/85 registered assets reporting, 0 never seen
  the unit trap     design 16F -> 8.89C published pre-converted
                    short by 0.09C; naive would read 7.2C

And the part the copilot structurally cannot do: the same generic tools now answer design questions about the Modbus pump, which has no entry in any asset database and no profile written for it anywhere.

What broke

WHAT ACTUALLY BROKE

A crash-looping flow is invisible

round_to is not a Bloblang method. The context flow restarted every second for minutes; agent health said nothing. Only the per-flow log showed it.

A bad mapping pollutes the namespace permanently

Rows with a null field published undefined metadata, creating a phantom asset. The topic is already compacted, and compaction keeps the newest record per key — so a key nobody writes to again is preserved by the very thing you would expect to clean it up. Removing it needs a tombstone.

One station's name is illegal in its own namespace

A station code contains #, which is the MQTT wildcard. The asset register holds an identifier the topic grammar cannot express, and both paths must sanitise it identically or the station silently never joins.

Redpanda advertises an address that is wrong for everyone else

It binds 0.0.0.0:9092 and advertises 127.0.0.1:9092, so any client on another container follows that to its own localhost and fails.

The convenient path drops the provenance

Redpanda's HTTP proxy avoids the Kafka client entirely and returns no message headers — which is exactly where the source register and address live.

3 of 83 stations have no design figure at all

They now appear as registered assets with a cooling load and no target, and the comparison declines to judge them rather than inventing one.

Every one of these was found by running it. None would have surfaced from reading the documentation.

Two of those deserve more than a card. The phantom asset is the one I keep thinking about: a mapping error published messages with undefined metadata, and correcting the configuration does not remove them. There is now an asset called northwind.undefined.undefined that every consumer replaying the log will see.

It is worth being precise about why, because the obvious answer is wrong. The topic is already compacted — and compaction will never help here. Compaction discards superseded records for a key and keeps the newest one, so a key nobody writes to again is preserved forever by the very mechanism you would expect to clean it up. Removing it means publishing a tombstone: a record with that key and a null value. Putting context on the wire is a commitment, not an edit.

The station whose code contains a # is the other. That character is the MQTT multi-level wildcard, so the asset register holds an identifier the namespace cannot legally express. The telemetry path sanitised it and the context path did not, so that one station silently failed to join, and 82 of 83 looked like success.

Verdict

A Unified Namespace buys reach, and the reach is real: 86 assets, two protocols, a thousand tags, none of them declared, and a new device answerable the moment it publishes. No profile library gets close.

It buys no judgement at all on its own. Everything that made the copilot look better in the first run came down to somebody having written down what the numbers mean — and that work is publishable. Move it onto the wire and the generic layer matches the specialised one, then passes it, because it also covers the assets nobody got round to profiling.

So the finding is not that a namespace beats a copilot, or loses to one. It is that context is data, and most plants are keeping it somewhere a model cannot reach. Convert units where context enters the namespace, put the direction of every limit in its name, and publish the asset register as context rather than guarding it in a database — then the AI layer stops needing to know anything about your plant.

I would still not let it write. That decision has not changed since the Modbus lab, and a namespace makes it more emphatic, not less: a write here fans out to every consumer subscribed downstream, which is a considerably worse blast radius than one holding register.

The wider argument this lab sits under is the DevOps and DataOps piece. Estate identifiers here are anonymised.

Newsletter

New essays, by email.

SCADA, cloud, AI, and the plant floor — a short email when something new is published. No noise, unsubscribe anytime.