Issue 320 and ADR 0253: a condition says itself to the operator in plain words
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:
+148
@@ -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
|
||||
Reference in New Issue
Block a user