diff --git a/00-META/glossary.md b/00-META/glossary.md index 543a21d5..3ed916a0 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -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). diff --git a/02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md b/02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md new file mode 100644 index 00000000..6733c7ad --- /dev/null +++ b/02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md @@ -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 `` `` 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 ``". 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 ``". + + 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 diff --git a/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md b/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md index 9f1c4187..9418fb20 100644 --- a/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md +++ b/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md @@ -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 diff --git a/04-ISSUES/320-the-operator-could-not-read-the-meshs-notifications/00-report.md b/04-ISSUES/320-the-operator-could-not-read-the-meshs-notifications/00-report.md new file mode 100644 index 00000000..58d89e44 --- /dev/null +++ b/04-ISSUES/320-the-operator-could-not-read-the-meshs-notifications/00-report.md @@ -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 `` has waited 56m0s for mesh-delivery's +> word to start: `mesh-delivery.show` for the delivery that landed as `` says why; `plans go +> --why ...` starts it by hand +> about: plan `` (waiting) +> since: 2026-10-08 10:40 UTC +> key: plan.``.waiting +> more: conditions show plan.``.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 `.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 @` 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.