# Daemon-hosted live-agent lifecycle — completion plan (M11-W0)

> **STATUS: PLANNED (2026-06-15).** JIT plan to *complete* the REQ-DAEMON-1
> consolidation: move the live-agent lifecycle (Psyche spawn, pulse, echo,
> drop-ingest, graceful signoff) out of the interim `api listen` process and
> into the always-on daemon brain, where the RECORDED DESIGN puts it
> (CONTEXT.md §spt-daemon: "all poll-listener logic, **and** all Psyche/pulse
> loops" / §Psyche: "a **loop inside the daemon** ... liveness daemon-held, not
> PID-based" — :30, :34, :38, :168, :177, :194). Rolls into **M11 as W0**
> (runs ahead of / parallel to the shell-substrate waves — orthogonal to them).
> `doyle` designs/gates → `todlando` builds. Heir to `M11-PLAN.md`.

---

## 1. Why now (the reconciliation that surfaced this)

REQ-DAEMON-1 is **partly done, not done**. The daemon-hosted lifecycle
**machinery is built and E2E-tested** (all impl/int-tagged in-tree today):
- `BrainLifecycle` + `run_pulse_loop` (scheduled, config-paced, killable) —
  `spt-daemon/src/lifecycle.rs:260` (impl-tagged).
- the dumb-pipe `Relay` (respawn-safe, no message loss) — `relay.rs` (impl-tagged).
- config-driven pulse period — `config.rs` (impl-tagged).
- `daemon_e2e.rs` (int-tagged) drives spawn→Psyche-loop→commune→
  brain-restart-survives→graceful-signoff.

But the **production wiring is staged, not connected**:
- `brainproc::run_brain` drives **no** lifecycle — `brainproc.rs:175-182`:
  *"Today the supervised daemon brain hosts no PTY sessions ... so this is a
  no-op now and forward-correct when daemon-hosted sessions land (the
  live-agent adapter)."* `run_pulse_loop` has **no production caller**.
- `spt api listen` still runs the **interim in-process** Psyche + pulse —
  `spt/src/api/startup.rs:286,289,323` (`LiveHost::spawn_psyche` + `pulse_tick`
  inside the relay loop), comment: *"interim; M3's daemon re-hosts."*

**Consequences of the interim coupling** (all violate the recorded design):
1. **spt-hosted live agents get no Psyche** — they `bind` (no `listen`), and the
   only spawn path is `cmd_listen`. CONTEXT:177 says spt-hosted Psyche is
   daemon-spawned "same as above"; today there is no such path.
2. **Psyche liveness is relay-process-bound**, not daemon-authoritative —
   contradicts CONTEXT:194 (alive iff the daemon's endpoint table says so).
   A relay swap/kill disturbs the Psyche.
3. The `int` evidence is **type-level, not production-level**: `daemon_e2e`
   constructs `BrainLifecycle` directly; nothing proves the *brain process*
   hosts it for a real endpoint. Green coverage over an unwired production path.

The trigger the brain comment names — *"the live-agent adapter"* — is
**claude-spt** (`perri`), now in build. So completing this unblocks the *correct*
(decoupled, daemon-hosted) live model for the first real consumer, instead of
shipping the interim coupling as the contract.

## 2. Requirements (no new REQ — this completes REQ-DAEMON-1)

REQ-DAEMON-1 already declares `required_stages = ["impl", "unit", "int"]`. This
plan does **not** mint a new id; it **re-points the `int` evidence** from the
type-level `daemon_e2e` to a **production-path** E2E (a real `api listen` / a
real `api bind` whose Psyche is brain-hosted), and adds the missing production
`impl` wiring. Tag the new wiring `// [impl]` on the brain
call-site, the new E2E `// [int]`. (If gate review judges the
production-host path a distinct invariant from the lifecycle machinery, mint
`REQ-DAEMON-1b` register-first — decide at W0.1.)

## 3. Design (settled — CONTEXT.md; do not re-litigate)

- **The brain hosts the lifecycle.** `brainproc::run_brain` owns an endpoint
  table; for each hosted **live** endpoint (manifest resolves `[session.psyche_init]`)
  it instantiates a `BrainLifecycle` and drives `run_pulse_loop` on the
  config-paced deadline. The machinery already exists — this is the *call site*,
  not a rewrite.
- **The relay becomes a true dumb pipe.** `spt api listen` stops spawning the
  Psyche / ticking the pulse; it connects to the brain and streams events
  (`relay::Relay`). All stateful lifecycle leaves the `spt` process (CONTEXT:38).
  `LiveHost`'s lifecycle calls (`spawn_psyche`/`pulse_tick`) are removed; the
  shim becomes pure relay glue or is retired.
- **Both topologies daemon-spawn the Psyche.** harness-hosted (`listen` relay)
  and spt-hosted (`bind`, no relay, direct PTY) both get the brain-hosted Psyche,
  keyed on the endpoint being a LiveAgent — *independent of message delivery*
  (CONTEXT:168, :177).
- **Liveness is daemon-authoritative** (CONTEXT:194) — the brain marks/holds the
  Psyche perch `online`; no relay-process / pid coupling. Brain restart re-hosts
  the lifecycle (the survival the broker/brain split, ADR-0018, already buys).
- **No behavior change for the Self session** — `api boundary`, `api state`,
  commune/signoff file-drops, the digest two-origin merge all keep working; only
  *where the loop runs* changes.

## 4. Task breakdown (waves within W0)

### W0.1 — Brain hosts the lifecycle (the production call site)
- Wire `brainproc::run_brain`'s endpoint table to instantiate `BrainLifecycle`
  for each hosted live endpoint and run `run_pulse_loop` on the config deadline
  (replace the `brainproc.rs:175-182` no-op). Re-hydrate hosted endpoints on
  brain (re)start from disk. `[impl]` `[unit]`
- Decide REQ shape (re-point vs `REQ-DAEMON-1b`) — gate with doyle (§6 Q1).

### W0.2 — Retire the interim relay-hosted Psyche
- Remove `LiveHost::spawn_psyche` / `pulse_tick` from `cmd_listen`
  (`startup.rs:286-346`); `api listen` becomes a pure `relay::Relay` consumer.
  Self-resume / boundary signals route to the brain (IPC), which owns the loop.
  `[impl]`

### W0.3 — spt-hosted live agents get the Psyche
- On `api bind` of a live endpoint (spt-hosted), the brain hosts the Psyche the
  same way — no `listen` required. Close the bind-no-Psyche gap. `[impl/unit]`

### W0.4 — Production-path int (close the type-level-only gap)
- E2E: a real `spt api listen <id>` (and a real `spt api bind <id>` for the
  spt-hosted rung) → the **brain process** spawns + hosts the Psyche → a commune
  drop folds in via the brain's scheduled pulse → **kill the relay**: the Psyche
  stays alive (daemon-authoritative liveness) → **restart the brain**: lifecycle
  re-hosts. Re-point `[int]` here. `[int]`

### W0.5 — Acceptance against the first real live adapter
- Validate with claude-spt `:live` (perri) or the mock live adapter: a live
  session's Psyche is brain-hosted, survives a relay swap, and the Self never
  notices. Coordinate the cutover with perri so her `/sptc:live` lands on the
  decoupled path (not the interim coupling).

## 5. Gate criteria (doyle, per wave + capstone)
- `traceable-reqs check` EXIT 0; REQ-DAEMON-1 `int` re-pointed to the
  production-path E2E (no type-level-only int masking unwired production).
- `run_pulse_loop` has a **production caller** in `brainproc`; `cmd_listen` no
  longer spawns the Psyche (grep proves the interim calls are gone).
- spt-hosted (`bind`, no `listen`) live agent gets a Psyche.
- Psyche liveness daemon-authoritative: relay kill/swap does not drop it.
- Brain restart re-hosts the lifecycle (rides the ADR-0018 survival).
- Full workspace suite green on hfenduleam + kitsubito.

## 6. Open questions for doyle (gate before W0.1)
1. **REQ shape:** re-point REQ-DAEMON-1 `int` + add production `impl`, or mint
   `REQ-DAEMON-1b` for the brain-hosting call site as a distinct invariant?
   Lean: re-point (same requirement, completing its promised production wiring).
2. **Cutover ordering vs perri:** complete W0 *before* perri's `/sptc:live`
   body (she builds against the final decoupled shape), or let her ship on the
   interim `listen`-coupling now and cut over after (no adapter change — she
   declares the seam + runs the relay either way)? Lean: let her proceed on the
   interim (it works), cut over in W0.5 with zero adapter change.
3. **Scope of relay rework:** minimal (strip lifecycle from `cmd_listen`, keep
   its current relay loop) vs full swap to `relay::Relay`. Lean: full swap —
   `relay::Relay` is built + `[impl]`, and the dumb-pipe is the
   recorded design.

## 7. Non-goals
- Re-designing the lifecycle machinery — `BrainLifecycle`/`run_pulse_loop`/
  `Relay` are built + unit-green; this is wiring + retirement, not a rewrite.
- The Psyche I/O / trust-boundary model (ADR-0012) — unchanged.
- Shell-substrate work (M11 W1-W5) — orthogonal; this W0 runs independently.
