Files
mesh-catalog/modules/messenger
jochen a6296dfd6f
mesh/merge-gate pass: builds messenger → novox; 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 feat/plain-notifications ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
Say each notification in plain words the operator reads at a glance
The desktop popups were walls of plan ids, commits, keys and verb syntax in UTC.
The messenger now shows the condition's headline, explanation and resolved line,
since when in the operator's time zone, and a clearance in one line (hq ADR 0253).
2026-10-08 13:26:26 +02:00
..

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).

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).