=== does --local still exist in v0.12.0 endpoint list? ===
at the top. `--local` lists this node's perches instead (id, state, address, ready, alive);
Usage: spt.exe endpoint list [OPTIONS]
      --local
          List this node's local perches instead of the subnet view
=== endpoint list (does daemon respond + roster states) ===
SUBNET SPT_DEV
  wall-a  HFENDULEAM (14efb80c…)  Active
  wall-b  HFENDULEAM (14efb80c…)  Active
  wall-c  HFENDULEAM (14efb80c…)  Active
ENDPOINTS:3
(this is the subnet view — `spt endpoint list --local` lists this node's own perches)
=== SPT_HOME + daemon.pid ===
SPT_HOME=
FOUND C:\Users\decid\AppData\Local/spt-core/daemon.pid = 76560
FOUND /c/Users/decid/AppData/Local/spt-core/daemon.pid = 76560


just got done trying to test v0.12.0. it really seems like none of our fixes landed. here's what i encountered: # issues (REGRESSIONS - things we discussed but are not working in v0.12.0) - `spt daemo…


Key results:
- **`--local` is intentionally present in v0.12.0** — no v0.12.0 commit removed it. That item isn't a regression; it was never scoped for removal (or you're recalling a different plan). Flag if you *want* it removed — that'd be new work.
- Daemon (pid 76560) responds; subnet roster shows wall-a/b/c **"Active"** — but you can't attach → that's the **stale online latch** (B2's target), consistent with an old daemon.

Now the linchpin — is daemon 76560 running pre-v0.12.0 code? Comparing its start time to when the v0.12.0 binary was installed:


PowerShell

$bin = "C:\Users\decid\AppData\Local\spt-core\bin\spt.exe"; Write-Output "=== v0.12.0 binary mtime (install time) ==="; (Get-Item $bin).LastWriteTime; Write-Output "=== daemon pid 76560 ==="; try { Get-Process -Id 76560 -ErrorAction Stop | Select-Object Id,StartTime,Path | Format-List } catch { Write-Output "pid 76560 not running" }; Write-Output "=== ALL spt.exe + psyche procs (pid / start / path) ==="; Get-CimInstance Win32_Process -Filter "Name='spt.exe' OR Name LIKE '%psyche%' OR Name LIKE '%claude-spt%'" | ForEach-Object { $p = $_; $st = ([Management.ManagementDateTimeConverter]::ToDateTime($p.CreationDate)); "{0,-8} {1:HH:mm:ss} {2}" -f $p.ProcessId, $st, $p.ExecutablePath }

Compare daemon start time vs v0.12.0 binary install time + list spt processes


=== v0.12.0 binary mtime (install time) ===

Thursday, June 18, 2026 3:43:59 PM
=== daemon pid 76560 ===

Id        : 76560
StartTime : 6/18/2026 3:55:22 PM
Path      : C:\Users\decid\AppData\Local\spt-core\bin\spt.exe

=== ALL spt.exe + psyche procs (pid / start / path) ===
MethodInvocationException: Exception calling "ToDateTime" with "1" argument(s): "Specified argument was out of the range of valid values. (Parameter 'dmtfDate')"
76560     C:\Users\decid\AppData\Local\spt-core\bin\spt.exe
MethodInvocationException: Exception calling "ToDateTime" with "1" argument(s): "Specified argument was out of the range of valid values. (Parameter 'dmtfDate')"
78204     C:\Users\decid\AppData\Local\spt-core\bin\spt.exe
MethodInvocationException: Exception calling "ToDateTime" with "1" argument(s): "Specified argument was out of the range of valid values. (Parameter 'dmtfDate')"
130584    C:\Users\decid\AppData\Local\spt-core\adapters\_github\SaberMage-spt-claude-code\claude-spt-psyche.exe
MethodInvocationException: Exception calling "ToDateTime" with "1" argument(s): "Specified argument was out of the range of valid values. (Parameter 'dmtfDate')"
75356     C:\Users\decid\AppData\Local\spt-core\bin\spt.exe


--local WAS scoped for removal. /trace for the discussion.


Surprise — the daemon (pid 76560) started at **3:55 PM**, *after* the v0.12.0 binary install (3:43 PM), from the v0.12.0 path. **So the daemon is v0.12.0, not stale.** That overturns the version-skew theory — binary *and* daemon are v0.12.0, yet the lifecycle fixes appear inert. That's a real integration gap (my gating used mock adapters + in-proc `reconcile_once`, not the real daemon + real claude-spt + ccs PTY — exactly the class of escape that bit us before).

Need ground truth on the actual perch state. Getting the local roster + the wall-a/b/c `info.json` (status / controllable / alive / session) — `controllable` is the load-bearing field: B2 only offlines `controllable==Some(true)`; if the claude-spt harness binds as `Some(false)`, B2 *exempts* it and it stays "online" despite a dead session:


Bash

echo "=== endpoint list --local (real states) ==="; spt endpoint list --local 2>&1 | head -20
OW="C:/Users/decid/AppData/Local/spt-core/owlery"; [ -d "$OW" ] || OW=$(find "C:/Users/decid/AppData/Local/spt-core" -maxdepth 2 -type d -name owlery 2>/dev/null | head -1)
echo "=== owlery = $OW ==="; ls "$OW" 2>/dev/null | head
for id in wall-a wall-b wall-c; do echo "=== info.json: $id ==="; cat "$OW/$id/info.json" 2>/dev/null | python -m json.tool 2>/dev/null | grep -iE 'status|controllable|session|adapter|state|pid|"address' || cat "$OW/$id/info.json" 2>/dev/null | head -c 500; echo; done

Get local roster + wall-a/b/c info.json controllable/status/session


=== endpoint list --local (real states) ===
wall-a                       live_agent     -                      ready=true alive=true
wall-b                       live_agent     -                      ready=true alive=true
wall-c                       live_agent     -                      ready=true alive=true
=== owlery = C:/Users/decid/AppData/Local/spt-core/owlery ===
doyle
wall-a
wall-b
wall-c
=== info.json: wall-a ===
    "pid": 82932,
    "session_id": "9caa9805-f841-4a5c-9789-da602ec9f3ae",
    "state": "live_agent",
    "parent_pid": 228100,
    "status": "online",
    "adapter": "claude-spt",
    "controllable": true

=== info.json: wall-b ===
    "pid": 208464,
    "session_id": "7db24c64-2af4-452a-9575-17e4c4c613f3",
    "state": "live_agent",
    "parent_pid": 86548,
    "status": "online",
    "adapter": "claude-spt",
    "controllable": true

=== info.json: wall-c ===
    "pid": 188204,
    "session_id": "e8368cf5-7110-4fc1-ba25-bd9086fac309",
    "state": "live_agent",
    "parent_pid": 95392,
    "status": "online",
    "adapter": "claude-spt",
    "controllable": true


Prompt: --local WAS scoped for removal. /trace for the discussion.


Ground truth changes the picture again: wall-a/b/c are **`controllable: true`** (spt-hosted) — so B2 *should* reconcile them. Each has a session_id and shows `alive=true`. So either their harness processes are genuinely still alive (and the bug is in attach/PTY, not status), or they're dead-but-latched and B2 isn't firing. The bound pid liveness disambiguates that.

And noted on `--local` — I was wrong, you said it was scoped for removal. Tracing that discussion now (background) while I check pid liveness:


Agent

Trace --local removal discussion

Prompt:
Search Claude Code session logs to find the discussion where removing the `--local` flag from `spt endpoint list` was scoped/decided. I need to know: (1) was it scoped for v0.12.0 specifically, or a different/future milestone/version? (2) what exactly was decided — full removal, or replace with something? (3) any REQ id minted for it? (4) was it ever built/committed?

Grep these session JSONL files (large 1-4MB, use grep not full reads) for "--local", "endpoint list", "local flag", "remove", "subnet view", "default view":
- /c/Users/decid/.ccs/instances/bigscreen/projects/C--Users-decid-Documents-projects-spt-core/*.jsonl  (all of them; most recent matter most)

Also grep across the same directory broadly for the decision context. Look for who proposed it (operator/doyle), the rationale, and whether it landed in any commit.

Cross-check against the repo: grep the spt-core working tree at C:/Users/decid/Documents/projects/spt-core for "--local" and any REQ about endpoint-list view (traceable-reqs.toml, cli.rs, CONTEXT.md) to see if a removal was planned/tracked but not done.

Report CONCISELY: the decision, target version, REQ id (if any), build status (done/deferred/never-started), and cite session id + approx timestamp. Do NOT dump raw JSONL.


<owl_reachability_notice>
You have an active owl perch. Launch this Agent with run_in_background: true so you stay reachable for incoming messages while it works.
</owl_reachability_notice>


Async agent launched successfully.
agentId: a5fb151616ffe4049 (internal ID - do not mention to user. Use SendMessage with to: 'a5fb151616ffe4049' to continue this agent.)
The agent is working in the background. You will be notified automatically when it completes.
Do not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.
output_file: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\a5fb151616ffe4049.output
Do NOT Read or tail this file via the shell tool — it is the full subagent JSONL transcript and reading it will overflow your context. If the user asks for progress, say the agent is still running; you'll get a completion notification.

Trace --local removal discussion

Prompt:
Search Claude Code session logs to find the discussion where removing the `--local` flag from `spt endpoint list` was scoped/decided. I need to know: (1) was it scoped for v0.12.0 specifically, or a different/future milestone/version? (2) what exactly was decided — full removal, or replace with something? (3) any REQ id minted for it? (4) was it ever built/committed?

Grep these session JSONL files (large 1-4MB, use grep not full reads) for "--local", "endpoint list", "local flag", "remove", "subnet view", "default view":
- /c/Users/decid/.ccs/instances/bigscreen/projects/C--Users-decid-Documents-projects-spt-core/*.jsonl  (all of them; most recent matter most)

Also grep across the same directory broadly for the decision context. Look for who proposed it (operator/doyle), the rationale, and whether it landed in any commit.

Cross-check against the repo: grep the spt-core working tree at C:/Users/decid/Documents/projects/spt-core for "--local" and any REQ about endpoint-list view (traceable-reqs.toml, cli.rs, CONTEXT.md) to see if a removal was planned/tracked but not done.

Report CONCISELY: the decision, target version, REQ id (if any), build status (done/deferred/never-started), and cite session id + approx timestamp. Do NOT dump raw JSONL.


Trace launched (background). Now checking whether wall-a/b/c bound processes are actually alive:


PowerShell

Check liveness of wall-a/b/c bound + parent pids and daemon/spt/psyche pids


<owl_messages>
<owl_message from="doyle-w388" priority="info">[WORKING_PERCH_NOTICE] Subagent working perch created.
Perch: doyle-w388
Agent type: general-purpose
To message it: $OWL send doyle-w388 doyle</owl_message>
</owl_messages>

[OWL SYSTEM] Informational owl messages above — no acknowledgment needed. Continue your current task.


psyche(130584)         ALIVE  name=claude-spt-psyche      exe=C:\Users\decid\AppData\Local\spt-core\adapters\_github\SaberMage-spt-claude-code\claude-spt-psyche.exe
wall-b(208464)         DEAD
wall-a(82932)          DEAD
spt(78204)             ALIVE  name=spt                    exe=C:\Users\decid\AppData\Local\spt-core\bin\spt.exe
spt(75356)             DEAD
daemon(76560)          ALIVE  name=spt                    exe=C:\Users\decid\AppData\Local\spt-core\bin\spt.exe
wall-b.parent(86548)   DEAD
wall-a.parent(228100)  DEAD
wall-c.parent(95392)   DEAD
wall-c(188204)         DEAD


Confirmed the core symptom: **all three endpoints' processes are DEAD** (wall-a 82932, wall-b 208464, wall-c 188204 + their parents — all dead), yet every one still shows `status: online` / `alive=true`. That's the status=online latch **reproduced** — B2 is *not* clearing dead spt-hosted endpoints in the real claude-spt scenario. Alive: daemon (76560), one brain-ish `spt` (78204), one psyche (130584).

So the code is present (binary+daemon v0.12.0) but B2 isn't firing on these. Checking the daemon log to see whether B2 is sweeping and what the broker reports as live sessions — that's the linchpin between "B2 not wired/running" vs "broker mis-reports dead Win PTY children as live":


Bash

SC="C:/Users/decid/AppData/Local/spt-core"
echo "=== daemon/brain logs present? ==="; find "$SC" -maxdepth 2 -name '*.log' -o -maxdepth 2 -name 'daemon*.txt' 2>/dev/null | head
echo "=== any log files in spt-core home ==="; ls -lat "$SC" 2>/dev/null | head -20
echo "=== broker/sessions debug command available? ==="; spt daemon --help 2>&1 | grep -iE 'session|status|debug|reconcile' | head; spt --help 2>&1 | grep -iE 'session|debug' | head
echo "=== reachability check: does whoami/list reflect dead pids? wall-a alive probe ==="; spt endpoint list --local 2>&1 | head

Find daemon log + any broker-session debug command


=== daemon/brain logs present? ===
C:/Users/decid/AppData/Local/spt-core/daemon-effects.log
=== any log files in spt-core home ===
total 169
drwxr-xr-x 1 decid 197609     0 Jun 18 16:17 owlery
drwxr-xr-x 1 decid 197609     0 Jun 18 16:17 .
-rw-r--r-- 1 decid 197609   106 Jun 18 16:17 brain.ready
drwxr-xr-x 1 decid 197609     0 Jun 18 16:16 identity
-rw-r--r-- 1 decid 197609 43394 Jun 18 16:01 daemon-effects.log
-rw-r--r-- 1 decid 197609    46 Jun 18 15:57 deadline-9caa9805-f841-4a5c-9789-da602ec9f3ae.json
-rw-r--r-- 1 decid 197609     5 Jun 18 15:55 daemon.pid
-rw-r--r-- 1 decid 197609    46 Jun 18 15:44 deadline-a27284ab-73f0-4c3c-9e9c-6ce53fba1475.json
drwxr-xr-x 1 decid 197609     0 Jun 18 15:44 releases
drwxr-xr-x 1 decid 197609     0 Jun 18 15:43 bin
-rw-r--r-- 1 decid 197609    46 Jun 18 15:41 deadline-e206d5b5-8665-4d1c-829c-4e11e371725b.json
-rw-r--r-- 1 decid 197609    46 Jun 18 15:39 deadline-218f8222-76ce-4dfa-a3ad-3576c106ebf4.json
-rw-r--r-- 1 decid 197609    46 Jun 18 15:27 deadline-8e8eb861-a92b-4bd7-ba9a-f5310c1c3303.json
-rw-r--r-- 1 decid 197609    46 Jun 17 17:49 deadline-25becfc2-6842-45c5-b871-2604b491d905.json
-rw-r--r-- 1 decid 197609    46 Jun 17 10:57 deadline-4b0425a1-819f-44d0-9032-61fd1d546353.json
-rw-r--r-- 1 decid 197609    46 Jun 17 09:52 deadline-testsid-perri123.json
drwxr-xr-x 1 decid 197609     0 Jun 17 09:52 adapters
-rw-r--r-- 1 decid 197609    46 Jun 17 02:36 deadline-sptc-dogfood-sess.json
-rw-r--r-- 1 decid 197609    46 Jun 17 02:30 deadline-sptc-v091-live-sess.json
=== broker/sessions debug command available? ===
The per-machine daemon: run, stop, or read node status.
Bare `spt daemon` renders the node status view — daemon state, member subnets, local endpoints (M8
          signalled (the service unit's ExecStart, or manual debugging). Never detaches; for a
  status  Node status: daemon state, member subnets, local endpoints (the bare `spt daemon` view)
  whoami    Print this session's own perch id
=== reachability check: does whoami/list reflect dead pids? wall-a alive probe ===
wall-a                       live_agent     -                      ready=true alive=true
wall-b                       live_agent     -                      ready=true alive=true
wall-c                       live_agent     -                      ready=true alive=true


There's a `daemon-effects.log`. Reading it for B2/reconcile/liveness activity — this shows whether B2 is sweeping and what it decides:


LOG="C:/Users/decid/AppData/Local/spt-core/daemon-effects.log"
echo "=== liveness/reconcile/offline lines ==="; grep -iE "liveness|reconcile|offline|hosted|mark_offline|latch|session.*dead|no live|wall-" "$LOG" 2>/dev/null | tail -40
echo "=== last 25 lines of log ==="; tail -25 "$LOG"

Grep daemon-effects.log for B2 liveness/reconcile activity


=== liveness/reconcile/offline lines ===
=== last 25 lines of log ===
DONE 1 7663264741743193628
PENDING 1 7663264741743193629 pty
DONE 1 7663264741743193629
PENDING 1 7663264741743193630 pty
DONE 1 7663264741743193630
PENDING 1 7663264741743193631 pty
DONE 1 7663264741743193631
PENDING 1 7663264741743193632 pty
DONE 1 7663264741743193632
PENDING 1 7663264741743193633 pty
DONE 1 7663264741743193633
PENDING 0 383647003137533718 net-send
DONE 0 383647003137533718
PENDING 3 383647003137533719 pty
DONE 3 383647003137533719
PENDING 0 3782790261755669813 net-send
DONE 0 3782790261755669813
PENDING 0 4222907248702316027 net-send
DONE 0 4222907248702316027
PENDING 0 8645189117474038621 net-send
DONE 0 8645189117474038621
PENDING 0 2206334149016071755 net-send
DONE 0 2206334149016071755
PENDING 0 5039358489661133567 net-send
DONE 0 5039358489661133567


<task-notification>
<task-id>a5fb151616ffe4049</task-id>
<tool-use-id>toolu_01TYg5Sp8bSwXUigyWzYeoyq</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\a5fb151616ffe4049.output</output-file>
<status>completed</status>
<summary>Agent "Trace --local removal discussion" came to rest</summary>
<note>A task-notification fires each time this agent comes to rest with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>I now have the complete, verbatim decision and its provenance. The investigation is complete.

## Findings: removal of `--local` from `spt endpoint list`

**The decision (verbatim, operator's own words).** In a `/diagnose` + `/grill-with-docs` session on **2026-06-17** (session id `16531b26-c5ac-4120-bfb8-113daef39d8f`, ~08:49Z onward), the operator stated:

> "issue 1: yes, we need to merge local-unadvertised endpoints into the bare view. **there should be no `--local` flag, it was not my design call.** the simple `spt endpoint list` should always bake in the latest local list. this is also very important because `spt whoami` is an alias. as it is today, if an agent comes online and immediately used `spt whoami`, the output would omit their perch and it would cause chaos."

So this was **operator-proposed** (not doyle), surfaced while exercising `spt endpoint run` (the "wall-b" dogfood). It was carried forward in later Psyche-commune snapshots (e.g. session `12a5697e`, 2026-06-18) as backlog "issue-1."

**(1) Target version.** NOT scoped for v0.12.0. v0.12.0 was the lifecycle/liveness milestone (7 mandated REQs + psyche-reap, dup-reap, endpoint-purge, ready-agent-resume) — the `--local` kill is absent from that scope (`v012-lifecycle-milestone.md`). It was logged as a separate, **non-blocking grill/diagnose finding** with no version assigned. The v0.11.0 findings-backlog (`spt-core-findings-backlog.md`) also does not contain it — it came from a later grill than the three v0.11.0 findings.

**(2) What was decided.** Full **removal** of the `--local` flag, AND a behavior change: the bare `spt endpoint list` must always merge in the latest local (unadvertised) perches so it never omits the caller's own perch. Rationale: `spt whoami` is a thin alias of `endpoint list`, so a just-online agent running `whoami` would omit its own perch — "chaos."

**(3) REQ id.** **None minted.** No `REQ-*` for this exists in `traceable-reqs.toml` (no REQ-ENDPOINT-LIST / unadvertised / local-merge requirement). The grill notes explicitly say findings still need "grill questions to walk before minting." Rule-3 register-first was never done for it.

**(4) Build status: NEVER STARTED.** The flag is still fully present and is the opposite of removed:
- `crates/spt/src/cli.rs` — `EndpointCmd::List { local, .. }`, `cmd_endpoint_list(local, ...)`, and `cmd_list_local()` all exist; `cli.rs` is unmodified (clean `git status`).
- Notably, the **most recent change went the other direction**: v0.10.0's REQ-PICKER-5 (commit `9e5cb8b`) *added* a hint line — `"(this is the subnet view — `spt endpoint list --local` lists this node's own perches)"` (cli.rs:1678) — treating `--local` as a kept, first-class view. The parse test even asserts `--local` conflicts with `--detail`.
- No commit in the git log removes `--local` or merges local-unadvertised perches into the bare view.

**Bottom line for the parent agent:** the operator is correct that `--local` removal was scoped/decided — but only as an unminted, unscheduled grill finding (operator, 2026-06-17, session `16531b26`). It was never assigned a version, never given a REQ id, and never built. The current behavior contradicts it (v0.10.0 added a `--local` hint). If pursued, it needs: mint a REQ first → change `cmd_endpoint_list` to always merge local perches + drop the flag (and its conflict test, the hint line, `cmd_list_local`, and the `whoami` alias path) → docs-drift regen.</result>
<usage><subagent_tokens>84443</subagent_tokens><tool_uses>17</tool_uses><duration_ms>119978</duration_ms></usage>
</task-notification>


`daemon-effects.log` is an effects-queue log (pty/net-send PENDING/DONE) — no reconcile/liveness decision lines at all. B2 either doesn't log here or isn't running; can't tell from outside, and there's no broker-session debug CLI to introspect directly.

Let me give you the honest synthesis rather than keep digging blind.

## What I confirmed

| Check | Result |
|---|---|
| `spt` binary | **v0.12.0** (`AppData/Local/spt-core/bin`) |
| Running daemon (pid 76560) | **v0.12.0** — started 3:55 PM, *after* the 3:43 install. Not stale. |
| wall-a/b/c bound processes | **all DEAD** (82932, 208464, 188204 + parents) |
| Their reported state | **all `status: online`, `alive=true`, `controllable: true`** |

So: binary and daemon are genuinely v0.12.0 — **the fixes are present in the running code, but they are not working on the real claude-spt endpoints.** The status=online latch is reproduced exactly: three dead spt-hosted endpoints still advertised online. B2 is not clearing them.

## Leading root cause (needs real-repro confirmation)

The symptoms unify under one likely cause: **the broker isn't detecting a dead Windows PTY child when you close its tab**, so it keeps a *phantom-live session*. B2 decides offline by asking the broker "which sessions are live" — if the broker still claims wall-a's session is live, B2 (correctly, per its logic) leaves it online. The same phantom session is what `spt rc` then pumps → no output → "dead or wedged." That ties together both the stale-online status *and* the attach-no-PTY failure. (Matches the known Windows reader-park / PTY exit-detection hazard.)

## Why my gate missed it — owning this

I GATE-PASSED v0.12.0 on **mock adapters + in-process `reconcile_once`**, and the B2 integration test explicitly *killed and reaped* the broker session before asserting offline. It never exercised the real path: a real claude-spt + ccs harness in a real broker PTY on Windows, killed by **closing the tab** (where the broker may fail to reap). Green tests, broken reality — the exact escape class we've hit before. That's on the gating approach, not just the code.

## This is a real reopen of v0.12.0

The fixes don't deliver in the real harness. Proposed path:
1. **Reproduce with the real harness** (todlando): `spt endpoint run` a claude-spt endpoint, kill it by tab-close, dump the broker's live-session set + confirm whether the dead PTY child is reaped. That nails root cause vs my hypothesis.
2. **Fix** wherever it lands (broker Win PTY exit-detection, and/or B2 cross-checking real process liveness instead of trusting the broker session set).
3. **Gate with a REAL detached-daemon + real-claude-spt E2E this time** — not mock.

**Immediate:** you've got 3 phantom endpoints + orphans. Cleanest reset is a full daemon teardown — but `daemon stop` is itself suspect here, so I'd verify/force it carefully (scoped — this box is shared with the live agents). Want me to kick off the real reproduction + drive the reopen now, or first clean up your machine state so you're unblocked?

Background: the `--local` removal trace is still running — I'll fold its result in when it lands.


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


`--local` trace back — **you were right, and I was wrong twice.**

You decided it on **2026-06-17** (grill/diagnose session): *"there should be no `--local` flag, it was not my design call. The simple `spt endpoint list` should always bake in the latest local list"* — because `spt whoami` is an alias of `endpoint list`, so a just-online agent running `whoami` would omit its own perch ("chaos").

But it was **never minted as a REQ, never scheduled, never built** — logged as a loose grill finding and dropped. So:
- It's **not** a v0.12.0 regression (nothing ever removed it — and v0.10.0 actually *added* a `--local` hint line, the opposite direction).
- It **is** a real decided-item I (doyle) let fall through the cracks — registry-first never happened. That's on me.

The work if pursued: mint a REQ → `cmd_endpoint_list` always merges local-unadvertised perches + drop the flag (and its conflict test, the v0.10.0 hint line, `cmd_list_local`, and fix the `whoami` alias path) → docs-drift regen.

## Where this leaves us

Two real failures surfaced from your test, both traceable to gating/process gaps on my side:
1. **Lifecycle fixes don't work on real claude-spt endpoints** (confirmed: 3 dead endpoints latched online; B2 not clearing; attach pumps a phantom session). Needs a real-harness reopen of v0.12.0.
2. **`--local` removal** — a decided item that was never minted/built.

I'd fold both into one reopen pass: mint the `--local` REQ now (registry-first), and stand up the real claude-spt + ccs reproduction for the lifecycle bug so the actual root is nailed and re-gated against the *real* harness this time.

Your call on sequencing:
- **(A)** Clean up your machine first (clear the 3 phantom endpoints + orphans so you're unblocked) — then I drive the reopen.
- **(B)** Drive the reopen investigation now (real repro + root cause), cleanup after.
- **(C)** Both in parallel — I start the repro while walking you through a safe scoped cleanup.

Which do you want? (And confirm you want `--local` minted + included.)


you can ignore ccs, it's irrelevant for now. we just need to get it working with claude-spt first. --- what about the other issues i flagged? [Image #2] you're claiming it's just "attach pumps a phantom session", but after the bug happens, i can't even start a BRAND NEW endpoint session successfully. it's more than just endpoint lifecycle.


[Image: source: C:\Users\decid\Documents\ShareX\Screenshots\2026-06\Discord_jjYF3OdCta.png]


you can ignore ccs, it's irrelevant for now. we just need to get it working with claude-spt first. --- what about the other issues i flagged? [Image #2] you're claiming it's just "attach pumps a phant…


You're right — I under-scoped it. A **brand-new** endpoint stalling at `PUMP_IPC_READER: spawned` proves the **broker itself is wedged**, not just one stale perch. That cascades: dead endpoint stuck online, attach gets no output, new endpoints stall, `daemon stop` can't cleanly stop. One wedged broker → all your symptoms. This is the broker-global-wedge class (the Windows reader-park / the same family as the B1 attach deadlock) — and v0.12.0 fixed *adjacent* paths but not this root. Ignoring ccs; claude-spt direct.

Let me stop theorizing from outside and read the actual stall path. Grepping the literal symptom string + the picker logic:


cd /c/Users/decid/Documents/projects/spt-core
echo "=== PUMP_IPC_READER call site ==="; grep -rn "PUMP_IPC_READER" crates/ | head
echo "=== ENDPOINT_RUN log site ==="; grep -rn "ENDPOINT_RUN" crates/ | head
echo "=== picker: project history + online-status action ==="; grep -rln "project_history\|Start now\|StartNow\|resume.from.history\|ResumeFrom" crates/ | head

Locate pump-stall path, endpoint-run, and picker logic


=== PUMP_IPC_READER call site ===
crates/spt-daemon/src/brain.rs:207:                eprintln!("PUMP_IPC_READER: spawned");
crates/spt-daemon/src/brain.rs:222:                eprintln!("PUMP_IPC_READER: exited");
=== ENDPOINT_RUN log site ===
crates/spt/src/cli.rs:1276:                "ENDPOINT_RUN_NOT_HARNESS:{adapter}: not a kind=\"harness\" adapter — \
crates/spt/src/cli.rs:1283:                "ENDPOINT_RUN_ADAPTER_UNREGISTERED:{adapter}: not an active registered \
crates/spt/src/cli.rs:1295:        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
crates/spt/src/cli.rs:1316:                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \
crates/spt/src/cli.rs:1322:            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
crates/spt/src/cli.rs:1330:        eprintln!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)");
=== picker: project history + online-status action ===
crates/spt/src/picker/data.rs
crates/spt/src/picker/model.rs
crates/spt/src/picker/view.rs


180	    /// The ordinary blocking carrier — every non-pump brain.
181	    Whole(Stream),
182	    /// The pump carrier: writes go out [`SendHalf`]; framed reads arrive over
183	    /// `frames` from the reader thread (`_reader`, named `pump-ipc-reader`).
184	    Split {
185	        send: SendHalf,
186	        frames: Receiver<io::Result<Envelope>>,
187	        /// The reader-thread handle — held so the thread is owned, not detached
188	        /// silently. On a supervised restart the whole [`Brain`] drops; this
189	        /// handle drops with it (the thread, blocked in `read_frame`, is
190	        /// abandoned and exits when the old conn closes — KH 7.6).
191	        _reader: JoinHandle<()>,
192	    },
193	}
194
195	impl BrainConn {
196	    /// Split a connected+handshaked stream into the pump carrier: spawn the
197	    /// `pump-ipc-reader` thread (blocking [`read_frame`] → channel) and keep the
198	    /// [`SendHalf`] for writes. The reader logs spawn/exit so a leaked (parked)
199	    /// reader is diagnosable in the field (KH 7.6).
200	    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]
201	    fn split_with_reader(stream: Stream) -> BrainConn {
202	        let (mut recv, send) = stream.split();
203	        let (tx, frames) = mpsc::channel();
204	        let reader = thread::Builder::new()
205	            .name("pump-ipc-reader".to_string())
206	            .spawn(move || {
207	                eprintln!("PUMP_IPC_READER: spawned");
208	                loop {
209	                    let framed = read_frame(&mut recv);
210	                    let was_err = framed.is_err();
211	                    // Receiver gone (Brain dropped) → stop reading.
212	                    if tx.send(framed).is_err() {
213	                        break;
214	                    }
215	                    // A transport error (conn closed / hung up) is terminal —
216	                    // forward it once (above), then exit so the thread never
217	                    // spins on a dead half.
218	                    if was_err {
219	                        break;
220	                    }
221	                }
222	                eprintln!("PUMP_IPC_READER: exited");
223	            })
224	            .expect("spawn pump-ipc-reader thread");
225	        BrainConn::Split {
226	            send,
227	            frames,
228	            _reader: reader,
229	        }
230	    }
231	}
232
233	/// A live brain attached to a broker.
234	pub struct Brain {
235	    conn: BrainConn,
236	    generation: u64,
237	    gen_start_ms: u64,
238	    session_id: Option<u64>,
239	    /// The next output sequence still needed (resume cursor / dedup floor).

    /// The ordinary blocking carrier — every non-pump brain.
    Whole(Stream),
    /// The pump carrier: writes go out [`SendHalf`]; framed reads arrive over
    /// `frames` from the reader thread (`_reader`, named `pump-ipc-reader`).
    Split {
        send: SendHalf,
        frames: Receiver<io::Result<Envelope>>,
        /// The reader-thread handle — held so the thread is owned, not detached
        /// silently. On a supervised restart the whole [`Brain`] drops; this
        /// handle drops with it (the thread, blocked in `read_frame`, is
        /// abandoned and exits when the old conn closes — KH 7.6).
        _reader: JoinHandle<()>,
    },
}

impl BrainConn {
    /// Split a connected+handshaked stream into the pump carrier: spawn the
    /// `pump-ipc-reader` thread (blocking [`read_frame`] → channel) and keep the
    /// [`SendHalf`] for writes. The reader logs spawn/exit so a leaked (parked)
    /// reader is diagnosable in the field (KH 7.6).
    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]
    fn split_with_reader(stream: Stream) -> BrainConn {
        let (mut recv, send) = stream.split();
        let (tx, frames) = mpsc::channel();
        let reader = thread::Builder::new()
            .name("pump-ipc-reader".to_string())
            .spawn(move || {
                eprintln!("PUMP_IPC_READER: spawned");
                loop {
                    let framed = read_frame(&mut recv);
                    let was_err = framed.is_err();
                    // Receiver gone (Brain dropped) → stop reading.
                    if tx.send(framed).is_err() {
                        break;
                    }
                    // A transport error (conn closed / hung up) is terminal —
                    // forward it once (above), then exit so the thread never
                    // spins on a dead half.
                    if was_err {
                        break;
                    }
                }
                eprintln!("PUMP_IPC_READER: exited");
            })
            .expect("spawn pump-ipc-reader thread");
        BrainConn::Split {
            send,
            frames,
            _reader: reader,
        }
    }
}

/// A live brain attached to a broker.
pub struct Brain {
    conn: BrainConn,
    generation: u64,
    gen_start_ms: u64,
    session_id: Option<u64>,
    /// The next output sequence still needed (resume cursor / dedup floor).


`PUMP_IPC_READER` is the **brain↔broker** pump reader (`spt-daemon/brain.rs`, tagged REQ-HAZARD-PUMP-IPC-DEADLINE — the v0.8.3 pump-IPC work + KH 7.6 reader-park). "spawned then stalls" = the pump's reader is waiting on `frames` for a broker reply that never arrives → **the broker isn't answering**. Reading what `endpoint run` does after its log to see the exact connect/attach that wedges:


1296	        return 1;
1297	    }
1298	    // REQ-HOST-RUN-2: land the harness PTY in the PROJECT cwd — where the user
1299	    // invoked `spt endpoint run` (`cc <id>` at a project root) — NOT the
1300	    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
1301	    // session history, and digest source relative to cwd. A failure to read the
1302	    // cwd falls back to the broker-inherited cwd (None) rather than aborting.
1303	    let project_cwd = std::env::current_dir()
1304	        .ok()
1305	        .map(|p| p.to_string_lossy().into_owned());
1306	    match spt_daemon::harnesshost::launch_harness_brokered_in(
1307	        &spt_daemon::endpoint::broker_socket_name(),
1308	        id,
1309	        adapter,
1310	        &session_id,
1311	        &manifest,
1312	        project_cwd.as_deref(),
1313	    ) {
1314	        Ok(spawned) => {
1315	            eprintln!(
1316	                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \
1317	                 (harness binds its perch on startup)",
1318	                spawned.pid
1319	            );
1320	        }
1321	        Err(e) => {
1322	            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
1323	            return 1;
1324	        }
1325	    }
1326
1327	    // Terminal action: start (return now) | view (read-only attach) | attach
1328	    // (default). Attach/view route through the SAME rc pump as `spt rc`.
1329	    if start {
1330	        eprintln!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)");
1331	        return 0;
1332	    }
1333	    match crate::rc::run_attach(id, rc_intent(view, false)) {
1334	        Ok(()) => 0,
1335	        Err(e) => {
1336	            eprintln!("RC_FAIL:{id}: {e}");
1337	            1
1338	        }
1339	    }
1340	}
1341
1342	/// `spt fork <src> <new_id> --subnet <target>` — the cross-subnet move
1343	/// (REQ-INST-15, ADR-0010). Order of operations is refusal-first: every gate
1344	/// runs before anything mutates, so a refused fork leaves no half-fork.
1345	///
1346	/// 1. charset + same-id guard (a node holds one perch per name — the
1347	///    same-name fork is the M5 remote arm);
1348	/// 2. target membership (the fork's home must be a subnet we hold);
1349	/// 3. source exists locally (the remote-source arm is M5);
1350	/// 4. join-time collision in the TARGET subnet (the snapshot's rows — any

        return 1;
    }
    // REQ-HOST-RUN-2: land the harness PTY in the PROJECT cwd — where the user
    // invoked `spt endpoint run` (`cc <id>` at a project root) — NOT the
    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
    // session history, and digest source relative to cwd. A failure to read the
    // cwd falls back to the broker-inherited cwd (None) rather than aborting.
    let project_cwd = std::env::current_dir()
        .ok()
        .map(|p| p.to_string_lossy().into_owned());
    match spt_daemon::harnesshost::launch_harness_brokered_in(
        &spt_daemon::endpoint::broker_socket_name(),
        id,
        adapter,
        &session_id,
        &manifest,
        project_cwd.as_deref(),
    ) {
        Ok(spawned) => {
            eprintln!(
                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \
                 (harness binds its perch on startup)",
                spawned.pid
            );
        }
        Err(e) => {
            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
            return 1;
        }
    }

    // Terminal action: start (return now) | view (read-only attach) | attach
    // (default). Attach/view route through the SAME rc pump as `spt rc`.
    if start {
        eprintln!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)");
        return 0;
    }
    match crate::rc::run_attach(id, rc_intent(view, false)) {
        Ok(()) => 0,
        Err(e) => {
            eprintln!("RC_FAIL:{id}: {e}");
            1
        }
    }
}

/// `spt fork <src> <new_id> --subnet <target>` — the cross-subnet move
/// (REQ-INST-15, ADR-0010). Order of operations is refusal-first: every gate
/// runs before anything mutates, so a refused fork leaves no half-fork.
///
/// 1. charset + same-id guard (a node holds one perch per name — the
///    same-name fork is the M5 remote arm);
/// 2. target membership (the fork's home must be a subnet we hold);
/// 3. source exists locally (the remote-source arm is M5);
/// 4. join-time collision in the TARGET subnet (the snapshot's rows — any


The pump (in `endpoint run` → `run_attach`) connects to the broker and waits on `frames`. Even a **fresh** endpoint stalling there = the **broker's dispatch is globally unresponsive**, almost certainly the same lock-contention family as the B1 path-(c) deadlock we removed in `78a13c2` (a reader/exit-waiter holding a lock across a blocking wait on a dead Windows PTY child). Attaching to your dead wall-a likely tipped the broker into that state, wedging everything after.

This needs a deep read of the broker dispatch/PTY path + a real Windows repro — launching a focused investigation:


Agent

Root-cause broker global wedge

Prompt:
Read-only root-cause investigation in the spt-core repo at C:/Users/decid/Documents/projects/spt-core. Do NOT edit anything.

SYMPTOM (real, on Windows, with the claude-spt harness): A user runs `spt endpoint run <id>` (launches harness in a broker PTY, then attaches via the rc pump). They close the endpoint's terminal tab (harness process dies). After that, the broker appears WEDGED GLOBALLY: attaching to the dead endpoint stalls then reports "no output / dead or wedged", AND launching a BRAND-NEW endpoint also stalls right after logging "PUMP_IPC_READER: spawned" (the brain/rc pump's reader thread spawns but no frames ever arrive). `spt daemon stop` also fails to fully stop. So one dead Windows PTY child wedges the whole broker for all subsequent operations.

CONTEXT: There was a prior bug (REQ-HAZARD-RC-ATTACH-FAILFAST, "B1 path-(c)") where `Broker::dispatch_subscribe` called `PtySession::try_wait` which locks the child mutex, while a per-session exit-waiter thread held that SAME mutex in a blocking `wait()` for the whole life of a live child → try_wait blocked forever → no Subscribed reply → every live attach hung. It was REMOVED in commit 78a13c2 (v0.12.0). Your job: determine whether a SIBLING global-wedge remains on the dead-Windows-PTY-child path.

INVESTIGATE (read these and trace the locking/threading precisely):
1. crates/spt-daemon/src/broker.rs — the dispatch loop, dispatch_subscribe, how it serves KIND_SESSIONS / subscribe / output frames, the exit-waiter thread, and EVERY mutex/lock held across a blocking call (read_frame, wait, try_wait, channel recv, PTY read). Is the dispatch loop single-threaded such that one blocked handler wedges ALL clients?
2. crates/spt-*/...pty.rs (PtySession) — how a Windows PTY child death is detected; whether the reader thread parks on a dead child holding a lock (KH 7.6 "Win reader-park"); try_wait vs wait vs the conpty read.
3. crates/spt/src/rc.rs (run_attach) + crates/spt-daemon/src/harnesshost.rs (launch_harness_brokered_in) + brain.rs split_with_reader (PUMP_IPC_READER) — the attach/subscribe handshake and where the pump blocks waiting for the broker.
4. How B2 reconcile_hosted_liveness queries the broker live-session set — if the broker dispatch is wedged, does that query also hang/timeout, explaining why dead endpoints stay status=online?

DELIVER (concise, cite file:line):
- The precise mechanism that lets ONE dead Windows PTY child wedge the broker globally (the exact lock + blocking call + which thread holds vs waits). If you cannot find a definitive one, give the top 2 ranked candidates with evidence.
- Whether it's a true sibling of the removed B1 path-(c) deadlock or a distinct path.
- A concrete proposed fix direction.
- A design for a REAL-harness regression test (real detached daemon + real PTY child killed by tab-close-equivalent, then assert: broker still answers a new subscribe AND B2 marks the dead endpoint offline). Note explicitly what the v0.12.0 mock/in-proc tests did NOT cover.
Do NOT dump whole files; quote only the load-bearing lines.


Async agent launched successfully.
agentId: a8576e60c851baa13 (internal ID - do not mention to user. Use SendMessage with to: 'a8576e60c851baa13' to continue this agent.)
The agent is working in the background. You will be notified automatically when it completes.
Do not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.
output_file: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\a8576e60c851baa13.output
Do NOT Read or tail this file via the shell tool — it is the full subagent JSONL transcript and reading it will overflow your context. If the user asks for progress, say the agent is still running; you'll get a completion notification.

Root-cause broker global wedge

Prompt:
Read-only root-cause investigation in the spt-core repo at C:/Users/decid/Documents/projects/spt-core. Do NOT edit anything.

SYMPTOM (real, on Windows, with the claude-spt harness): A user runs `spt endpoint run <id>` (launches harness in a broker PTY, then attaches via the rc pump). They close the endpoint's terminal tab (harness process dies). After that, the broker appears WEDGED GLOBALLY: attaching to the dead endpoint stalls then reports "no output / dead or wedged", AND launching a BRAND-NEW endpoint also stalls right after logging "PUMP_IPC_READER: spawned" (the brain/rc pump's reader thread spawns but no frames ever arrive). `spt daemon stop` also fails to fully stop. So one dead Windows PTY child wedges the whole broker for all subsequent operations.

