Issue 320 and ADR 0253: a condition says itself to the operator in plain words
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request

The operator could not read the mesh's notifications: they were the summary meant
for an agent, with ids, commits, keys and verbs, in UTC. The condition now carries
a headline, an explanation and a resolved line beside its summary.
This commit is contained in:
jochen
2026-10-08 13:28:50 +02:00
parent 73fa97efde
commit 7c49aa0ffc
4 changed files with 251 additions and 1 deletions
@@ -0,0 +1,148 @@
---
topic: the mesh
status: accepted
date: 2026-10-08
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md
---
# 253. A condition says itself to the operator in plain words, beside its summary
## Context
The operator could not read the mesh's notifications
([issue 320](../04-ISSUES/320-the-operator-could-not-read-the-meshs-notifications/00-report.md)). A walk
waiting for its delivery's word reached the laptop's desktop as five lines: the summary, with a commit, a
plan id and two verbs' syntax in backticks; then `about:`, `since:` in UTC, `key:` and `more:`. The operator
reads a notification between other work, and has to understand it in one pass: what happened, whether
they need to act, and what to do.
What the mesh had, on the main branches of 2026-10-08:
- **A condition had one line of words.** To-be 45 §2 gave it a `summary`, "one line in the mesh's words".
The producers wrote it for whoever looks closer, an agent or a person at `conditions`, and named the
plan, the commit and the verb that acts. That reader needs those, and keeps needing them.
- **Issue 277 holds every summary to the operator channel's content rule** (ADR 0234 §6): no address,
domain, path or secret. Identifiers, verbs and markup pass that rule, so they reached the operator.
- **The message added detail of its own.** The messenger built every message from the summary, and added
the subject, the kind, since when in UTC, the key and the verb that shows more (to-be 45 §5). A digest
added each key in brackets. A clearance was "CLEARED after …" and the whole summary again.
- **The controller raises some sixty condition kinds**, from some eighty places in its code. Each knows
what it is about. Several know more than the condition's subject says: a walk knows its modules, a
failed unit its name, a hand-act its cause.
- **Channels are about to multiply** (ADR 0234: Telegram and the desk today; other kinds later). Each
channel shows a title and a body. None of them can work out what a key means.
## Considered Options
1. **A formatter in each channel**, which turns the summary into something readable. It is the smallest
change, in one module. But it has to guess what a summary means from its words. It would turn
"the walk of `<repository>` `<commit>` has waited …" into a title only by knowing every producer's
sentence, and each new producer would break it without notice. Every new channel would need it again.
The producer knows what it means; the channel does not. Rejected as the main mechanism; kept as the
fallback for a condition that carries no plain words (option 4, rule 6).
2. **Rewrite every summary in plain words.** Then the reader who looks closer loses the plan, the commit
and the verb, which are what an agent acts on. One line cannot serve both readers. Rejected.
3. **A table of words per condition kind in the messenger.** The messenger would know the controller's
kinds, which inverts ADR 0234's rule that the controller says what is wrong and the router only
routes. It also cannot see what only the producer knows, such as a walk's modules. Rejected.
4. **The condition carries plain words beside its summary, made where the condition is made.** Each
producer can say what it knows. A wording per kind covers the rest. A checked rule keeps identifiers
out. Every channel shows the same words. Chosen.
## Decision
1. **Every condition carries three fields in plain words, beside its summary.**
- **headline**: a few words naming the thing and what is wrong, at most sixty characters, such as
"openrazer not working on `<machine>`". It is the title of every message about the condition.
- **explanation**: one or two sentences saying what happened, what it means for the operator, and
whether they need to act, at most 360 characters.
- **resolved line**: the one line said when it clears, such as "openrazer works again on `<machine>`".
The **summary** keeps its role and its words: one line for whoever looks closer, naming plans, commits
and verbs. The events and the `conditions` verb carry all four.
2. **They are made where the condition is made.** A producer that knows more than the subject says them
itself: a walk names its modules, a failed unit its name without its suffix, a hand-act its cause and
count, a held delivery its repository and how long. Otherwise the **wording registered for the kind**
says them from the condition's subject. A kind with no wording is said from its scope ("a walk needs a
look") and reported. **A kind without words is a defect of its producer**, as a summary that carries an
address is (issue 277).
3. **The plain rule.** The three fields name no identifier, no code and no time of their own: no hash, no
numbered id, no dotted name (a key, a verb, a unit's file name), no duration in code (`56m0s`), no clock
time, date or zone, no command flag, no markup and no line break. They also pass the content rule of
ADR 0234 §6. A machine's or a module's name is a name, and may appear. The controller holds every
condition to the rule when it takes an observation. Words that break the rule are replaced by the
scope's words and reported. A condition is never refused for its words.
4. **Whether to act is said, not implied.** An explanation says one of: nothing to do (and when that
changes), what the operator does, or that an agent can do it. A condition a healer gave up on
(`resolver: operator` with attempts, to-be 45 §2) has "The mesh tried to repair it and could not: it
needs you now." added to its explanation, whatever the kind's words said before.
5. **The channel shows the words and says when.** The title is the headline. "Urgent:", "Still open:" or
"Now urgent:" goes before it where that applies. The body is the explanation, then since when, in the
operator's time zone: a clock time today, a weekday this week, a date before that. A clearance is one
line: "Resolved: " and the resolved line, then how long it was open. A digest lists titles. No message
carries a key, an id, a commit, a verb or markup. Those stay in the controller's `conditions` and in the
messenger's history, for whoever looks closer.
6. **The operator's time zone is a setting of the messenger**, `time-zone`, an IANA zone name. When it is
not given, the zone of the machine the messenger runs on is used.
7. **A condition without plain words** comes from a controller older than this decision, or from a module's
`notify` without a headline. It is said from its summary, with its code spans taken out. `notify`
accepts the same three fields.
## Consequences
- The popup of issue 320 becomes a title and two short lines, in plain words:
"openrazer delivery waiting to start". The body says that the change to openrazer is merged and built,
that it has waited 56 minutes for mesh-delivery (the module that decides when a delivery goes out) to
let it start, that there is nothing to do yet, that it becomes urgent after 4 hours, and that an agent
can start it by hand. Then "Since 12:40." in the operator's zone. When it clears, one line:
"Resolved: openrazer delivery no longer waiting, after 26 min".
- A new channel shows the same words with no formatter of its own.
- A new condition kind needs words. The controller's suite fails until it has them, the same way it fails
a summary that carries an address.
- An explanation is written when a condition is raised, and kept current on each observation. A message
already sent is not edited when the words change, which matches how the summary already behaves.
- The fallback for a condition without plain words is still the summary, with its identifiers. It lasts
only until the controller that makes the words runs. A module's `notify` keeps it until the module gives
a headline.
- The words are English, as everything else is. A second language is not decided here.
## How it is checked
- **The controller** (`mesh-controller`):
- `internal/conditions/plain_test.go`:
- the plain rule refuses each shape it names and passes the words conditions are made of;
- a condition carries a producer's own words, else its kind's, else its scope's;
- words that break the rule are replaced and reported;
- the events carry all three fields;
- an escalated condition says it needs the operator, once.
- `cmd/mesh-controller/plain_words_test.go`:
- the real notifications of issue 320 are raised from the same facts, and each shows its summary
unchanged and its plain words: a delivery waiting (warning, urgent, many modules), a module
unhealthy, failed units on a machine, a healer wanted, a delivery held;
- every registered wording is plain for a subject of every scope.
- **A lint over the whole suite** (`sayable_test.go`, beside issue 277's): every condition raised in any
test of the controller's command has plain words of its own kind or its producer's. A kind without
them, or words that break the rule, fails the suite and names the kind and its source.
- **The messenger** (`mesh-catalog`, `modules/messenger/cmd/messenger/words_test.go`):
- the four real notifications, decoded from the events a controller with plain words sends, show
exactly their title and body on the desktop, in the operator's zone, with no key, id, commit, verb or
markup, and clear in one line;
- a condition without plain words is said from its summary, without code spans;
- times are said in the operator's zone;
- a zone the machine does not know is said, not guessed.
The holder's tests check that no message carries a key.
- **Live**, once both are delivered: `operator-channel.history` lists the next messages, and their titles
are headlines. `messenger_status` names the time zone used.
## References
- [Issue 320](../04-ISSUES/320-the-operator-could-not-read-the-meshs-notifications/00-report.md)
- [Issue 277](../04-ISSUES/277-one-unanswered-question-was-an-urgent-alert-nobody-could-read/00-report.md)
(a summary held to the content rule; this record's lint sits beside that one)
- [ADR 0227](0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md) (rule 6:
the mesh says when it is wrong), [ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md)
(the channels, the router, the content rule)
- [To-be 45](../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md) §2 and §5