# Mid-session activation leaves the session dark — JIT plan (SEAL-DUAL-TRIGGER-ABSENCE item 1)

> Owner: perri. Opened 2026-07-26. Item 2 of doyle's binding guard
> (`REQ-HAZARD-DIGEST-ZERO-INPUT-SILENT`) shipped @cf6267c; this is the last item before the
> seed closes. Item 3 (kin seed NEVER-SEALING-OBSERVABILITY) is diagnostic-only and stays out.

## Scope

doyle's gap: a Claude Code session that starts **without** the sptc plugin/adapter live gets no
SessionStart hook run. Running `/sptc:setup` mid-session installs the binary and registers the
adapter — every LATER hook fires — but nothing re-runs the SessionStart work for the session
already in flight. If that omission leaves the session's digest extraction broken, the operator
gets no signal at all: liam's went undetected for 2 days.

Deliverable: a loud RESTART-REQUIRED-class notice at setup completion (or a live in-session
rebind, if the probe says one is possible), with a `REQ-*` id and tests.

## Probe FIRST — no code before it

Standing correction from the 0.25.10 field fault: design from what the mechanism DOES, not from
what the source suggests. The hypothesis to falsify is "no SessionStart ran under the hook, so the
session has no seed/registration and nothing mid-session recreates it" — a hypothesis, not a
finding.

Rig (built 2026-07-26, `scratchpad/seal-abs/`):

- An isolated `CLAUDE_CONFIG_DIR` with credentials only — **no plugins**, so the sptc plugin is
  genuinely absent at session start. Global spt-core state (adapter registry, daemon, live
  perches) is untouched: the adapter registry is spt-core-side, not CC-config-side.
- An interactive `claude` under psmux (`psmux send-keys` / `capture-pane -p`), because the two
  steps that matter — `claude plugin install` mid-session and `/reload-plugins` — do not exist in
  headless mode.
- A disposable perch id for the probe session. Never `perri`.

Questions the probe must answer, each as an observation:

1. Does `/sptc:setup` mid-session leave extraction working, or dark?
2. Does `/reload-plugins` heal it (hooks re-registered mid-session)?
3. What specifically does NOT heal until a fresh session — seed? bind? the sid carrier? the
   `{CLAUDE_CONFIG_DIR}` read-var capture?
4. Control: same rig with the plugin present from the start — extraction works.

## PROBE RESULT — 2026-07-26 (all OBSERVED unless marked read)

Rig: `scratchpad/seal-abs/` — isolated `CLAUDE_CONFIG_DIR` (credentials + account only), sptc
absent from `installed_plugins.json` at launch, interactive `claude` under psmux, launched
**detached** through `Win32_Process.Create` so no perched process is an ancestor.

**Rig lesson first (v1, discarded run):** the first rig launched psmux from this session's own
Bash tool, so the probe CC was a DESCENDANT of `perri`. `spt whoami` resolves identity by process
LINEAGE (observed: same binary, same instant — from my tree `{"id":"perri"}`, from a WMI-parented
process `{"id":null}`), so the unregistered probe session's hook fell back to `perri` and moved
**perri's own session pin onto the probe's sid**. Repaired by re-binding perri with the recorded
sid as proof. Two things ride out of that accident: (a) probe rigs must be launched detached;
(b) an unregistered session with a perched ANCESTOR silently adopts that ancestor's identity —
an identity-hijack hazard, logged separately, not this REQ.

Sequence probed: plugin absent at launch → one turn (no hooks, no sptc skills in the listing:
premise verified in the transcript) → `claude plugin install sptc@cplugs` → `/reload-plugins` →
`/sptc:setup` → `/sptc:ready <id>`.

1. **`/reload-plugins` heals the SURFACE, not the session.** "Reloaded: 1 plugin · 11 hooks";
   afterwards the `/sptc:*` skills are invocable and `UserPromptSubmit` hooks fire
   (`identity[core-fallback]` in the trace, skill strings injected, spill file written). What it
   does NOT do is re-run SessionStart, so nothing ever seeds the session.
2. **`/sptc:setup` completes with no notice of any kind** — it reports the adapter active, runs
   `spt adapter use claude-spt`, and says setup is complete. Nothing tells the operator the
   session itself is unregistered.
3. **The bind then captures a SYNTHETIC session id.** `/sptc:ready` runs `spt ready <id>`, and the
   perch record reads `"session_id":"sess-43676-1785052739814160000"` — a core-minted id, not the
   CC session id. No `adapter` field, no `read_env`. Control (same rig, plugin present at launch,
   SessionStart hook fires): `"session_id":"0665609b-…"` — the real CC sid.
4. **Extraction is therefore permanently dark.** Extractor run by hand against both:
   - real sid → NDJSON records, as designed.
   - synthetic sid → `claude-spt digest: no transcript for session sess-43676-… under any projects
     root (tried: …)`, exit 0. `spt endpoint digest <id>` answers `NO_DIGEST … (no session-log
     source / no records yet?)`. Nothing in either message tells the operator WHY, and the
     extractor's stderr reaches nobody.