CONTEXT: There was a prior bug (REQ-HAZARD-RC-ATTACH-FAILFAST, "B1 path-(c)") where `Broker::dispatch_subscribe` called `PtySession::try_wait` which locks the child mutex, while a per-session exit-waiter thread held that SAME mutex in a blocking `wait()` for the whole life of a live child → try_wait blocked forever → no Subscribed reply → every live attach hung. It was REMOVED in commit 78a13c2 (v0.12.0). Your job: determine whether a SIBLING global-wedge remains on the dead-Windows-PTY-child path.

INVESTIGATE (read these and trace the locking/threading precisely):
1. crates/spt-daemon/src/broker.rs — the dispatch loop, dispatch_subscribe, how it serves KIND_SESSIONS / subscribe / output frames, the exit-waiter thread, and EVERY mutex/lock held across a blocking call (read_frame, wait, try_wait, channel recv, PTY read). Is the dispatch loop single-threaded such that one blocked handler wedges ALL clients?
2. crates/spt-*/...pty.rs (PtySession) — how a Windows PTY child death is detected; whether the reader thread parks on a dead child holding a lock (KH 7.6 "Win reader-park"); try_wait vs wait vs the conpty read.
3. crates/spt/src/rc.rs (run_attach) + crates/spt-daemon/src/harnesshost.rs (launch_harness_brokered_in) + brain.rs split_with_reader (PUMP_IPC_READER) — the attach/subscribe handshake and where the pump blocks waiting for the broker.
4. How B2 reconcile_hosted_liveness queries the broker live-session set — if the broker dispatch is wedged, does that query also hang/timeout, explaining why dead endpoints stay status=online?

