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, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
A replaced ask's message was left open as if the newer ask took it over, but the newer ask opens a message of its own, so the old one kept offering answers nobody takes. It now ends on every channel with 'Replaced by a newer question', and the minute's sweep ends any such message left from before. A record that clears no longer keeps what a walk owed of it (the review of PR 238).
239 lines
20 KiB
Markdown
239 lines
20 KiB
Markdown
# messenger
|
|
|
|
The holder of the `operator-channel` seat: the router (novox/hq to-be 45 §5, to-be 46, ADR 0227, ADR 0234,
|
|
ADR 0259). It decides what is said to the operator and on which channel, holds every ask a module makes of
|
|
the operator, judges each answer and issues the **warrant** its asker acts on. It performs nothing, holds no
|
|
channel's client and no channel's secret.
|
|
|
|
- **Declares three seats:** `operator-channel` (held here: `open`, `history`, `notify`, `asks`; an ask and
|
|
its cancel accepted under the asker's own name, the warrant emitted to that asker alone; the asks kept as
|
|
a record each asker reads under its own name), and the two kinded benches its channels hold, `channel`
|
|
(`show`, `edit`, `send`, each under the holder's kind) and `intake` (`choice`, `link`, `failed`, `standing`,
|
|
and the proof `code`, request and reply only). Only `channel` and `intake` may be kinded.
|
|
- **Reads the controller's open conditions** (`seat:mesh-controller.conditions`) and **consumes its
|
|
condition events**; the events are read in one place, `cmd/messenger/condition.go`.
|
|
- **The channels are modules of their own**: `telegram` (kind `telegram`, reaches the operator away, proves
|
|
its sender) and `desk-channel` (kind `desktop`, the machines' notifiers, proves nothing). Which kinds exist
|
|
and what each promises is the controller's word, in this module's membership; whether each can send now
|
|
is the channel's own, said on `intake.standing`.
|
|
- **Keeps its state on the bus** (ADR 0201): `open` and `sent` (the messages, and the restart passes made),
|
|
`asks` (every ask, closed ones for 30 days), `identities` (the operator's linked accounts, one per kind, and a
|
|
link asked for), `walks` (the delivery walks open now, each with what it holds).
|
|
- **Lost answers are said again from here, never from a channel's memory** (issue 384,
|
|
`cmd/messenger/restart.go`): a channel whose messages' answers were lost — the desk's notifier started
|
|
again — says when in its standing (`answers-lost-at`). The router says again on that kind, directly and not
|
|
through the outbox, each message that is still open, asked there, never answered and last said before that
|
|
time: one per problem, under its handle, with the words and answers of now, so it replaces the message in
|
|
place. **A restart says nothing it has said** (novox/hq ADR 0308 §6, issue 531): a silenced message, one
|
|
withheld for its content, one answered and one a walk holds are left as they are, and what ended never comes
|
|
back. The standings of one restart (a notifier on each machine, seconds apart) are one pass: a loss is acted
|
|
on once it stood 10 s, and the pass made is kept in the state (`sent`, key `passes`), so a router that starts
|
|
again does not make it again. A kind that cannot send waits; it never goes to another channel. Telegram's
|
|
answers are never lost and it says no time.
|
|
|
|
## Asks, answers and warrants (novox/hq ADR 0259)
|
|
|
|
- **An ask** arrives on `mesh.seat.operator-channel.accept.ask.<asker>`; the asker is the subject's last
|
|
token, which the bus grants that module alone. It is checked (the asks contract, the content rule, at
|
|
most three open per asker) and recorded before its work is taken; a refusal is said to the asker on its
|
|
`decided` subject, in words.
|
|
- **It is offered on every kind that can carry one of its answers**: an acknowledgement wherever a choice
|
|
can be made; an approval only where the kind proves its sender and an account is linked; nothing that
|
|
destroys until an authenticator-app code exists. Each option has a one-time ticket per kind; the words
|
|
say where an answer not offered here can be given ("Release or Stop: answer on Telegram, or from the mesh
|
|
MCP server."). An ask about a condition is shown on that condition's message.
|
|
- **An answer is judged in order**, refused at the first failure and said where it was given: the ticket is
|
|
known, unused and for this kind; the ask is open and unexpired; the sender is the linked account where
|
|
the kind proves its sender; the kind's capabilities meet the level; an approval comes from an account
|
|
linked an hour or more. The ask is then closed by compare-and-set — a second answer loses — and the
|
|
warrant published to the asker alone. A warrant the bus did not take is published again every minute.
|
|
- **What the operator sees when they answer** (issue 372): first, in reply to the answer where it was given,
|
|
that it was recorded and whether its asker was told ("You chose Approve: recorded, and claude-code was
|
|
told.") — said only once the ask's end is recorded — or, refused, why; then the ask's message edited to how
|
|
it ended, as its title ("You chose Approve at 13:30 on Telegram · <headline>"), with no answers left on it.
|
|
What the asker then did is not said: the asks protocol carries no result back to the router, so the
|
|
message says only that the asker was told and acts on it itself.
|
|
- **An ask never defaults**: at its expiry it ends `expired` and its asker does what it said it would.
|
|
- **Linking** (`/start` to the bot): a six-digit code is shown, with the account's name and number, on a
|
|
channel whose words stay private (the desk) for ten minutes; the operator sends it to the bot, and the
|
|
account is linked. It acknowledges at once and approves after an hour. **The code is shown now or never**:
|
|
while the desk cannot show it (its notifications paused, nobody there), no link is begun and no code waits
|
|
in a queue. At most three links an hour per account, and ten per kind, counted in the router's state. Every
|
|
link and unlink is said on every channel. An account unlinked less than a day ago is not linked again by a
|
|
code; one account per kind is checked again as a link is made.
|
|
- **Unlinking is asked of the account that is linked** (`messenger_unlink` asks it): an approve question put
|
|
to that account alone. Its approval unlinks it, told to it first, by name, then everywhere. **A link within
|
|
a day of an unlink waits for the replaced account's approval**, asked of it by name. Each unlink or relink
|
|
question binds the account it is about, so its approval never acts on another account linked meanwhile.
|
|
**An account still in its first hour**, which cannot approve yet, is unlinked from the desk: when another
|
|
account sends `/start`, the desk is asked whether to unlink it.
|
|
- **When that account is lost with its phone**, root at the router's machine leaves a file in this module's
|
|
state directory — `unlink.<kind>`, or `allow-link.<kind>` for a link that waits, a regular file owned by
|
|
uid 0 — and the router does it within the minute, said on every channel with what was done, and every open
|
|
question of the router's own ends. A file not made by root is refused and said.
|
|
- **A channel proving its sender says only to the linked account**: none linked, it is not ready, and asks
|
|
for approval are not offered on it. **An approval rests on the controller's positive word**, asked of its
|
|
verb `root-free` for the router's machine and the channel's, live when an answer arrives (a word at most
|
|
30 s old renders the buttons): only a machine it says is free carries an approval. A router that does not
|
|
know its machine, a controller that does not answer, a machine its answer leaves out, or a word on the
|
|
channel kinds older than two minutes all fail closed.
|
|
- **An ask that offers an approval is answered only where it could be approved**, whichever option is
|
|
chosen: the desk, which proves nothing of who clicked, is not offered it, and an acknowledgement from a
|
|
fresh link does not end it. An asker wanting an acknowledgement at the desk asks it alone.
|
|
- **Each ask's message says who asks** (as the bus names it), what each answer does, and what happens if
|
|
nobody answers. A warrant is published under a message id nobody can foresee.
|
|
- **The router runs as its own account `messenger`, on a bus account of its own**, never carried by the
|
|
machine's runtime (ADR 0259 §8): its state and its bus credential are that account's.
|
|
|
|
## 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`).
|
|
|
|
**A condition's message says which issue tracks it** (novox/hq ADR 0285, issue 400). Below the explanation,
|
|
one line from the issue tracker's `tracking` verb, asked outside the work at most once a minute and used for
|
|
ten minutes: "Tracked as issue 386: fix-386 on g14 is fixing it since 21:10.", "…: nobody is working on it.",
|
|
"…: its fix is merged and on its way.", "…: it waits for your answer.", up to three issues and then a count;
|
|
"No issue tracks this yet." when no issue links the condition's key; "Whether an issue tracks this could not
|
|
be checked." when the tracker gave no answer in ten minutes. "Nothing for you to do." becomes "Needs you:
|
|
answer issue N." while an issue tracking it waits for the operator, and is left out while nobody works on any
|
|
issue tracking it. The line changes words only, never when a message is sent (`cmd/messenger/tracking.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 message offers the answers of the ask about it, and Details** (`cmd/messenger/answers.go`). The router
|
|
calls no verb of a condition's: the controller asks for the answers a condition names, and performs the one
|
|
chosen on its warrant. Details is a ticket per kind, good for a day, used once, shown where it was chosen. A
|
|
warning kept quiet is also said once after a day.
|
|
|
|
| When | What |
|
|
|---|---|
|
|
| `condition-raised` | one message, deduplicated by the condition's key |
|
|
| still open after 1 h (urgent) or 12 h (warning) | once more, in place of its message — never sooner than that after it was last said |
|
|
| `condition-changed` from warning to urgent | once more, in place of its message, to both channels |
|
|
| `condition-cleared` | once it stays cleared 2 min (10 min once it came back): its message edited to say so; said inside a digest, a line in the next one |
|
|
| cleared and raised again before its clearing was said | nothing: to the operator it never ended — a unit failing again after each apply is one message, not one per apply; back as urgent, or with a reminder waiting when it cleared, that is said |
|
|
| cleared, said so, and raised again within 10 min | the same message, edited back to open — not a new one (urgent: said again in its place, so it rings) |
|
|
| silenced | its message, where shown, edited to "Silenced until <day>: <headline>" with the reason, no answers and no urgency (the desk's popup times out); nothing more while it lasts; silence over and still open, the message is open again; resolved meanwhile, said only where an edit notifies nobody |
|
|
|
|
**One message per problem** (issue 373). A record keeps the id of its message on each channel; a reminder or
|
|
an escalation is said **in place of it** — the channel takes the earlier one away and shows the new one as a
|
|
new message is shown, so it rings once and no second message stands beside the first (the desk replaces the
|
|
notification by its id; Telegram deletes the earlier message). A message said again goes alone, never as a
|
|
line of a digest beside the message it follows; one first said inside a digest gets a message of its own the
|
|
next time, and later words replace or edit that one. An ask's buttons live on that one message, so an ask that
|
|
ends takes them away everywhere; its expiry is said with its day when it is not today. An ask replaced by a
|
|
newer one about the same thing ends its own message too, which then says "Replaced by a newer question"; the
|
|
newer ask shows a message of its own (issue 548). A message left open by an ask that already ended is ended on
|
|
the next minute's sweep.
|
|
|
|
- **Routing:** urgent to every kind that can send; a warning to the desk where it can send, otherwise to
|
|
the channel that reaches the operator away; an ask to every kind that carries one of its answers.
|
|
- **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.
|
|
|
|
## While a delivery walks (novox/hq ADR 0308)
|
|
|
|
The controller's `plan-moved` says each walk's start and end: `walk`, `phase` (`started`, `ended`), `touches`
|
|
(`[{machine, modules}]`: what it moves and what a moved tool runner or node-engine carries), `outcome`
|
|
(`delivered`, `failed`, `stopped`), `bound_seconds` and `budget_class` (`cmd/messenger/walks.go`).
|
|
|
|
- **Held:** while a walk is open and within its bound, a message whose subject is a machine it touches, or a
|
|
module it touches (on a machine it touches, when the subject names one), is held: kept on the walk with when
|
|
and what, and the history says `held`. A message several open walks touch is told when the last ends or passes
|
|
its bound. At a walk's end, a later open walk that touches a message takes it over only as a first hold would:
|
|
still open, not silenced, not said since, and not one that is never held.
|
|
- **Never held:** an ask waiting for the operator (and a condition an open ask is about), a secret given or
|
|
replaced, a subject no open walk touches, and a walk's own failure (a plan, walk, delivery or build).
|
|
- **The end is one message:** the outcome, the modules and machines reached, how long; each held message still
|
|
open, one line each; the ones that cleared counted. Delivered with nothing held and nothing open: one line —
|
|
and nothing on a kind listed in the setting `only-what-needs-me`. What it carries is marked told on a kind
|
|
only once the message was sent there. Until then each is kept owed in the state (`owed`): a message lost with a
|
|
restart before it was sent leaves them untold and owed, and the restarted router says each on its own. A
|
|
record that clears owes nothing more.
|
|
- **The bound:** at its start plus `bound_seconds` (900 when not given), what is held and still open is said,
|
|
with "The walk … is still running, since HH:MM", and the cleared counted; the walk holds nothing more. A walk
|
|
whose end never comes is forgotten a day after it started. A start heard after its end opens nothing: the
|
|
controller's moments are compared on its own clock, never with one capped at the router's now.
|
|
- **Kept in the state** (`walks`): a router that starts again mid-walk holds and tells as before.
|
|
|
|
## What the operator gives
|
|
|
|
1. **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.
|
|
**The kinds that say only what needs the operator**, as a setting: `{"only-what-needs-me": ["<kind>"]}`. A
|
|
walk that delivered with nothing held and nothing open is not told on them (ADR 0308 §4); empty by default.
|
|
2. **The channels**: assign `telegram` and `desk-channel` (each says in its README what it needs). Linking
|
|
an account as the operator is done on the channel, never by a setting.
|
|
|
|
## 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 |
|
|
| `operator-channel.asks` | the asks held, open first: asker, state, where offered, the warrant, whether the asker was told |
|
|
| `messenger_status` | whether the operator can be reached and why not, per channel kind |
|
|
| `messenger_recent` | the recent sends, edits, digests, answers and failures |
|
|
| `messenger_test` | a test message now, to one kind or every one |
|
|
| `messenger_preview` | what a condition's message would say, tracking line and all, without sending it: the words a send would carry, reduced where the content rule refuses its own |
|
|
| `messenger_check` | whether some words may leave the mesh |
|
|
| `messenger_identities` | the accounts linked as the operator, and from when each approves |
|
|
| `messenger_unlink` | unlink a kind's account, said on every channel |
|
|
|
|
## Where it runs
|
|
|
|
Held once for the mesh, on the control node: its settings are root's files at 0600 (as every
|
|
runtime-carried module's are). It holds no secret.
|
|
|
|
## 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).
|