# F-030 — PSYCHE-EPHEMERAL: per-event psyche driver + residency retirement (DESIGN)

Status: **READY FOR BUILD 2026-07-04 — wave called by operator.** Author: doyle.
Inputs: perri `PSYCHE-EPHEMERAL-PLAN.md` (adapter half, doyle-gated PASS; built on
parked branch `psyche-ephemeral-parked`, publish held for this core wave), the
2026-07-04 field case (both directions — v0.13.2 bad-ship + hall-bf multi-subnet
poll refusal, evidence table §2), doyle code recon (anchors verified @f8428a8).

Scope split: **all wiring, custody, resolution, and residency-retirement work is
spt-core** (todlando's wave). **The psyche role binaries/templates are
adapter-side** (perri's parked branch — publishes ONLY after the core floor
releases; see §7 sequencing, a BINDING lesson from the v0.13.2 bad ship).

---

## 1. Problem

The daemon hosts each live endpoint's Psyche as a **long-lived resident
process** and enforces that residency:

- `livehost.rs:497 → host_one:518` spawns the psyche
  (`spawn_psyche_owned(Some(&session_id), None)`) and starts `run_pulse_loop`
  as its driver; `confirm_residency_or_unhost`
  (REQ-HAZARD-LIVEHOST-NONRESIDENT) probes the owned child and re-hosts every
  ~5 s reconcile tick when it isn't resident.
- Meanwhile the **per-event driver stack already exists, tested, with ZERO
  production callers**: `resume_psyche` (spt-live resume.rs:175, mode-validated
  spawn + nested rebind), `run_psyche_turn` (spt-live turn.rs:72, bounded
  stdin→stdout turn; its only production caller is the ADR-0013 conflict-merge
  in reconcile.rs), and `psyche_turn_and_relay` (spt-daemon psyrelay.rs:168,
  turn + intent-parse + sanitized relay).

The field killed the resident model from **both directions on the same day**
(2026-07-04):

**(a) The adapter cannot HOLD residency.** The resident runner's per-pulse
liveness poll (`spt ready <id> --once`) hard-refuses on a multi-subnet home
(`READY_FAIL: … pass --subnet`). The nested psyche process **cannot know its
subnet/home**: core fills only `base_keys` = `{id}`, `{session_id}`, `{node}`
(lifecycle.rs:338) plus role-local extras — no `{subnet}`, no `{home}`. On
hfenduleam (2 subnets since BIGNET joined 7/3) the runner died instantly,
the daemon re-hosted, ~5 s churn forever: hall-bf-psyche ordinal 6491+, at
one point 54 stamped host attempts. Adapter v0.13.4 made the death LOUD
(`PSYCHE_POLL_FAIL`), but residency remains structurally impossible there.

**(b) The daemon EXPECTS residency, and that expectation is destructive.**
Adapter v0.13.2 accidentally shipped the parked ephemeral shim: its by-design
exit after the seed turn tripped the residency machinery, which tore down the
endpoint's hosted state — ready marker removed, never re-stamped, every
force-native gated `leg=cli-gate-not-hosted` **permanently** (field brick,
operator-hit on hall-bf, contained same-day by v0.13.3).

Additional structural debts of the resident model:

- **Sid custody:** `{session_id}` handed to the psyche is the **PARENT's** sid
  (livehost.rs:518). The psyche has no session identity of its own; a parent
  boundary orphans the psyche's conversational thread.
- **`.live-bin` own-copy:** exists solely so the long-lived resident's image
  doesn't Windows-image-lock the adapter registry dir during `spt adapter
  update` (the crc-swap `.old`-litter hazard class). Ephemeral turns hold the
  image only for a bounded turn — the own-copy machinery loses its reason.
- Psyche TURNS already work daemon-driven today (`psyche_download`, revival
  prompt, reconcile merge) — proof the per-event shape is the natural one.

## 2. Field evidence (2026-07-04 — provenance)

| # | Observed | Where | Direction |
|---|----------|-------|-----------|
| 1 | `LIVEHOST_PSYCHE_NONRESIDENT` xN in EVERY iso-home run, both adapter versions | perri RC + wakefin rigs | daemon expects residency |
| 2 | hall-bf-psyche boot-churn ~5 s cadence, ordinal 6491+, synthetic `sess-<pid>-<ts>` ids, no real session ever binds; starts exactly at BIGNET join (7/3) | operator box, real home | adapter can't hold residency |
| 3 | Poll-verb root repro: `spt ready <id> --once` on 2-subnet home → exit 1, `READY_FAIL … pass --subnet`, empty stdout | perri, clean repro | adapter can't hold residency |
| 4 | v0.13.2 shim-exit → residency teardown → ready removed → permanent `leg=cli-gate-not-hosted` | hall-bf + perri wake-confirm | residency expectation destructive |
| 5 | Psyche turns land fine via daemon-driven paths while the resident churns | hall-bf digest (`psyche_download` at every boundary) | per-event shape already proven |
| 6 | ~12/min re-host cadence NEVER tripped the C3(b) thrash guard (boot records are apparently not ledger boundaries to the guard) | hall-bf | guard blindness (rides this wave) |

## 3. Design

Target invariant (headline): **the Psyche is a per-event turn, not a process.
Liveness of the psyche = "turns succeed", never "a pid is resident."**

### W1 — wire the per-event driver (retire the resident spawn)

`host_one` stops spawning a resident psyche. The pulse loop **stays as the
daemon-side scheduler** (its thread, stop-flag, and drop-dir watching are
correct); what changes is what a fire DOES: each pulse/event invokes **one
bounded per-event turn** through the existing stack —

- event turns with outbound intents → `psyche_turn_and_relay`
  (psyrelay.rs:168, the D7.5a driver, sanitize boundary included);
- session-custody transitions (start/commune/checkpoint resume) →
  `resume_psyche` (resume.rs:175) with the right `ResumeMode`;
- pure merges keep their existing `run_psyche_turn` call (reconcile.rs —
  untouched).

The daemon drives every invocation, so every substitution key is filled from
**daemon-known context** — the child never self-resolves home/subnet/perch.
Direction-(a) failures become impossible by construction, not by fixing the
poll.

Turn failures consume a **bounded failure budget** (reuse the C3(b)
budget/cooldown shape): N consecutive turn failures → stamp
`psyche_host_error` (kept name; it is already the harness-reachable pattern) +
cooldown, reset on a successful turn. No respawn storm exists anymore because
there is nothing resident to respawn.

### W2 — psyche-own sid custody

The psyche mints and keeps **its own session id**, stored in the nested
`{id}-psyche` perch record — `{session_id}` in psyche role templates becomes
**the psyche's sid**, never the parent's (today's fill at livehost.rs:518 is
the parent's — the custody bug). Rules:

- Parent boundary (/clear, /compact) does **NOT** rotate the psyche sid — the
  psyche's conversational thread survives parent resets (that is its job).
- `resume_psyche(ResumeMode::…)` validates the custody key before spawn
  (already implemented — resume.rs:183).
- **Reseed path:** when the psyche's own session is lost/invalid (first host,
  store wipe, harness-side expiry), fall back to
  a fresh-with-preload turn — `download_psyche_context` composes
  role/live/project into the psyche-download file whose path fills
  `{psyche_context_file}` (file-always; non-empty content = fresh preload,
  0-byte = continue — replaces the former `{psyche_context}` body key) and a
  fresh psyche session seeds from it. Loud marker on reseed (`PSYCHE_RESEED:{id}`) so a
  custody-loss loop is visible, plus the W1 failure budget bounds it.
- If the parent's sid is still needed by any template, it gets its OWN key
  (`{parent_session_id}`) — explicit, never aliased.

### W3 — residency machinery retirement

- `confirm_residency_or_unhost` and the residency-grace probe retire with the
  resident child. REQ-HAZARD-LIVEHOST-NONRESIDENT's **spirit** (a psyche that
  cannot work must be visible on the perch, and hosting must not churn)
  transfers to the W1 failure budget; the REQ's traceable entry gets a
  SUPERSEDED pointer to the new REQ (same pattern as LIVENESS-DECAY →
  SUPERSEDED in C-1).
- **The teardown that made direction (b) destructive is deleted**: no psyche
  state may ever remove the PARENT endpoint's ready marker or hosted state.
  Psyche trouble stamps psyche fields only. (This is the conformance test's
  core assertion.)
- C3(b) guard rework rides here (evidence #6): the failure budget counts
  **attempts via the psyche perch ledger** (boot records / turn records), not
  whatever it counted that let 12/min churn run invisibly. Red-first: a
  12/min synthetic failure loop MUST trip the budget.

### W4 — nested-resolution seam (F-017 sibling; general, not psyche-only)

Even fully ephemeral, child-side verbs acting on nested perches must resolve
without a `--subnet` the child cannot know. Fix in core, general form:
**nested `{parent}/{nested}` ids resolve under the parent's home** —
subnet-less resolution for nested perches (`resolve_perch_path`/ready-verb
family). Additionally expose a single `{subnet}` base_keys fill **when
known** (lifecycle.rs:338; absent → LOUD missing-key fail, the `{node}`
precedent on :344). **There is deliberately NO `{home}` key** (doyle W4 Q1: the
endpoint home subnet is one concept, and `home:` is already the subnet-name
qualifier position — a `{home}` template key would only invite meaning-drift).
Red-first = evidence #3's exact repro: 2-subnet home, nested-id verb, must
succeed.

### W5 — `.live-bin` dissolution

Remove the own-copy spawn path for psyche roles (spawn from the registry
install dir like every other bounded adapter invocation — image-lock exposure
is now turn-bounded, same class as `translate`/`digest` today). GC existing
`owlery/*/.live-bin/` dirs (displace+GC pattern from F-028 C1 if any is
image-locked at update time). Compat: a manifest that still names the
own-copy path resolves install-dir-first anyway (REQ-INSTALL-11 unchanged).

### W6 — docs

- `docs/MANIFEST.md`: psyche role contract rewritten for the ephemeral model
  (per-event turn semantics, custody keys, reseed, failure budget); fix the
  substitution-key list drift vs `spt-runtime/src/runtime.rs:55` (the
  `{psyche_context}`→`{psyche_context_file}` keylist bug — MANIFEST.md:129
  region); document `{parent_session_id}` and the single `{subnet}` fill (no
  `{home}` key — doyle W4 Q1).
- `docs/KNOWN-HAZARDS.md`: two new entries (§5 REQ seeds: RESIDENCY-EXPECTATION,
  THRASH-GUARD-BLIND) — both paid-for field bugs.
- ADR: **amend the livehost/psyche-hosting decision record** (the ADR that
  chose daemon-resident hosting) — residency premise changes; per
  gate-against-documented-design, the amendment lands IN this wave, not after.
- CONTEXT.md glossary: Psyche definition gains the ephemeral phrasing
  (per-event turn; custody).

### What F-030 deliberately does NOT do

- No adapter-aware code in core: core wires WHEN turns run and WHAT keys they
  get; the adapter's parked branch owns what a psyche turn does (glue-model
  boundary holds).
- No relay-unregister fix (the post-clear `owl send → NO_PERCH` casualty) —
  separate seed, C-2 follow-up (a), field-repro'd 2026-07-04, sequenced with
  the operator's v0.24.0 bug batch, likely same milestone but its own item.
- No wake-arc work — closed end-to-end 2026-07-04 (five layers, wakefin3 +
  hall-bf field proof).
- Iso/no-subnet NONRESIDENT variant (evidence #1) is VERIFIED inside W1's int
  rig, not separately fixed — the ephemeral model should erase it; if a
  residue survives, it becomes its own seed with the rig already in hand.

## 4. Coverage matrix

| Problem | Prevented by | Caught loud by |
|---------|--------------|----------------|
| Runner poll multi-subnet refusal (churn) | W1 (no child self-resolution) + W4 (seam for remaining child verbs) | v0.13.4 `PSYCHE_POLL_FAIL` (interim), W1 failure budget |
| Residency teardown bricking parent (0.13.2 shape) | W3 (teardown deleted; psyche state never touches parent hosted state) | conformance int (W3) |
| Parent-sid custody (psyche thread orphaned at boundary) | W2 own-sid custody | W2 red-first int (boundary keeps psyche sid) |
| Thrash-guard blindness (12/min invisible) | W3 ledger-counted budget | red-first synthetic loop trips it |
| `.live-bin` image-lock class | W5 (bounded turns from registry dir) | existing crc-swap displace+GC net |
| Reseed loop (custody lost repeatedly) | W2 reseed + W1 budget | `PSYCHE_RESEED` marker + budget stamp |

## 5. Requirement seeds (register at wave start, `required_stages=[]` until the chunk activates)

- `REQ-PSYCHE-EPHEMERAL-DRIVER` — each psyche-relevant event runs exactly one
  bounded per-event turn through resume/turn/relay; no resident psyche process
  exists between events. Red-first: fire an event on a hosted live endpoint →
  assert one turn ran (side-effect proof file — transcript-based asserts are
  structurally blind, 2026-07-04 rig lesson) and no `{id}-psyche` process
  survives the turn.
- `REQ-PSYCHE-SID-CUSTODY` — psyche templates receive the psyche's OWN sid;
  parent boundary does not rotate it; reseed is loud. Red-first: parent
  `api boundary clear` → nested perch sid UNCHANGED (today it is the parent's
  — guard-revert reproduces).
- `REQ-PSYCHE-NESTED-RESOLUTION` — nested-perch verbs resolve under the parent
  home without `--subnet` on a multi-subnet node; a single `{subnet}` fill
  present-or-loud (no `{home}` key). Red-first: evidence #3 repro (2-subnet home rig).
- `REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION` — hazard (paid-for, hall-bf +
  0.13.2): psyche failure of ANY shape must not remove/alter the parent
  endpoint's ready/hosted state, and hosting must not churn-respawn.
  Conformance int = the hall-bf shape: multi-subnet home, live endpoint,
  failing psyche → parent stays deliverable, no rehost churn, error stamped.
- `REQ-HAZARD-THRASH-GUARD-BLIND` — hazard (paid-for, evidence #6): the
  failure budget counts real attempts (ledger-derived); a 12/min failure loop
  trips it. Red-first synthetic loop.
- Adapter-side (perri's repo, NOT core REQs): parked-branch ephemeral roles;
  `min_spt_core` floor = the release that ships this wave; publish gated on
  that floor being LIVE (bad-ship lesson, binding).

## 6. Sequencing & halves

- **Core (todlando):** branch off current main after the v0.24.0 bug-batch
  triage lands (operator sequencing); suggested chunking W1→W2→W3 (one gate
  each; W3's conformance int is the wave's heart), W4 parallel-safe, W5+W6
  close. Per-wave traceable activation (never full end-state upfront). Gates:
  isolated worktree per sha + fresh CARGO_TARGET_DIR, nextest, clippy
  --workspace, docs-drift xtask. `[twohost]` NO — but W4's int needs the
  **single-node multi-subnet rig** (the hall-bf shape: 2 subnets, one home).
- **Adapter (perri):** `psyche-ephemeral-parked` branch rebased once core API
  shape freezes (she reviews W1/W2 key contract before her rebase — field
  review slot like F-027 §7); publish ONLY after core floor releases.
- **hall-bf interim:** stays turn-functional + visibly stamped (v0.13.4) until
  this wave lands; no interim core patch — the churn is bounded and loud now.

## 7. Open questions (field-review slot — perri + operator)

1. Per-event turn timeout default (today's turn paths carry caller-chosen
   timeouts; the pulse-fired turn needs a config default — propose the
   existing pulse-period-derived bound, floor 30 s).
2. Does the checkpoint/commune choreography want a custody-transition turn at
   parent boundary (psyche notices the parent reset), or stay purely
   event-driven? (Design assumes purely event-driven; cheap to add later.)
3. `{subnet}` fill naming — RESOLVED (doyle W4 Q1): a single `{subnet}` key,
   no `{home}` key (the endpoint home subnet is one concept; `home:` is already
   the subnet-name qualifier position). Confirmed against CONTEXT.md glossary.
