# spt-core outage on HFENDULEAM — 2026-09-10

Written by liam (liaison session, Claude Code) at the operator's request, before a reboot.
Machine: HFENDULEAM (node 14efb80c…). spt-core data dir: `%LOCALAPPDATA%\spt-core`.

## Timeline (local time)

| Time | Event |
|---|---|
| 09-09 16:40 | Original daemon (pid 37384, 0.68.0) starts. Log shows `ELEVATED_DAEMON`, serving with elevated token. A second daemon (pid 31664) exists concurrently with a different pipe id. |
| 09-10 ~15:50 | Operator reports agents unreachable, `spt node stop --force` hangs. Plugin `sptc@cplugs` install record pointed into `.claude-spt\accounts\alt`; reinstalled cleanly (unrelated to outage). |
| 16:2x | Operator asks for force-kill of all spt processes. `Stop-Process` + `taskkill /F /T` on spt.exe, spt-win-head.exe, spt-mobile-host.exe. Tree-kill also took down the Claude sessions hosted by `spt go` (doyle, deployah, gears, perri, sparrow, flynn, webbie, lia). |
| 16:2x | 36 spt processes remain enumerable with `HasExited=True`. Daemon respawns within seconds of every tool call (sptc hooks run `spt`, any `spt` invocation auto-starts the daemon). |
| 16:3x | `spt.exe` renamed to `spt.exe.disabled` to stop respawn. Restored, single daemon started. `spt daemon status` answers but every real RPC hangs. |
| 16:39 | Rolled back to 0.67.1 using the installer's own archive `bin\spt.exe.old-104` (probed: `spt 0.67.1`). 0.68.0 kept as `spt.exe.old-105`. `gh` could not be used: token for SaberMage is invalid (401). |
| 16:5x | Root cause found in `logs\daemon.stderr.log`: every broker since the kill logs `BROKER_BIND_FAIL: Access is denied. (os error 5)`; brain logs `BRAIN_UP: connected to broker spt-daemon-broker-78bf…` then idles. |
| 17:0x | Confirmed the 36 "dead" spt processes are zombies stuck in termination (1–2 threads each, 27–194 handles, kernel wait). They still own listening TCP sockets and both daemon pipe sets. Attempts to close their handles from outside block in the kernel too. |
| 17:1x | Stopped. Reboot recommended. |

## What is broken

- Both stale named-pipe sets still exist: `spt-daemon-{broker,digest,drive,seed,service,tunnel}-0c8bd7089d5bd936.sock` and `…-78bf18a9fd96fdc6.sock`. `WaitNamedPipe` reports an instance *listening* on `broker-0c8b` and `service-78bf` even though their owners are dead.
- Listening TCP sockets owned by dead pids (37384:5474, 55496:52180, 3292, 37068, 27372, 63540, 47436, 29364).
- New broker cannot claim the pipe name (`FILE_FLAG_FIRST_PIPE_INSTANCE` on an existing name → `ERROR_ACCESS_DENIED` 5). Brain connects to the stale broker instead, never becomes ready (`brain.ready` still names the dead brain pid 32064). Every CLI call that needs the daemon hangs: `node status`, `node stop`, `endpoint list/stop/suspend/shutdown/digest`.
- Endpoint records still say online for endpoints whose processes are gone (doyle: `owlery\doyle\info.json` binder pid 31020, all dead).

## Suspected causes