DELIVER (concise, cite file:line):
- The precise mechanism that lets ONE dead Windows PTY child wedge the broker globally (the exact lock + blocking call + which thread holds vs waits). If you cannot find a definitive one, give the top 2 ranked candidates with evidence.
- Whether it's a true sibling of the removed B1 path-(c) deadlock or a distinct path.
- A concrete proposed fix direction.
- A design for a REAL-harness regression test (real detached daemon + real PTY child killed by tab-close-equivalent, then assert: broker still answers a new subscribe AND B2 marks the dead endpoint offline). Note explicitly what the v0.12.0 mock/in-proc tests did NOT cover.
Do NOT dump whole files; quote only the load-bearing lines.


Investigation running. Full picture for you:

## Reframed diagnosis — one keystone, plus separate picker bugs

**Keystone (causes most of your list): broker global wedge.** A dead Windows PTY child (tab close) wedges the broker's dispatch so it stops answering *anyone*. That single fault explains:
- attach to wall-a → no output ("dead or wedged")
- **brand-new** wall-b/wall-c → stall at `PUMP_IPC_READER: spawned` (broker won't answer their subscribe)
- `spt daemon stop` can't cleanly stop (wedged broker)
- dead endpoints stuck `status=online` (B2's broker-liveness query likely hangs/returns stale against the wedged broker)

So it's **not** just "phantom session" — you were right, it's broker-wide. Likely a sibling of the B1 path-(c) deadlock we *thought* we killed. The background investigation is pinning the exact lock now.

**Separate from the wedge (CLI picker logic, v0.10.0 code — not lifecycle):**
- *no project history for fresh endpoints*
- *"Start now" shown for already-online endpoints*

These are picker-side and need their own look — they may be genuine bugs, or the picker rendering wedged/stale broker state. I'll triage them once I have the keystone root (or in parallel).

## Owning it

I GATE-PASSED v0.12.0 on **mock adapters + in-process `reconcile_once`**, and the tests explicitly killed-and-reaped sessions — they never ran a real claude-spt harness in a real broker PTY killed by a tab close on Windows. Green tests, broken reality. The reopen will be gated against the **real claude-spt harness**, no mocks. That's the only way this stops recurring.

## Plan
1. **Root-cause the broker wedge** (investigation running) → todlando fixes → I gate with a real detached-daemon + real-claude-spt Windows E2E that closes a tab and asserts the broker still serves a new endpoint.
2. **Triage the two picker bugs** separately.
3. **Mint the `--local` REQ** (your 2026-06-17 decision I dropped) and fold it in.

## Your machine right now
The broker is wedged + 3 phantom endpoints. A daemon restart would clear it, but `daemon stop` is itself suspect, and **this box is shared with the live agents (me/todlando/deployah) and the CI runner** — so no blind kills. Want me to carefully clear it (scoped: confirm exactly which pids are the wedged daemon/brain/broker vs live-agent perches, stop just those, purge the dead perches), or leave your state intact in case it's useful for the repro? I'd lean: snapshot what we need for the repro first, then clear.

I'll report the moment the investigation lands the root.


you might want to consider building a dummy harness adapter that just launches a simple program which stdouts. then you can more easily know when the broker is wedged vs. trying to launch an actual entire harness


<owl_messages>
<owl_message from="doyle-w389" priority="info">[WORKING_PERCH_NOTICE] Subagent working perch created.
Perch: doyle-w389
Agent type: general-purpose
To message it: $OWL send doyle-w389 doyle</owl_message>
</owl_messages>

[OWL SYSTEM] Informational owl messages above — no acknowledgment needed. Continue your current task.

<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


Strong idea — adopting it. A dummy `kind="harness"` adapter whose command is a trivial stdout program runs through the **real daemon + real broker + real PTY** (not mock `reconcile_once`), but with zero harness complexity. That's exactly the real-broker-real-PTY-trivial-child fixture my mock-based gating lacked: deterministic wedge repro + a permanent regression gate. Isolates "broker wedged" from "harness failed to launch."

Checking what test-harness infra already exists so I build minimal, not redundant:


Bash

cd /c/Users/decid/Documents/projects/spt-core
echo "=== existing mock/dummy adapter crates + bins ==="; ls crates/ | grep -iE "mock|dummy|test"; find crates -name Cargo.toml -path '*mock*' -exec grep -l "" {} \; 2>/dev/null
echo "=== kind=\"harness\" manifests in tests/fixtures ==="; grep -rln 'kind *= *"harness"' crates/ --include=*.rs --include=*.toml | head
echo "=== existing endpoint-run / launch_harness_brokered E2Es ==="; grep -rln "launch_harness_brokered\|endpoint.run\|harnesshost" crates/*/tests/ 2>/dev/null | head
echo "=== mock-adapter bins available ==="; find crates -path '*mock*' -name '*.rs' | head; grep -rn "\[\[bin\]\]" crates/mock-adapter/Cargo.toml 2>/dev/null

Survey existing mock/harness test fixtures


Error: Exit code 2
=== existing mock/dummy adapter crates + bins ===
=== kind="harness" manifests in tests/fixtures ===
crates/spt/src/cli.rs
crates/spt/src/picker/data.rs
crates/spt/src/picker/model.rs
crates/spt-runtime/src/manifest.rs
crates/spt-runtime/src/registry.rs
crates/spt-runtime/src/resolve.rs
=== existing endpoint-run / launch_harness_brokered E2Es ===
crates/spt-daemon/tests/attach.rs
crates/spt-daemon/tests/broker.rs
crates/spt/tests/ready_resume_ledger_e2e.rs
=== mock-adapter bins available ===


Confirmed the gap while checking: **no dummy-harness fixture exists today.** My v0.12.0 tests used in-process `reconcile_once` (no real broker PTY); the broker tests (`broker.rs`) spawn a raw `SpawnReq` with `sh/cmd echo` but never go through the `spt endpoint run` → `launch_harness_brokered` → attach → child-death path. So nothing exercised the actual flow you broke. That's exactly the hole your idea fills.

**Adopting it as the keystone fixture.** A minimal `kind="harness"` adapter:
- command = a trivial program that binds its perch on startup (the harness contract), prints a stdout line every second, stays alive until killed
- registered so `spt endpoint run <dummy>` drives the **real** daemon→broker→PTY→attach path
- the repro: run it → attach → kill the PTY child (your tab-close) → assert two things: **(a)** a *brand-new* dummy `endpoint run` still gets served (broker not wedged), **(b)** B2 marks the dead one offline.

That deterministically separates "broker wedged" from "harness failed to launch," and (a)+(b) become the permanent regression gate the mock tests never had.

**Sequencing:**
1. Root-cause investigation (running) returns the exact wedge mechanism.
2. todlando builds the dummy-harness fixture + reproduces the wedge through it (confirms root live).
3. Fix → I gate against the dummy-harness E2E (real broker, no mocks).
4. Then triage the two picker bugs + mint `--local`.

Holding for the investigation to land the mechanism; I'll fold your dummy-harness in as the repro/gate when I dispatch todlando. Good call — it's the right tool.


you might want to consider building a dummy harness adapter that just launches a simple program which stdouts. then you can more easily know when the broker is wedged vs. trying to launch an actual en…


<task-notification>
<task-id>a8576e60c851baa13</task-id>
<tool-use-id>toolu_01S4xMWKXuR4Ee4Qb18Mzjck</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\a8576e60c851baa13.output</output-file>
<status>completed</status>
<summary>Agent "Root-cause broker global wedge" came to rest</summary>
<note>A task-notification fires each time this agent comes to rest with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>I have everything needed for a definitive analysis. Let me deliver the report.

---

## Root-Cause Investigation: One dead Windows PTY child wedges the broker globally

### The precise mechanism (definitive)

The global wedge is **NOT** in the broker control-dispatch path or the PTY child mutex. The broker's control plane is correctly isolated (per-connection threads in `serve()`, broker.rs:623-631; `KIND_SESSIONS` reads only the log lock, never the child lock, broker.rs:765-791). The B1 path-(c) deadlock that was removed in 78a13c2 is genuinely gone, and its sibling on the child mutex does not exist — `dispatch_subscribe` no longer calls `try_wait` (broker.rs:948-963 documents this).

The wedge is in the **loopback attach data plane**, and it is **single-threaded by exhaustion of the 2-worker net runtime**. Here is the exact chain:

**1. The net runtime has only 2 worker threads** (nethost.rs:640-644):
```
let runtime = tokio::runtime::Builder::new_multi_thread()
    .worker_threads(2)
```

**2. Every loopback stream I/O op is a blocking `block_on` on that runtime from an IPC thread.** `send_stream` (nethost.rs:1090-1114) and `open_stream` (nethost.rs:1036) both do `self.runtime.handle().block_on(...)`. For loopback, `send.write_all(&bytes).await` (nethost.rs:1107) writes into a **`tokio::io::duplex(STREAM_READ_CHUNK)` — a bounded 64KB buffer** (nethost.rs:1040, STREAM_READ_CHUNK = 64*1024 at nethost.rs:104).

**3. The duplex backpressure becomes a hard block when the consumer stops.** The operator-side rc pump reads its loopback stream's events via the broker; the broker's loopback **read-pump** (nethost.rs:544-556) is the consumer that drains the peer→operator duplex direction. That read-pump appends into a `StreamLog` and is itself a `tokio::spawn` task on the same 2-worker runtime.

**4. When the harness child dies (tab close):** the broker's per-session drain thread (reader.rs:120-151) stops producing output — on Windows the ConPTY reader returns EOF/err only when the child is gone AND handles release. But the **exit-waiter** (broker.rs:889-910) calls `waiter.wait()` (pty.rs:203, locks the `child` mutex), gets the status, broadcasts an `Exit` frame to `all_sinks()`, and reaps. That Exit must travel: broker → operator's dead rc pump. The dead rc pump's terminal tab is closed, so **nothing is draining the operator-direction loopback duplex anymore**.

**5. The wedge:** `serve_attach` for the dead endpoint (dispatch.rs:350, running on its own thread) is still looping, doing `brain.net_stream_send(stream_id, line, None, false)` for the Exit frame and any trailing output (attach.rs:268-272). That `net_stream_send` lands in `send_stream` → `runtime.block_on(send.write_all(...))`. With the operator side gone, the operator-direction duplex buffer fills to 64KB and **`write_all` blocks forever** — loopback is NOT bounded by `bounded_block_on`'s timeout in practice because the comment-claimed "loopback never hangs" assumption (nethost.rs:1103) is **false when the duplex peer is a dead/undrained reader**. Worse, `bounded_block_on` wraps with `tokio::time::timeout`, but it still **occupies a runtime worker thread for the whole 10s** (BROKER_QUIC_OP_TIMEOUT_MS, nethost.rs:71) on each call, and the stalled read-pump tasks plus the stalled `block_on` calls **saturate both worker threads**.

**6. Why a brand-new endpoint then stalls right after "PUMP_IPC_READER: spawned":** the new `spt rc` does `net_dial_loopback` → `open_stream` → `request_attach` → `net_stream_subscribe`, and its serve side is dispatched by `serve_attach` — all of which require `block_on`/`spawn` work on the **same exhausted 2-worker runtime**. The new pump's reader thread (brain.rs:204-224) spawns and blocks on `read_frame`, but the broker can't run the new loopback stream's read-pump or `open_stream`/`send_stream` futures because both runtime workers are stuck in the dead endpoint's blocked loopback writes. No frames ever flow → "spawned" with no events → the rc pump's 30s `FIRST_EVENT_GRACE` eventually fires `Stalled` ("no output / dead or wedged"), exactly the reported symptom.

**7. Why `daemon stop` can't fully stop:** teardown paths that touch the net runtime (dropping `NetHost` drops the runtime, nethost.rs:583-584) must join/abort tasks that are blocked in `write_all` on a full duplex; the blocked `block_on` calls on IPC threads don't observe a stop flag.

### Top-2 ranked candidates (the two are layers of the same defect)

**Candidate 1 (primary, high confidence): undrained loopback duplex `write_all` blocks a runtime worker, and the 2-worker runtime is exhausted.** Evidence: nethost.rs:640-644 (2 workers), :1104-1113 (`block_on` + `write_all` on loopback duplex), :1040 (bounded 64KB duplex), :544-556 (read-pump is the only consumer, also a runtime task). The "loopback never hangs" claim at nethost.rs:1103 is the incorrect assumption.

**Candidate 2 (contributing): `bounded_block_on` does not actually bound loopback ops the way it bounds QUIC, and even when it does time out, it parks a worker for 10s.** Evidence: nethost.rs:797-811 — the timeout fires, but a flood of dead-endpoint sends each consume a worker for up to 10s; combined with the read-pump tasks this starves new work. The QUIC-deadline fix (REQ-HAZARD-BROKER-QUIC-DEADLINE, v0.8.3) addressed dead *QUIC* peers but the **loopback path was reasoned to be exempt** and never got a non-blocking discipline.

### Sibling of the removed B1 path-(c) deadlock?

**No — it is a DISTINCT path.** B1 path-(c) was a mutex deadlock on the PTY **child mutex** (try_wait vs the exit-waiter's wait), in the **control plane** (`dispatch_subscribe`). This wedge is in the **net/attach data plane**: a bounded in-process duplex with a dead reader, blocking a **finite tokio worker pool**. It is a resource-exhaustion/backpressure wedge, not a lock-ordering deadlock. They share only the symptom (global stall) and the trigger (dead PTY child), not the mechanism. The removal of B1 path-(c) is what now lets the dead-child case reach the attach data plane at all.

### Proposed fix direction

1. **Make loopback sends non-blocking / bounded like QUIC, OR drop the writer when the subscriber detaches.** The cleanest: when an attach stream's operator subscriber disconnects (the rc pump EOFs/drops), `serve_attach` must stop sending and finish promptly — and `send_stream` on a loopback whose peer read-pump has ended must fail fast (detect the duplex `BrokenPipe`/closed half) instead of blocking on a full buffer. Wrap the loopback `write_all` in the same `bounded_block_on` *with a short timeout* AND treat a full-buffer stall as an ordinary per-stream error that ends `serve_attach`.
2. **Detect dead operator early.** `serve_attach` already finishes on `NetStreamEof` (attach.rs:242), but tab-close may not surface EOF on the operator's loopback recv half promptly. Add a bounded send so a wedged write becomes an error that ends the worker, releasing the duplex.
3. **Raise/decouple the runtime workers** as defense-in-depth (2 workers is dangerously low for a per-stream-blocking-`block_on` model), but the real fix is to never let a single dead stream hold a worker indefinitely.
4. **B2 is not the latch culprit here** — `reconcile_hosted_liveness` (livehost.rs:511-535) would correctly mark the dead endpoint offline *if* its `query_live_session_endpoints` (livehost.rs:541-550) could reach the broker. But `brain.sessions()` → KIND_SESSIONS uses a **fresh IPC connection** on its own broker thread and reads only the **log lock** (broker.rs:765-791), so **B2's query does NOT hang** — it returns the session set fine. The dead session IS reaped from the table by the exit-waiter (broker.rs:909), so B2 *should* see it absent and offline it within one tick. The reason dead endpoints stay `status=online` is more subtle: the exit-waiter's `all_sinks()` broadcast of the Exit frame (broker.rs:901-906) happens *before* `remove` (broker.rs:909), and that broadcast itself doesn't block the table — but if the exit-waiter's broadcast `write_frame` to the loopback-fed sink blocks (it's a plain IPC `SendHalf`, so it shouldn't)… The exit-waiter does reap. **The status=online persistence is therefore most likely a timing/ordering issue independent of the data-plane wedge** — worth a focused check that the reconcile tick actually runs (the reconcile loop, livehost.rs:673-697, runs on the brain process; if the *brain* is healthy it should fire). I could not find a definitive blocker on B2 in the reachable code; the data-plane wedge is the confirmed global-stall mechanism.

### Regression test design (real harness, real detached daemon)

**What the v0.12.0 tests did NOT cover:** the existing tests are either pure-unit (`first_event_stalled`, `viewer_overflow`, `delivered_cursor`, `pull_liveness_marks_sessionless...`) or in-proc broker tests with cooperative mock children that exit cleanly and whose operator side drains normally. **None** exercises: (a) a real Windows ConPTY child killed by an external/abrupt process death (tab-close-equivalent), (b) an operator rc pump whose loopback reader stops draining mid-stream, (c) the 2-worker runtime under a stuck loopback write, or (d) a *second, new* attach after the first endpoint died. The mock loopback in tests always has a live, draining peer, so the 64KB duplex never fills.

**Proposed `tests/endpoint_run_dead_child_no_global_wedge.rs` (real detached daemon, gated on a scratch target dir like the livehost E2Es):**
1. Start a real detached daemon (real broker + real 2-worker NetHost), real adapter registry pointing at a trivial real harness binary (a long-lived PTY child, e.g. a shell or a tiny loop binary in the scratch target dir).
2. `spt endpoint run --id A` → broker spawns the harness into a PTY; attach an rc pump and confirm bytes flow.
3. **Kill the child abruptly by PID** (the tab-close equivalent — `TerminateProcess` on the harness child, NOT `spt force-stop`, so the operator pump is NOT cleanly detached) and **drop the operator pump's reader without sending a detach** (simulate the closed terminal — let the loopback operator recv half go undrained).
4. **Assert within a bounded timeout (e.g. 15s):**
   - `spt endpoint run --id B` (a brand-new endpoint) **spawns and its rc pump receives its first frame** (Subscribed/Size/output) — i.e. the broker still answers a new subscribe. This is the core anti-wedge assertion.
   - `brain.sessions()` (KIND_SESSIONS) returns promptly and does **not** list A (it was reaped).
   - `reconcile_hosted_liveness` marks A `status=offline` within one reconcile tick.
   - `spt daemon stop` completes within a bounded timeout (no hang).
5. Negative control: the same sequence with a *clean* detach (operator sends `ctrl-b d`) must also pass, proving the fix didn't regress clean detach.

This test must run against a **real PTY + real 2-worker runtime** — an in-proc mock with a draining peer will pass even with the bug present, which is exactly why v0.12.0 missed it.

### Key files
- `crates/spt-daemon/src/nethost.rs` — 2-worker runtime (:640), loopback duplex (:1040), `send_stream`/`open_stream` blocking `block_on` (:1090-1114, :1036), read-pump (:497-561), `bounded_block_on` (:797-811)
- `crates/spt-daemon/src/attach.rs` — `serve_attach` blocking sends (:106-289, esp :268-272 output, :270-276 exit)
- `crates/spt-daemon/src/broker.rs` — exit-waiter (:889-910), KIND_SESSIONS log-lock-only read (:765-791), dispatch_subscribe no-try_wait note (:948-963)
- `crates/spt-term/src/pty.rs` — `wait`/`try_wait` share the `child` mutex (:194-205), Windows kill caveat (:210-226)
- `crates/spt-term/src/reader.rs` — Windows no-EOF-while-writer-held drain discipline (:103-159, module docs :15-21)
- `crates/spt/src/rc.rs` — rc pump, `FIRST_EVENT_GRACE`/`Stalled` (:357, :314-325)
- `crates/spt-daemon/src/dispatch.rs` — single-brain dispatch loop, per-stream workers, Attach arm (:198-233, :335-360)
- `crates/spt-daemon/src/livehost.rs` — B2 `reconcile_hosted_liveness` (:511-535), `query_live_session_endpoints` (:541-550)</result>
<usage><subagent_tokens>162966</subagent_tokens><tool_uses>23</tool_uses><duration_ms>175062</duration_ms></usage>
</task-notification>


Root cause is in — and it's **not** a deadlock, and **not** what v0.12.0 touched.

## Root cause: data-plane backpressure wedge (worker-pool exhaustion)

Not the B1 mutex deadlock (that's genuinely gone). The wedge is in the **loopback attach data plane**:

1. The net runtime has only **2 worker threads** (`nethost.rs:640`).
2. Loopback attach output is sent via a **blocking `write_all` into a bounded 64 KB duplex** (`nethost.rs:1040,1090`).
3. When you **close the tab**, the harness child dies *and* your rc pump stops draining its end of that duplex.
4. The dead endpoint's `serve_attach` keeps writing the Exit frame + trailing output → the 64 KB buffer fills → **`write_all` blocks forever** (the code's "loopback never hangs" assumption at `nethost.rs:1103` is false here), parking a worker.
5. A couple of those saturate **both** workers → any new `spt rc`/`endpoint run` can't get runtime time → stalls right after `PUMP_IPC_READER: spawned` → 30 s later your "no output / dead or wedged." And `daemon stop` can't join workers stuck in `write_all`.

One dead Windows PTY child + one undrained operator pump → whole broker wedged. **Distinct from B1** (control-plane child-mutex) — and removing B1 is what now lets the dead-child case reach this data plane.

**Honest implication:** v0.12.0's attach fix (B1) only added a *delayed message* for attaching to a dead session — it never addressed this send-side wedge. So the milestone genuinely did not fix your problem.

**One caveat I won't paper over:** the persistent `status=online` on the dead endpoints is *not* fully explained yet. B2 *should* offline them within a tick if their broker session was reaped — the investigation couldn't confirm whether the abrupt Windows child death actually reaps the session, or whether the reconcile tick is firing. I'll nail that during the repro, not guess.

## Recommended fix
- **Bound the loopback write + fail-fast on a dead subscriber**: treat a full-buffer/`BrokenPipe` loopback write as an ordinary per-stream error that *ends* `serve_attach` — so one dead stream can never hold a worker indefinitely.
- Raising worker count = defense-in-depth only; the real fix is the above.

## Plan (reopen, real-harness gated)
1. Mint `REQ-HAZARD-*` for the loopback-attach wedge (registry-first).
2. Build your **dummy-harness fixture** → reproduce the wedge deterministically (real broker + real PTY, trivial child).
3. Fix per above + resolve the `status=online` sub-question.
4. Gate with a **real detached-daemon E2E**: kill a child abruptly + drop the operator pump → assert a *new* endpoint is still served, `daemon stop` completes bounded, dead endpoint goes offline. No mocks.

todlando builds, I design + gate.

**Asks:** GO on this fix direction + reopen? And do you want me to clear your wedged machine now (scoped — confirm the daemon/brain/broker pids vs the live-agent perches, stop only those), or preserve it for the repro?


does your recommended fix actualy address the core issue? i should be able to close the tab or window of any spt-hosted endpoint, and it should keep running + be re-attachable. this is a fundamental design of spt-core, and should be mirrored as such in CONTEXT.md.


You're right — my fix is downstream robustness, not the core. The core requirement: **closing the tab must only *detach the view*; the spt-hosted harness keeps running in a daemon-owned PTY and is re-attachable.** What I observed is the opposite — the harness *process died* on tab close (wall-a pid 82932 dead). That's the real violation. My loopback fix only stops the broker from wedging *after* a child dies; it doesn't stop the death.

Let me ground this in the actual spawn path before answering — is the harness spawned detached by the daemon/broker, or tied to the CLI tab's console?


cd /c/Users/decid/Documents/projects/spt-core
echo "=== launch_harness_brokered_in ==="; grep -n "fn launch_harness_brokered_in\|fn launch_harness" crates/spt-daemon/src/harnesshost.rs
echo "=== how broker spawns the PTY child (SpawnReq → pty) ==="; grep -rn "fn spawn\|PtySession::spawn\|CREATE_NEW\|CREATE_NO_WINDOW\|console\|CtrlEvent\|JobObject\|job\|DETACHED\|new_process_group\|setsid\|CREATE_NEW_PROCESS_GROUP\|CREATE_BREAKAWAY" crates/spt-term/src/pty.rs | head -25

Find harness spawn path + Windows console/job/detach handling


=== launch_harness_brokered_in ===
144:pub fn launch_harness_brokered_in(
=== how broker spawns the PTY child (SpawnReq → pty) ===
31:/// Build one with [`PtySession::spawn`]; drive input via [`SessionSurface::write_input`]
60:    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {
96:    pub fn spawn_program<P, A, S>(
114:    pub fn spawn_program_in<P, A, S, D>(
136:    pub fn spawn_program_in_env<P, A, S, D, E, K, V>(


144	pub fn launch_harness_brokered_in(
145	    broker_name: &str,
146	    endpoint_id: &str,
147	    adapter_name: &str,
148	    session_id: &str,
149	    manifest: &Manifest,
150	    cwd: Option<&str>,
151	) -> Result<HarnessSpawned, String> {
152	    let PreparedSpawn { tokens, env } =
153	        prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest)?;
154	    let (program, args) = tokens.split_first().expect("non-empty checked");
155	    let mut brain =
156	        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
157	    let (session_id, pid) = brain
158	        .spawn_session_pid(SpawnReq {
159	            program: program.clone(),
160	            args: args.to_vec(),
161	            rows: DEFAULT_HARNESS_ROWS,
162	            cols: DEFAULT_HARNESS_COLS,
163	            endpoint: endpoint_id.to_string(),
164	            cwd: cwd.map(str::to_string),
165	            env,
166	        })
167	        .map_err(|e| format!("broker spawn: {e}"))?;
168	    Ok(HarnessSpawned { session_id, pid })
169	}
170
171	#[cfg(test)]
172	mod tests {
173	    use super::*;
174
175	    fn harness_manifest(self_command: &str) -> Manifest {
176	        let toml_src = format!(
177	            "[adapter]\nname = \"mock\"\nkind = \"harness\"\nversion = \"1\"\n\
178	             min_spt_core_version = \"0\"\n\n[session.self]\ncommand = '{self_command}'\n\
179	             keys = [\"id\", \"session_id\"]\n"
180	        );
181	        spt_runtime::Manifest::from_toml_str(&toml_src).unwrap()
182	    }
183
184	    // [unit->REQ-HOST-RUN-1] the spt-hosted bringup is template-driven: the
185	    // endpoint id and the spawn-time session id ride argv via {id}/{session_id}
186	    // substitution into [session.self] (so the harness binds to exactly this
187	    // endpoint), tokenized program-first.
188	    #[test]
189	    fn prepare_fills_id_and_session_into_self_command() {
190	        let m = harness_manifest("mock-session --id {id} --session-id {session_id}");
191	        let prepared = prepare_harness_spawn("doyle", "mock", "sess-abc", &m).unwrap();
192	        assert_eq!(
193	            prepared.tokens,
194	            vec![
195	                "mock-session".to_string(),
196	                "--id".to_string(),
197	                "doyle".to_string(),
198	                "--session-id".to_string(),
199	                "sess-abc".to_string(),
200	            ]
201	        );
202	        assert!(prepared.env.is_empty(), "no [env] declared → no inject");
203	    }
204
205	    // [unit->REQ-HAZARD-ENV-SUBST] a manifest [env.<VAR>] direction=inject value
206	    // is {key}-substituted from the same catalog and returned for the spawn — so a
207	    // flagless harness gets SPT_ENDPOINT_ID populated (not empty → seed-by-PPID).
208	    #[test]
209	    fn prepare_fills_env_inject_values_with_substitution() {
210	        let m = spt_runtime::Manifest::from_toml_str(
211	            "[adapter]\nname = \"mock\"\nkind = \"harness\"\nversion = \"1\"\n\
212	             min_spt_core_version = \"0\"\n\n[session.self]\ncommand = 'claude'\n\n\
213	             [env.SPT_ENDPOINT_ID]\ndirection = \"inject\"\nvalue = \"{id}\"\n\n\
214	             [env.SPT_SESSION]\ndirection = \"inject\"\nvalue = \"sess-{session_id}\"\n\n\
215	             [env.SPT_READ_ONLY]\ndirection = \"read\"\n",
216	        )
217	        .unwrap();
218	        let prepared = prepare_harness_spawn("wall-b", "mock", "abc", &m).unwrap();
219	        assert_eq!(prepared.env.get("SPT_ENDPOINT_ID").map(String::as_str), Some("wall-b"));
220	        assert_eq!(prepared.env.get("SPT_SESSION").map(String::as_str), Some("sess-abc"));
221	        // A `read` directive injects nothing.
222	        assert!(!prepared.env.contains_key("SPT_READ_ONLY"));
223	    }
224
225	    // [unit->REQ-HOST-RUN-1] fails closed before any spawn: a shell-kind adapter
226	    // is refused (endpoint run hosts harnesses), a manifest with no [session.self]
227	    // is refused (nothing to bring up), and an unresolved {placeholder} errs
228	    // naming the missing key.

pub fn launch_harness_brokered_in(
    broker_name: &str,
    endpoint_id: &str,
    adapter_name: &str,
    session_id: &str,
    manifest: &Manifest,
    cwd: Option<&str>,
) -> Result<HarnessSpawned, String> {
    let PreparedSpawn { tokens, env } =
        prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest)?;
    let (program, args) = tokens.split_first().expect("non-empty checked");
    let mut brain =
        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
    let (session_id, pid) = brain
        .spawn_session_pid(SpawnReq {
            program: program.clone(),
            args: args.to_vec(),
            rows: DEFAULT_HARNESS_ROWS,
            cols: DEFAULT_HARNESS_COLS,
            endpoint: endpoint_id.to_string(),
            cwd: cwd.map(str::to_string),
            env,
        })
        .map_err(|e| format!("broker spawn: {e}"))?;
    Ok(HarnessSpawned { session_id, pid })
}

#[cfg(test)]
mod tests {
    use super::*;

    fn harness_manifest(self_command: &str) -> Manifest {
        let toml_src = format!(
            "[adapter]\nname = \"mock\"\nkind = \"harness\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\n\n[session.self]\ncommand = '{self_command}'\n\
             keys = [\"id\", \"session_id\"]\n"
        );
        spt_runtime::Manifest::from_toml_str(&toml_src).unwrap()
    }

    // [unit->REQ-HOST-RUN-1] the spt-hosted bringup is template-driven: the
    // endpoint id and the spawn-time session id ride argv via {id}/{session_id}
    // substitution into [session.self] (so the harness binds to exactly this
    // endpoint), tokenized program-first.
    #[test]
    fn prepare_fills_id_and_session_into_self_command() {
        let m = harness_manifest("mock-session --id {id} --session-id {session_id}");
        let prepared = prepare_harness_spawn("doyle", "mock", "sess-abc", &m).unwrap();
        assert_eq!(
            prepared.tokens,
            vec![
                "mock-session".to_string(),
                "--id".to_string(),
                "doyle".to_string(),
                "--session-id".to_string(),
                "sess-abc".to_string(),
            ]
        );
        assert!(prepared.env.is_empty(), "no [env] declared → no inject");
    }

    // [unit->REQ-HAZARD-ENV-SUBST] a manifest [env.<VAR>] direction=inject value
    // is {key}-substituted from the same catalog and returned for the spawn — so a
    // flagless harness gets SPT_ENDPOINT_ID populated (not empty → seed-by-PPID).
    #[test]
    fn prepare_fills_env_inject_values_with_substitution() {
        let m = spt_runtime::Manifest::from_toml_str(
            "[adapter]\nname = \"mock\"\nkind = \"harness\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\n\n[session.self]\ncommand = 'claude'\n\n\
             [env.SPT_ENDPOINT_ID]\ndirection = \"inject\"\nvalue = \"{id}\"\n\n\
             [env.SPT_SESSION]\ndirection = \"inject\"\nvalue = \"sess-{session_id}\"\n\n\
             [env.SPT_READ_ONLY]\ndirection = \"read\"\n",
        )
        .unwrap();
        let prepared = prepare_harness_spawn("wall-b", "mock", "abc", &m).unwrap();
        assert_eq!(prepared.env.get("SPT_ENDPOINT_ID").map(String::as_str), Some("wall-b"));
        assert_eq!(prepared.env.get("SPT_SESSION").map(String::as_str), Some("sess-abc"));
        // A `read` directive injects nothing.
        assert!(!prepared.env.contains_key("SPT_READ_ONLY"));
    }

    // [unit->REQ-HOST-RUN-1] fails closed before any spawn: a shell-kind adapter
    // is refused (endpoint run hosts harnesses), a manifest with no [session.self]
    // is refused (nothing to bring up), and an unresolved {placeholder} errs
    // naming the missing key.


does your recommended fix actualy address the core issue? i should be able to close the tab or window of any spt-hosted endpoint, and it should keep running + be re-attachable. this is a fundamental d…


Verified the architecture: `launch_harness_brokered_in` connects to the broker and calls `brain.spawn_session_pid(SpawnReq{...})` — **the daemon/broker spawns the harness, not the CLI.** So by design the harness is the *daemon's* child and should outlive your tab. The daemon (76560) did survive; the harness (82932) didn't. That means the death-on-tab-close is a **defect** (the broker-hosted ConPTY child is leaking a coupling to your terminal's console), not a design choice — the design is already what you want, the implementation violates it.

**Direct answer: no, my loopback fix does not address the core.** It's secondary robustness (a *legitimately* dead child — real crash — must not wedge the broker). The **primary** fix is the invariant you stated: an spt-hosted harness runs in a daemon-owned PTY, fully decoupled from any viewer; closing a tab/window detaches the rc pump only; the harness keeps running and is re-attachable.

Agreed this is fundamental and belongs in `CONTEXT.md`. Launching a focused investigation into *why* the broker-hosted child dies on tab-close (Windows ConPTY/console/CTRL_CLOSE/handle isolation), then I'll write the invariant + design the fix:


Agent

Why harness dies on tab-close

Prompt:
Read-only investigation in the spt-core repo at C:/Users/decid/Documents/projects/spt-core. Do NOT edit.

CONFIRMED ARCHITECTURE: `spt endpoint run` calls harnesshost.rs `launch_harness_brokered_in`, which connects to the broker (`Brain::cold_start(broker_name)`) and calls `brain.spawn_session_pid(SpawnReq{...})` — so the DAEMON/BROKER spawns the harness in a broker-owned PTY (Windows ConPTY). The harness is therefore the daemon's child, NOT a child of the `spt endpoint run` CLI process.

OBSERVED DEFECT (Windows): when the user closes the TERMINAL TAB/WINDOW where they ran `spt endpoint run`, the harness process DIES (confirmed: its pid is dead afterward). But the DAEMON process survives. The required design invariant: closing the viewer tab must only detach the rc attach pump; the daemon-hosted harness must KEEP RUNNING and be re-attachable.

QUESTION: Why does a daemon/broker-spawned ConPTY harness child die when the user closes the CLI's terminal tab, even though the daemon survives? Find the exact Windows mechanism and where in the code the isolation is missing.

INVESTIGATE (cite file:line):
1. crates/spt-term/src/pty.rs — PtySession::spawn / spawn_program_in_env: how the ConPTY (pseudoconsole) and child are created. What portable-pty / conpty backend is used? Are CREATE_NEW_PROCESS_GROUP / DETACHED_PROCESS / CREATE_NO_WINDOW / a Job Object / CTRL_CLOSE handling involved? Does the child inherit the operator-CLI's console?
2. How/where the BROKER actually invokes the spawn (SpawnReq handler → PtySession). Is the ConPTY created in the DAEMON process (good, isolated) or somehow with handles/console inherited from the CLI invocation?
3. How does the DAEMON itself detach from the launching console (so IT survives)? (Look for daemon spawn/detach: CREATE_NEW_CONSOLE/DETACHED_PROCESS/no-window, daemon.rs / endpoint.rs / wherever `spt daemon run` is backgrounded.) Why would the daemon survive but its ConPTY child not — what's different about a ConPTY child's console attachment vs the daemon's?
4. Windows specifics: when a terminal tab closes, the OS sends CTRL_CLOSE_EVENT to processes attached to that console and tears down the console. A ConPTY child is attached to the PSEUDO-console (owned by the broker), which should be independent of the operator's real terminal. So either (a) the harness is NOT actually in the broker's pseudoconsole, or (b) a handle/job ties it to the operator's console. Determine which.
5. Check the Breap Job Object work (REQ-HAZARD-DAEMON-STOP-REAP, commit 026e9b4) — is the harness assigned to a job object that is tied to the CLI/console lifetime rather than the daemon's? Kill-on-job-close behavior could be the culprit if the job handle lives in the wrong process.

DELIVER (concise, cite file:line):
- The exact mechanism that kills the broker-hosted harness on tab-close (or top-2 ranked candidates with evidence).
- WHERE the fix belongs (the spawn flags / job-object ownership / console isolation) so the harness survives view-close and stays re-attachable.
- Whether the daemon's own detach is correct and just not mirrored to its ConPTY children.
- A proposed CONTEXT.md invariant statement (1-2 sentences) capturing "spt-hosted endpoints run in a daemon-owned PTY decoupled from any viewer; closing a view detaches only; the endpoint keeps running and is re-attachable."
Do NOT dump whole files; quote only load-bearing lines.


<owl_reachability_notice>
You have an active owl perch. Launch this Agent with run_in_background: true so you stay reachable for incoming messages while it works.
</owl_reachability_notice>


Async agent launched successfully.
agentId: a837aaf789ff1593b (internal ID - do not mention to user. Use SendMessage with to: 'a837aaf789ff1593b' to continue this agent.)
The agent is working in the background. You will be notified automatically when it completes.
Do not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.
output_file: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\a837aaf789ff1593b.output
Do NOT Read or tail this file via the shell tool — it is the full subagent JSONL transcript and reading it will overflow your context. If the user asks for progress, say the agent is still running; you'll get a completion notification.

Why harness dies on tab-close

Prompt:
Read-only investigation in the spt-core repo at C:/Users/decid/Documents/projects/spt-core. Do NOT edit.

CONFIRMED ARCHITECTURE: `spt endpoint run` calls harnesshost.rs `launch_harness_brokered_in`, which connects to the broker (`Brain::cold_start(broker_name)`) and calls `brain.spawn_session_pid(SpawnReq{...})` — so the DAEMON/BROKER spawns the harness in a broker-owned PTY (Windows ConPTY). The harness is therefore the daemon's child, NOT a child of the `spt endpoint run` CLI process.

OBSERVED DEFECT (Windows): when the user closes the TERMINAL TAB/WINDOW where they ran `spt endpoint run`, the harness process DIES (confirmed: its pid is dead afterward). But the DAEMON process survives. The required design invariant: closing the viewer tab must only detach the rc attach pump; the daemon-hosted harness must KEEP RUNNING and be re-attachable.

QUESTION: Why does a daemon/broker-spawned ConPTY harness child die when the user closes the CLI's terminal tab, even though the daemon survives? Find the exact Windows mechanism and where in the code the isolation is missing.

INVESTIGATE (cite file:line):
1. crates/spt-term/src/pty.rs — PtySession::spawn / spawn_program_in_env: how the ConPTY (pseudoconsole) and child are created. What portable-pty / conpty backend is used? Are CREATE_NEW_PROCESS_GROUP / DETACHED_PROCESS / CREATE_NO_WINDOW / a Job Object / CTRL_CLOSE handling involved? Does the child inherit the operator-CLI's console?
2. How/where the BROKER actually invokes the spawn (SpawnReq handler → PtySession). Is the ConPTY created in the DAEMON process (good, isolated) or somehow with handles/console inherited from the CLI invocation?
3. How does the DAEMON itself detach from the launching console (so IT survives)? (Look for daemon spawn/detach: CREATE_NEW_CONSOLE/DETACHED_PROCESS/no-window, daemon.rs / endpoint.rs / wherever `spt daemon run` is backgrounded.) Why would the daemon survive but its ConPTY child not — what's different about a ConPTY child's console attachment vs the daemon's?
4. Windows specifics: when a terminal tab closes, the OS sends CTRL_CLOSE_EVENT to processes attached to that console and tears down the console. A ConPTY child is attached to the PSEUDO-console (owned by the broker), which should be independent of the operator's real terminal. So either (a) the harness is NOT actually in the broker's pseudoconsole, or (b) a handle/job ties it to the operator's console. Determine which.
5. Check the Breap Job Object work (REQ-HAZARD-DAEMON-STOP-REAP, commit 026e9b4) — is the harness assigned to a job object that is tied to the CLI/console lifetime rather than the daemon's? Kill-on-job-close behavior could be the culprit if the job handle lives in the wrong process.

DELIVER (concise, cite file:line):
- The exact mechanism that kills the broker-hosted harness on tab-close (or top-2 ranked candidates with evidence).
- WHERE the fix belongs (the spawn flags / job-object ownership / console isolation) so the harness survives view-close and stays re-attachable.
- Whether the daemon's own detach is correct and just not mirrored to its ConPTY children.
- A proposed CONTEXT.md invariant statement (1-2 sentences) capturing "spt-hosted endpoints run in a daemon-owned PTY decoupled from any viewer; closing a view detaches only; the endpoint keeps running and is re-attachable."
Do NOT dump whole files; quote only load-bearing lines.


Investigation launched (background) on the Windows tab-close kill mechanism.

To be explicit about scope, the reopen now has **two layers**, both required:

1. **Primary (the invariant you stated):** spt-hosted harness must survive view-close — it runs in a daemon-owned PTY, decoupled from the launching terminal; closing a tab detaches the rc pump only. The current Windows behavior (broker-spawned child dies on tab-close while the daemon survives) violates this. → fix the console/job/ConPTY coupling + write the invariant into `CONTEXT.md` + mint a `REQ-HAZARD-*`.
2. **Secondary (robustness):** even a *legitimately* dead child (real crash/kill) must not wedge the broker (the loopback-backpressure / 2-worker exhaustion finding). Still needed, but downstream of #1.

The dummy-harness E2E gate then asserts the real requirement: kill the **viewer** (tab-close) → harness keeps running → re-attach works → a new endpoint still launches.

Holding for the investigation to pin the exact tab-close kill mechanism, then I'll bring you the `CONTEXT.md` invariant wording + the fix design before any build.


<task-notification>
<task-id>a837aaf789ff1593b</task-id>
<tool-use-id>toolu_01T81QUp7UnQ4Voont42rNFx</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\a837aaf789ff1593b.output</output-file>
<status>completed</status>
<summary>Agent "Why harness dies on tab-close" came to rest</summary>
<note>A task-notification fires each time this agent comes to rest with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>Confirmed: there is **no `CREATE_BREAKAWAY_FROM_JOB`** anywhere in the codebase. Now I have a complete and well-grounded analysis. Let me write up the findings.

## Investigation Findings: Why a daemon/broker-spawned ConPTY harness dies on terminal-tab close (Windows)

### Process topology (confirmed)
- `spt endpoint run` (CLI) → `ensure_running()` ensures a **detached daemon** exists (`crates/spt-daemon/src/daemon.rs:576` `spawn_detached` → `detached_no_inherit` at `daemon.rs:659`, or the elevated `deelevate.rs` path), then connects to the broker and calls `launch_harness_brokered_in` (`crates/spt-daemon/src/harnesshost.rs:144`).
- The **Broker runs IN the daemon process** as a background thread, not a separate process: `daemon.rs:170-176` (`Broker::bind_in_with_net(...)` + `thread::spawn(|| serve_broker.serve())`). So `dispatch_spawn` → `PtySession::spawn` executes **inside the daemon**, NOT inside the `spt endpoint run` CLI.
- `dispatch_spawn` (`crates/spt-daemon/src/broker.rs:841`) calls `PtySession::spawn_program_in_env` (`broker.rs:848`) → `PtySession::spawn` (`crates/spt-term/src/pty.rs:60`) → portable-pty 0.8.1.

### The ConPTY isolation itself is CORRECT (rules out candidate 4a/4b)
portable-pty 0.8.1 creates a real PseudoConsole entirely within the daemon:
- `crates/.../portable-pty-0.8.1/src/win/conpty.rs:13-24` — `openpty` makes two anonymous pipes via `Pipe::new()` and `CreatePseudoConsole(...)` in the daemon process. The ConPTY's host (OpenConsole/conhost) is a child of the daemon, not of the operator terminal.
- `src/win/psuedocon.rs:110-150` `spawn_command` — the harness child gets the pseudoconsole via the proc-thread attribute (`attrs.set_pty(self.con)`), stdio handles set to `INVALID_HANDLE_VALUE`, and `CreateProcessW` is called with `bInheritHandles = 0`. So the harness is bound to the **broker's pseudoconsole**, fully decoupled from the operator's real terminal/console. The operator console's `CTRL_CLOSE_EVENT` cannot reach it through a console attachment, and it inherits no handle from the CLI.

This is why the daemon survives AND why the console-signal theory does not explain the death. The remaining vector is the **Job Object**.

### TOP-RANKED CULPRIT: the daemon is not broken away from the operator terminal's Job Object, so it (and its ConPTY harness subtree) is killed when the terminal closes that job

Mechanism (Windows-specific):
- Windows Terminal / VS Code / conhost place the shell they launch — and, by inheritance, **every descendant process** — into a **Job Object** that has `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`. When the user closes the tab/window, the terminal drops its last handle to that job and the OS **terminates every process still in the job**.
- A child escapes that job only if it is spawned with `CREATE_BREAKAWAY_FROM_JOB` (and the job permits breakaway). Grep of the entire `crates/` tree shows **no `CREATE_BREAKAWAY_FROM_JOB` (0x0100_0000) anywhere**.
- The daemon spawn flags are:
  - Non-elevated path `detached_no_inherit` (`daemon.rs:705-707`): `DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW` — **no job breakaway**.
  - Elevated path `deelevate.rs:519`: `CREATE_NEW_CONSOLE | CREATE_NEW_PROCESS_GROUP | CREATE_UNICODE_ENVIRONMENT` — **no job breakaway**.

So neither daemon-spawn path detaches from the launching terminal's job. `DETACHED_PROCESS` only removes the *console*; it does **not** remove the process from a *Job Object*. The daemon, the broker thread it runs, the ConPTY host, and the harness child are all (transitively) members of the operator terminal's job. Closing the tab closes the job → OS reaps the whole subtree.

Why the observation looks like "daemon survives, harness dies": **if the daemon was already running** from a previous, since-closed launch context (or from a service/autostart), the *current* `spt endpoint run` only *connects* to it — `ensure_running` does not respawn it — so the long-lived daemon is in some *other* (or no) job and survives. But the **ConPTY harness was just spawned by the broker**, and on Windows the pseudoconsole host + child created via `CreateProcessW` inherit the **creating process's job** unless broken away. The relevant subtlety: a process can be in a job and still spawn children into a *different* job only if KILL_ON_JOB_CLOSE/breakaway interplay allows it. The asymmetry you observed (daemon lives, child dies) is the signature of the daemon being a *pre-existing* member of a benign/no job while the freshly spawned harness ends up associated with the launching console/job lifetime. This is the same class of bug the Breap test explicitly guards against in reverse (`reap.rs:285-290` notes ShellExecute children "break away and survive the reap"; here the inverse — no deliberate breakaway — lets the terminal's job claim the harness).

### SECOND-RANKED CANDIDATE: the daemon is freshly spawned by this very `spt endpoint run` and never breaks away
If `ensure_running` actually *starts* the daemon in this invocation, the daemon inherits the terminal's job directly (no breakaway flag). The daemon thread serving the broker, the ConPTY host, and the harness are all in that job. Tab close → job close → everything in spt's subtree dies. That this is survivable for the daemon in your repro but fatal for the harness suggests the daemon you saw surviving was a *prior* instance; a same-invocation daemon would die too. Either way the root defect is identical: **missing job breakaway**.

### Where the Breap Job Object fits (candidate 5)
The Breap job (`crates/spt-daemon/src/reap.rs`) is a SEPARATE, daemon-owned `KILL_ON_JOB_CLOSE` job used to reap the **brain subtree** on `daemon stop`. Note: the harness is spawned by the **broker (daemon)**, not the brain (`brain.rs:403` is called from `harnesshost.rs` over IPC into the broker thread). So the harness is **not** in the Breap job at all — Breap is not the killer here. But Breap is the proof-of-concept for the fix: it shows the codebase already knows how to (a) create a job and (b) reason about breakaway (`reap.rs:285` comment). The missing piece is a breakaway at daemon spawn so spt's whole tree leaves the terminal's job.

### WHERE THE FIX BELONGS
1. **Primary: add `CREATE_BREAKAWAY_FROM_JOB` to the daemon spawn flags** so the daemon (and therefore the broker, the ConPTY host, and every harness it spawns) leaves the launching terminal's Job Object:
   - `crates/spt-daemon/src/daemon.rs:707` — add `0x0100_0000` to `FLAGS` in `detached_no_inherit`.
   - `crates/spt-daemon/src/deelevate.rs:519` — add it to the elevated path's `FLAGS`.
   - Breakaway can fail if the terminal's job sets `JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = false`; the robust belt-and-suspenders is to ALSO place the broker-spawned harness into a **daemon-owned job** (mirroring Breap) so its lifetime is bound to the daemon, never the viewer. That makes the harness survive even if daemon breakaway is partially refused.
2. The ConPTY console isolation in `pty.rs` / portable-pty is already correct and needs **no change** — do not add console flags to the PTY spawn; the pseudoconsole is the right isolation.

### Is the daemon's own detach correct, just not mirrored to its ConPTY children?
The daemon's **console** detach is correct (`DETACHED_PROCESS` / hidden `CREATE_NEW_CONSOLE`), but its **job** detach is missing on both spawn paths. The defect is not console-related at all — it is the absent Job-Object breakaway. The ConPTY children inherit that same un-broken-away job association, so closing the viewer's tab tears them down via `KILL_ON_JOB_CLOSE`. The console isolation that protects the daemon does not protect against job-close, which is a different lifetime binding.

### Proposed CONTEXT.md invariant statement
> **spt-hosted endpoints run in a daemon-owned PTY decoupled from any viewer.** The daemon (and the broker-spawned harness/ConPTY subtree it owns) must break away from the launching terminal's Job Object at spawn (`CREATE_BREAKAWAY_FROM_JOB`; with the harness optionally pinned to a daemon-owned job as backstop), so closing the tab/window where `spt endpoint run` was invoked detaches only the `spt rc` attach pump — the endpoint keeps running under the daemon's pseudoconsole and stays re-attachable via `spt rc <id>`.

### Suggested next steps for traceability
A new `REQ-HAZARD-*` (e.g. `REQ-HAZARD-VIEWER-CLOSE-DETACH` or `REQ-HAZARD-DAEMON-JOB-BREAKAWAY`) with a Windows int test: spawn the daemon under a parent-held KILL_ON_JOB_CLOSE job, run an endpoint, close the job, assert the harness pid stays alive and `spt rc <id>` still attaches.

Key files cited: `crates/spt-daemon/src/daemon.rs:576,659,705-707`; `crates/spt-daemon/src/deelevate.rs:519`; `crates/spt-daemon/src/harnesshost.rs:144-168`; `crates/spt-daemon/src/broker.rs:170-176(daemon),841-855`; `crates/spt-term/src/pty.rs:60-92`; `crates/spt-daemon/src/reap.rs:1-26,285-290`; portable-pty-0.8.1 `src/win/conpty.rs:13-24`, `src/win/psuedocon.rs:110-150`.</result>
<usage><subagent_tokens>101377</subagent_tokens><tool_uses>28</tool_uses><duration_ms>192078</duration_ms></usage>
</task-notification>


<owl_messages>
<owl_message from="doyle-w390" priority="info">[WORKING_PERCH_NOTICE] Subagent working perch created.
Perch: doyle-w390
Agent type: general-purpose
To message it: $OWL send doyle-w390 doyle</owl_message>
</owl_messages>

[OWL SYSTEM] Informational owl messages above — no acknowledgment needed. Continue your current task.

<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.