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:
@@ -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).
|
||||
|
||||
+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
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user