mesh/merge-gate pass: builds dunst, messenger → g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group fix/notifications-after-review ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
Review found that any process of the operator's account can choose a dunst action, so a click could release or stop a delivery in the operator's name; that tokens lived for ever; that a warning's answers died in ten seconds; and that a restarted notifier left dead buttons (hq ADR 0258).
135 lines
9.4 KiB
Markdown
135 lines
9.4 KiB
Markdown
# messenger
|
|
|
|
The holder of the `operator-channel` seat: how the mesh tells its operator what it noticed (novox/hq
|
|
to-be 45 §5, ADR 0227, research 028 — the minimal form, Q1a, Q5a, Q8).
|
|
|
|
- **Declares and holds `operator-channel`**, held once for the mesh, serving `open`, `history` and
|
|
`notify`.
|
|
- **Reads the controller's open conditions** (`seat:mesh-controller.conditions`) — the state it
|
|
believes, before and beside the events.
|
|
- **Consumes the controller's condition events** — `mesh-controller.condition-raised`, `-changed`,
|
|
`-cleared` — and decides what is sent. The controller calls nobody. The events are read in one
|
|
place, `cmd/messenger/condition.go`, which states every assumption it makes about their shape.
|
|
- **Two channels:** Telegram (a bot to the operator's chat) and the desktop notifier — the
|
|
`node-notifier` seat's `send` verb on the machine the operator sits at (ADR 0208), asked through the
|
|
mesh. Nothing new runs on that machine.
|
|
- **Keeps its open messages in its own state** (`open`; the recent sends in `sent`), so a restart
|
|
forgets nothing (ADR 0201).
|
|
|
|
## What is sent, and when
|
|
|
|
**History is never news** (novox/hq issue 271). The events stream hands everything it holds to a
|
|
consumer that is new, was made again, or whose holder was away — on 2026-10-06 a newly made consumer
|
|
handed over three hours of raised-and-cleared conditions at once, and each was said as if new. So
|
|
what is said is decided by the state now and by when a thing happened, never by when it arrived:
|
|
|
|
- **The state first.** On start, every 10 min, and at once when an old event arrives, the holder reads
|
|
what is open now from the controller (`mesh-controller.conditions`). An event older than that
|
|
reading is already in it and is not acted on. What is open here and not there ended meanwhile: it is
|
|
forgotten quietly (Telegram's message is edited, which notifies nobody; the desktop is not woken). An
|
|
urgent condition open and never said is said — all of them in **one** message. A warning raised in
|
|
the last 10 min and not heard is said like any other; an older one is state.
|
|
- **Old is state.** An event whose own time (`at`) is more than **10 min** old is recorded, never said.
|
|
- **A clearing of something never said says nothing**, and a message still held when its condition
|
|
clears is dropped.
|
|
- **Start grace.** Nothing is sent in the **first minute** after start but that one urgent message;
|
|
what arrives meanwhile goes out, coalesced, when the minute ends.
|
|
|
|
**A message reads in one glance** (novox/hq ADR 0253). Its title is the condition's headline — a few
|
|
plain words such as "openrazer not working on g14" — prefixed "Urgent:", "Still open:" or "Now urgent:"
|
|
where that applies; its body is the condition's explanation (what happened, what it means, whether the
|
|
operator needs to act) and since when, in the operator's time zone. No key, id, commit, command or markup:
|
|
those stay in the controller's `conditions` and in this module's history. A clearance is one line, the
|
|
condition's resolved line and how long it was open: "Resolved: openrazer works again on g14, after 26 min".
|
|
A condition from a controller older than plain words, or a module's notice without a headline, is said from
|
|
its summary, with code spans taken out (`cmd/messenger/words.go`).
|
|
|
|
**Every explanation opens with a verdict**: "Nothing for you to do." or "Needs you:" and one thing the
|
|
operator can do themselves. **A warning that needs nothing is not sent**: it stays open here and in the
|
|
controller's conditions, the history says it was kept quiet, and it is said the moment it needs the
|
|
operator (urgent at its bound, or its words now say "Needs you:"). An urgent condition is always said.
|
|
|
|
**A notification offers answers** (`cmd/messenger/answers.go`), limited by novox/hq ADR 0258: **a desk
|
|
click performs only an acknowledgement**. Any process of the operator's account can choose a dunst action
|
|
(`dunstctl action`), so a click proves nothing about who chose. Until answers are authorised (to-be 46
|
|
phase 5) the only answers offered and performed are **Details** and **Silence for a week** (the allow-list
|
|
`answerable`); release, stop, start and restart are said in words ("Needs you: release it, or stop it.")
|
|
and offered as no answer. Each answer is a token made for one message of one condition: bound to the
|
|
machines it was shown on, good for a day, used once, refused for a condition cleared or silenced, and one
|
|
event is acted on once. A refused answer is said on the desk where it was chosen, never dropped. When the
|
|
dunst bundle says it started (`dunst.started`), the open notifications that offer answers are shown there
|
|
again with fresh tokens. A silence is recorded with the cause `operator-answer`, which the hand-act log
|
|
does not count as a repair. A warning kept quiet is also said once after a day. Telegram shows the words
|
|
but no buttons.
|
|
|
|
| When | What |
|
|
|---|---|
|
|
| `condition-raised` | one message, deduplicated by the condition's key |
|
|
| still open after 1 h (urgent) or 12 h (warning) | once more — never sooner than that after it was first said |
|
|
| `condition-changed` from warning to urgent | once more, to both channels |
|
|
| `condition-cleared` | the first message edited to say so; said inside a digest, a line in the next one |
|
|
| cleared and raised again within 10 min | the same message, edited back to open — not a new one |
|
|
| silenced | nothing, its clearing included |
|
|
|
|
- **Routing:** urgent to Telegram and the desktop; warning to the desktop when a session there
|
|
answers, otherwise to Telegram. The notifier answering is how "the operator's session is there" is
|
|
read until presence is decided (research 028 Q4).
|
|
- **Bursts are one message.** What is to be said on a channel is held **30 s** from the first and goes
|
|
out together: alone, as itself; several, as one digest (`WARNING: 5 new warnings`, a line each). An
|
|
urgent message goes at once when nothing went out on its channel in the last 30 s.
|
|
- **The desktop is gentle:** warnings reach it as at most **one message every 15 min**; urgent ones
|
|
are not held to that.
|
|
- **Cap:** at most 20 messages an hour per channel — a last line of defence the above keeps far away.
|
|
Reached, it is said **once**, in a message of its own, and the rest is held to go out as one digest
|
|
when the hour allows.
|
|
- **What may leave the mesh:** roles and words. A message carrying an address (IP, host name, URL,
|
|
mail address), a path, or anything shaped like a secret (a token, a key block, a long random or
|
|
hexadecimal string, `password=…`) is refused, logged, stated as the `refused` event, and sent in its
|
|
place as `channel-refused` with the words that carried it withheld. The rule is `cmd/messenger/content.go`.
|
|
- **Nothing silent:** a channel that cannot send says so in `messenger_status` and in the log, and
|
|
what it holds is tried again every minute while the condition is open. `messenger_status` also says
|
|
when the open conditions were last read, why they could not be, and how many events were taken as
|
|
history. An event that cannot be read
|
|
is refused by name, counted, and told to the operator once an hour.
|
|
- **No answering back.** Acknowledging is `conditions silence`, through the mesh.
|
|
|
|
## What the operator gives
|
|
|
|
Nothing is sent until these are given; `messenger_status` says which is missing.
|
|
|
|
1. **The bot token**, as this module's own secret: `secret accept <machine> messenger telegram-token`,
|
|
then push that machine. A value the mesh minted because none was accepted is recognised as not a
|
|
bot token and said so.
|
|
2. **The chat id**, as a setting: `settings` for `messenger` with `{"telegram-chat-id": "<id>"}`.
|
|
3. **The desktop machines**, as a setting: `{"desktop-machines": ["<the machine the operator sits at>", …]}`.
|
|
Without it, warnings go to Telegram.
|
|
4. **The time zone**, as a setting: `{"time-zone": "<an IANA zone name>"}`. Every time a message says is
|
|
said in it; not given, the zone of the machine this runs on. A name this machine does not know is
|
|
logged and that zone used instead.
|
|
|
|
## Tools
|
|
|
|
| tool | does |
|
|
|---|---|
|
|
| `operator-channel.open` | what is open now, urgent first, with where it went, silenced, reminded, held, refused, unsent |
|
|
| `operator-channel.history` | what was said lately, and the refusals |
|
|
| `operator-channel.notify` | a message from a module using the seat: key, severity, summary, and best a plain `headline`, `explanation` and `resolved` line; `clear` to end it |
|
|
| `messenger_status` | whether the operator can be reached and why not; `check` asks Telegram whether the token works |
|
|
| `messenger_recent` | the recent sends, edits, digests and failures |
|
|
| `messenger_test` | a test message now, to telegram, desktop or both |
|
|
| `messenger_check` | whether some words may leave the mesh |
|
|
|
|
## Where it runs
|
|
|
|
Held once for the mesh: assign it to one machine whose tool runtime runs as root, since its secret
|
|
and settings are root's files at 0600 (as every runtime-carried module's are).
|
|
|
|
## Not yet
|
|
|
|
- **`notify` is a served verb, not a work queue.** The design has the seat *accept* `notify` so a
|
|
message waits for a holder; the node's tool runtime does not yet hand a bundle its seat's queue,
|
|
and an accepted queue nobody takes would hold messages silently. It is answered request-and-reply
|
|
until the runtime takes a seat's queue for a bundle (mesh-tools).
|
|
- **Channels are inside the holder**, not modules contributing to the seat: a seat a module declares
|
|
cannot receive contributions yet (ADR 0212 kinds are compiled for the mesh's own seats).
|