# ADR-0013 — The daemon is spawned through a relay hop, so nothing is its parent

- **Status:** **Superseded** at W25 by the ResidentService (`[service]`, spt-core v0.44.0
  ADR-0049) — Accepted 2026-07-25, retired 2026-07-26. Kept, not deleted: the diagnosis in
  it is why the replacement has the shape it does.
  The relay hop was **correct and insufficient**. It severed *parentage*, so a `taskkill /T`
  tree walk had nothing to reach — and it was measured doing exactly that. What it could not
  sever was **job membership**, which inherits to every descendant regardless of parentage,
  so a job close still swept the daemon and the shell together (field-observed 2026-07-25
  ~17:47, across disjoint parentage and disjoint spawning contexts). Supervision fixes both
  halves at once by never creating the relationship: the daemon is spawned job-neutrally by
  spt's daemon, not re-parented after the fact by alchemy.
  **What retires with it:** `spawn_relay`, `spawn_detached`, auto-ensure-at-bind, and the
  `daemon` verb's `ensure` / `restart` / `stop` ops. Alchemy's start-path count is zero.
- **Requirements:** `REQ-HAZARD-DAEMON-ORPHAN-DEATH` (KNOWN-HAZARDS 1.5)
- **Replaces:** ADR-0012 (Rejected the same day). That one moved the *start* out of
  alchemy; this one fixes the start alchemy already had.

## Context

The Hub Daemon was dying with whichever shell auto-ensured it, taking node-wide Watch
delivery with it (measured: pid → 0 within 0.1s of a teardown, still 0 after 15s). The
first diagnosis blamed spt-core's force-close — `taskkill /PID <pid> /T /F`, a process
**tree** kill — for reaching a process `CONTEXT.md` says is detached and independent.

The Operator's question is what corrected it: **how can a force-close kill a detached
process it never spawned and does not know about?** It cannot. It does not need to.

`taskkill /T` enumerates descendants by **ParentProcessId, at kill time**. The flags the
spawn used — `CREATE_NO_WINDOW`, `CREATE_NEW_PROCESS_GROUP`, null stdio — detach the
console and the signal group. **None of them sever parentage.** A daemon spawned straight
from a shell process remains that shell's child in the process table, so the tree walk
finds it by construction. The daemon was never as detached as the spec claimed; the spec
was right and the implementation was not.

Isolated A/B probe on this node (2026-07-25), stand-in processes, `taskkill /PID <parent>
/T /F`:

| spawn shape | outcome |
| --- | --- |
| parent spawns the long-lived process **directly** | killed with the tree |
| parent spawns an **intermediate** that spawns it and exits | **survives** |

## Decision

**One relay hop.** `spawn_detached` re-invokes this same binary with a hidden op
(`daemon _spawn-relay`); that process spawns `daemon run` and exits immediately. The
caller waits for the relay to exit — which is what guarantees the daemon's recorded parent
is already gone — then reads the daemon's pid from the node lock's info file, which is the
authority anyway.

Both hops are built by one `daemon_command` helper so the relay cannot drift from the real
spawn, and both carry `ALCHEMY_CONFIG_DIR` explicitly: the relay is not a shell and
inherits nothing useful.

Verified with the real binary, not only the stand-ins: after `daemon ensure` from a
stand-in shell, the daemon's ParentProcessId pointed at a dead process, and
`taskkill /PID <shell> /T /F` left the daemon running.

## What this is not

- **Not a second start path.** The daemon is still started by a shell instance's
  auto-ensure at bind, exactly as PHASE3-PLAN ruling 3 and `CONTEXT.md` describe. The
  Operator ruled against an autostart mechanism (ADR-0012); this adds none. There is one
  start path and it now satisfies the property it always claimed.
- **Not a fix for spt-core.** The force-close remains a tree kill on Windows and a
  single-pid kill on Unix, so it can still reach detached descendants of adapters that
  have not done this. That is smaller than it looked and no longer blocks alchemy;
  Request #22 records it. REQ-EP-8 / ADR-0023 (AlwaysOnEndpoint) remains the ratified
  target shape for a resident service.

## Consequences

- `ensure` costs one extra short-lived process and up to ~100ms of lock polling before it
  can report the daemon's pid. It runs at bind, so this is not a hot path.
- A relay that fails to produce a daemon within 3s is reported as an error rather than a
  spawn that silently produced nothing.
- On Unix the relay is harmless and is the classic double-fork daemonize shape anyway.
- The unit test asserts the *shape* (relay op, this exe, config dir on both hops) rather
  than spawning anything — the suite still spawns no daemons.