5. **Nothing in-session heals it.** Further turns keep the synthetic pin. `/clear` fires
   SessionStart, and the boundary path logs
   `boundary(clear) SKIPPED — no endpoint id resolvable; the perch stays pinned to the departed
   session id and will strand`. So the fault survives the one in-session event that re-runs
   SessionStart.
6. **A live rebind IS possible — the published fallback covers exactly this case.**
   `harness-contract/api.md` (read): the SessionStart seed lives in daemon memory only and is gone
   for "a session that goes live late"; `listen --session-id <sid>` binds directly from the given
   harness session id. Observed in the probe session:
   `spt api --adapter claude-spt listen <id> --session-id $CLAUDE_CODE_SESSION_ID --subnet <s>`
   produced a perch indistinguishable from a seeded one — real sid, `"adapter":"claude-spt"`, and
   `read_env.CLAUDE_CONFIG_DIR` captured — and `spt endpoint digest` then rendered the session's
   turns. (`spt ready` has no `--session-id`; the bare `spt api listen` refuses
   `ADAPTER_UNRESOLVED` under the Monitor's `bash.exe` child, hence the group-level `--adapter`.)
7. **Why the existing skills cannot save themselves.** `live-normal.md` already chains
   `seed && listen --parent-pid`, but both legs read `$OWL_SESSION_ID` / `$SPT_HOST_PID` — session
   env exported by the SessionStart hook. In a mid-session-activated session all three of
   `OWL_SESSION_ID`, `SPT_HOST_PID`, `CLAUDE_ENV_FILE` are EMPTY (observed), so the live arm seeds
   with an empty session id and the ready arm never carries one at all.

## Fix shape — and the two things the rig corrected about it

First design: "re-fire the SessionStart seed and the session heals." **The rig refuted it.** With the
heal's seed in place, the same `/sptc:ready` still bound the SYNTHETIC id; the same command with
`OWL_SESSION_ID=<sid>` in front of it bound the real one. Core takes the session id from the
**bringup process's env**, and that env can never be repaired mid-session — `$CLAUDE_ENV_FILE` is a
SessionStart/CwdChanged/FileChanged-only channel (CC hooks reference). So the heal splits in two:

1. **Once per session, on the first prompt:** seed on the resolved claude anchor + a notice naming
   what is empty (`$OWL_SESSION_ID`, `$SPT_HOST_PID`) and the values to substitute. No anchor
   resolves ⇒ nothing sent to core and the notice is the RESTART-REQUIRED one, naming
   `/reload-plugins` as explicitly not the cure.
2. **On every `/sptc:ready`|`/sptc:live` turn:** the explicit bringup command, because bringup is
   the one moment the damage becomes permanent and it happens turns after the notice.

Second correction, also from the rig: the override was first *appended* to the ready body. Together
they blew CC's 1800-byte additionalContext cap and **spilled to a file the ready skeleton tells the
agent not to read** ("do NOT investigate, do NOT read files") — so the agent ran the skeleton's
command and the perch came up synthetic anyway. The override now REPLACES the ready body (whose
command is wrong on this session regardless) and a test pins the emission under the cap. `live`
keeps its body — commune/wake/signoff mechanics live there — with the override leading.

Detector: `$SPT_ENDPOINT_ID` unset (not spt-hosted) AND `$OWL_SESSION_ID` unset. Never a MISMATCH: a
subagent inherits its parent's export with its own sid.

## Verified end to end (2026-07-26)

Rebuilt binary, fresh mid-flight-activated session (`SPTC_HOOK_BIN` pointed at the build so the
node's live agents kept running the shipped adapter): notice landed inline on the first prompt,
`/sptc:ready probever2` followed the override, and the perch recorded
`"session_id":"cdb8fec4-343a-464b-b792-920749da8d39"` — byte-identical to the session's transcript
file name, `state` still `ready_agent`. Rig torn down, probe endpoints purged.

## Tasks

- [x] Build the rig and run the probe; record observed-vs-read for every claim.
- [x] Write the finding up (this plan).
- [x] Add the `REQ-*` to `traceable-reqs.toml` (`REQ-HAZARD-LATE-ACTIVATION-DARK`), then satisfy it.
- [x] Implement the late-activation heal + notice + bringup override, with unit tests over the
      detector, both notice bodies, the override (both forms + the cap bound) and the once-guard.
- [x] KNOWN-HAZARDS §7.3, including the lineage identity-hijack observation as adjacent-not-covered.
- [x] Open the identity-hijack requirement of its own — `REQ-HAZARD-INHERITED-IDENTITY-ADOPTION`,
      KNOWN-HAZARDS §7.4, registered OPEN (`required_stages = []`). Probing it corrected the cause:
      it is NOT whoami-by-lineage (ancestry with the env scrubbed resolves `{"id":null}`) but the
      INHERITED `$SPT_ENDPOINT_ID`, which beats an explicit session id matching no perch. Fix shape
      (deferred to its own wave): scrub `$SPT_ENDPOINT_ID`/`$SPT_AGENT_ID` on `self_id`'s whoami call.
- [x] Ship: v0.25.12 — version bump + changelog + the three triples, per docs/RELEASE-RUNBOOK.md.
      Released on the private home with the fat `adapter.spt`, public mirror pushed, node updated.

## Gate

`sh ci/run-gates.sh` PASS + `traceable-reqs check` green.
