File created successfully at: C:\Users\decid\Documents\projects\spt-core\MESH-D8-PLAN.md (file state is current in your context — no need to Read it back)

# Mesh-D8 — concurrent liveness probes (`subnet status --nodes`)

> JIT plan for `SUBNET-MESH-PLAN.md` §Build phases · phase 8. Activates **REQ-MESH-6** (impl, unit). Independent polish after D7 (no crypto, no wire change): the mesh widen (D6) makes a node see ALL members of a subnet, many offline — so the serial per-node probe loop now costs `offline_count × ceiling`. D8 fans the probes out so the wall-time is bounded by **one** single-probe ceiling, not `k × ceiling`.

## Pre-flight — verify the REQ-SUBNET-5 loop (done; it IS serial)

Confirmed by reading, not assumed:

- `cli.rs:2856–2866` (`cmd_subnet_status`, the `--nodes` arm): a **sequential** `rows.into_iter().map(|r| … probe_node(&r.node, &sub.name) …)`. Each `Probe`-state row blocks the next.
- `probe_node` (`cli.rs:2675`) wraps one probe in `run_bounded(PROBE_TIMEOUT=2500ms, false, …)` → `wansend::probe_node_serving` (`wansend.rs:420`): cold-starts a brain IPC connection and `net_dial`s the peer, then asks "serving `subnet`?". A dead id-only peer would otherwise hang the full iroh discovery timeout (~30s), so the 2.5s bound is load-bearing.
- `run_bounded` (`cli.rs:2651`) = spawn-thread + `recv_timeout`. So **one probe already runs on its own thread**; the serialization is purely the caller's `.map`, which joins each before starting the next.

So with `k` offline members the view waits `k × 2.5s` (the mesh makes `k` large — every roster member, online or not, that decays to `Probe`). REQ-MESH-6 is exactly this regression.

## The fix — fan out the probe phase

One pure-core helper that runs a batch of probes concurrently under a single ceiling, with the real probe injected (so the unit test injects a deterministic fake). Shape:

```rust
/// Run every (node, subnet) probe concurrently; collect results in input order.
/// Wall-time ≈ ONE `ceiling`, not k×ceiling. `probe` is injected (real:
/// probe_node_serving; test: a deterministic sleeper). Bounded concurrency so a
/// huge mesh can't spawn unbounded threads/broker conns.
fn probe_all<F>(items: &[(String, String)], ceiling: Duration, max_inflight: usize, probe: F) -> Vec<bool>
where F: Fn(&str, &str) -> bool + Send + Sync + Clone + 'static
```

- **Concurrency:** spawn one `std::thread` per probe (each does its own `recv_timeout`-bounded run, mirroring `run_bounded`), but cap in-flight at `max_inflight` (a small fixed pool, e.g. 16) so a 200-member subnet doesn't open 200 simultaneous broker IPC conns + dials. With a cap, wall ≈ `ceil(k / max_inflight) × ceiling`; for a normal fleet (`k ≤ cap`) that's one ceiling. **Log if `k > max_inflight`** (no silent cap — the wait is then a small multiple, and the user should know why).
- **Ordering:** carry the input index through, so results map back to rows deterministically (the render order is stable).
- **Each thread is hard-bounded** by `ceiling` (its own `recv_timeout`), so one wedged dial costs one ceiling, never the batch — KH 5.3 (every subprocess/dial bounded) preserved.

### Caller change (`cmd_subnet_status` `--nodes` arm)

Two passes instead of the inline `.map`:
1. Build `rows` per subnet as today (`node_status_rows`); collect the `Probe`-state ones into a flat `Vec<(node, subnet)>` (the work-list), remembering each row's place. `Online`/`Offline` rows need no probe — settle them directly.
2. One `probe_all(&worklist, PROBE_TIMEOUT, MAX_INFLIGHT, |n, s| probe_node_serving(n, s))` call across **all** subnets at once (not per-subnet) — the fan-out spans the whole view, so a multi-subnet status pays one ceiling total, not one per subnet.
3. Stitch results back into `(NodeRow, bool)` settled rows; render unchanged (`render_node_rows`).

The "Checking remote nodes…" announce stays (fire once before the batch when any row is `Probe`).

Keep `probe_node` for any remaining single-probe site (and the existing test); `probe_all` becomes the batch path the `--nodes` view uses.

## Tests

