diff --git a/02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md b/02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md new file mode 100644 index 0000000..7fbfb1d --- /dev/null +++ b/02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md @@ -0,0 +1,130 @@ +--- +topic: what runs on it +status: accepted +date: 2026-08-31 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0004-a-node-and-how-it-joins.md +--- + +# 26. The mesh has a session of its own, and it is the node session's mechanism + +## Context + +[ADR 0004](0004-a-node-and-how-it-joins.md) gives every node a session: one per node, permanent, +remembering across callers, its system prompt the node's engram, reachable over the broker like +everything else. **Any node can message any node**, and that is called the one part of the system +that is genuinely a mesh — symmetric, with no centre. + +**There is no way to address the mesh itself.** A question that spans machines — *what is running +across all of this*, *which nodes are behind*, *why is it built this way* — has to be put to some +node, which then asks the others. That works, and it makes a mesh-wide question **nobody's +question**: every node answers it as a foreigner, from a position where the whole is not in view. + +**Three things independently arrived at the same missing piece.** + +[ADR 0025](0025-the-design-record-is-read-not-copied.md), taken hours before this one, commits to +an agent that reads the design repository directly and answers into search. That agent has to +exist, run somewhere, and be askable — and nothing in the record says what it is or where it +lives. + +[`14-model-access.md`](../03-DESIGN/01-to-be/14-model-access.md) records, as a gap deliberately +not half-built: *this worker uses that licence is a binding to an agent, not to a node* — and the +provisions model has no consumer identity other than a node. A session that must be assigned a +licence is exactly that consumer, and node sessions are already one. + +**And ADR 0004 never said how a session is set up.** It describes behaviour and stops: nothing +states how a session starts, where its context lives, how the engram reaches it, or how a message +off the broker becomes a prompt. There is no design document for it. That gap was invisible until +something had to be built *like* a node session, because describing a second instance of a +mechanism requires the mechanism to have been described once. + +## Considered Options + +1. **No mesh session; keep relaying through a node.** Costs nothing and works today. **Rejected.** + It leaves mesh-wide questions belonging to nobody, and it does not survive contact with + ADR 0025 — that agent still needs a home, so the thing gets built anyway, unnamed, as an + attachment to whichever node happened to host it. + +2. **A new kind of agent, built separately.** Purpose-built for the whole mesh. **Rejected.** It + would hold a session, a memory, a licence and broker plumbing — every one of which the node + session already has. Two implementations of one mechanism drift, and the vocabulary collision + that [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) exists to undo began exactly this + way: two things that were nearly the same, built twice, until neither word meant one thing. + +3. **The same mechanism, started in a different context.** **Adopted.** + +## Decision + +**The mesh has one session, addressed as the mesh, and it is a node session in every respect but +three.** + +| | | +|---|---| +| **the context it starts in** | the mesh's, not a machine's — this is the whole of what makes it different | +| **its engram** | the mesh's system prompt, as a node's engram is that node's | +| **its licence binding** | assigned in its own right, not inherited from the machine it runs on | + +Everything else is unchanged and deliberately so: it is permanent, it remembers, it is reachable +over the broker, it holds its own tools, and switched off it still answers *I am switched off* +rather than falling silent. + +**It runs on the node that holds the control plane** — not for convenience, but because that node +is already the one place excepted from *compromise of a node is compromise of that node* +(ADR 0004). An agent able to reach everything, placed anywhere else, creates a **second** such +place. Putting it where the authority already sits concentrates nothing new. + +**It is an addition to per-node messaging and never a replacement.** Every node remains directly +addressable. This is not a preference: ADR 0001 holds that losing the control plane costs *change, +not operation*, and a mesh whose only conversational surface lives on that node would lose the +ability to ask anything while every machine kept running perfectly. **The front door may not be +the single point.** + +**It is not an employee** ([ADR 0003](0003-agents-are-persistent-employees.md)). Nobody hires it, +it holds no task queue, it is never drained or reassigned. What it does with work that belongs +somewhere else is **dispatch it** — to node sessions, or to workers — which is what a node session +already does when asked something it does not have. + +**It is ADR 0025's reader.** The agent that reads the design repository and answers into search is +this session, not a second one. One agent, one memory, one place to reach; two would both need +that repository and would eventually disagree about what it says. + +**"One per node" is about address, not about process count.** ADR 0004's rule — *two and nothing +decides which replies* — forbids ambiguity in who answers when a **node** is addressed. The mesh +session answers when the **mesh** is addressed. The control-plane node therefore hosts two +sessions and no ambiguity, and stating this here is what stops it reading as a contradiction +later. + +## Consequences + +**The node session's setup must now be designed, and it never was.** This decision is expressed as +*the same as a node session, elsewhere*, which is only meaningful once that mechanism is written +down. The design document covering both is the immediate consequence of this record, not a +follow-up to it. + +**A consumer that is not a machine stops being deferrable.** The licence binding above is the gap +`14-model-access.md` names, and it now has two consumers rather than a hypothetical one. Until it +exists, a session's model access can only be expressed as *this module on this machine*, which +cannot say *this node's session uses the personal licence and the mesh's uses the company one* — +the thing the binding is for. + +**Symmetry is preserved, and it is worth being precise about why.** ADR 0004's claim is about what +a node can reach, and it is untouched: node-to-node messaging is unchanged, nothing is routed +through the mesh session, and it is a participant rather than a hop. What arrives is a +participant that happens to be the one a person usually addresses. + +**Availability degrades to inconvenience rather than to silence** — but only because of the +addition rule above. If that rule is ever relaxed, this consequence inverts, and it inverts +quietly: everything keeps working and nobody can ask about it. + +**The surface a person uses is not decided here.** That a board is a good place to talk to it is +likely and is not this record's business; the session is reachable over the broker like everything +else, and what puts a text box in front of it is a separate choice. + +## References + +- [ADR 0004](0004-a-node-and-how-it-joins.md) — the node session this extends +- [ADR 0025](0025-the-design-record-is-read-not-copied.md) — the reader this session is +- [ADR 0003](0003-agents-are-persistent-employees.md) — the vocabulary this is not +- [`03-DESIGN/01-to-be/14-model-access.md`](../03-DESIGN/01-to-be/14-model-access.md) — *a + consumer that is not a machine*, the gap this makes concrete diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index b99144c..434e363 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -98,6 +98,7 @@ python3 00-META/checks/index.py fail if stale - **0009** — [Modules and the graph](0009-modules-and-the-graph.md) - **0010** — [Delivery](0010-delivery.md) - **0024** — [Model access is a provision, and a licence is a thing with a name](0024-model-access-is-a-provision.md) *(proposed)* +- **0026** — [The mesh has a session of its own, and it is the node session's mechanism](0026-the-mesh-has-a-session-of-its-own.md) ### How it is built diff --git a/03-DESIGN/01-to-be/14-model-access.md b/03-DESIGN/01-to-be/14-model-access.md index 8499bc1..25b9f9d 100644 --- a/03-DESIGN/01-to-be/14-model-access.md +++ b/03-DESIGN/01-to-be/14-model-access.md @@ -81,6 +81,13 @@ to a node. What is delivered still lands on a machine; what is **chosen** is cho the provisions model has no consumer identity other than a node. What exists today is per module per machine, which is a step toward it and is not it. +*2026-08-31: this gap now has named consumers rather than hypothetical ones.* +[ADR 0026](../../02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md) puts two sessions on the +control-plane node — the node's own and the mesh's — each bound in its own right. **A per-machine +binding cannot express that at all**, not merely awkwardly: the two sessions share a machine and +must be able to hold different licences. See +[`15-the-agent-session.md`](15-the-agent-session.md). + **Switching is a reaction, not a declaration.** A licence that hits its limit and must be swapped is a response to something observed. Expressing it as a declaration would make the declaration mean *whatever is working right now*, which is not a thing anybody declared. It belongs with diff --git a/03-DESIGN/01-to-be/15-the-agent-session.md b/03-DESIGN/01-to-be/15-the-agent-session.md new file mode 100644 index 0000000..60faa9c --- /dev/null +++ b/03-DESIGN/01-to-be/15-the-agent-session.md @@ -0,0 +1,192 @@ +--- +layer: to-be +status: designed +code: [mesh-control, mesh-host] +updated: 2026-08-31 +decisions: + - 02-DECISIONS/0004-a-node-and-how-it-joins.md + - 02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md + - 02-DECISIONS/0024-model-access-is-a-provision.md + - 02-DECISIONS/0025-the-design-record-is-read-not-copied.md +--- + +# The agent session + +**One mechanism, started twice.** A node's session and the mesh's session are the same thing +pointed at different context. This document describes the mechanism; where the two differ it says +so, and the differences are few enough to list here: + +| | node session | mesh session | +|---|---|---| +| **address** | the node's name | the mesh | +| **context root** | the node's | the mesh's | +| **engram** | that node's | the mesh's | +| **licence** | bound in its own right | bound in its own right | +| **runs on** | that node | the node holding the control plane | +| **how many** | one per node | one | + +Everything below applies to both unless it says otherwise. + +## What a session is, restated for what is being built + +[ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md) settled the behaviour: permanent, +remembering across callers, its own tools, reachable over the broker, and — switched off — still +answering *I am switched off* rather than falling silent. + +**Nothing said how one is set up**, and that gap is what this document closes. It was invisible +until [ADR 0026](../../02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md) needed to describe +a second instance, because *the same as that one, elsewhere* means nothing until the first has +been written down. + +## The context root is the whole of the difference + +**A session is defined by the directory it starts in.** That directory holds the engram, the +session's tools, and whatever standing instruction it works under. Two sessions differing only in +their root are two different agents, and nothing else has to differ to make them so. + +This is deliberately a **small** definition. The alternative — a session type, with the mesh +session as a distinct kind — would mean two implementations of one mechanism, and +[ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) records what that +costs: two things nearly the same, built twice, until neither word means one thing. + +**The root is delivered as declared state, not carried by the session.** It is files on a machine, +which is precisely what the host applies +([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)). A session's context therefore changes the +way anything else changes — the mesh declares it, the host writes it — and there is no second +mechanism for shipping an engram. + +**Changing the engram is changing a file.** So it is versioned, reviewable, and rolled back like +any other declared state; and a node whose engram was changed reports having applied it, the same +as it reports anything else. + +## Where each one runs + +**A node's session runs on that node**, and cannot be moved. Moved, one machine is answering as +another (ADR 0004). + +**The mesh's session runs on the node holding the control plane.** The reasoning is in ADR 0026 +and is worth carrying here because it is easy to get backwards: this is not *the important agent +goes on the important machine*. It is that the control-plane node is already the one place +excepted from *compromise of a node is compromise of that node*, and an agent able to reach +everything, placed anywhere else, would create a second such place. + +## Messages + +**A session is reached over the broker** +([ADR 0002](../../02-DECISIONS/0002-nodes-communicate-over-a-broker.md)). There is no second +transport, nothing is dialled at a session, and the mesh session is not a hop: node-to-node +messages continue to travel directly, and nothing is routed through it. + +**A message becomes a prompt; a reply travels back the same way.** Whoever asked — a person at a +surface, another session, a worker — is a caller, and the session remembers what each of them +asked, together, over time. + +**Being asked something it does not have, a session may ask another.** How it does so is its own +business and follows from its engram rather than from a message format: it may say who wants to +know, or simply ask. A person relaying a question makes the same choice. + +**A switched-off session answers.** The queue is still read and the state is the reply, with no +model involved. This is the same rule the host follows about a service that does not exist, and +it is the rule this repository has now paid for three times: **absence must never be +indistinguishable from a failure to answer** +([005](../../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md), +[008](../../04-ISSUES/008-the-documented-node-rescue-does-not-exist/00-report.md)). + +## Model access is bound to the session, not to the machine + +**A session is a consumer in its own right.** This is the gap +[`14-model-access.md`](14-model-access.md) records as *a consumer that is not a machine*, and it +is the one part of this design that cannot be built from what exists: the provisions model has no +consumer identity other than a node, so today a binding can only say *this module on this +machine*. + +That is not sufficient here, and the shortfall is concrete rather than theoretical: + +- the control-plane node hosts **two** sessions, which must be able to hold **different** + licences — a per-machine binding cannot express it at all; +- *this node's session uses the personal licence, the mesh's uses the company one* is the ordinary + case, not an exotic one. + +**What is delivered still lands on a machine. What is chosen is chosen per session.** Delivery +follows the session's context root, which is where its credentials belong — the same rule ADR 0001 +states for an agent's config directory, with the root standing in for it. + +**Switching a licence remains a reaction, not a declaration** (ADR 0024). A session that exhausts +a licence is observed and its binding changed; the binding is then declared as usual. Nothing here +grows a conditional in the declaration language. + +## What the mesh session knows + +**It holds the design record by reading it** +([ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)), not by holding a +copy. It is that reader; there is not a second agent for it. + +**It answers into a symptom search**, so what it knows appears beside ordinary results rather than +only when it is asked. **And when it cannot be reached, the search says so.** A result set that +silently omits this material looks identical to one where nothing matched — the same rule as the +switched-off session above, at a different layer. + +**Reading is one-way.** It reads the repository and answers from it; nothing flows back. The +repository is public and the mesh is not, and a return path is how installation-specific detail +arrives into documents that must not carry it. + +## What it does with work + +**It dispatches; it does not hold a queue.** Asked for something that belongs elsewhere, it asks a +node session or hands the work to a worker. It is not an employee +([ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)): nobody hires it, it is +never drained, and it is not reassigned. + +The distinction is the one ADR 0001 was written to keep: a session comes with the thing it belongs +to and goes when that thing goes; a worker is hired, holds tasks, and moves. Built from the same +parts, run on entirely different terms. + +## The surface is separate + +That a person usually reaches the mesh session through a board is likely and is not settled here. +The session is reachable over the broker like everything else; what puts a text box in front of it +is a different design, and the session does not know which surface asked. + +## Consequences + +**Two sessions on one node, and no ambiguity.** The control-plane node hosts its own node session +and the mesh session. ADR 0004's *one per node* forbids ambiguity about who answers when a **node** +is addressed; these answer to different addresses. + +**A new node gets a session by being declared, not by being set up.** The context root is declared +state, so a joining node's session arrives the way its packages and services do. + +**The mesh session is a single point of convenience, and must never become one of reach.** Every +node stays directly addressable. If that ever stops being true, the failure is quiet in the worst +way: every machine keeps working and nobody can ask about it. + +## How it is checked + +**A test names the decision it defends** ([ADR 0017](../../02-DECISIONS/0017-a-test-defends-a-decision.md)), +and these run in the lab on real machines: + +| Check | Defends | +|---|---| +| a node is asked something and its session answers | ADR 0004 | +| the mesh is asked something and the mesh session answers, on the control-plane node | ADR 0026 | +| both sessions on that node answer, to their own addresses, without ambiguity | ADR 0026 | +| a session switched off replies saying so, rather than timing out | ADR 0004 | +| a session whose engram was changed reports having applied it, like any declared file | ADR 0005 | +| the two sessions on one node hold different licences, and each uses its own | ADR 0024 | +| a node is messaged directly while the mesh session is stopped, and answers | ADR 0001 | +| a search consulting an unreachable mesh session says it was not consulted | ADR 0025 | + +The last two are the ones worth writing first. Both defend properties that are invisible while +everything works, and both describe a mesh that looks entirely healthy at the moment it has +stopped telling the truth. + +## Deliberately not decided + +**Whether the mesh session's memory is its own or assembled from the node sessions each time.** +It remembers what it has been asked, which is settled. Whether *what the mesh knows* is a thing it +holds or a thing it gathers on demand is a real question with a cost either way, and nothing here +depends on the answer yet. + +**How a person's identity reaches a session.** Callers are distinguished, but who a caller *is*, +and whether a session should act differently for different people, is the human-agent question +ADR 0001 leaves open and this does not close. diff --git a/03-DESIGN/01-to-be/README.md b/03-DESIGN/01-to-be/README.md index 93e7596..395486e 100644 --- a/03-DESIGN/01-to-be/README.md +++ b/03-DESIGN/01-to-be/README.md @@ -20,6 +20,11 @@ document is written and this one's status becomes `implemented`. | [`08-connectivity.md`](08-connectivity.md) | One context in full — overlay, resolution, exposure, filtering, certificates | [ADR 0007](../../02-DECISIONS/0007-connectivity.md), [0050](../../02-DECISIONS/0007-connectivity.md), [0051](../../02-DECISIONS/0004-a-node-and-how-it-joins.md), [0055](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md) | | [`09-the-node-lifecycle.md`](09-the-node-lifecycle.md) | How a machine becomes a node, stays one, and stops being one | [ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md), [0051](../../02-DECISIONS/0004-a-node-and-how-it-joins.md) | | [`10-delivery.md`](10-delivery.md) | Modules, the three edges, and how a change becomes a running thing | [ADR 0010](../../02-DECISIONS/0010-delivery.md), [0064](../../02-DECISIONS/0009-modules-and-the-graph.md), [0065](../../02-DECISIONS/0009-modules-and-the-graph.md) | +| [`11-a-board.md`](11-a-board.md) | What a person sees of the mesh, and why it is read from what runs | [ADR 0008](../../02-DECISIONS/0008-a-context-owns-its-store.md), [ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md) | +| [`12-a-module-repository.md`](12-a-module-repository.md) | A module repository, and what builds it | [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md), [ADR 0010](../../02-DECISIONS/0010-delivery.md), [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) | +| [`13-credentials-and-their-rotation.md`](13-credentials-and-their-rotation.md) | Credentials, and moving them without a consumer holding one the provider does not know about | [ADR 0001](../../02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md), [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) | +| [`14-model-access.md`](14-model-access.md) | Model access as a provision, and what a licence is bound to | [ADR 0024](../../02-DECISIONS/0024-model-access-is-a-provision.md), [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) | +| [`15-the-agent-session.md`](15-the-agent-session.md) | One mechanism started twice — a node's session and the mesh's | [ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md), [ADR 0026](../../02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md) | ## Not yet written