From 84bb85d0373786b00a941f299bbf4b3fb9b12ae7 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 22:26:52 +0100 Subject: [PATCH] Design 39 bus-and-switch: the licence manager keeps its data on the bus and switches on its own usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The operator, 2026-11-11: the licence manager itself moves every consumer off a licence at 99 % of its session or weekly window to the licence with the most room, configurable, with its Postgres database replaced by bus state and tools for all it keeps and does. That night a session meant to switch missed the readings, so no session does it any more. The new milestone part writes every line it builds (kept data, the one carry-over of today's rows, the vault move of ADR 0550 §3, bindings and holdings as events, the switch, its warning and condition, the tools, adding a licence), and names the three questions no record settles; the main file is put in the shape a builder reads whole. Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ --- 00-META/glossary.md | 9 + .../01-to-be/39-milestones/bus-and-switch.md | 428 ++++++++++++++++++ .../39-the-anthropic-licence-manager.md | 114 +++-- 3 files changed, 511 insertions(+), 40 deletions(-) create mode 100644 03-DESIGN/01-to-be/39-milestones/bus-and-switch.md diff --git a/00-META/glossary.md b/00-META/glossary.md index 926d5d17..de6b22b8 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -535,6 +535,11 @@ and the operator act. **Recorded by** the controller's `identity` and `licences` coding agent on a node (ADR 0183). Taking a consumer off a licence is **unbinding** it; the licence manager's verb for it is still `release`, an identifier to rename. +- **room** of a licence — how much of its vendor's usage windows is still unused: 100 less the higher of its + session window's (five hours) and weekly window's percentage in its latest reading, known only for a subscription + read recently and healthy. The licence manager moves every consumer off a licence at its threshold to the one with + the most room (design 39 bus-and-switch §25, §26). + *Not:* ~~headroom~~ - **proof** — what an authorising answer carries: a verified sender, a one-time code, a security key's touch (ADR 0234). How much proof a kind of ask needs is its **assurance level**. - **controller's terminal** — a command line the controller runs as typed by its operator, not asked through a @@ -991,6 +996,10 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283, 0564, 0668. with its largest size in seven days by disk-load on that node, in its own state, one key `..` written only when it changes; the holder of `node-backup`, mounts, nfs-server and the self-check read them there, never the controller (ADR 0571 §5). +- **carry-over** — a module's one copy of the data it kept in its old form into its bus state, at its first start + on the new form and before it does anything else, recorded in a key of its own so it is never copied again + (ADR 0437 §7; design 39 bus-and-switch §11 to §13). Not a take-over, which moves a job between parts. + *Not:* ~~data take-over~~ - **sticky binding** — a consumer's tie to the data a provider keeps for it, which moves only by a person (ADR 0232). - **bus state** — what a module keeps on the bus, asked for in `requires` as a provision of the bus module, by a diff --git a/03-DESIGN/01-to-be/39-milestones/bus-and-switch.md b/03-DESIGN/01-to-be/39-milestones/bus-and-switch.md new file mode 100644 index 00000000..f5a732b8 --- /dev/null +++ b/03-DESIGN/01-to-be/39-milestones/bus-and-switch.md @@ -0,0 +1,428 @@ +--- +layer: to-be +design: 39 +milestone: bus-and-switch +updated: 2026-11-11 +code: [mesh-catalog modules/claude-licence-manager, mesh-catalog modules/claude-code, mesh-catalog modules/mesh-vault] +decisions: + - "ADR 0183" + - "ADR 0206" + - "ADR 0209" + - "ADR 0054" + - "ADR 0550" + - "ADR 0595" + - "ADR 0596" + - "ADR 0437" + - "ADR 0564" + - "ADR 0570" + - "ADR 0230" + - "ADR 0380" + - "ADR 0253" + - "ADR 0289" + - "ADR 0349" +--- + +# Design 39, milestone bus-and-switch — the licence manager on the bus, switching on its own usage + +**What it builds.** The seat `anthropic-licence-manager` keeps everything it holds in the bus's own storage, by the +kept-data templates, and its store database goes; every grant and token moves into the vault, as ADR 0550 §3 has it; +and the manager itself moves every consumer off a licence whose session or weekly usage reaches a threshold, to the +licence with the most room, says each switch as an event and to the operator, and raises a condition when no licence +has room. Its tools read all it keeps and start by hand all it does. The whole shape is in the design's main file +([39](../39-the-anthropic-licence-manager.md)); this part holds every line the milestone builds, cited as "design 39 +bus-and-switch §n". + +**Why now.** The operator, 2026-11-11 22:10 to 22:20 CET: "build it in the licence manager, but make it configurable +of course, also remove the postgres database in favor of a bus store, implement useful tools, as much relevant state +as possible"; earlier that evening: "when the current licence usage is at 99%, switch to a licence that still has +room" (first said at 98, then 99), "on all nodes". That night every node was bound to one licence that reached 99 % of +its weekly window, and a session meant to switch them missed the readings: so the manager switches on its own +readings, and never a session. Issue 1409 (no warning before a licence's limit) is fixed in the same change (§30). + +**Where.** One repository, mesh-catalog, and three of its modules: `claude-licence-manager` (the seat's holder), +`claude-code` (the agent module on every node, the consumer) and `mesh-vault` (the seat `mesh-vault`), with hq for this +part, the main file and the glossary. The order between them is the module graph's, each an edge the delivery plan +derives (§50). + +**Open design questions.** Three questions no accepted record settles; the lines that wait on one say so, and develop +does not start on them before the operator's answer is written here (playbook 02, *Answering a design question*): + +- **Q1, the vault's own-account rule.** ADR 0550 §3 moves the grants and tokens into the vault and waits until the + licence manager and each agent module run under a bus account of their own (ADR 0349 §11, issue 569); both run + today in their machine's tool runner, under its account. Lines §15 to §20 wait on it. +- **Q2, the record behind the switch.** ADR 0183 §21 (moving a consumer is a person's act), ADR 0206 §16 (no + `licence.switched` event) and ADR 0206 §18 (every later change is a person's act) are in force; the operator's + answers of 2026-11-11 change them. A record changing those three clauses comes before develop (playbook 02, + *Changing a decision*). Lines §24 to §33 are written from the operator's answers and wait on it. +- **Q3, how the first start reads today's rows.** The carry-over (§11 to §14) reads the store database with the + module's own credential, and a build whose manifest no longer requires `postgres-database` is composed with no + credential for it, while a retiring provision keeps its account only until the operator's Approve (ADR 0570 §5). + Line §11's source waits on it. + +## The kept data on the bus + +1. **The manifest asks for its kept data by template and requires no database.** `requires` loses + `postgres-database` and the `secret` requirement `grant-key` (its key ends with the grants' move, §20); `contributes`, + `binds`, `secrets`, the resources `database-url` and `prepare` and the run-once step `prepare` go with them. It + gains one `bus-state` entry per piece of kept data, each in the typed long form (ADR 0595 §1): the local name, the + storage type, the template and its options, as §2 to §9 list them. The old section `state` (today `bindings`) and + the `reads` entry `claude-code.holdings` go: the module moves to the new form whole (ADR 0595 §11), keeping the local + name `bindings` so its data stays. Why: the operator, 2026-11-11, and principle 1; the templates are ADR 0595 §2. + How it is checked: the manifest validator accepts the manifest at the merge check; a catalog test finds no + `postgres-database`, `reads` or old `state` in it. +2. **State `licences`: one key per licence, no secret in it.** Template `state`, option `history` 1. The key is the + licence's key form (§10). The value: `name`, `kind` (`subscription` or `api-key`), `account` (the account id), + `address`, `organisation`, `refreshFingerprint`, `accessExpiresAt`, `refreshExpiresAt`, `failures` (refreshes + failed in a row), `notifiedAt`, `adoptedAt`, `adoptedFrom` (the node), `rotatedAt`, `generation` (the vault + secret's, §15), and `usage`, the latest reading of §24 (`sessionPct`, `sessionResetsAt`, `weeklyPct`, + `weeklyResetsAt`, `sonnetPct`, `sonnetResetsAt`, `readAt`, `readError`). Written by the manager alone, at each + adoption, rotation, failure and reading; deleted by no act of the milestone (a licence is never removed today, and + none is added here). It replaces the table `licence`, less its column `sealed`, which goes to the vault (§12). + Why: ADR 0550 §3 keeps the licence records without a secret in the manager; principle 4 keeps the latest reading + beside its licence so `licences` answers from one key. How it is checked: unit, a rotation and a reading each write + the key and no field holds a token; the after-move scan of §20. +3. **State `bindings`: one key per consumer.** Template `state`, option `history` 8, so the last moves of a consumer + are read with the state's `history` call. The key is the consumer's: the node's name for a node's agent, `.` + for a worker (ADR 0183 §19). The value: `licence` (its name), `kind`, `generation` (one more at every rotation and + every switch of that consumer, ADR 0206 §13), `since`, `why` and `by` (who or what moved it last: a person's + name, `login` for ADR 0209's move, `usage` for §26). Written by the manager alone; deleted by `release`. It replaces + the table `binding` and today's old-section state of the same name. Why: ADR 0206 §13; the eight past values are + the design's choice, for `licences_switches` to show a consumer's last moves. How it is checked: unit, `bind`, + `switch`, `release` and §26 each write or delete the key with its `why` and `by`. +4. **State `leases`: one key per lease, ended by its holder or its lifetime.** Template `state`, option `history` 1, + with a lifetime per key of 2 minutes (the time one exchange with the vendor is given today). The key is + `licence.` or `account.`; the value names the holder (the process) and `until`. A lease + is taken by creating the key, which fails while another holds it, before the licence is read (ADR 0183 §6); the + holder deletes it at the end; a holder that dies leaves it to its lifetime. It replaces the table `lease`. Why: + ADR 0183 §6 keeps one rotation source; a create that fails is the bus's own compare-and-set. How it is checked: + unit, two refreshes started together rotate once and the second answers "another run holds its lease" + (the main file's first acceptance test). +5. **State `offered`: one key per grant a node offered.** Template `state`, option `history` 1, a lifetime per key + of 90 days. The key is the refresh token's fingerprint; the value: `node`, `account`, `outcome` (`adopted`, + `dead`, `refused`), `why`, `at`. Written at each candidate's outcome (ADR 0206 §9, §10); deleted by its lifetime, + after which a fingerprint offered again is tried once more and recorded again. It replaces the table `offered`. + Why: the manager never exchanges a dead candidate twice within its lifetime; 90 days is longer than a refresh + token lives. How it is checked: unit, a dead candidate is not exchanged at its second report. +6. **Log `usage`: every reading, kept 35 days.** Template `log`, options `max-age-days` 35 and `max-mib` 256, one + subject per licence key. Each entry: `at`, the flattened reading of §24 and `raw`, the vendor's answer whole + (ADR 0054). Written at every reading, never rewritten. It replaces the table `usage`. Why: ADR 0054 keeps the raw + reading beside the normalised one; 35 days hold five weekly windows, and at five minutes for four licences + the log stays under a quarter of its cap. How it is checked: unit, a reading appends one entry under its licence's + subject; the bus module's fill condition (ADR 0595 §6) warns before the log is full. +7. **Log `audit`: every act, kept for good.** Template `log`, option `max-mib` 64, no age, one subject per kind: + `adopted`, `refused`, `failed`, `rotated`, `bound`, `switched`, `released`, `drift`, `carried`, `settings`. Each + entry: `at`, `by`, `why` and the act's own fields (for `switched`: §27's). Written after each act, never rewritten. + It replaces the table `audit`. Why: ADR 0183 §5 keeps the audit of every switch; a log keeps it in order and for + good. How it is checked: unit, each act appends one entry under its kind. +8. **State `carried`: the carry-over's record.** Template `state`, option `history` 1, one key `postgres`: `at`, and + for each table of today's database the rows read and the entries written. Written once, at the end of §11; never + deleted, since its presence is what keeps a second start from carrying over again. Why: ADR 0437 §7's way of + carrying a module's state over once. How it is checked: §13. +9. **State `findings` and the events `finding-events`.** The manager is an observer (design 47 G §383): it asks + the state `findings` by the template `state` and the stream `finding-events` by the template `events`, with + `max-age-days` 7 and `max-mib` 16, its field `events` listing `finding-written`, `finding-cleared` and + `findings-stated`, as design 47 G §529 has every part that is not of the foundation declare them; `consumes` + gains `mesh-conditions.findings-asked`. It writes its findings (§29 to §31, §33) with mesh-sdk's finding writer + (design 47 G §461), which states them at its start and on the keeper's ask. Why: a condition reaches the operator + only from a finding the condition keeper hears (ADR 0564 §2 as design 47 G §383 changes it). How it is checked: + whole (manager), a finding written raises its condition at the keeper and its clearing clears it. +10. **A licence's key form.** A licence's name is its account's address, or its id where it has none, or the name + given at `adopt` for an API key (today's `NameFor`); a name holds characters a key may not. Its key is the name + with each `@` written as `_at_` and every other character outside letters, digits, `-` and `.` written as `_`. An + adoption whose key another licence already has is refused, said in the audit and as `licence.refused`. Every + verb and tool still takes and answers the name; the key is the store's alone. Why: a key of the bus takes only + those characters; the form is the design's choice, readable in `licences_kept`. How it is checked: unit, a table + of names and their keys, and the refused clash. + +## Carrying over today's rows + +11. **At its first start the manager carries over every row before it does anything else.** When the state + `carried` has no key `postgres`, the manager, before it hears an event, reads a holding report, refreshes or reads + usage, reads every row of the six tables of its database `claude_licences` and writes them as §12 says, then the + key `carried.postgres`. Until that key exists it serves its read verbs and tools only, answering every other + with "the carry-over has not ended", and writes the finding `carry-over-failed` (urgent; "Needs you: the licence + manager could not carry over its licences; no consumer is moved and no token is refreshed until it does", naming + the table and the error) when a read or a write fails, cleared when the key is written. Why: a manager that ran on + an empty state would take every holding report for a candidate (ADR 0206 §5) and exchange refresh tokens it + already holds; failure is loud (principle 3). Its source, the database read with the module's own credential, + waits on Q3. How it is checked: whole (manager, a store database filled from a copy of today's rows), every row + is carried and the key written; with the database unreachable the finding is raised and no vendor call is made. +12. **What each table becomes.** + - `licence` → a key of `licences` (§2) per row; each row's `sealed` grant is opened with today's `grant.key` and + handed to the vault as §15 says, its generation 1; the row's other columns become the key's fields. + - `binding` → a key of `bindings` (§3) per row, its `generation` kept, `why` "carried over", `by` the manager. + - `lease` → nothing: a lease lives two minutes, and none taken before the start still holds. + - `offered` → a key of `offered` (§5) per row, its lifetime counted from the row's `at`. + - `usage` → one entry of the log `usage` (§6) per row of the last 35 days, oldest first, its `at` kept. + - `audit` → one entry of the log `audit` (§7) per row, oldest first, its `at` kept, under the subject its `what` + names (`drift` for any other). + Why: no licence, binding, grant or history is lost (the operator, 2026-11-11); usage older than the log's age + would be dropped by the log at once. How it is checked: whole, the counts in `carried.postgres` equal the rows read + for every table but `lease`, and `mesh-vault.get` of each licence's grant (§16) answers it. +13. **A second start carries over nothing.** With `carried.postgres` present the manager never reads the database, + and a carry-over that failed halfway is started again from the beginning, every write of §12 being idempotent + (a key put again with the same value, a log entry not appended twice: before appending, the manager reads its + log's last entry for that subject and skips rows at or before its `at`). Why: ADR 0437 §7. How it is checked: + whole, a second start writes nothing; a start stopped after `bindings` and started again leaves every count equal + to the rows read. +14. **The database's fate, and that this delivery moves data.** Once `carried.postgres` exists and §20's scan finds + no grant left outside the vault, the database `claude_licences` is retired: the provision is retiring once no + build a want names requires it, and its account is removed only on the operator's Approve (ADR 0570 §5); the + database is renamed aside by the postgres module's `postgres_retire_database` and deleted only by a person + (ADR 0230 §14). The delivery of this milestone moves data, so it is said to the operator before it starts + (which module, its machine, that its licences, bindings and grants move to the bus and the vault), and the + manager's machine is backed up before the module moves, as for every module that holds data. Why: ADR 0570 §5, + ADR 0230; the operator's instruction that a delivery moving data is said first. How it is checked: the delivery's + plan names the backup; after the delivery `mesh-store.databases` lists `claude_licences` until the operator's + Approve, then its renamed-aside form. + +## Grants and tokens in the vault + +*Lines §15 to §20 build ADR 0550 §3 and wait on Q1.* + +15. **The vault holds two secrets per licence, handed to it by the manager alone.** `licence..grant` + holds, for a subscription, the refresh token, the access token and both expiries, and for an API key the key; + `licence..access` holds a subscription's access token and its expiry, or the API key. The vault + serves a new verb on its seat, `licence-put`: `licence`, `part` (`grant` or `access`), `generation` and the value + sealed to the vault's key; callable by the holder of the seat `anthropic-licence-manager` only, refused for any + other caller naming the rule; it keeps the newest generation and refuses an older one; and it says its event + `secret-changed` with the secret's name and generation, never the value. The manager calls it after every + adoption, rotation and carry-over (§12), the `grant` first. Why: ADR 0550 §3 (the manager hands what it gets back + to the vault, keeping nothing at rest); the names, the verb and its fields are the design's choice. How it is + checked: unit (vault), `licence-put` from another caller is refused and an older generation is refused; unit + (manager), a rotation calls it twice and keeps no token after the call returns. +16. **The manager reads a grant from the vault when it needs one, and keeps it only for that act.** To refresh, + adopt again or read usage, the manager calls `mesh-vault.get` for `licence..grant`, sealed to its own + public key, under the licence's lease (§4), and drops the value when the act ends. Why: ADR 0550 §3, ADR 0183 §6. + How it is checked: unit, the manager's state and log hold no token after a rotation. +17. **A consumer's agent module gets its token from the vault, which serves only the licence a binding names.** The + vault answers `mesh-vault.get` for `licence..access` only to an asker whose machine is the node the + asking consumer's binding names, and only for the licence that binding names. It learns the bindings from the + manager's `binding.changed` (§21), which it consumes, and at its start from `anthropic-licence-manager.bindings`, + its first view, keeping them in its own state `licence-bindings` (template `state`, one key per consumer, the + licence and generation, deleted on a release). The agent module never receives a refresh token (ADR 0206 §11). + Why: ADR 0550 §3; ADR 0596 §2 (the vault asks the manager, never reads its state); the state is the design's + choice. How it is checked: unit (vault), a token is refused to a machine no binding names and for a licence its + binding does not name (the main file's acceptance test), and given after a `binding.changed` naming it. +18. **`current` answers which licence and its health, never the token.** The seat verb `current` takes `consumer` + and answers `licence`, `kind`, `generation`, `health` (`ok`, `failing`, `expiring`) and `accessExpiresAt`; its + argument `public_key` goes. Why: ADR 0550 §3. How it is checked: unit, no answer of `current` holds a token. +19. **The agent module applies a new token when the vault says it changed or its binding moved.** The agent module + on a node asks `mesh-vault.get` for its licence's `access` sealed to its own key, when it hears a + `binding.changed` for its consumer with a newer generation than it applied, when it hears the vault's + `secret-changed` for the licence it holds, and at its start; it then writes the credentials file or the key-helper + as design 36 §5 has it, unchanged. Why: ADR 0550 §3, ADR 0206 §14 as ADR 0596 §2 changes it (an event and a first + view in place of a watched key). How it is checked: whole, `switch` changes a workstation's token end to end (the + main file's acceptance test). +20. **What ends with the move.** The manager's `grant.key`, its `secret` requirement and the code that seals a grant + at rest go; the `public-key` verb and the manager's keypair stay, since a login offered by a node is sealed to it + (ADR 0183 §23, ADR 0550 §3). After the delivery the vault's `secret_holders` lists both secrets of every licence + and the manager's own secret `grant-key` is retired with `mesh-vault.retire` on the operator's word. Why: + ADR 0550 §3 (a scan after the move that the manager holds no grant). How it is checked: `licences_kept` (§41) + shows no field holding a token in any entry, and `secret_holders` the two secrets per licence. + +## Bindings and holdings said as events + +21. **Every change of a binding is said as `binding.changed`.** The manager declares in `emits` the event + `binding.changed`, said after the key of `bindings` is written or deleted: `consumer`, `licence` (empty on a + release), `kind`, `generation`, `from` (the licence before), `why`, `by` and `at`; never a token. Why: ADR 0596 §2, + §3 (the owner gains the event its readers need before they move); the name and fields are the design's choice. + How it is checked: unit, `bind`, `switch`, `release`, a login's move and §26 each say one event per consumer + moved. +22. **The agent module follows its binding by that event, and asks once at its start.** `claude-code` gains + `consumes` `claude-licence-manager.binding.changed`, acting only on the event naming its node's consumer, and at + its start asks `anthropic-licence-manager.bindings` with `consumer` its node for its first view; its `reads` + entry `claude-licence-manager.bindings` goes. A node that was away asks once when it is back (ADR 0206 §15), by its + start or its reconnect. Why: ADR 0596 §2, §4. How it is checked: unit (claude-code), an event for another + consumer is ignored; whole, a node started after a switch applies the new licence from its first view. +23. **A node's holdings are said as events, and asked for by the manager.** `claude-code` declares in `emits` + `holdings-changed`, said at its start and whenever its holdings change (ADR 0206 §2), carrying its holdings + report whole as ADR 0206 §1 lists it, never a token, and gains `consumes` + `claude-licence-manager.holdings.asked`, on which it says `holdings-changed` again. The manager declares + `holdings.asked` (`at`, `by`) and says it after its carry-over (§11) and on `licences_resync` (§42), and consumes + `claude-code.holdings-changed` in place of today's `reads`. Its state `holdings` stays `claude-code`'s own. Why: + ADR 0596 §2, §3; a first view by events, since the holdings are one key per node; the names are the design's + choice. How it is checked: whole (manager and a fixture agent module), a `holdings.asked` is answered by one + `holdings-changed` per node, and a report carrying an unknown refresh token's fingerprint makes a candidate + (ADR 0206 §4). + +## Switching on usage + +*Lines §24 to §33 are the operator's answers of 2026-11-11 and wait on Q2's record.* + +24. **The manager reads each subscription's usage on a cadence, and keeps the reset times.** Every + `usage_minutes` (§33), for each licence of kind `subscription`, the manager reads the vendor's usage with the + licence's access token (§16) and flattens it: `sessionPct` and `sessionResetsAt` (the five-hour window), + `weeklyPct` and `weeklyResetsAt` (the seven-day window), `sonnetPct` and `sonnetResetsAt`. It writes the entry of + §6, the key's `usage` of §2, and says `usage.read` with those fields, never a token. A failed reading writes + `readError` (the vendor's status and reason) and keeps the last percentages. This is the manager's one timer: the + vendor says nothing as usage grows. Why: ADR 0054; the operator's "reset times"; the reset times of the weekly and + sonnet windows are read beside the session's, which alone is read today. How it is checked: unit, a vendor + answer is flattened with its three reset times; a failed answer keeps the percentages and writes the reason. +25. **A licence's room, and when it is known.** A licence's room is 100 less the higher of its `sessionPct` and + `weeklyPct` in its latest reading. It is known only for a licence of kind `subscription`, read successfully within + `usage_stale_minutes`, whose refresh has not failed `failures_to_notify` times in a row and whose refresh token is + not expired. A licence whose room is not known is never a target of §26: an API key has no window to read and is + billed per use, so it is never one. Why: the operator's "a licence that still has room"; a licence's room is known + only from a reading; the sonnet window is shown and does not count, since the operator named the session and + weekly windows. How it is checked: unit, a table of readings, ages and healths and the room each gives. +26. **On each reading, a licence at its threshold moves every consumer bound to it.** After writing a reading of + licence L, when `switch_auto` is on, L has one or more consumers bound to it, and L's `sessionPct` or `weeklyPct` + is at or above `switch_at_pct`, the manager chooses the target: among the other licences whose room is known and + whose `sessionPct` and `weeklyPct` are both below `room_below_pct`, the one with the most room, the first by name + on a tie. It then moves every consumer bound to L to the target, each as the `switch` verb does (its binding's + generation one more, `why` naming L, the window, its percentage and its reset, `by` `usage`), says + `binding.changed` per consumer (§21) and one `licence.switched` (§27), and tells the operator (§28). A consumer + that inherits its node's licence (ADR 0183 §20) follows its node. A licence below its threshold, or with no + consumer, moves nothing; a consumer is never moved back by the rule, only off a licence at its threshold, so no + consumer flaps between two licences. Why: the operator, 2026-11-11 ("switch to a licence that still has room", + "on all nodes"; the manager itself, never a session); the order of choice and the tie are the design's choice. + How it is checked: unit, a table of licences, readings and bindings and the moves each gives; whole (manager, a + fixture vendor), a reading of 99 % moves four bound consumers to the licence with the most room in one act. +27. **Every switch is said as `licence.switched`.** The manager declares `licence.switched` in `emits` again and + says it once per act that moves consumers, by `switch` or by §26: `from`, `to`, `consumers` (the list), `by` (a + person's name or `usage`), `why`, and for §26 `window` (`session` or `weekly`), `pct`, `resetsAt` and the target's + `room`. The same fields are the log entry `switched` (§7). Why: the operator, 2026-11-11 ("each switch said as an + event"); this changes ADR 0206 §16 (Q2). How it is checked: unit, one event per act with the consumers moved. +28. **Every switch the manager makes is told to the operator.** After an act of §26 the manager calls + `operator-channel.tell` (ADR 0380): `subject` the seat `anthropic-licence-manager`, `headline` " moved + to ", and one line " reached % of its window, which resets at