Merge pull request 'Issue 320 and ADR 0253: a condition says itself to the operator in plain words' (#203) from feat/plain-notifications into main
This commit was merged in pull request #203.
This commit is contained in:
@@ -421,6 +421,12 @@ 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 / verdict / explanation / resolved line** — what a condition says to the operator in plain
|
||||
words: a few words naming the thing and what is wrong; "Nothing for you to do." or "Needs you:" and the
|
||||
one thing the operator can do themselves (its **needs**); one or two sentences on what happened and what
|
||||
it means; the line said when it clears. Its **actions** are the answers a notification offers. 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).
|
||||
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-08
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md
|
||||
---
|
||||
|
||||
# 253. A condition says itself to the operator in plain words beside its summary, says whether the operator is needed, and is answered from the notification
|
||||
|
||||
## 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.
|
||||
|
||||
A first rewording made the same popup plain: "openrazer delivery waiting to start", then "The change to
|
||||
openrazer is merged and built, and has waited 56 minutes for mesh-delivery … Nothing to do yet; it becomes
|
||||
urgent after 4 hours. To start it now, have an agent start it by hand." The operator read it and asked:
|
||||
*what does that notification mean, I need to do something? I need to open a session and tell it to
|
||||
deliver it?* Plain words were not enough. Three more things were wrong:
|
||||
|
||||
- **It did not say plainly whether the operator was needed.** "Nothing to do yet" and "to start it now"
|
||||
in one sentence read as an instruction.
|
||||
- **It was shown at all.** A walk waiting under its bound needs nothing from anyone; the mesh says it
|
||||
again when it does.
|
||||
- **What it offered could not be done from where it was read.** "Have an agent start it by hand" sends the
|
||||
operator to open a session, find the verb and say why. That is the work a notification should save.
|
||||
|
||||
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**, and a clearance repeated the whole summary.
|
||||
- **The controller raises some sixty condition kinds**, from some eighty places in its code. Several know
|
||||
more than the condition's subject says: a walk knows its modules, a failed unit its name, a hand-act its
|
||||
cause.
|
||||
- **The verbs that answer the common questions exist**, as seat verbs any granted principal may call:
|
||||
`mesh-delivery.release` and `stop` (with why), the controller's `plans` with `go` and `stop` (with why),
|
||||
`conditions` with `silence` (with why), and every machine's `node-service-manager.restart`.
|
||||
- **The desk could not take an answer.** `node-notifier.send` had no actions. To-be 46 §11 designs them
|
||||
(phase 2 of its build): the dunst holder offers them and emits the chosen one. Its router, asks and the
|
||||
controller's `authorise request` and `answer` (phases 3 to 5) are not built. No dashboard exists where an
|
||||
operator could release a delivery. The one way to act was an agent session.
|
||||
- **ADR 0234 §8 says a desk click alone never authorises.** It defines three tiers, and *acknowledge*
|
||||
"performs only what any granted principal may already do … it is not an authorisation, and needs no
|
||||
proof". No verb declares a tier yet: that is phase 5.
|
||||
|
||||
## Considered Options
|
||||
|
||||
### Where the words are made
|
||||
|
||||
1. **A formatter in each channel**, which turns the summary into something readable. It has to guess what
|
||||
a summary means from its words, breaks silently with each new producer, and is needed again for each new
|
||||
channel. Rejected as the mechanism; kept as the fallback for a condition that carries no plain words.
|
||||
2. **Rewrite every summary in plain words.** The reader who looks closer loses the plan, the commit and the
|
||||
verb. One line cannot serve both readers. Rejected.
|
||||
3. **A table of words per condition kind in the messenger.** The router would know the controller's
|
||||
kinds, which inverts ADR 0234, and could not see what only the producer knows. Rejected.
|
||||
4. **The condition carries plain words beside its summary, made where the condition is made.** Chosen.
|
||||
|
||||
### Whether the operator is needed
|
||||
|
||||
1. **Leave it to each explanation's wording.** The first rewording did, and the operator could not tell.
|
||||
Rejected.
|
||||
2. **A verdict every explanation opens with**, from a field the condition carries (`needs`): empty is
|
||||
"Nothing for you to do.", otherwise "Needs you:" and that one thing. Chosen: it is the first thing read,
|
||||
it is the same on every channel, and a router can act on the field.
|
||||
|
||||
### What is shown
|
||||
|
||||
1. **Everything, gently** (the desktop's fifteen-minute rhythm for warnings). The rhythm still shows a
|
||||
popup that asks nothing. Rejected.
|
||||
2. **Nothing that needs nothing, while it is a warning.** It stays open in the controller's conditions and
|
||||
in the channel's history, and is said when it needs the operator: at its urgent bound, or when its words
|
||||
change to "Needs you:". Chosen.
|
||||
|
||||
### How the operator answers
|
||||
|
||||
1. **Name the verb in the words**, for the operator to call. It sends them to a session. Rejected.
|
||||
2. **Wait for the conversation design** (to-be 46 phases 2 to 6) and say "Needs you:" with nothing to press
|
||||
until then. Honest, but leaves the common answers, which already exist as verbs, a session away for
|
||||
weeks. Rejected for the answers that exist.
|
||||
3. **The condition names its answers as actions; the desk offers them; the operator channel's holder
|
||||
calls the verb.** It builds the part of to-be 46 phase 2 these answers need (the desk's actions, and the
|
||||
chosen one said as an event), and nothing of the router or of authorising. Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **Every condition carries, beside its summary, words for the operator:**
|
||||
- **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.
|
||||
- **needs**: one sentence, the one thing the operator can do themselves — "release it, or stop it.",
|
||||
"log out of every session on `<machine>` and log in again." — or empty when nothing is needed.
|
||||
- **explanation**: opened by its **verdict**, "Nothing for you to do." when `needs` is empty and
|
||||
"Needs you:" and `needs` otherwise, then one or two sentences on what happened and what it means.
|
||||
- **resolved line**: the one line said when it clears.
|
||||
- **actions**: the answers the operator may give from the notification, each a label and the seat verb
|
||||
with its arguments (Release, Stop, Start, Restart, Silence for a week). An argument `why` left empty is
|
||||
the operator channel's to fill.
|
||||
|
||||
The **summary** keeps its role and its words. The events and the `conditions` verb carry all of it.
|
||||
2. **They are made where the condition is made.** A producer that knows more than the subject says them
|
||||
itself; otherwise the wording registered for the kind does; otherwise the scope's words do, and the
|
||||
kind is 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.** Headline, needs, explanation and resolved line name no hash, numbered id, dotted
|
||||
name (a key, a verb, a unit's file name), duration in code, clock time, date or zone, command flag,
|
||||
markup or line break, and pass the content rule. **They never send the operator elsewhere**: no
|
||||
"agent", no "session", and `needs` never says "by hand". `needs` is one sentence ending in a full stop.
|
||||
Words that break the rule are replaced by the scope's words and reported; a condition is never refused
|
||||
for its words.
|
||||
4. **Whether the operator is needed is decided per condition, by its producer.** "Nothing for you to do."
|
||||
is said where the mesh handles it or it passes by itself; "Needs you:" where only the operator can do
|
||||
the next thing. A condition a healer gave up on (`resolver: operator` with attempts) that needed nothing
|
||||
now needs the operator: its verdict becomes "Needs you:".
|
||||
5. **A warning that needs nothing is not sent.** The operator channel keeps it open, its history says it
|
||||
was kept quiet, and it is said the moment it needs the operator: when it becomes urgent at its bound, or
|
||||
when its words change to "Needs you:". An urgent condition is always said. At this decision these kinds
|
||||
need nothing while they are warnings:
|
||||
- a walk waiting for its delivery's word (under four hours), or stuck at a step;
|
||||
- a machine silent, not confirming a change, waiting for a push, its tools silent or unreachable, or its
|
||||
network check failing or rewritten;
|
||||
- a healer wanted, a listener behind, the bus being upgraded, a check that could not run, slow status,
|
||||
outdated facts or instructions, a merge or build not picked up;
|
||||
- a provider failing or silent, the mesh's own software restarting, an update put back on its first
|
||||
machine, data not measured, and unused data the operator chose to keep.
|
||||
6. **The notification offers its answers, and the operator channel performs them.**
|
||||
- The holder of `operator-channel` gives each action, and **Details**, a token, and sends them with the
|
||||
message. A message that offers answers is never folded into a digest.
|
||||
- **The desk:** `node-notifier.send` takes actions (token and label). The dunst holder offers them
|
||||
(dunst's actions: a middle click, or its context menu) and says the chosen token as its event
|
||||
`action-chosen` {id, token, machine}. It is that module's event until seats publish events of their
|
||||
own (to-be 46 phase 3), when it becomes the seat's.
|
||||
- **The holder calls the action's verb itself**, with a why that names the operator, the channel and the
|
||||
label ("the operator chose Release on the desktop notification …"), then edits the notification to say
|
||||
what came of it: done, or the refusal in words. Details shows the summary, the key and since when, on
|
||||
the machine that asked; a line the content rule refuses is said withheld.
|
||||
- **The tier.** None of these verbs declares an authorising tier: each is a verb any granted principal
|
||||
may already call, which is ADR 0234's *acknowledge*. So a desk click performs it, as ADR 0234 allows.
|
||||
When to-be 46 phase 5 declares a tier for one of them, its answer goes through `authorise request` and
|
||||
`answer` instead, and the desk asks for the code. Nothing here lowers ADR 0234's bar.
|
||||
- **Telegram** shows the words and no buttons until it holds the `intake` seat (to-be 46 phase 6).
|
||||
7. **Times are the operator's.** The body ends with since when, in the operator's time zone (the
|
||||
messenger's setting `time-zone`): a clock time today, a weekday this week, a date before. A clearance is
|
||||
one line: "Resolved:", the resolved line, and how long it was open. No message carries a key, an id, a
|
||||
commit, a verb or markup; those stay in the controller's `conditions`, the holder's history and Details.
|
||||
8. **A condition without plain words** (a controller older than this, or a module's `notify` without a
|
||||
headline) is said from its summary, with its code spans taken out, and is never kept quiet. `notify`
|
||||
accepts the same words.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The popup of issue 320 is not shown.** A walk waiting under four hours needs nothing; it is in
|
||||
`conditions` and in `operator-channel.history`, marked as kept quiet. Past four hours it is urgent and
|
||||
reads "openrazer delivery waiting to start", then "Needs you: start it, or stop it." and why, then "Since
|
||||
12:40.", with the answers Start, Stop and Details. The operator chooses Start, and the notification says
|
||||
"Start: done."
|
||||
- A held delivery offers Release and Stop; a failed service, Restart; failed units nobody manages, and data
|
||||
that shrank or went, Silence for a week; a module whose account waits for a new login says so, which no
|
||||
button can do.
|
||||
- **Some "Needs you:" have no button yet**, and say what to do in words: approving a retirement, deciding
|
||||
where a module's data lives, undoing a catalogue change, restarting the controller, putting the bus back.
|
||||
Their verbs either take what the operator must see first (`retire approve` takes the set it approves) or
|
||||
need a terminal on another machine. Each is a next step: an action where a verb exists, the authorising
|
||||
path of to-be 46 phase 5 where it must be proven.
|
||||
- An answer is performed by the messenger's own grant. Its manifest invokes exactly the verbs its actions
|
||||
name, and a new action names a new verb there. The verb's own record (a hand-act, a delivery's
|
||||
transition) says who chose it, through the why.
|
||||
- dunst waits for the answer in its bundle. A restart of that bundle ends the waits it held: the
|
||||
notification stays and its answers do nothing then. A notification closed or expired says nothing.
|
||||
- A new condition kind needs words, including its verdict. The controller's suite fails until it has them.
|
||||
- The words are English, as everything else is.
|
||||
|
||||
## How it is checked
|
||||
|
||||
- **The controller** (`mesh-controller`):
|
||||
- `internal/conditions/plain_test.go`: the plain rule refuses each shape it names; an explanation opens
|
||||
with its verdict; words that send the operator to an agent, a session or "by hand" are refused; a
|
||||
condition carries its producer's words, else its kind's, else its scope's, and the events carry them
|
||||
all; an escalated condition turns "Nothing for you to do." into "Needs you:", once.
|
||||
- `cmd/mesh-controller/plain_words_test.go`: the real notifications of issue 320, raised from the same
|
||||
facts, each with its exact verdict, explanation and actions — a walk waiting under its bound (nothing)
|
||||
and past it (Start, Stop, and the plan each calls), a module unhealthy (Restart, the unit and the
|
||||
account's manager), relogin needed (no button), failed units (Silence), a healer wanted (nothing), a
|
||||
delivery held (Release, Stop); and every registered wording plain for a subject of every scope.
|
||||
- **A lint over the whole suite** (`sayable_test.go`): every condition raised in any test has words of
|
||||
its producer or its kind, and they pass the rule.
|
||||
- **The messenger** (`mesh-catalog`, `modules/messenger`), `words_test.go`: the same notifications decoded
|
||||
from the events the controller sends: a warning that needs nothing is not shown and is kept as quiet in
|
||||
the history; each shown one has its exact title, body and answers; choosing an answer calls exactly the
|
||||
seat verb and arguments its condition names, with the operator's why, and edits the notification; the
|
||||
clearance is one line. A quiet warning is said when it becomes urgent. Details are shown where they were
|
||||
asked. The holder's tests fail a message that carries a key.
|
||||
- **dunst** (`mesh-catalog`, `modules/dunst`), `actions_test.go`: `send` with actions answers the id before
|
||||
the answer comes, asks `notify-send` for each action and to wait, says the chosen token as
|
||||
`action-chosen` with the id and machine, says nothing for a notification closed without an answer or with
|
||||
an answer it did not offer, and refuses actions it cannot carry.
|
||||
- **Live**, once delivered: `operator-channel.history` shows warnings kept quiet; the next held delivery
|
||||
shows Release and Stop on the desk; choosing one is in `mesh-delivery.show` with the operator's why.
|
||||
|
||||
## 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 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md) (the channels, the content rule, the
|
||||
tiers), [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 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)
|
||||
(a walk waits for its delivery's word)
|
||||
- [To-be 45](../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md) §2 and §5;
|
||||
[to-be 46](../03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md) §11 and §13
|
||||
@@ -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,9 @@ 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, needs, explanation, resolved | what the operator reads, in plain words (ADR 0253): a few words naming the thing and what is wrong; the one thing the operator can do themselves, or nothing; the explanation, opened by its verdict ("Nothing for you to do." or "Needs you:" and that thing); the line said when it clears |
|
||||
| actions | the answers a notification offers (Release, Stop, Start, Restart, Silence for a week), each a label and the seat verb the operator channel calls when it is chosen (ADR 0253) |
|
||||
| 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 +138,18 @@ 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, and says whether the operator
|
||||
is needed.** The headline, needs, explanation, resolved line and actions 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. The explanation opens with its verdict. All of it is 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; nothing that sends the operator to an agent or a session; 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 that needed
|
||||
nothing now 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 +299,16 @@ 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, opened by its verdict, and since
|
||||
when, in the operator's time zone (the messenger's setting `time-zone`). It offers the condition's
|
||||
actions and Details; the desk takes the answer (`node-notifier.send` gains actions, and the dunst holder
|
||||
says the chosen one as its event), and the holder calls the action's verb with the operator's why. It
|
||||
carries no key, id, commit, verb or markup: those stay in `conditions`, the holder's history and
|
||||
Details. **A warning that needs nothing is not sent**: it is kept, and said when it becomes urgent or
|
||||
needs the operator. A clearance is one line, the resolved line and how long it was open. A digest lists
|
||||
titles, and a message that offers answers is never in one. 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
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-10-06
|
||||
updated: 2026-10-08
|
||||
decisions:
|
||||
- 02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md
|
||||
- 02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md
|
||||
- 02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md
|
||||
---
|
||||
@@ -448,6 +449,11 @@ needs the lost factor's code. So:
|
||||
|
||||
- **`node-notifier.send` gains actions** (token, label). It still answers at once; the dunst holder
|
||||
listens for the chosen action and emits it as an event on its node seat.
|
||||
*Amended 2026-10-08, [ADR 0253](../../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md):*
|
||||
built ahead of the router for the answers a condition offers. Until seats publish events of their own
|
||||
(phase 3) the chosen token is the dunst module's event `action-chosen` {id, token, machine}, and the
|
||||
output seat's holder takes it and calls the action's verb. Only verbs that declare no tier are offered
|
||||
so; one that declares a tier waits for phase 5.
|
||||
- The router's desktop adapter, holding `channel/desktop` and `intake/desktop`, turns that into a
|
||||
`choice` envelope.
|
||||
- **For `text`, `number`, `date` and codes**, the notification's single action opens the
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
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, the answers), mesh-catalog modules/dunst (the desk's actions)]
|
||||
fixed-by: novox/mesh-controller PR #141, novox/mesh-catalog PR #126
|
||||
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.
|
||||
|
||||
**A first fix was read, and still failed.** Reworded in plain words, the same popup said "… Nothing to do
|
||||
yet; it becomes urgent after 4 hours. To start it now, have an agent start it by hand." The operator asked:
|
||||
*what does that notification mean, I need to do something? I need to open a session and tell it to deliver
|
||||
it?* It did not say plainly whether they were needed; it was shown although it needed nothing; and the only
|
||||
thing it offered was a session.
|
||||
|
||||
## 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.
|
||||
|
||||
**Nothing said whether the operator was needed, or let them answer.** No field of a condition said whether
|
||||
the operator was needed, so every warning was shown, and an explanation could imply an instruction. The
|
||||
verbs that answer the common cases existed (release or stop a delivery, start a waiting walk, restart a
|
||||
service, silence a condition), but the desk could not take an answer: `node-notifier.send` had no actions,
|
||||
and the conversation design that adds them (to-be 46) was not built. The one way to act was a session.
|
||||
|
||||
**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**, what it **needs** of the operator, an
|
||||
**explanation** opened by its verdict ("Nothing for you to do." or "Needs you:"), a **resolved line** and
|
||||
its **actions**, made where the condition is made and held to a checked plain rule. A warning that needs
|
||||
nothing is not sent. The desk offers the actions, and the operator channel's holder performs the chosen one
|
||||
through its seat verb. Every channel 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, keeps quiet what needs nothing, offers and performs
|
||||
the answers, 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`;
|
||||
dunst's `send` takes actions and says the chosen one as its event.
|
||||
|
||||
## 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 fields, and §5's message is no longer the key, kind and verb but the headline, the
|
||||
explanation with its verdict, since when and the answers; a warning that needs nothing is not sent. To-be
|
||||
46 §11 notes the desk's actions built ahead of its router.
|
||||
Reference in New Issue
Block a user