docs/adr/0055-launcher-owned-local-services.md

ADR 0055: The launcher owns every local service

Status: accepted (James, 2026-07-25). Applies to the single-service pi architecture.

Context

Clankie is present through several long-lived local processes. Starting them by hand leaves no durable ownership record, no dependency-aware restart, and no reliable health gate.

ADR 0055: The launcher owns every local service

Decision

apps/tui/bin/service-supervisor.ts owns the process mechanics and apps/tui/bin/services.ts declares the services and their dependency order. The backend is one clankie process: the HTTP API, pi captain, presence state, media, and game bodies all live there. The Discord bridge depends on it. The optional lab user-session body depends on it too and stays off until enabled (ADR 0098 (user-session shares)). The activity surface and tunnel publish what he plays.

Every managed process gets:

  1. an atomically written mode-0600 pid record under the Clankie state root;
  2. a live command check before any signal, so a recycled pid cannot kill an unrelated process; and
  3. a service-specific health gate before start succeeds.

Unowned conflicts are scoped to the resource the service uses: the configured TCP port for Clankie, relay and activity, or the configured activity tunnel name. A process command identifies the kind of service but cannot distinguish instances whose ports and state roots travel in environment variables. lsof supplies address-independent listener evidence; a bind probe cannot reliably detect wildcard versus loopback conflicts on macOS. Failed inspection falls back to the conservative command scan. Discord bodies keep the command scan because a separate local port does not establish a separate Discord identity. Health probes and the live-command check before signalling retain their own roles.

The force-kill deadline is also service-specific. Generic services retain the launcher's ten-second grace. Clankie's outer grace is its configured play shutdown deadline plus two seconds, leaving its play host time to finish a normal summary or publish the bounded forced terminal report before the supervisor escalates to SIGKILL.

Restart follows dependencies. Restarting clankie also restarts the bridge and the lab user-session body, because both hold live claims against the service instance. Stopping one named service remains scoped to that service.

A restart requested from Clankie's own operator-turn bash tool is handed to a detached launcher helper. Pi already exposes the durable PI_SESSION_FILE; the launcher uses its conversation's append-only event log to wait for that turn's terminal event before stopping the service. The operator face retries only a dropped durable tail read, then resumes from its persisted cursor. It never replays the prompt or any tools that already ran.

ADR 0055 launcher-owned local services

Editable Turbopuffer tldraw source

The compatibility aliases captain, captain-eve, eve, control-plane, and cp all resolve to clankie; they do not name separate processes.

Consequences

  • clankie restart and clankie status cover the full local stack. The headless command contract is docs/cli.md.
  • A self-restart finishes the conversation turn before replacing its backend; a dropped tail reconnects without repeating the turn.
  • A process started outside the launcher is reported but never adopted or killed.
  • Restarting during play preserves terminal accounting instead of leaving the next process to infer an avoidable lease lapse.
  • Settings remain the source of Discord allowlists; the launcher supplies only repository paths and brokered service credentials.