From a2dbce1f89fe71b595a0c8efd750e46233190062 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 12 Oct 2026 02:09:55 -0500 Subject: [PATCH 1/2] =?UTF-8?q?docs(events):=20the=20step=20editor=20and?= =?UTF-8?q?=20event=20templates=20=E2=80=94=20MODULE=5FAPI=201.13.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The contract half of website feat/event-step-editor (Rust PLAN_REDESIGNS §2): MODULE_API.md §1.1/§2.4 gain registerEventTemplates, a param's duration, a lease's example and an option's note; EVENTS.md §F (the note), §I (what the 1.13.0 editor settled) and the API surface (the templates route, draftAhead). PLAN_REDESIGNS §2.8 records the core half as built and the Module-uo proof. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN_REDESIGNS.md | 20 ++++++++++++++ website/EVENTS.md | 44 ++++++++++++++++++++++++++++++- website/MODULE_API.md | 48 +++++++++++++++++++++++++++++++--- 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index 7c89a83..ce412c4 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -389,6 +389,26 @@ Author a kit weekend from the template on each rig; a lease step with the phase' shows); a draft edited after publishing (Start asks); a boolean lease (a toggle); and a Module-uo event opened in the same editor, which must be unchanged apart from the controls. +### 2.8 As built: the core half (2026-10-12) + +website `feat/event-step-editor`, MODULE_API **1.13.0** (§8; `website/MODULE_API.md` §1.1, `website/EVENTS.md` §I). +Everything in §2.1–§2.3 and §2.5's core half, with three things settled while building: + +- **The phase guard counts the RUN, not the phase.** Teardown gives a held thing back when the run ends, so + a 10-minute lease in phase 1 is safe if phases 2 and 3 take ten minutes between them. The floor is the sum + of every later phase's `after`; a phase with no rule counts as no time, and one that waits on the game makes + the floor unknown, so it is not warned about. Only a phase with **no** rule is given one, and only when a + change can create or lengthen a hold. +- **"Is the draft ahead?" is answered by the server** (`draftAhead`): the working spec against the + published one, as data with keys sorted. A timestamp cannot answer it, because publishing writes the row too. +- **A template's inputs are params**, checked by the same function, so they get the same controls, sources and + duration unit as a step's fields. The draft comes back unsaved, from + `POST /admin/events/catalog/templates/:templateId`. + +**The Module-uo proof**, against this branch: the frozen-manifest job (76 routes, all documented; `uo` +registers its 12 actions), the server suite 723/723 and the client suite 45/45 — the same numbers as 1.11.0's. +The Module-Rust half (the kit weekend, `linked`, `kits.uses`, the reward message) follows on `edge`. + --- ## 3. Zones: ZoneManager's options, and a dome (§4.4; D166, D167) diff --git a/website/EVENTS.md b/website/EVENTS.md index ac41417..2317fb9 100644 --- a/website/EVENTS.md +++ b/website/EVENTS.md @@ -1103,6 +1103,10 @@ visible warning rather than blocking the form. For module-uo it is answered enti atlas — offline, with the shard down. Without it the authoring UI in [§I](#i--admin-ui-authoring-and-the-calendar) is a JSON editor with better fonts. +**A row may carry a `note`** (MODULE_API 1.13.0): a sentence, at most 300 characters, that the editor +shows under the field while that row is the chosen value — advice such as *"9 of 41 linked players +have used this kit up"*. It is never a rule; the save path does not read it. + ### Integration Kit A fifth chapter, obeying the kit's own rule — teach and link out, re-specify nothing. The four things @@ -1342,6 +1346,43 @@ sent a term — so the one screen that picks a spawner was picking from a 2,000- `searchable`, never the length of the answer: inferring it from a short list reads correctly right up until the same source is a dropdown on one shard and a search box on another. +### What the step editor settled at MODULE_API 1.13.0 + +The first Rust player walk (`modules/rust/PLAN_FIXES.md` U-3–U-6, F11) found the Phase 13 form +honest and hard to use, and `modules/rust/PLAN_REDESIGNS.md` §2 reworked it. Core-only and +game-agnostic: Module-uo's steps get the new controls and nothing else. + +- **A field is the thing it does.** No ` · int` tag and no bare asterisk; a required field says so in + words, under itself and in the save's list, only once a save has been tried with it empty. +- **A boolean is a toggle when required and three buttons — Not set, Yes, No — when optional** + (Rust D334). The Phase 13 reason for a three-way control stands: "not set" is the action's own + default, and a two-state control posts `false` for every field nobody touched. +- **A choice of two or three is a row of buttons**, whether a lease's `values` or a source that + answered with that few. The free-text box goes only while the source has answered and the value is + one of its choices, so the §F rule — a source that cannot answer leaves a text box — still holds. +- **`core.lease` is drawn as the chosen lease.** Picked by its label, then the value as the lease's + type with its range or `values` and its own `example`, the target as a picker over the lease's + `target.source` (`core.lease`'s own `target` param has no source, on purpose), and the minutes + with the lease's longest hold beside them. The params written are still `core.lease`'s. +- **A timed step keeps its run open** (U-6). A held thing is given back at teardown, when the RUN + ends, so a 10-minute lease in a one-phase event with no advance rule lasted a second. A param + that is a length of time says so (`duration: 'minutes'`). Adding or lengthening a timed step in a + phase with no advance rule gives the phase `after: `, on screen; a rule the + author chose is never overwritten. Under any phase whose run can end sooner than its longest hold + — counting every later phase's `after`, and nothing for a phase that waits on the game — the + editor says when the hold is given back. +- **Start asks when the draft is ahead** (U-5). A run is pinned to the published version, so an + edit saved and not published did not run, silently. The definition read the editor loads carries + `draftAhead` — the working spec compared with the published one as data, key order ignored — and + the start dialog then asks: **Publish and start**, or **Start version N**, the published one. It + does not choose: publishing moves scheduled occurrences too. +- **A new event may start from a template** (Rust D192). A module's `registerEventTemplates` entry + offers a few inputs; core checks them, the module builds a plan, core validates it as a spec and + the editor opens it as an unsaved draft (`POST /admin/events/catalog/templates/:templateId`). + Nothing records which template made a definition, and the runner never sees one. +- **Core's own examples name no game** (Rust PLAN_REDESIGNS §0.9): `core.lease` offered + `uo.rate.skillgain` and the announcements spoke of Britain and Cove to every deployment. + ### Four affordances worth building in from the start - **Dry run.** *(Built, Phase 6.)* Materialise the steps, dispatch each with `verify: true`, report what *would* happen @@ -1854,7 +1895,7 @@ no URL moved. | Route | Gate | | | --- | --- | --- | | `GET /admin/events` | staff | definitions, state, next occurrence, health | -| `GET /admin/events/:id` | staff | one definition, working spec included — what the editor reads | +| `GET /admin/events/:id` | staff | one definition, working spec included — what the editor reads. With **`draftAhead`** (MODULE_API 1.13.0): whether the working spec differs from the published version, so the start dialog can ask which to run. `PUT` and `publish` answer with it too | | `POST /admin/events` | admin, editor | create a draft | | `PUT /admin/events/:id` | admin, editor | edit the draft spec | | `POST /admin/events/:id/publish` | admin | snapshot a version and go `ready` | @@ -1871,6 +1912,7 @@ no URL moved. | `POST /admin/events/runs/:runId/steps/:stepId/skip\|retry\|confirm` | admin, moderator | Phase 3. `confirm` resolves a GM cue step; `retry` re-queues the step a paused run is stopped at and resumes it | | `POST /admin/events/runs/:runId/cleanup` | admin | re-run cleanup over unreverted resources (Phase 8). **`admin` rather than admin+moderator, unlike the seven live controls beside it**: this is not incident response, it asks core to write to the world again, which §K puts in the same row as the world-changing actions themselves. Legal on a TERMINAL run only — a run still in flight has a ledger that is still growing. It is the only thing that clears `revert_attempts`, and it answers **200 whatever it found**, for the dry run's reason: some resources may still be out there, and a 4xx would make that indistinguishable from a bad run id | | `GET /admin/events/catalog` | staff | registered actions, param schemas, risk classes, budget dimensions — and, since Phase 5, the **trigger** catalog and condition operators the advance form renders. Phase 7 added `budgets`, `leases` and `optionSources`: the other three registrations of the module contract, served beside the actions because the step editor needs all four to draw ONE step, and four requests to draw one form would be four chances to render half of it. Served here rather than borrowed from `/admin/engagement/triggers` because that route is `adminOnly` while a definition is authored by `admin` **and** `editor` | +| `POST /admin/events/catalog/templates/:templateId` | admin, editor | **fill an event template in** (MODULE_API 1.13.0) — the body's `inputs` are checked against the template, the module builds a plan and core validates it as a spec. The answer is a `draft` (`title`, `summary`, `spec`) and **nothing is written**: the editor opens it and the author saves it through `POST /admin/events`. `400` for refused inputs, `404` for an unknown template, `502` when the module fails or builds a plan core cannot accept | | `GET /admin/events/catalog/options/:sourceId` | staff | **the values behind a param's `source`** (Phase 7) — resolved by the module that registered it, on its own request rather than inside the catalog, because a source can be slow or down and must not take the catalog with it. A refusal is a **`200` with `ok: false` and a `reason`**, deliberately: the field degrades to free text with a warning rather than the form breaking | | `GET /admin/events/series` | staff | the arcs a definition may belong to | | `POST /admin/events/series` | admin, editor | create an arc (Phase 4) | diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 40a5e15..491f4ed 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -26,13 +26,41 @@ here extends the contract first, in this file, before the module is written agai Core exports a single integer-major semver string from `server/src/modules/version.js`: ```js -const MODULE_API_VERSION = '1.12.0' +const MODULE_API_VERSION = '1.13.0' ``` The client half carries the same number (`client/src/modules/version.js`) and a test asserts the two agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first module chunk evaluates, which is earlier than any network round trip could answer. +**1.13.0 — the event step editor** (`website/EVENTS.md` §F, §I; Rust `modules/rust/PLAN_REDESIGNS.md` +§2, D192). Four additions and no removal, so minor. Each is optional, and each is a declaration the +editor, core's own page, renders. + +- **`api.registerEventTemplates([{ id, label, description, inputs, build }])`** — a named starting + point for a new event. `inputs` are params (§2.4), at most 8, drawn by the same field a step's + params are, sources and all. On **New event** the author picks a template and fills its inputs; + core checks them, calls `build({ inputs })` (async allowed, 10 s) and validates what comes back — + `{ title, summary?, spec: { phases } }` — as a spec. The editor opens it as an ordinary unsaved + draft. **Nothing about the template is stored**: no column, no link, and the runner never sees + one. A refusal the author should read is `{ ok: false, error }`; a throw, a hang or an invalid + spec is logged and answered with core's own sentence, never the module's stack. +- **A param's `duration: 'minutes'`** — on an `int` or `float` param that is a length of time, so + the editor can see a timed step. A held thing is given back when the run ends, so the editor gives + a phase with no advance rule `after: ` when a timed step is added, and says so + under any phase whose run can end sooner. `core.lease`'s `minutes` declares it. +- **A lease's `example`** for its value: one scalar, shown as the placeholder when an author picks + that lease. The editor draws `core.lease` as the chosen lease's own form — its type's control, its + range, its `values`, its `target.source` as the target's picker and its `maxDurationMs` beside the + minutes — so a lease step no longer shows core's one example for every lease. +- **An option row's `note`** — a sentence, at most 300 characters, the editor shows under the field + while that option is the chosen one. It is advice about the value, never a rule: the save path + does not read it. + +**Module-uo is unaffected.** It declares `^1.10.0` and uses none of the four. Its actions, leases and +sources come out of the registry exactly as they went in; its steps render with the new controls and +nothing else. The proof website#209 ran for 1.11.0 was run again against this core (website PR body). + **1.12.0 — an event action's optional `progress(envelope)`** (`website/EVENTS.md` §I; RunicNPC `runicnpc/PLAN.md` stage 8, D304, D305). One addition and no removal, so minor. @@ -391,6 +419,10 @@ branches lacks — so it is the only place `permits` is true between two values between `admin` and `owner`, `members` or `subscribers`. `permits`, `meet` and `meetAll` are otherwise unchanged, and so is every rule about composition narrowing rather than widening. +**1.13.0 — the event step editor** (`website/EVENTS.md` §F, §I; Rust PLAN_REDESIGNS §2). +`registerEventTemplates`, a param's `duration`, a lease's `example` and an option's `note`; four +optional additions, so minor; see §1.1. Module-uo's `^1.10.0` still resolves. + **1.12.0 — an action's `progress()`** (`website/EVENTS.md` §I, RunicNPC D304). One optional member on an action, so minor; see §1.1. Core asks it while a run is live and publishes the answer, one of two shapes, on the public event. Module-uo's `^1.10.0` still resolves. @@ -806,10 +838,11 @@ api.registerSlashCommands([{ name, description, options, access, handler }]) // api.registerEventTriggers([{ id, label, kind, subjectKey, audience, ceiling, version, variables }]) // 1.7.0 api.registerAudiences([{ id, label, params, ceiling, resolve }]) // 1.7.0 api.registerEngagementSeeds({ templates, ruleGroups }) // 1.9.0 -api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile, progress }]) // 1.10.0; progress 1.12.0 +api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile, progress }]) // 1.10.0; progress 1.12.0; a param's duration 1.13.0 api.registerEventBudgets([{ id, label, unit }]) // 1.10.0 -api.registerEventLeases([{ id, label, type, min, max, values, target, maxDurationMs, read, apply, restore, inForce }]) // 1.10.0 -api.registerEventOptionSources([{ id, label, searchable, resolve }]) // 1.10.0 +api.registerEventLeases([{ id, label, type, min, max, values, example, target, maxDurationMs, read, apply, restore, inForce }]) // 1.10.0; example 1.13.0 +api.registerEventOptionSources([{ id, label, searchable, resolve }]) // 1.10.0; a row's note 1.13.0 +api.registerEventTemplates([{ id, label, description, inputs, build }]) // 1.13.0 api.onBoot(async (ctx) => {}) api.onShutdown(async () => {}) ``` @@ -1216,6 +1249,13 @@ rather than implementation and belong here: would hide what the line describes answers `null` for that reader; a public page must never say what the module's own pages would hide. Core gives it 2 s and drops a late answer. It is a read: it must change nothing, and it may be called every few seconds for as long as a run is live. +- **A template is a shortcut past the editor, never a second way to make an event** (1.13.0). What + `build()` returns is validated as a spec and opened as an unsaved draft; the author saves it + through the same route, with the same checks, as one they typed. So a template cannot make a + definition the save path would refuse, and changing or removing one touches no definition it + ever made. `build()` may read the module's own data — the Rust kit weekend looks up a kit's + permission — and answers `{ ok: false, error }` with the author's sentence when the inputs make + no sense; anything it throws stays in the log. - **A module reports who took part on the envelope, and there is no other door.** `participants` rides back from `perform()` exactly as `resources` does, on both success shapes — including `await: 'human'`, because a cue's confirm finishes the step without a second dispatch and that is -- 2.49.1 From 756c13d47b449e83e7e8783f1f65750910141ed3 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 12 Oct 2026 02:48:12 -0500 Subject: [PATCH 2/2] =?UTF-8?q?docs(rust):=20protocol=2014=20=E2=80=94=20k?= =?UTF-8?q?its.uses=20and=20reward=20notices=20(PROTOCOL=20=C2=A720,=20PLA?= =?UTF-8?q?N=5FREDESIGNS=20=C2=A72.9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wire half of PLAN_REDESIGNS §2 (D332, D333): PROTOCOL.md §2 says 14, and §20 specifies kits.uses (counts only, bounded by the sync's bound, POST /kits/uses) and perm.sync notices (the whole untold set, told once when the player is awake, told keys kept on disk, noticesTold in the report). PLAN_REDESIGNS §2.9 records the Rust half as built and its compile on both rigs. Stacked on docs/event-step-editor (docs#335) for §2.8. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- modules/rust/PLAN_REDESIGNS.md | 26 ++++++++++++++ rust-link/PROTOCOL.md | 65 +++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index ce412c4..86b3023 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -409,6 +409,32 @@ Everything in §2.1–§2.3 and §2.5's core half, with three things settled whi registers its 12 actions), the server suite 723/723 and the client suite 45/45 — the same numbers as 1.11.0's. The Module-Rust half (the kit weekend, `linked`, `kits.uses`, the reward message) follows on `edge`. +### 2.9 As built: the Rust half, protocol 14 (2026-10-12) + +Branches `feat/protocol-14-step-editor` in Rust-Plugins, Rust-Link and Module-Rust, into `edge`; the wire is +`rust-link/PROTOCOL.md` §20. + +- **The kit weekend** is `rust.template.kit_weekend`: inputs kit, minutes (a duration), message and delivery. + It reads the kit from its server when it is filled in, so the lease names the kit's real permission. A kit + with no permission is open to everyone already and gets step 2 alone; a kit with no use limit has no + "one more use" and gets step 1 alone. The phase is the weekend: `advance: { after }` equals the minutes. + Run through the real 1.13.0 core (website#212), it registers, and the plan it builds validates as a spec. +- **`linked`** is "every linked account that has played on this server" — a stats row there — because a link + is fleet-wide and the server is what narrows it. It reads no tally, and past the server's recipient bound it + is refused with the bound's name (`EventsMaxRecipients`), never trimmed. +- **The reward message** is checked with the kit's name in place of `{kit}`, against the chat bound, so a + long kit name cannot push past what the game will say. It is stored on each grant row + (`rust_perm_run_grants.notice`, `notice_delivery`, `notice_told_at`). +- **The kit warning** is the `note` on `rust.options.grouppermissions` rows. That source is searchable, so the + editor may not hold the chosen row; core asks for that one value when it needs its note (website#212). +- **Timed params:** `rust.zone.open` and `rust.participation.open` declare `minutes` a duration, and the + group-permission lease gives `true` as its value example. +- **Compiled on both rigs, 2026-10-12.** Carbon 2.0.262 on Rust 25823813 (the latest of both): the bridge + compiled and loaded in 3.8 s, protocol 14, its told-notices store read at load. Oxide 2.0.7820 on the same + Rust build (the rig was behind on both, and was updated first: its first restart's steamcmd update did not + finish, the second did): compiled in 5.3 s and loaded. Both rigs were put back on their protocol-13 bridge afterwards, to match their sidecars until + the end-of-plan walk sets up protocol 14 (D331). + --- ## 3. Zones: ZoneManager's options, and a dome (§4.4; D166, D167) diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index 81fe256..b20553f 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -50,7 +50,7 @@ it is listening without one. ## 2. Versioning -The wire version is a single integer — **13** as of the first player walk's fixes (§19) — declared in +The wire version is a single integer — **14** as of the step editor (§20) — declared in **four** places that must agree: | Where | Repo | @@ -2638,3 +2638,66 @@ unaffected. The refusal is checked before anything is spawned, with the two reas The site checks the same thing first, from the hello's `integrations.runicNpc`, and refuses the step before it is sent; the bridge's check is for a site that missed the hello. `rg.npc` says the same when RunicNPC is not loaded. + +--- + +## 20. Protocol 14 — the step editor + +Opened by [`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §2 after protocol 13 shipped with RunicNPC +v1.0.0 (D332). It is built on `edge` in Rust-Plugins, Rust-Link and Module-Rust and released once, after the +end-of-plan walk (D331); the first-class optional plugins (§11 there) join it. **This section grows as the +pieces land.** `PROTOCOL_VERSION` becomes 14 in all four places (§2). + +### 20.1 `kits.uses` — how many have used a kit up (§2.6) + +The warning beside a kit weekend: *"Kit vip: 2 uses, 24 h cooldown — 9 of 41 linked players have used it up."* +The website asks about the players it already knows — the linked players of the server — and the answer is +**counts, never a list**. + +```json +{"cmd":"kits.uses","reqId":"r-12","kit":"vip","steamIds":["76561190000000001","76561190000000002"]} +{"kind":"kits.uses","type":"reply","reqId":"r-12","kit":"vip","max":2,"cooldown":86400,"asked":2,"exhausted":1,"onCooldown":0} +``` + +- **Main thread**, two Kits reads per player (`GetPlayerKitUses`, `GetPlayerKitCooldown`), plus `GetKitMaxUses` and + `GetKitCooldown` once. `exhausted` counts players whose uses have reached `max`; a kit with no use limit + (`max` 0) is never used up. `onCooldown` counts players with any cooldown remaining. `asked` is how many ids + parsed. +- **Bounded by the sync's own bound** (`PermMaxOps`, 20,000): the site sends no more players than a sync + carries. +- Refusals are `kits.error`, as for `kits.list`: `kits-missing` (Kits is not loaded), `unknown-kit`, + `too-large`. +- **The sidecar route is `POST /kits/uses`**, a thin forward like `/kits`; the list rides in the body because it + can be as long as a server's linked players. Nothing is kept. +- **The website** asks once per kit that a permission gates, per server, at most 25 kits, and keeps the answer + a minute. It attaches the sentence as the `note` of every `rust.options.grouppermissions` row whose + permission is that kit's (core's option note, MODULE_API 1.13.0). A failure is no note. + +### 20.2 `notices` on `perm.sync` — the reward's message (§2.4, D333) + +"You earned kit vip — type /kit vip", said once to the player a reward step picked. `rust.kit.entitle` gains an +optional `message` and `delivery`; the site writes them on each grant row, and every sync carries the rows' +**whole untold set** for that server, beside `credits`: + +```json +{"cmd":"perm.sync", …, "notices":[{"key":"7:3:11","steamId":"76561190000000001","message":"You earned kit vip — type /kit vip","delivery":"popup"}]} +``` + +- **`key`** is `::`: one reward step tells one person once, whichever of their linked + accounts it picked. **`steamId`** is that account, while it is still linked to the person. +- **When it is said is the plugin's.** At the end of the sync, to a player who is connected and **awake** — + a sleeping or loading player sees no popup and reads no chat — and otherwise on `OnPlayerSleepEnded`, the + next time they wake on this server. A popup goes through PopupNotifications when `delivery` is `popup` and + the plugin is loaded, and chat otherwise; the reward was granted long before this moment, so the player is + told either way. At most 2,000 notices per sync; a message is cut at the chat bound (`EventsMaxChat`). +- **Told keys are kept on disk** (`data/RunicGateway/notices.json`) until the site stops sending them, so a + reload between telling a player and the report that says so cannot tell them twice. Each sync forgets every + told key the set no longer carries. +- **The report lists them back**: `perm.report` gains `noticesTold`, every key of this sync's set that has been + told — at this sync or before it. Present only when the sync carried `notices`, like the credit counts. The + site marks those rows told, and they leave the next sync's set. +- **A withdrawn reward takes its message with it.** Teardown deletes the grant row, so the notice leaves the set + untold, and nobody is told about a reward that is no longer theirs. +- **Absent is not empty**, as for `credits`: a sync with no `notices` field leaves the plugin's set alone. The + site sends the field on every sync from protocol 14, empty when there is nothing to say. The notices are part + of the sync's digest, so a new one is pushed at once. -- 2.49.1