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
+5
View File
@@ -421,6 +421,11 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234.
holder of kind `desktop`) and not *bot* (that is one service's account).
- **router** — the module that holds the `operator-channel` seat, orders the channels and checks every
sender ([ADR 0234](../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md)).
- **headline / explanation / resolved line** — what a condition says to the operator in plain words: a
few words naming the thing and what is wrong; one or two sentences on what happened, what it means and
whether to act; the line said when it clears. Carried by the condition beside its **summary**, which
keeps the plans, commits and verbs for whoever looks closer
([ADR 0253](../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md)).
- **ask** — a request for the operator's input, of a declared kind (yes-no, one-of, text, number,
date, acknowledge). An **authorising ask** is one whose answer performs an action; the controller
holds it and checks its proofs (ADR 0234).
@@ -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
@@ -4,6 +4,7 @@ status: in-progress
code: [mesh-controller, mesh-host, mesh-tools, mesh-catalog, mesh-sdk, mesh-lab]
updated: 2026-10-08
decisions:
- 02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md
- 02-DECISIONS/0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md
- 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
- 02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md
@@ -126,7 +127,8 @@ cleared on the first that does not. Designed in [to-be 48](48-a-module-says-how-
| kind | the condition kind, from the signals table, the probe registry or an event kind |
| subject | scope, id, and the machine it concerns when there is one |
| severity | `urgent` (needs the operator now) or `warning` (when they can) — two levels, no more |
| summary | one line in the mesh's words |
| summary | one line in the mesh's words, for whoever looks closer: it may name plans, commits and verbs |
| headline, explanation, resolved | what the operator reads, in plain words (ADR 0253): a few words naming the thing and what is wrong; one or two sentences on what happened, what it means and whether to act; the line said when it clears |
| evidence | the newest observations, at most ten, each with its time |
| source | the signals-table row, probe or event that raised it |
| raised, last observed | times; and how many observations since raised |
@@ -135,6 +137,17 @@ cleared on the first that does not. Designed in [to-be 48](48-a-module-says-how-
| silenced | until when, by whom, why — empty when not silenced |
| epoch | the controller lease epoch that last wrote it |
*Added 2026-10-08, [ADR 0253](../../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md):*
**a condition says itself to the operator in plain words, beside its summary.** The headline, explanation
and resolved line are made where the condition is made: by the producer when it knows more than the
subject (a walk's modules, a failed unit's name, a hand-act's cause), otherwise by the wording registered
for the kind, otherwise from the scope, which is reported. All three are held to the plain rule when the
controller takes an observation: no hash, numbered id, dotted name, duration in code, clock time, flag,
markup or line break, and the content rule besides. Words that break the rule are replaced by the scope's
words, never refused. A condition a healer gave up on says in its explanation that it needs the operator.
Checked by the plain rule's test and by a lint over every condition the controller's suite raises, which
fails a kind without words.
**The life of one:**
```
@@ -284,6 +297,12 @@ section; to-be 46 §13 builds what follows from it.
secrets, and this seat's holder becomes the router (to-be 46 §2, §3, §8).
- **A message** is: the condition's key, its subject (a machine's role, a module, a plan), kind,
severity, the one-line summary, since when, and the verb that shows more. **Deduplicated by the key.**
*Amended 2026-10-08, [ADR 0253](../../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md):*
a message reads in one glance. Its title is the condition's headline, with "Urgent:", "Still open:" or
"Now urgent:" before it where that applies. Its body is the explanation and since when, in the
operator's time zone (the messenger's setting `time-zone`). It carries no key, id, commit, verb or
markup: those stay in `conditions` and in the holder's history. A clearance is one line, the resolved
line and how long it was open. A digest lists titles. Still deduplicated by the key.
- **When:** on `condition-raised`; once more if still open after 1 hour (urgent) or 12 hours
(warning); on `condition-cleared`, by editing the first message where the channel can. A silenced
condition sends nothing. Urgent goes to both channels; warning to the desktop notifier when the
@@ -0,0 +1,78 @@
---
status: located
opened: 2026-10-08
located-in: [mesh-controller internal/conditions (the condition's words), mesh-controller cmd/mesh-controller (every producer of a condition), mesh-catalog modules/messenger (compose, the digest)]
fixed-by:
amended-design: 03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md
---
# 320. The operator could not read the mesh's notifications
## Symptom
On 2026-10-08 the operator said they could not read the mesh's desktop notifications. One, exactly as
it appeared on the laptop, with the plan id and the commit replaced here by placeholders:
> CLEARED after 26 min: the walk of novox/mesh-catalog `<commit>` has waited 56m0s for mesh-delivery's
> word to start: `mesh-delivery.show` for the delivery that landed as `<commit>` says why; `plans go
> <plan id> --why ...` starts it by hand
> about: plan `<plan id>` (waiting)
> since: 2026-10-08 10:40 UTC
> key: plan.`<plan id>`.waiting
> more: conditions show plan.`<plan id>`.waiting
What is wrong with it, as the operator listed it:
- It is a wall of text, five lines and some sixty words for one fact.
- It is full of identifiers: a plan id, a commit, a condition key and a verb's syntax.
- Its time is UTC, not the operator's.
- Its backticks are markdown, which a desktop popup shows literally.
- A clearance repeats the whole problem.
- It does not say in plain words what happened, whether the operator needs to act, or what to do.
The same shape held for every open condition on the mesh that day: a module unhealthy on the laptop
("… its unit `<name>.service` failed in the account's own service manager (exit-code)"), three failed
units on a workstation, a cause repaired by hand thirty-five times, five deliveries held past their bound,
each with `mesh-delivery.show <repository>@<commit>` in its line.
## Cause
**The condition had one line of words, and it served two readers.** To-be 45 §2 gave a condition one
`summary`, "one line in the mesh's words", and §5 made a message out of that summary with the key,
subject, kind, since-when and the verb that shows more. The summary was written for whoever looks closer
— an agent at the mesh MCP server, a person reading `conditions` — and rightly carried what they act on:
the plan, the commit, the verb. Nothing in the condition was written for the operator who reads a popup
between other work, so the channel showed the agent's line to the operator.
Issue 277 had already found the narrower half: a summary carried detail the content rule refuses, and the
controller now holds every summary to that rule. Identifiers, verbs and markup pass that rule — they are
not addresses, paths or secrets — so they reached the operator untouched.
**The message added its own detail.** The messenger's compose added `about:`, `since:` in UTC, `key:` and
`more:` lines to every message, and a digest added each key in brackets. A clearance was the whole
summary again behind "CLEARED after …".
## Fix
[ADR 0253](../../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md):
every condition carries, beside its summary, a **headline**, an **explanation** and a **resolved line**
in plain words, made where the condition is made and held to a checked plain rule; every channel shows
those and says the time in the operator's zone; a clearance is one line.
- novox/mesh-controller: the condition's three new fields, the plain rule, a wording for every kind the
controller raises, the richer words at the producers that know more (a walk's modules, a unit's name, a
hand-act's cause, a delivery's repository), and a lint over every condition the suite raises.
- novox/mesh-catalog: the messenger shows the words, says since when in the operator's zone (the new
setting `time-zone`), says a clearance in one line, names nothing but titles in a digest, and lets a
module give the same words through `notify`.
## How it is checked
ADR 0253 §How it is checked: the plain rule's own test, a test per real notification above showing the
words before and after, the suite-wide lint, and the messenger's tests of the four messages as the desktop
shows them.
## Design
To-be 45 §2 gains the three fields, and §5's message is no longer the key, kind and verb but the
headline, the explanation and since when.