# ResidentService: the supervised-substrate split of always-on endpoints

Status: accepted (2026-07-26) — amends ADR-0023

## Context

ADR-0023 ratified the always-on endpoint and, in doing so, rejected a "faceless
supervised service (no perch/identity)" because "agents must address it two-way
to drive the bridged channel." Field experience with spt-alchemy's Hub Daemon
(2026-07-25/26: the tree-kill teardown, the relay hop, the Job-Object sweep, the
node-wide silent Watch-delivery outage) surfaced a consumer for which that
rejection reason does not hold: a resident service whose **two-way agent-facing
surface already exists one layer up** — the adapter's *shell* is the endpoint
with identity, perch, and admin verbs, while the service itself takes zero
inbound spt traffic (its inputs are its own gateway, polling, and config-file
re-reads) and needs only to *invoke* the spt CLI outbound (identityless
`spt send`, spooled, ADR-0002). Forcing such a service to bind an addressable
`#`-identity is ceremony without value and dilutes the glossary term.

## Decision

Split the substrate out of the endpoint. A **ResidentService** is a
daemon-supervised binary an adapter declares in a **`[service]` manifest
section** — core-owned from birth: spawned **job-neutrally by the daemon**
(never a shell's child, so shell tree-kill `/T` and terminal Job Objects cannot
reach it), supervised with the wake-watcher scaffolding (backoff, give-up
latch, one-per-instance lock, orphan-kill, brain-side reconcile), running
independent of any agent's liveness. It has **no perch, no identity, no
address**. An **AlwaysOnEndpoint (ADR-0023) is henceforth a ResidentService
that additionally fronts `#`-addressed endpoints** via `api bind` — the
substrate carries the process; the endpoint layer carries the address.

- **Start trigger**: a `[service]` entry always means core owns the process.
  `start = "boot"` is **desired-state-running**, not an event — the supervisor
  reconciles toward running at daemon boot, at **adapter registration against a
  live daemon** (installing an adapter never requires restarting spt to bring
  its service up), at update-hold release, and at first shell bind as the
  defensive ensure. `start = "bind"` launches lazily at the adapter's first
  shell bind. Supervised identically once running.
- **Cardinality**: one supervised instance per registered adapter-option
  (`<adapter>[:profile]`) — ADR-0023's unit, consumer-confirmed as a coherent
  **target shape** (not exercisable by the first consumer until per-option
  config dirs exist). The supervisor **threads per-option identity** (option
  name + per-option runtime dir) into the spawn environment so adapters can
  scope their private guards and config per option — without this, two options
  sharing one adapter config dir yield a silent flap: the adapter's own kernel
  lock correctly refuses the second instance, the supervisor correctly
  backoff-relaunches what it saw start-then-die, and two correct components
  disagree about the unit forever. Relatedly, **consecutive fast exits are a
  configuration fault, not a crash**: they trip the give-up latch early with a
  loud `STARTUP_FAULT` diagnostic carrying captured stderr.
- **Update is a first-class supervisor operation with an explicit HOLD**:
  quiesce → hold (stopped and never relaunched while held) → bits swap →
  start new bits → release. Adapter update-apply performs this ordered
  operation; crash-relaunch (with backoff) applies only when not held. A
  supervisor that eagerly relaunched during a swap would re-pin the old exe
  mid-deploy (Windows exe lock) — the forcing case the stale-online wave
  already ruled against.
- **Quiesce is cooperative exit + deadline**: the supervisor places a
  stop-request marker (file in the service's runtime dir — polling services
  observe it on their next cycle); the service exits when safe, and the
  kernel-observed exit IS the acknowledgement. A manifest-declared grace
  deadline bounds the wait, then force-kill. Delay is possible; veto is not.
  An optional advisory status line may be surfaced in `service status`
  display — never consulted for decisions. **Errata (W1 delivery):** that
  display verb is `spt adapter service status <adapter[:profile]>` — nested
  under the ADAPTER group, never a bare `spt service`, because `spt daemon`
  already uses "service" for the registered OS service. Ownership scopes both
  meanings: the OS-service belongs to the daemon, the resident service to the
  adapter.
- **Liveness is derived, never recorded**: the supervisor is the parent and
  holds the child handle — exit is kernel-observed. No supervisor-maintained
  "running" record exists to go stale (the v0.43.0 stale-online lesson,
  applied one layer down).
- **The service can invoke the spt CLI**: the supervisor threads the daemon's
  environment so `spt send` et al. work from the service; the identityless
  `cli@<node>` from-label posture is unchanged.

## Alternatives considered

- **Force the Hub into a full AlwaysOnEndpoint.** Rejected: it binds an
  address nobody uses; access whitelist, sigil grammar, and roster presence
  become dead weight, and the glossary's "addressable" definition is diluted
  by a member that isn't.
- **A new third mechanism (manifest binary + autostart flag, independent of
  ADR-0023).** Rejected: duplicates the `[always-on]` supervision design
  nearly verbatim and grows the carefully bounded set of boot-launched
  third-party binary classes.
- **Reverse ADR-0023's faceless-service rejection outright.** Not needed: the
  rejection reason was consumer-scoped (the Discord bridge *is* driven by
  inbound messages). It stands for that consumer; the substrate split serves
  both.

## Consequences

- CONTEXT.md gains **ResidentService**; **AlwaysOnEndpoint** is redefined as
  ResidentService + addressable front. The "two classes of third-party binary
  spt-core boot-launches" stays at two by counting the substrate: shell
  wake-watcher, ResidentService.
- `REQ-EP-8` splits: the substrate (supervision, `[service]`, hold, quiesce,
  cardinality, derived liveness) moves to `REQ-RESIDENT-SERVICE`; REQ-EP-8
  retains the endpoint layer (`api bind` channel fronting, wake verb) and
  builds ON the substrate. `REQ-EP-9` (sigil) is unchanged.
- `REQ-SHELL-ADAPTER-OWNED-DETACHED-SERVICE`'s owed ruling is hereby ratified:
  adapter-owned detached descendants remain unsupported; the supported shape
  is the manifest-declared ResidentService. alchemy's relay hop (its ADR-0013)
  retires once supervised.
- The remaining half of `REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE` closes for
  services: a daemon-spawned service is never inside a launching terminal's
  Job Object.
- A ResidentService is **least-trusted third-party code**, same posture as
  shells and harness binaries (unchanged from ADR-0023).
- First consumer: spt-alchemy's Hub Daemon (watch engine + Discord gateway),
  whose adapter-side conditions — hold-before-relaunch and derived-not-recorded
  liveness — are both structural here, not promises. Alchemy declares
  `start = "boot"` (operator-ruled 2026-07-26): alchemy's ADR-0012 (no
  autostart mechanism) is **deliberately revised**, its reasoning honored —
  the ban was on a second start mechanism *inside* alchemy, and the
  alchemy-side ensure-at-bind retires with core's supervisor as the single
  start authority. Flynn records the revision in the alchemy repo. The
  delivery gate's boot specimen is a mock adapter, never the first consumer.