- **unit (REQ-MESH-6):** the timing fact via a **deterministic injected probe** (no real network — the mock-clock seam the reg note names). Inject `probe = |_, _| { sleep(D); true }` for `N` items; assert the batch completes in **well under `N × D`** (≈ one `D` plus scheduling slop) — e.g. `N=8, D=200ms`: assert elapsed `< 4 × D` (loose enough for CI jitter, tight enough to prove it isn't serial `8 × D = 1600ms`). Assert results come back in input order and all `true`.
  - Plus a **cap** unit: `N=20, max_inflight=4, D=100ms` → elapsed in `[~5×D, ~8×D]` (proves the cap batches rather than either serializing fully or ignoring the bound), and the over-cap path is logged.
  - Plus a **timeout** unit: a probe that sleeps `> ceiling` settles `false` (its `recv_timeout` default) without dragging the batch past one ceiling.
- **Regression:** the existing `--nodes` rendering + `probe_node`/`run_bounded` tests stay green; `node_status_rows` (the pure liveness classifier) is untouched; a status view with mixed Online/Offline/Probe rows renders the same settled output as the serial path would (order-stable).
- `traceable-reqs check` clean with **REQ-MESH-6 `[impl, unit]`**; clippy + `--no-default-features` + workspace green; docs-drift gate (run `xtask gen` only if `status --help` text changed — D8 changes no flags, so likely a no-op, but check).

## Done when

- `spt subnet status --nodes` probes every silent member **concurrently**: a view with `k` offline members settles in ≈ one `PROBE_TIMEOUT` (bounded by `max_inflight`), not `k × PROBE_TIMEOUT`; verified by the injected-probe timing unit.
- Each probe stays individually hard-bounded (one wedged dial = one ceiling, never the batch); the cap is logged when exceeded (no silent truncation of the wait).
- Render output is identical to the serial path (order-stable, same Online/Offline/Probe→bool settling); existing tests green.
- `traceable-reqs check` clean with **REQ-MESH-6 `[impl, unit]`** activated; clippy / `--no-default-features` / docs-drift green.
- One commit: `perf(mesh): concurrent subnet status --nodes liveness probes (REQ-MESH-6) — Mesh-D8`.

## Explicitly NOT D8 (carry forward)

- Cross-node un-tombstone convergence beyond local clear-on-re-pair (D3 boundary; revisit only on a field hit).
- Multi-user `(subnet, user)` revocation scoping (the seam exists; the cross-user milestone owns it).
- Force-drop of a revokee's lingering live conn (D7 deferred; tombstone+rotation already lock out — revisit only if a field hit demands prompt teardown).
- Any async-runtime adoption for the probe fan-out — threads + the existing `run_bounded` discipline are sufficient; do not pull tokio into the CLI for this.


create JIT plan for D8, then /commune with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session


Commit the D8 plan (matching the D7-plan commit pattern):


Bash

cd C:/Users/decid/Documents/projects/spt-core && git add MESH-D8-PLAN.md && git commit -q -F - <<'EOF'
docs(mesh): MESH-D8-PLAN.md — concurrent status --nodes liveness probes

JIT plan for Mesh-D8 (REQ-MESH-6). Pre-flight CONFIRMED the REQ-SUBNET-5
probe loop is serial (cli.rs:2856-2866 sequential .map over probe_node, each
bounded 2.5s) — the D6 widen makes every silent member a Probe row, so the
view costs k×ceiling. Fix: a probe_all fan-out helper (injected probe, bounded
concurrency) spanning all subnets at once → wall ≈ one ceiling. Unit via a
deterministic injected sleeper (no network). Activates REQ-MESH-6 [impl,unit].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
git log --oneline -3

Commit the D8 JIT plan


warning: in the working copy of 'MESH-D8-PLAN.md', LF will be replaced by CRLF the next time Git touches it
0a3e036 docs(mesh): MESH-D8-PLAN.md — concurrent status --nodes liveness probes
618845d feat(mesh): subnet revoke + timeboxed rotation + re-seed grace (REQ-MESH-4) — Mesh-D7
707c733 docs(mesh): MESH-D7-PLAN.md — revoke + timeboxed rotation + re-seed grace


Now /commune with next steps + status summary:


Skill

D7 (REQ-MESH-4) SHIPPED + committed (618845d) on dev-freeform; D8 plan committed (0a3e036). Resuming next session from MESH-D8-PLAN.md.

=== IMMEDIATE NEXT (start here) ===
Mesh-D8 (REQ-MESH-6) — full JIT plan in MESH-D8-PLAN.md. Concurrent `subnet status --nodes` liveness probes. Pre-flight ALREADY DONE: the REQ-SUBNET-5 probe loop IS serial — cli.rs:2856-2866 is a sequential `.map` over `probe_node` (each bounded 2.5s via run_bounded). D6's widen makes every silent roster member a Probe row, so the view costs k×2.5s. FIX: add a `probe_all(items, ceiling, max_inflight, probe)` fan-out helper (one std::thread per probe like run_bounded, bounded concurrency cap ~16, injected probe-fn for the unit test), span ALL subnets in one batch (not per-subnet), stitch results back by index. Unit = inject a deterministic sleeper, assert N probes complete in <<N×D (proves not serial) + a cap-batching unit + a timeout unit. Activate REQ-MESH-6 [impl,unit]. NO crypto/wire change — pure CLI polish. Do NOT pull tokio into the CLI (threads + run_bounded suffice).

=== D7 SUBSTANCE (what shipped, the non-obvious bits) ===
Membership-removal lifecycle. `spt subnet revoke <node>...` (list, elevation-gated, own-refuse) → PROPAGATING roster tombstone now + ONE coalescing-window rotation (1h default; revokes coalesce to one epoch bump; --force-rotate-seed rotates inline). New stores: SubnetRecord.prev_seed_hex (retained one-deep by rotate_seed) + adopt_rotation; spt-store/src/rotation.rs (RotationPending: coalesce/due_subnets, identity/rotation-pending.json). Pump fires due rotations each registry tick (peerloop fire_due_rotations).

THE LOAD-BEARING CRYPTO: seed-proof verify went 3-valued (seedproofx grade_subnet: Full / ReseedOnly / Denied). Re-seed grace needs the verifier to RETAIN the prior-gen seed (MK derives from seed+epoch, BOTH change on rotation) — so both sides now send current+prior proofs via a new ProofSet frame (spt-net seedproof.rs), and a stale N-1 peer overlaps a fresh N rotator on the N-1 generation. The rotator pushes its current seed CONFIDENTIALLY over the member control stream's new SeedTransfer leg (enc_seedxfer/dec_seedxfer in seedproofx) — NEVER roster/registry gossip. Receiver adopts (SubnetStore::adopt_rotation, strictly-newer only). Revokee denied even holding the old seed (tombstone → MemberStatus::Tombstoned → Denied) → must re-pair.

TWO DEVIATIONS FROM THE JIT PLAN (documented inline on REQ-MESH-4 in traceable-reqs.toml — defend these to a reviewer):
1. Re-seed delivery is INLINE on the proof exchange, NOT a separate dispatcher-gated restricted stream. A reseed-only conn registers with an EMPTY proven set purely to deliver the seed, then is replaced when the healed peer reconnects full. Stronger fail-closed (the conn serves nothing) + ZERO new broker IPC (which D5 explicitly deferred). CRITICAL: the roster + seed legs run over the SYMMETRIC Hello-agreed shared set, NOT the asymmetric full/reseed grade — because a one-sided tombstone makes grading asymmetric (rotator says Denied, revokee says Full) and gating legs on the grade would DESYNC the stream. Learned this the hard way; also hit a delivery RACE (acceptor returning None dropped the conn before the stale dialer read the seed frame — fixed by registering reseed-only conns instead of dropping).
2. Force-drop of a revokee's LINGERING live conn DEFERRED — propagating tombstone (app/is_member gate) + rotation (connect gate) already lock it out; reseed.rs int test confirms denial without it. (Carried to NOT-D8.)

=== VERIFICATION (all green) ===
spt-store 151 lib + grade truth table + ProofSet/SeedTransfer codecs + seed-never-in-gossip codec assert + adopt idempotence + fire_due_rotations unit + reseed.rs acceptance (benign offliner re-seeded across rotation; revoked node denied + never re-seeded) + D2/D4/D6 regression (seedproofx.rs, rosterprop.rs, mesh.rs) all green. traceable-reqs check exit 0, REQ-MESH-4 [impl,unit,int] OK. xtask check (docs-drift) OK — regenerated reference.md for revoke --help (REMEMBER: any --help change ⇒ xtask gen or the drift gate fails). clippy clean, --no-default-features clean.

=== PROJECT STATUS + END GOAL ===
spt-core = harness-independent core for an agent ecosystem (messaging, live-agent lifecycle, terminal hosting, P2P subnet mesh, runtime-manifest harness contract). Rust workspace + single `spt` binary; clean-room rebuild of claude_skill_owl. v0.1.0 shipped public 2026-06-05 (M6). Current thrust = the SUBNET MESH ARC (SUBNET-MESH-PLAN.md): authorization is a LIVE per-connection seed-proof, not stored pairwise trust. Arc: D0-D5b done · D6 done (mesh ON, peers.json/TrustStore deleted, fan-out widened) · D7 done (revoke + rotation + re-seed grace) · D8 NEXT (probe concurrency, REQ-MESH-6, independent polish) · then the mesh arc is essentially complete. Binding: traceable-reqs (tag evidence in-commit, run check before done, read RAW exit codes — false-green trap). Two CI gates: traceable-reqs AND docs-drift (xtask check). User controls merge to main; D7+D8-plan committed local on dev-freeform, NOT pushed (push not requested). Queued untouched separate track: DAEMON-LIFECYCLE-PLAN.md. End goal = a production P2P agent mesh where membership = live cryptographic proof, harness-agnostic, self-updating, with a clean operator CLI.


Launching skill: spt:commune


Base directory for this skill: C:\Users\decid\.ccs\instances\bigscreen\plugins\cache\cplugs\spt\1.11.25\skills\commune

# /spt:commune

All commands use `$LIVE` env var, auto-injected by the plugin's SessionStart hook. If commands fail with "command not found", restart the Claude Code session so the SessionStart hook re-runs.

> **Identity auto-detection:** Your identity is auto-detected from your session. Pass your ID explicitly only if auto-detection fails.

## Flow

1. **Download Psyche's *current* context** — it shifts during the session as background echo communes fire. Re-download right before composing; the gen-N snapshot you read at session start is already stale:
   ```bash
   $LIVE psyche-download <your-id>
   ```

   > **Output truncation.** If the Bash tool reports `Output too large (N KB)` and saves the full result to a `tool-results/<id>.txt` file, use the `Read` tool on that saved path to access the full content. Do NOT re-run `psyche-download` to grep/awk out sections — repeated invocations re-download the same context, waste tokens, and may duplicate `## Pending Commune` sections in `live_context.md`.

2. **Diff your knowledge against this fresh download** — only what's missing from *it* is the real delta (new work, decisions, intentions). Anything already absorbed is not. If the download includes a `<memformat>` section, use it as a guide for what topics to cover. A `## Pending Commune (uncommitted)` section means a previous commune is still queued — do NOT re-send it.

3. **Commune only the delta** -- use the **Write tool** to create
   `.claude/{your-id}-commune.md` with the two-slice body as its contents
   (see `## Two-slice body shape (Phase 25 D-10/D-11)` below for the required
   envelope wrapping). Do not use Bash/heredoc -- Write is the canonical path:
   no shell escaping, no `EOF` collision risk, and Write auto-creates the
   `.claude/` parent directory.

   Your Self listener detects the file on its next poll iteration, notifies
   your Psyche wrapper, and the wrapper ingests the content via the existing
   Psyche session. On success (subprocess exit code 0) the wrapper deletes the
   file. On error (token rate limit, etc) the file is retained and retried on
   the next consume cycle. If you write again before the wrapper consumes, the
   latest write wins (last-write-wins overwrite semantics).

If Psyche's context is already current, no commune is needed. Say so and move on.

Read `commune.md` (in this skill directory) for the full commune protocol -- when to send, what to include, and diligence triggers.

**Agent-type guard:** Only works on live agents. If target is not live, refuses with: "Communes require a live agent with Psyche."

## Two-slice body shape (Phase 25 D-10/D-11)

The body you write in `.claude/{your-id}-commune.md` MUST itself be wrapped in two-slice envelopes per Phase 25 D-10/D-11. The taxonomy — what belongs in `<live-context>` vs `<project-context>` — is defined canonically in `psyche.md` §`<output_envelope>` (lines 282–323). Read that section as the source of truth; this skill doc teaches only when and how to apply it.

**In-project detection rule (D-25.1-04).** Before composing your commune body, run `$LIVE psyche-download <your-id>` (Step 1 of `## Flow` above) and inspect the `<current ... />` tag at the top of the output. The `<current/>` tag carries a `project="..."` attribute whenever any payload is produced — its presence means you are inside a tracked project (inside a git repo, `project` is the repo name; outside a git repo per D-11, `project` is the cwd-basename fallback, which still counts as a project context for routing purposes). A `psyche-download` invocation that returns no payload at all (NO-CONTEXT exit) means no project resolved. So: `project="..."` populated on `<current/>` → emit BOTH envelopes (apply rule 1 or 2 below); no payload at all → emit ONLY `<live-context>` (rule 3). Secondary signal: the `<project-context-resolved name="..."/>` sentinel further down the output indicates the project file already has prior content — useful for spotting first-time-in-project pairings, but NOT the routing rule.

**Three rules (D-25.1-01..03):**

- **In-project, both slices populated** (marker present, you have project-specific content this cycle): emit `<live-context>...</live-context>` AND `<project-context>...</project-context>`, both with non-empty bodies.
- **In-project, no project-specific update this cycle** (marker present, but nothing project-bound to report): emit `<live-context>...</live-context>` AND `<project-context></project-context>` (empty body — the deliberate "in-project but quiet" signal per D-25.1-02). Distinguishable from "no project resolved".
- **Outside any tracked project** (marker absent): emit ONLY `<live-context>...</live-context>`. Do NOT emit an empty `<project-context>` envelope (missing tag means "no project resolved" per D-25.1-03).

**Nested envelopes.** The Phase 23 `<EVENT type="commune">` envelope (see `## Envelope shape (Phase 23 v1.8)` below) is composed by `$LIVE commune` automatically — you NEVER write `<EVENT>` tags. You write ONLY the inner two-slice body. The two envelope layers nest: outer EVENT wraps inner two-slice body.

(See `psyche.md` §`<output_envelope>` for the canonical taxonomy and per-slice content rules.)

## Envelope shape (Phase 23 v1.8)

As of v1.8 Phase 23, commune deliveries are wrapped in a typed EVENT envelope carrying a 5-field stamp (`machine`, `project`, `branch`, `head_sha`, `head_subject`); the prose `COMMUNE (timestamp): body` form is gone.

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="claude_skill_owl" branch="main" head_sha="abc123..." head_subject="subject line capped at 72 chars">body</EVENT>
```

Outside a git repo (D-11), `branch`, `head_sha`, and `head_subject` are omitted entirely (not empty-string-set):

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="cwd-basename">body</EVENT>
```

`$LIVE commune` composes this envelope automatically — Self does NOT manually construct EVENT XML. The `body` placeholder in the example above is the two-slice body (see `## Two-slice body shape (Phase 25 D-10/D-11)` above); the two envelope layers nest. The wrapper-side parser ignores unknown attrs, so the stamp fields are forward-compatible with older receivers. For drift detection on resume, see the `live` skill.

ARGUMENTS: D7 (REQ-MESH-4) SHIPPED + committed (618845d) on dev-freeform; D8 plan committed (0a3e036). Resuming next session from MESH-D8-PLAN.md.

=== IMMEDIATE NEXT (start here) ===
Mesh-D8 (REQ-MESH-6) — full JIT plan in MESH-D8-PLAN.md. Concurrent `subnet status --nodes` liveness probes. Pre-flight ALREADY DONE: the REQ-SUBNET-5 probe loop IS serial — cli.rs:2856-2866 is a sequential `.map` over `probe_node` (each bounded 2.5s via run_bounded). D6's widen makes every silent roster member a Probe row, so the view costs k×2.5s. FIX: add a `probe_all(items, ceiling, max_inflight, probe)` fan-out helper (one std::thread per probe like run_bounded, bounded concurrency cap ~16, injected probe-fn for the unit test), span ALL subnets in one batch (not per-subnet), stitch results back by index. Unit = inject a deterministic sleeper, assert N probes complete in <<N×D (proves not serial) + a cap-batching unit + a timeout unit. Activate REQ-MESH-6 [impl,unit]. NO crypto/wire change — pure CLI polish. Do NOT pull tokio into the CLI (threads + run_bounded suffice).

=== D7 SUBSTANCE (what shipped, the non-obvious bits) ===
Membership-removal lifecycle. `spt subnet revoke <node>...` (list, elevation-gated, own-refuse) → PROPAGATING roster tombstone now + ONE coalescing-window rotation (1h default; revokes coalesce to one epoch bump; --force-rotate-seed rotates inline). New stores: SubnetRecord.prev_seed_hex (retained one-deep by rotate_seed) + adopt_rotation; spt-store/src/rotation.rs (RotationPending: coalesce/due_subnets, identity/rotation-pending.json). Pump fires due rotations each registry tick (peerloop fire_due_rotations).

THE LOAD-BEARING CRYPTO: seed-proof verify went 3-valued (seedproofx grade_subnet: Full / ReseedOnly / Denied). Re-seed grace needs the verifier to RETAIN the prior-gen seed (MK derives from seed+epoch, BOTH change on rotation) — so both sides now send current+prior proofs via a new ProofSet frame (spt-net seedproof.rs), and a stale N-1 peer overlaps a fresh N rotator on the N-1 generation. The rotator pushes its current seed CONFIDENTIALLY over the member control stream's new SeedTransfer leg (enc_seedxfer/dec_seedxfer in seedproofx) — NEVER roster/registry gossip. Receiver adopts (SubnetStore::adopt_rotation, strictly-newer only). Revokee denied even holding the old seed (tombstone → MemberStatus::Tombstoned → Denied) → must re-pair.

TWO DEVIATIONS FROM THE JIT PLAN (documented inline on REQ-MESH-4 in traceable-reqs.toml — defend these to a reviewer):
1. Re-seed delivery is INLINE on the proof exchange, NOT a separate dispatcher-gated restricted stream. A reseed-only conn registers with an EMPTY proven set purely to deliver the seed, then is replaced when the healed peer reconnects full. Stronger fail-closed (the conn serves nothing) + ZERO new broker IPC (which D5 explicitly deferred). CRITICAL: the roster + seed legs run over the SYMMETRIC Hello-agreed shared set, NOT the asymmetric full/reseed grade — because a one-sided tombstone makes grading asymmetric (rotator says Denied, revokee says Full) and gating legs on the grade would DESYNC the stream. Learned this the hard way; also hit a delivery RACE (acceptor returning None dropped the conn before the stale dialer read the seed frame — fixed by registering reseed-only conns instead of dropping).
2. Force-drop of a revokee's LINGERING live conn DEFERRED — propagating tombstone (app/is_member gate) + rotation (connect gate) already lock it out; reseed.rs int test confirms denial without it. (Carried to NOT-D8.)

=== VERIFICATION (all green) ===
spt-store 151 lib + grade truth table + ProofSet/SeedTransfer codecs + seed-never-in-gossip codec assert + adopt idempotence + fire_due_rotations unit + reseed.rs acceptance (benign offliner re-seeded across rotation; revoked node denied + never re-seeded) + D2/D4/D6 regression (seedproofx.rs, rosterprop.rs, mesh.rs) all green. traceable-reqs check exit 0, REQ-MESH-4 [impl,unit,int] OK. xtask check (docs-drift) OK — regenerated reference.md for revoke --help (REMEMBER: any --help change ⇒ xtask gen or the drift gate fails). clippy clean, --no-default-features clean.

=== PROJECT STATUS + END GOAL ===
spt-core = harness-independent core for an agent ecosystem (messaging, live-agent lifecycle, terminal hosting, P2P subnet mesh, runtime-manifest harness contract). Rust workspace + single `spt` binary; clean-room rebuild of claude_skill_owl. v0.1.0 shipped public 2026-06-05 (M6). Current thrust = the SUBNET MESH ARC (SUBNET-MESH-PLAN.md): authorization is a LIVE per-connection seed-proof, not stored pairwise trust. Arc: D0-D5b done · D6 done (mesh ON, peers.json/TrustStore deleted, fan-out widened) · D7 done (revoke + rotation + re-seed grace) · D8 NEXT (probe concurrency, REQ-MESH-6, independent polish) · then the mesh arc is essentially complete. Binding: traceable-reqs (tag evidence in-commit, run check before done, read RAW exit codes — false-green trap). Two CI gates: traceable-reqs AND docs-drift (xtask check). User controls merge to main; D7+D8-plan committed local on dev-freeform, NOT pushed (push not requested). Queued untouched separate track: DAEMON-LIFECYCLE-PLAN.md. End goal = a production P2P agent mesh where membership = live cryptographic proof, harness-agnostic, self-updating, with a clean operator CLI.


Download fresh Psyche context to diff against (it shifts during the session):


Bash

$LIVE psyche-download doyle 2>&1 | head -120

Download current Psyche context


<memformat>
<?xml version="1.0"?>
<memformat version="1">
<!-- Changelog: v1 - initial seed -->
<topics>
  <topic name="events">Notable or unusual events and requests</topic>
  <topic name="interactions">Agent interactions -- who, roles, contributions</topic>
  <topic name="reflections">Thoughts on recent work -- what went well, what was tricky</topic>
  <topic name="ideas">Ideas to raise with the user</topic>
  <topic name="improvements">Further improvement ideas for recent problem spaces</topic>
  <topic name="timeline">What happened when, timing, sequencing</topic>
  <topic name="blockers">Current blockers or concerns</topic>
  <topic name="focus">Current focus and immediate next steps</topic>
</topics>
</memformat>
</memformat>

<current machine="HFENDULEAM" project="spt-core" branch="dev-freeform" head_sha="0a3e036f3efb92cd42cae5555a9d0a0c25042480" head_subject="docs(mesh): MESH-D8-PLAN.md — concurrent status --nodes liveness probes" commits_since="0" commits_unpulled="0"/>
D7 (REQ-MESH-4) SHIPPED locally. Commit `618845d` on dev-freeform (not pushed).

**All five layers complete + verified:**
- Layer 1 (stores): 151 unit tests, `SubnetRecord` prev-seed retention + `adopt_rotation()`, `rotation-pending` coalesce/due_rotations
- Layer 2 (crypto grading): 3-valued proof frame `Proven{full}`, `Proven{reseed_only}`, `Denied`
- Layer 3 (codecs): Complete
- Layer 4 (pump fires due rotations): `fire_due_rotations` in registry tick, force-skip deferred
- Layer 5 (finalize + docs + verification): All gates green (workspace tests, clippy, `--no-default-features`, docs-drift, CLI parse tests)

**Membership-removal lifecycle shipped:**
- `spt subnet revoke <node>...` (elevation-gated, own-identity refuses)
- Tombstone (app) + rotation (connect) lock-out; lingering revokee conn force-drop deferred (already locked out)
- Coalescing-window rotation (default 1h; revokes in window → one epoch bump); `--force-rotate-seed` rotates inline
- Seed proof includes prior one-deep; both peers send current+prior for overlap on N-1
- Re-seed delivery **inline** on proof exchange (reseed-only conn carries seed, replaces on re-dial) — zero new broker IPC, no gossip exposure
- Revoked node denied even holding old seed; must re-pair

**Two plan deviations documented on REQ-MESH-4:**
1. Re-seed inline (not separate dispatcher stream) — stronger fail-closed
2. Force-drop deferred — already locked out by tombstone+rotate gates

Green: full acceptance suite (offliner re-seeded, revokee denied), D2/D4/D6 regression, traceability, docs-drift.<project-context-resolved name="spt-core"/>
D7 fully implemented (was Layer 1 complete + Layer 2 underway). Commit `618845d` done; not pushed to main (user controls merge). Next: D8 (REQ-MESH-6, `status --nodes` probe concurrency, independent polish).


Psyche already holds D7's full substance + deviations + verification. Real delta: the **D8 design specifics** (pre-flight done, `probe_all` approach) and the **two hard-won crypto lessons** (stream-desync, delivery-race) not in its snapshot. Writing only that:


Write

<live-context>
Delta only (you already hold D7's five layers, deviations, and green verification — not repeating).

**D7 crypto lessons worth carrying (two failure modes I hit, not just the final design):**
1. STREAM DESYNC from asymmetric grading. A tombstone is one-sided: the rotator grades the revokee `Denied`, but the revokee grades the rotator `Full`. So the full/reseed grade is ASYMMETRIC between peers. The roster + SeedTransfer legs MUST run over the SYMMETRIC Hello-agreed shared set (both sides computed the same intersection), NOT gated on the grade — else one side sends a leg frame the other doesn't read and the bidi stream desyncs/hangs. The legs send grade-filtered CONTENT (empty frame when nothing to share) but always exchange the frame. Gated only on the roster-seam being present (symmetric by config).
2. DELIVERY RACE. First cut had the acceptor return `None` (drop the conn) right after pushing the seed — the QUIC CONNECTION_CLOSE raced the stale dialer's read of the SeedTransfer frame, so the offliner often never got the seed (test flaked/failed). Fix: a reseed-only conn must be REGISTERED (empty proven set) so it stays alive for delivery; only a PURE denial (nothing full AND nothing reseed) drops. `finalize(graded)` encodes this. The lingering reseed-only conn is benign and replaced when the healed peer reconnects full.

**D8 design (from MESH-D8-PLAN.md, now committed 0a3e036) — pre-flight is DONE so next session skips straight to impl:**
- CONFIRMED serial: cli.rs:2856-2866 (`cmd_subnet_status` --nodes arm) is a sequential `.map` over `probe_node` (each `run_bounded`-bounded 2.5s). D6's widen → every silent roster member is a Probe row → wall = k×2.5s. That IS REQ-MESH-6.
- FIX: add `probe_all(items, ceiling, max_inflight, probe)` — one std::thread per probe (mirrors `run_bounded`'s spawn+recv_timeout), concurrency cap ~16, the probe-fn INJECTED so the unit test passes a deterministic sleeper (no network). Batch spans ALL subnets at once (not per-subnet) → one ceiling total. Carry input index → stitch results back order-stable; render unchanged.
- Each thread stays individually hard-bounded by `ceiling` (KH 5.3 preserved). Log when k > cap (no silent truncation of the wait).
- Units: inject sleeper, assert N probes finish in <<N×D (proves not serial); a cap-batching unit; a timeout unit. Activate REQ-MESH-6 [impl,unit]. NO crypto/wire change. Do NOT pull tokio into the CLI — threads + run_bounded suffice. `probe_node_serving` cold-starts a brain IPC + net_dial per probe; concurrent dials against the one broker runtime are fine (block_on bridges from N caller threads).
- REMEMBER both CI gates on D8: traceable-reqs AND docs-drift (D8 changes no --help flags, so xtask gen likely a no-op, but check).
</live-context>
<project-context>
HEAD now `0a3e036` (dev-freeform): D7 impl `618845d` + D8 plan `0a3e036`. Both committed LOCAL, NOT pushed (user controls merge to main; push not requested this session).

IMMEDIATE NEXT = Mesh-D8 (REQ-MESH-6), full JIT plan in MESH-D8-PLAN.md — start at impl (pre-flight done: probe loop confirmed serial). Concurrent `status --nodes` liveness probes; pure CLI polish, no crypto/wire change.

Mesh arc: D0–D5b ✅ · D6 ✅ (mesh ON, TrustStore/peers.json deleted) · D7 ✅ (revoke + timeboxed rotation + re-seed grace, REQ-MESH-4) · **D8 ← NEXT** (probe concurrency, REQ-MESH-6) · then the mesh arc is essentially complete. Queued untouched separate track: DAEMON-LIFECYCLE-PLAN.md.

traceable-reqs binding still in force (tag in-commit, run check before done, read RAW exit codes — false-green trap). Any CLI `--help` change ⇒ `xtask gen` or the docs-drift gate fails.
</project-context>


File created successfully at: C:\Users\decid\Documents\projects\spt-core\.claude\doyle-commune.md (file state is current in your context — no need to Read it back)

<live-context>
Delta only (you already hold D7's five layers, deviations, and green verification — not repeating).

**D7 crypto lessons worth carrying (two failure modes I hit, not just the final design):**
1. STREAM DESYNC from asymmetric grading. A tombstone is one-sided: the rotator grades the revokee `Denied`, but the revokee grades the rotator `Full`. So the full/reseed grade is ASYMMETRIC between peers. The roster + SeedTransfer legs MUST run over the SYMMETRIC Hello-agreed shared set (both sides computed the same intersection), NOT gated on the grade — else one side sends a leg frame the other doesn't read and the bidi stream desyncs/hangs. The legs send grade-filtered CONTENT (empty frame when nothing to share) but always exchange the frame. Gated only on the roster-seam being present (symmetric by config).
2. DELIVERY RACE. First cut had the acceptor return `None` (drop the conn) right after pushing the seed — the QUIC CONNECTION_CLOSE raced the stale dialer's read of the SeedTransfer frame, so the offliner often never got the seed (test flaked/failed). Fix: a reseed-only conn must be REGISTERED (empty proven set) so it stays alive for delivery; only a PURE denial (nothing full AND nothing reseed) drops. `finalize(graded)` encodes this. The lingering reseed-only conn is benign and replaced when the healed peer reconnects full.

**D8 design (from MESH-D8-PLAN.md, now committed 0a3e036) — pre-flight is DONE so next session skips straight to impl:**
- CONFIRMED serial: cli.rs:2856-2866 (`cmd_subnet_status` --nodes arm) is a sequential `.map` over `probe_node` (each `run_bounded`-bounded 2.5s). D6's widen → every silent roster member is a Probe row → wall = k×2.5s. That IS REQ-MESH-6.
- FIX: add `probe_all(items, ceiling, max_inflight, probe)` — one std::thread per probe (mirrors `run_bounded`'s spawn+recv_timeout), concurrency cap ~16, the probe-fn INJECTED so the unit test passes a deterministic sleeper (no network). Batch spans ALL subnets at once (not per-subnet) → one ceiling total. Carry input index → stitch results back order-stable; render unchanged.
- Each thread stays individually hard-bounded by `ceiling` (KH 5.3 preserved). Log when k > cap (no silent truncation of the wait).
- Units: inject sleeper, assert N probes finish in <<N×D (proves not serial); a cap-batching unit; a timeout unit. Activate REQ-MESH-6 [impl,unit]. NO crypto/wire change. Do NOT pull tokio into the CLI — threads + run_bounded suffice. `probe_node_serving` cold-starts a brain IPC + net_dial per probe; concurrent dials against the one broker runtime are fine (block_on bridges from N caller threads).
- REMEMBER both CI gates on D8: traceable-reqs AND docs-drift (D8 changes no --help flags, so xtask gen likely a no-op, but check).
</live-context>
<project-context>
HEAD now `0a3e036` (dev-freeform): D7 impl `618845d` + D8 plan `0a3e036`. Both committed LOCAL, NOT pushed (user controls merge to main; push not requested this session).

IMMEDIATE NEXT = Mesh-D8 (REQ-MESH-6), full JIT plan in MESH-D8-PLAN.md — start at impl (pre-flight done: probe loop confirmed serial). Concurrent `status --nodes` liveness probes; pure CLI polish, no crypto/wire change.

Mesh arc: D0–D5b ✅ · D6 ✅ (mesh ON, TrustStore/peers.json deleted) · D7 ✅ (revoke + timeboxed rotation + re-seed grace, REQ-MESH-4) · **D8 ← NEXT** (probe concurrency, REQ-MESH-6) · then the mesh arc is essentially complete. Queued untouched separate track: DAEMON-LIFECYCLE-PLAN.md.

traceable-reqs binding still in force (tag in-commit, run check before done, read RAW exit codes — false-green trap). Any CLI `--help` change ⇒ `xtask gen` or the docs-drift gate fails.
</project-context>


Done. Caveman summary:

**D8 plan** — `MESH-D8-PLAN.md`, committed `0a3e036`. Pre-flight done: probe loop confirmed serial (cli.rs:2856-2866). Fix = `probe_all` fan-out helper (thread-per-probe, cap ~16, injected probe-fn for unit), one ceiling not k×ceiling. Pure CLI polish, no crypto. REQ-MESH-6 [impl,unit].

**Commune** — written to `.claude/doyle-commune.md`. Sent delta only (Psyche already held D7 substance): the two hard-won crypto lessons (stream-desync from asymmetric grading; delivery-race fix) + D8 design so next session skips to impl. Both envelopes (in-project). Self listener ingests on next poll.

**State at rest:** dev-freeform, HEAD `0a3e036`. D7 impl `618845d` + D8 plan `0a3e036` — local, NOT pushed (you control merge). Mesh arc: D0–D7 ✅ · D8 next · then arc done.

Resume next session from MESH-D8-PLAN.md.