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.
This commit is contained in:
2026-08-28 01:24:07 +02:00
parent f1b1cd9aa0
commit ba0d01788e
3 changed files with 149 additions and 0 deletions
@@ -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`
+101
View File
@@ -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.