From ba0d01788eb6846eb2c2484888207f83313fc439 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 28 Aug 2026 01:24:07 +0200 Subject: [PATCH] 0062: a host may be episodic; 0060's Android gap closed 0060 named the gap and did not close it: everywhere else an init runs the launcher at boot, and Android grants neither an init to register with nor anything worth supervising, because a supervisor would be killed alongside what it supervises. Closed by narrowing what is required rather than building something. A host is resident or episodic, and both are hosts. Being killed by the platform is disconnection, which 0036 already made ordinary -- and every mechanism an episodic host needs already exists because it was built for laptops that close. A partial host can join a mesh and cannot be the first node, since every bootstrap step is a shape it refuses. Its bundle says so. Two consequences that are easy to miss: last-heard-from means much less on an episodic host, so a healthy phone reads as a dead server unless the reader knows which kind it is; and a declaration may take a long time to land, which makes 0058's outstanding-versus-failed distinction load-bearing. Still open, and in that order: what an Android node is FOR, and only then how it is started. --- ...-the-host-is-built-per-operating-system.md | 10 ++ 02-DECISIONS/0062-a-host-may-be-episodic.md | 101 ++++++++++++++++++ 03-DESIGN/01-to-be/09-the-node-lifecycle.md | 38 +++++++ 3 files changed, 149 insertions(+) create mode 100644 02-DECISIONS/0062-a-host-may-be-episodic.md diff --git a/02-DECISIONS/0060-the-host-is-built-per-operating-system.md b/02-DECISIONS/0060-the-host-is-built-per-operating-system.md index 14e84bb..bc07dbd 100644 --- a/02-DECISIONS/0060-the-host-is-built-per-operating-system.md +++ b/02-DECISIONS/0060-the-host-is-built-per-operating-system.md @@ -87,6 +87,16 @@ reports which shapes it implements so the control plane never sends one it canno filesystem and a way to run something, which makes a partial host a real thing rather than a broken one. +**A partial host can join a mesh and cannot be the first node.** Every step of raising a +substrate is a `package`, a `container`, or an `action` against one, so the shapes it refuses +are exactly the ones a bootstrap needs. Its bundle says so rather than being an empty +placeholder. + +**How such a host is started was left open here and is closed by +[ADR 0062](0062-a-host-may-be-episodic.md)** — by narrowing what is required rather than +building something. A host may be *episodic* rather than resident, and being killed by the +platform is disconnection, which is already ordinary. + ## Consequences - **Each implementation stays as sharp as its operating system allows.** The `LoadState` diff --git a/02-DECISIONS/0062-a-host-may-be-episodic.md b/02-DECISIONS/0062-a-host-may-be-episodic.md new file mode 100644 index 0000000..6742eed --- /dev/null +++ b/02-DECISIONS/0062-a-host-may-be-episodic.md @@ -0,0 +1,101 @@ +--- +status: accepted +date: 2026-08-28 +deciders: jochen +reconstructed: false +extends: 0060-the-host-is-built-per-operating-system.md +--- + +# 62. A host may be episodic, and being killed is ordinary + +## Context + +[ADR 0060](0060-the-host-is-built-per-operating-system.md) makes a partial host a real thing — +Android implements `file`, `directory` and `action` and refuses the rest — and leaves one gap +open, named but not closed: + +> **Being STARTED on Android is not solved by this file, and it is the real gap.** Everywhere +> else an init runs the launcher at boot. Here the equivalent is the app framework — a +> foreground service, or something under Termux — both of which the system may kill when it +> wants memory. + +There is no way to keep a process running on an ordinary Android device. Registering with init +needs root and an unlocked bootloader. A foreground service is the sanctioned alternative and is +still subject to the system reclaiming memory, to Doze, and to whatever the manufacturer added +on top. **The correct model is not a daemon that occasionally dies; it is something that runs +when it is allowed to.** + +[ADR 0061](0061-the-host-asks-an-init-for-start-and-restart.md) asks an init for *start at boot* +and has a launcher supervise the host. Android grants neither half: nothing to ask, and nothing +worth supervising, because the supervisor would be killed alongside what it supervises. + +## Decision + +**A host is either resident or episodic, and both are hosts.** + +| | resident | episodic | +|---|---|---| +| started by | an init, at boot | whatever the platform allows — an app's foreground service, a scheduled wake | +| supervised by | the launcher ([ADR 0061](0061-the-host-asks-an-init-for-start-and-restart.md)) | nothing; the platform decides when it runs | +| the link | held open | opened while it runs | +| stopping | shutdown, or a failure | **ordinary, and needs no explanation** | + +**Being killed is not a failure to detect. It is disconnection**, which +[ADR 0036](0036-a-node-is-a-managed-machine.md) already made an ordinary situation rather than +an exception — *a node that is switched off, roaming, or behind a connection that has dropped +has not become a lesser kind of thing.* An episodic host is that, more often. + +This needs no new mechanism, and that is the argument for it. The store is already authoritative +while disconnected. Reconcile already happens on start. The mesh already reports *last heard +from* rather than alarming on silence +([`09-the-node-lifecycle.md`](../03-DESIGN/01-to-be/09-the-node-lifecycle.md)). Every one of +those was decided for laptops that close, and an episodic host is the same case with a shorter +period. + +### What an episodic host does not have + +- **No launcher.** There is nothing to supervise it and nothing for it to supervise. The + platform starts it; the platform stops it. +- **No rollback.** [ADR 0061](0061-the-host-asks-an-init-for-start-and-restart.md)'s recovery + reinstalls a previous package, and an episodic host has no package manager to reinstall from. + A bad version is replaced the way the platform replaces applications. +- **No bundle, and therefore no bootstrap.** Every step of raising a substrate is a shape a + partial host refuses, so **a partial host can join a mesh and cannot be the first node.** The + android bundle says exactly that instead of being an empty placeholder. + +### What it still is + +A node. It has an identity, it holds a store, it applies declarations, it reads back and +reports. It is reachable in the inventory, it can be assigned work of the kinds it supports, and +it is not a second class of thing in the model — +[ADR 0036](0036-a-node-is-a-managed-machine.md) is explicit that reachability is state rather +than class, and this is that rule doing the work it was written for. + +## Consequences + +- **The gap 0060 left is closed by narrowing what is required, not by building something.** No + Android daemon, no keep-alive service, no fighting the platform's process management — which + would be a losing fight and a permanent source of bugs. +- **The heartbeat matters more and means less.** An episodic host reports when it runs, so *last + heard from* on a phone is a much weaker signal than on a server. Anything reading that fact + has to know which kind of host it is looking at, or a healthy phone reads as a dead node. +- **A declaration may take a long time to land**, because the node applies it only when the + platform next runs it. *Outstanding* was already separated from *failed* + ([ADR 0058](0058-delivery-ends-in-a-declaration.md)) and this makes that separation + load-bearing rather than tidy. +- **How an episodic host is actually started is still platform work and is not designed here.** + An APK with a foreground service, or Termux with its boot addon — both are real, both have + costs, and choosing between them wants an actual device and an actual purpose for it. +- **What an Android node is FOR remains unanswered**, and it should be answered before the + platform work is done. A device that can write files and run commands is not a workload host; + it is a presence, or somewhere an agent runs. Building the start mechanism before deciding + that would be building it for nobody. + +## References + +- [ADR 0036](0036-a-node-is-a-managed-machine.md) — disconnection as an ordinary situation, + which this is an instance of rather than an extension to. +- [ADR 0060](0060-the-host-is-built-per-operating-system.md) — partial hosts, and the gap this + closes. +- [ADR 0061](0061-the-host-asks-an-init-for-start-and-restart.md) — what a resident host has + that this one does not. diff --git a/03-DESIGN/01-to-be/09-the-node-lifecycle.md b/03-DESIGN/01-to-be/09-the-node-lifecycle.md index 991bc2f..30cf821 100644 --- a/03-DESIGN/01-to-be/09-the-node-lifecycle.md +++ b/03-DESIGN/01-to-be/09-the-node-lifecycle.md @@ -14,6 +14,7 @@ decisions: - 02-DECISIONS/0058-delivery-ends-in-a-declaration.md - 02-DECISIONS/0060-the-host-is-built-per-operating-system.md - 02-DECISIONS/0061-the-host-asks-an-init-for-start-and-restart.md + - 02-DECISIONS/0062-a-host-may-be-episodic.md --- # The node lifecycle @@ -189,6 +190,43 @@ used months later on node two. --- +## Two kinds of host + +Everything above assumes a machine with an init that runs the host at boot. Not every machine +has one ([ADR 0062](../../02-DECISIONS/0062-a-host-may-be-episodic.md)). + +| | **resident** | **episodic** | +|---|---|---| +| examples | Alpine, Arch | Android | +| started by | an init, at boot | whatever the platform allows | +| supervised by | the launcher | nothing — the platform decides when it runs | +| the link | held open | opened while it runs | +| being stopped | shutdown, or a failure | **ordinary** | +| shapes | all six | `file`, `directory`, `action` | +| can be the first node | yes | **no** | + +**An episodic host being killed is disconnection, not failure.** That is +[ADR 0036](../../02-DECISIONS/0036-a-node-is-a-managed-machine.md) doing the work it was written +for: reachability is state, not class. Everything the design already does for a laptop that +closes — an authoritative local store, reconcile on start, *last heard from* reported without an +alarm — is what an episodic host needs, at a shorter period. + +**It cannot be the first node**, and that is not a limitation to work around. Every step of +raising a substrate is a `package`, a `container` or an `action` against one, and a partial host +refuses the first two. So `mesh-host-android bundle` returns a file that says so rather than an +empty placeholder waiting to be filled in. + +**Two things this changes for anything reading the mesh.** *Last heard from* is a much weaker +signal on an episodic host — a healthy phone looks like a dead server — so a reader has to know +which kind it is looking at. And a declaration may take a long time to land, which makes +[ADR 0058](../../02-DECISIONS/0058-delivery-ends-in-a-declaration.md)'s separation of +*outstanding* from *failed* load-bearing rather than tidy. + +**Still open:** how an episodic host is started in practice — an APK with a foreground service, +or Termux with its boot addon — and, first, **what an Android node is for.** A device that can +write files and run commands is not a workload host; it is a presence, or somewhere an agent +runs. Building the start mechanism before deciding that would be building it for nobody. + ## Adoption: what happens to what is already there Adoption is not a state. It is what the **first apply** does when it is told to own something a