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..6802d066 --- /dev/null +++ b/03-DESIGN/01-to-be/39-milestones/bus-and-switch.md @@ -0,0 +1,449 @@ +--- +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). + +**The lead's answers, by the principles.** Four questions no accepted record settled were answered on 2026-11-11 by +the lead of issue 1409, against hq's principles, as the operator's way of working since 21:15 that evening has it (the +lead answers develop and design questions against the principles; no decision record is written for a detail). Each +line they reach names its answer: + +- **A1, the vault's own-account rule** (§15 to §17). ADR 0550 §3 waits until the licence manager and each agent + module run under a bus account of their own (ADR 0349 §11, issue 569); both run in their machine's tool runner, + under its account. Settled by principle 6 (inside the mesh every agent and module is trusted; who may do what is not + a question) and the operator's instruction that who may do what is not the operator's question (approved + 2026-11-11 21:15 CET): the vault serves licence secrets to the tool runner's account. Its check that the asking + machine is the node the binding names stays, as a guard against mistakes, not as a permission. +- **A2, the switch without a record** (§24 to §33). 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 + decided, 2026-11-11 about 21:10 to 22:20 CET: switch automatically at 99 %, on all nodes, configurable; and said that + night not to write records for what they decided. So the operator's answer of 2026-11-11 replaces those three + clauses for this module, and no record is written. +- **A3, how the first start reads today's rows** (§1, §11, §14). A build whose manifest no longer requires + `postgres-database` is composed with no credential for it, and `mesh-store.query` refuses the grant column. Settled + by principle 7 (the simplest thing that works): this milestone keeps `requires postgres-database`, and the `secret` + `grant-key` that opens the grants at rest, as the carry-over's read-only source; dropping both, and the database's + retirement on the operator's Approve, is the module's next change once `licences_carried` (§44) shows the + carry-over done. The delivery gap this names: no path hands a build the credential of a provision its manifest + dropped, so a module cannot read its retiring database at the first start of its new form. +- **A4, no wait for design 47's milestone G** (§9, §50). Settled by principle 7: this milestone ships soon, built only + on what main holds today (mesh-sdk, the vault, the operator channel, and today's way a module says a condition, + ADR 0437 §9), so it uses neither G's writer nor its stream. + +## The kept data on the bus + +1. **The manifest asks for its kept data by template, and keeps its database only as the carry-over's source.** + 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. `requires` keeps `postgres-database` and the `secret` + `grant-key`, with their `contributes`, `binds`, `secrets` and the resource `database-url`, read only by §11 and + never written (A3); the run-once step `prepare`, which makes the tables, goes, since nothing writes them again. + `uses` gains `operator-channel` (§9, §28). Why: the operator, 2026-11-11, and principle 1; the templates are + ADR 0595 §2; A3. How it is checked: the manifest validator accepts the manifest at the merge check; a catalog test + finds no `reads`, old `state` or `prepare` in it; a unit test finds no write to the database in the code. +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. **Its conditions are said on the operator channel, and kept in its own state `conditions`.** The manager says + each condition of §11, §29 to §31 and §33 as any module does on main today (ADR 0437 §9): published on + `mesh.seat.operator-channel.accept.condition.claude-licence-manager` (the subject the operator channel's contract + names, through its client in mesh-contracts), carrying the condition's `key`, `severity` (`urgent` or `warning`), + `scope` and `id`, `raised` or `cleared`, its `summary` and ADR 0253's words (`headline`, `needs`, `explanation` + opened by its verdict, `resolved` and `actions`); the router alone decides whether and where the operator is told. + It keeps each condition it said raised in its state `conditions` (template `state`, option `history` 1, one key per + condition key, its body and `since`), written before it is said and deleted when it is said cleared. At its start + it judges each kind anew from its own state and readings, says raised again each it still holds and cleared each + key of `conditions` it no longer holds. A raise or clear is said again only when what it says changes (ADR 0289 + §1). Why: A4 (today's way a module raises a condition, without G's writer); the state lets a restart clear what + it said. How it is checked: unit, a condition is written in `conditions` and published once with these fields; + a start with a key it no longer holds says it cleared; whole (manager and the router on a test bus), a raised + condition reaches the desk channel 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 says the condition `carry-over-failed` (§9; 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 is the database read with the module's own credential, which this milestone keeps for it alone (A3). 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 condition is raised and no vendor call is made. A table that does not + exist (a fresh install) is read as no rows. +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.** In this milestone the database `claude_licences` + stays, read once by §11 and never written. Once `licences_carried` (§44) shows the carry-over done and §20's scan + finds no grant left outside the vault, the module's next change drops `requires postgres-database` and + `grant-key`; the provision is then retiring, its account removed only on the operator's Approve (ADR 0570 §5), + and 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: A3, + 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` still lists `claude_licences`. + +## Grants and tokens in the vault + +*Lines §15 to §20 build ADR 0550 §3, on the tool runner's account (A1).* + +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; answered for a call from the machine that holds the seat `anthropic-licence-manager` + (as the seat's placement names it), and refused from any other machine naming the rule, a guard against a + mistaken caller rather than a permission (A1); 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 machine 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` to the tool runner's account of a machine, + and only when that machine is the node the asking consumer's binding names and the licence is the one that + binding names: a guard against a node applying another's licence by mistake, not a permission (A1). Its rule that a + value goes only to a module under a bus account of its own (ADR 0349 §11) does not apply to the licence + secrets. 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; A1; 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 answer of 2026-11-11, which replaces ADR 0183 §21 and ADR 0206 §16 and §18 for +this module (A2): the manager moves consumers by itself on its own readings, and says every switch as an event.* + +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 replaces ADR 0206 §16 for this module (A2). 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