1. **Primary: non-cancellable pending I/O in the original daemon tree.** The killed processes never finished terminating. Their threads sit in a kernel wait that TerminateProcess could not cancel, so their handle tables (pipe server instances, sockets) survive. Most likely a pending `ConnectNamedPipe`/read on the daemon pipes, possibly interacting with a filter driver. Whatever holds it, user mode cannot release it: `DuplicateHandle(DUPLICATE_CLOSE_SOURCE)` on those handles blocks as well.
2. **Contributing: two daemons with two pipe identities running at once** (`0c8b…` and `78bf…`) before any of this. Suggests an earlier partial restart or an elevated/non-elevated split. Worth understanding; it may be the original trigger for the "broken" state the operator saw.
3. **Contributing: daemon auto-start on every `spt` invocation.** Hooks in every Claude session re-spawn the daemon within seconds, so a broken daemon can never be held down without renaming the binary. `spt node stop` is supposed to make the stop stick, but it hangs here because it needs the wedged daemon to answer.
4. **Unverified: 0.68.0 itself.** Operator judged 0.68.0 as the bad version. The bind failure is not version-specific (0.67.1 fails identically against the stale pipes), so 0.68.0 cannot be cleared or blamed until a clean daemon runs.
5. **Adapter/core mismatch after rollback (possible).** Adapters (claude-spt 0.40.0, alchemy 0.26.0, omp-spt, mobile, PACER) were last written 09-09/09-10 and may target 0.68.0.

## Current on-disk state

- `bin\spt.exe` = 0.67.1 (copy of `spt.exe.old-104`). `spt.exe.old-105` = 0.68.0.
- 64 archived `spt.exe.old-NN` copies in `bin` (~2 GB). Untouched.
- `daemon.json` `startup_endpoints`: mobile-gw (mobile), lia (claude-spt), gw (cc). doyle is *not* auto-started.
- Scheduled tasks `spt-core daemon` and `spt-mobile-gateway-autostart` are logon-triggered and enabled.
- Service `actions.runner.SaberMage-spt-core.hfenduleam` left running.
- Killed and not restored: 5 stale `alchemy.exe` + 1 `pacer-shell.exe` shell hosts from 09-08; 23 orphaned pipeline tools (findstr/grep/tail/tee/cat/bash) from dead agent sessions.
- `gh auth status`: token for SaberMage invalid.
- Sysinternals `handle64.exe` downloaded to the liaison session scratchpad only.

## Next steps after reboot

1. **Verify clean daemon.** `spt node status` should answer in under a second. Check `logs\daemon.stderr.log` for a fresh generation with no `BROKER_BIND_FAIL`, and `brain.ready` naming the live brain pid. Confirm exactly one `spt-daemon-*` pipe id exists.
2. **Decide the version.** Daemon will be 0.67.1. If everything behaves, keep it. If the operator wants 0.68.0 back: `Copy-Item bin\spt.exe.old-105 bin\spt.exe` after `spt node stop`. Re-auth `gh auth login -h github.com` first if the intent is to pull a release from `BigscreenVR/spt-bs-core`.
3. **Reconcile stale endpoint records.** `spt endpoint list`, then for each endpoint shown online with no host process (doyle at minimum; also deployah, gears, perri, sparrow, flynn, webbie, lia, hertz, todlando, emphasys): `spt endpoint stop <id>` or `suspend <id>`. `spt endpoint gc` (report only) to see perch residue; the earlier census said 15 reapable, 10 refused (spool), 6 refused (empty).
4. **Bring agents back deliberately.** `spt go <id>` per agent, starting with doyle (alt account, cwd `projects\spt-core`). Watch for the two-daemon condition reappearing.
5. **Check adapters vs 0.67.1.** If shells or hooks misbehave, run `spt adapter update` or reinstall adapters matching the core version.
6. **Get doyle's plain-language summary** (the original request) once he is live: `spt send doyle`.
7. **Follow-ups worth filing against spt-core:**
   - Broker bind should fail loudly and stop the brain, not let the brain attach to a stale pipe and hang every client.
   - Liveness must use exit status, not "pipe exists" or "OpenProcess succeeds" (see the earlier SHELL_ALREADY_ONLINE finding).
   - Find why two daemons with two pipe ids coexisted.
   - Stale `startup_endpoints`/perch `info.json` should be re-validated against real pids at daemon start.
   - Plugin install record for `sptc@cplugs` was rewritten with the alt account's `CLAUDE_CONFIG_DIR` path; the account switcher leaks install paths across config dirs.
