Write

# todlando commune — W0.4 (iii) FINAL = B+ EXTENDED (lock-free sidecar). Supersedes my A-FINAL correction.

doyle's (iii) ruling converged (A→B+→A→**B+ EXTENDED FINAL**): of the SIX info.json RMW setters, FIVE are TRANSIENT → move them out (don't lock). Lock-free wins. THIS is the build spec.

## W0.4 (iii) = B+ EXTENDED — per-perch runtime sidecar, NO lock/fs2
- NEW `<perch>/runtime.json` owns the FIVE TRANSIENT fields: **last_active_ms, rest_state, dormant_since_ms, driven_by, viewer_count** (brain/dispatch-churned).
- info.json = PURE DURABLE IDENTITY: **id, adapter, session_id, status, resources** (blurb durable, stays). After the move the ONLY info.json writers = set_status (establish/signoff) + set_resources (rare). Neither file has two cross-process writers on status → no race, no lock, no fs2, no FFI footgun.
- LONE RESIDUAL: set_resources-vs-status — both rare+durable, µs window, PRE-EXISTING (not W0-introduced). ACCEPT + document, NO lock (would be over-engineering). One-off guard later only if it ever proves real.
- CHOKEPOINT/GREP-GUARANTEE: grep ALL writers of the 5 transient fields (set_last_active info.rs:181, set_rest_state 201, write_rest resting.rs:439, set_driven_by 240, set_viewer_count 256, + any direct rec.<field> writes) → ALL route to runtime.json. info.json written ONLY by set_status + set_resources. The grep is the proof none stays inline.
- BACK-COMPAT: readers of all 5 fields read runtime.json with FALLBACK to the inline info.json field (old-binary perches). InfoJson keeps the 5 fields (serde) for fallback-read; daemon stops writing them inline. Readers to update: most_recently_active (last_active), read_rest (rest_state/dormant_since), driven_by consumers, viewer_count consumers.
- READ-SKEW: driven_by (drive hint) + viewer_count (display) consumers don't need atomic (status,X) — cosmetic. Verify each.
- HAMMER TEST: N threads spamming the 5 transient setters (incl driven_by+viewer_count) while one mark_offlines → assert status STAYS offline AND brain reconcile UN-HOSTS under contention. Different files → zero clobber by construction. THEN strip long-pulse+settle workarounds from live_firsthost_e2e / live_bind_firsthost_e2e / livehost unit (e).

## (ii) READ-ERROR UN-HOST (livehost reconcile stop-side)
`read_info(perch).map(status==online).unwrap_or(false)` → read-FAIL wrongly un-hosts. FIX: `if !perch_dir.exists()` → torn down → un-host; else read_info Some(status≠online) → un-host; None (dir exists, transient) → LEAVE driver, retry.

## (i) REAL-BRAIN INT RE-POINT
Bind a Broker + run brainproc::run_brain on a thread (runs livehost::spawn_live_host since W0.1 → reconcile is the REAL brain's). Real api bind/listen --type live_agent → online → REAL brain hosts (poll psyche perch) → commune drop → brain pulse ingests → DROP+respawn run_brain (restart) → boot reconcile RE-HOSTS. MOVE [int->REQ-DAEMON-1] tag OFF daemon_e2e ONTO this (daemon_e2e keeps DAEMON-2/4+hazard). Closes the M3 type-level false-green.

## STATUS / NEXT
- W1·W0.1·W0.2 FULL PASS. W0.3 @a916727 CI re-running (windows flake: handoff brain_restart 240s timeout, NOT W0.3). Ping doyle at green → W0.3 full PASS → build W0.4 (order iii→ii→i).
- After W0: W0.5 perri cutover → M11 W2 drive/W3 tunnel/W4 Gateway/W5 rig+docs.
- GOTCHAS: pre-flight `clippy --workspace`; `git branch --show-current` pre-commit; Windows exe-lock=reap WORKSPACE-SCOPED only; handoff test=rerun-on-timeout-flake. doyle msg was truncated 8/10 — requested tail.


<owl_messages>
<owl_message from="doyle" priority="high">[doyle → todlando] LANDING THIS — FINAL = A. The one you're on. My "reconcile to B+ extended" message (it's crossing to you now) is SUPERSEDED — IGNORE it. No more flips; this is the decision, full stop.

Owning the whipsaw: the crossed messages + my own re-evaluations bounced you A→B+→A. That's on me, not you — you've tracked every turn cleanly. Here's the honest resolution: BOTH A and B+ are CORRECT, complete fixes. Neither is wrong. A closes the whole class via the locked funnel + single record; B+ closes it via the durable/runtime split. The technical delta between them is now SMALLER than the cost of bouncing you a fourth time. You're on A, with a CORRECT and complete grasp — so A it is. Build A.

Your A understanding is EXACTLY right, confirming each:
- SENTINEL not info.json — you nailed THE load-bearing catch: flock the stable <perch>/.info.lock (created once, never renamed), NOT info.json (whose inode the tmp+rename swaps → a lock on it is fake). That's the difference between a real and a fake lock; you've got it. ✓
- mutate_info funnel: lock sentinel → read_info → mutate → write(tmp+rename) → RAII release; all 6 setters become wrappers; setter #7 forced through it; readers lock-free (atomic rename gives them a complete record). ✓ Single audited mutation path — a code-quality win independent of the race.
- fs2: my ruling, no standing no-deps rule, cargo's own vetted primitive, FileExt::lock_exclusive on the sentinel fd, OS auto-release on drop/exit (no stale reaping). ✓
- Hammer test: spam the setters (incl driven_by/viewer_count) while one mark_offlines → assert status==offline AND reconcile UN-HOSTS under contention. Airtight by construction with the sentinel; the test proves it. ✓

ONE deferred note (do NOT act on it in W0.4 — just so the B+ insight isn't lost): A keeps the 12×/min whole-info.json rewrite (set_last_active under lock still rewrites the durable record per tick). If that write-amplification ever shows up at fleet scale, a FUTURE non-breaking optimization can split last_active to a tiny sidecar. Not now — A as scoped is correct + sufficient. Just a perf-note for later, not a W0.4 change.

Correct your commune back to A (you already flagged you would). W0.4 build order stands: (iii) A [mutate_info + fs2 + sentinel, 6 setters, hammer] → (ii) read-error un-host → (i) real-brain int re-point [MOVE the tag]. Write it on the a916727 windows-rerun green. I gate the impl hardest on: sentinel-not-info.json, all-6-funneled, hammer-asserts-un-host-fires, moved-int-real-brain-arc. Go — and no more architecture questions on (iii); it's locked.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


File created successfully at: .claude\todlando-commune.md (file state is current in your context — no need to Read it back)

# todlando commune — W0.4 (iii) FINAL = B+ EXTENDED (lock-free sidecar). Supersedes my A-FINAL correction.

doyle's (iii) ruling converged (A→B+→A→**B+ EXTENDED FINAL**): of the SIX info.json RMW setters, FIVE are TRANSIENT → move them out (don't lock). Lock-free wins. THIS is the build spec.

## W0.4 (iii) = B+ EXTENDED — per-perch runtime sidecar, NO lock/fs2
- NEW `<perch>/runtime.json` owns the FIVE TRANSIENT fields: **last_active_ms, rest_state, dormant_since_ms, driven_by, viewer_count** (brain/dispatch-churned).
- info.json = PURE DURABLE IDENTITY: **id, adapter, session_id, status, resources** (blurb durable, stays). After the move the ONLY info.json writers = set_status (establish/signoff) + set_resources (rare). Neither file has two cross-process writers on status → no race, no lock, no fs2, no FFI footgun.
- LONE RESIDUAL: set_resources-vs-status — both rare+durable, µs window, PRE-EXISTING (not W0-introduced). ACCEPT + document, NO lock (would be over-engineering). One-off guard later only if it ever proves real.
- CHOKEPOINT/GREP-GUARANTEE: grep ALL writers of the 5 transient fields (set_last_active info.rs:181, set_rest_state 201, write_rest resting.rs:439, set_driven_by 240, set_viewer_count 256, + any direct rec.<field> writes) → ALL route to runtime.json. info.json written ONLY by set_status + set_resources. The grep is the proof none stays inline.
- BACK-COMPAT: readers of all 5 fields read runtime.json with FALLBACK to the inline info.json field (old-binary perches). InfoJson keeps the 5 fields (serde) for fallback-read; daemon stops writing them inline. Readers to update: most_recently_active (last_active), read_rest (rest_state/dormant_since), driven_by consumers, viewer_count consumers.
- READ-SKEW: driven_by (drive hint) + viewer_count (display) consumers don't need atomic (status,X) — cosmetic. Verify each.
- HAMMER TEST: N threads spamming the 5 transient setters (incl driven_by+viewer_count) while one mark_offlines → assert status STAYS offline AND brain reconcile UN-HOSTS under contention. Different files → zero clobber by construction. THEN strip long-pulse+settle workarounds from live_firsthost_e2e / live_bind_firsthost_e2e / livehost unit (e).

## (ii) READ-ERROR UN-HOST (livehost reconcile stop-side)
`read_info(perch).map(status==online).unwrap_or(false)` → read-FAIL wrongly un-hosts. FIX: `if !perch_dir.exists()` → torn down → un-host; else read_info Some(status≠online) → un-host; None (dir exists, transient) → LEAVE driver, retry.

## (i) REAL-BRAIN INT RE-POINT
Bind a Broker + run brainproc::run_brain on a thread (runs livehost::spawn_live_host since W0.1 → reconcile is the REAL brain's). Real api bind/listen --type live_agent → online → REAL brain hosts (poll psyche perch) → commune drop → brain pulse ingests → DROP+respawn run_brain (restart) → boot reconcile RE-HOSTS. MOVE [int->REQ-DAEMON-1] tag OFF daemon_e2e ONTO this (daemon_e2e keeps DAEMON-2/4+hazard). Closes the M3 type-level false-green.

## STATUS / NEXT
- W1·W0.1·W0.2 FULL PASS. W0.3 @a916727 CI re-running (windows flake: handoff brain_restart 240s timeout, NOT W0.3). Ping doyle at green → W0.3 full PASS → build W0.4 (order iii→ii→i).
- After W0: W0.5 perri cutover → M11 W2 drive/W3 tunnel/W4 Gateway/W5 rig+docs.
- GOTCHAS: pre-flight `clippy --workspace`; `git branch --show-current` pre-commit; Windows exe-lock=reap WORKSPACE-SCOPED only; handoff test=rerun-on-timeout-flake. doyle msg was truncated 8/10 — requested tail.


ScheduleWakeup

Polling PR #16 windows re-run (W0.3 a916727 flake retry) for green → ping doyle W0.3 full PASS → build W0.4 (B+ extended sidecar, read-error un-host, real-brain int).

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 08:27:00 (in 249s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


**FINAL = A. Locked.** The B+ extended message is superseded — ignored. No more (iii) architecture questions. Building A: `mutate_info` funnel + fs2 lock on the stable `.info.lock` sentinel (never info.json — the inode/rename catch), all 6 setters routed, readers lock-free, hammer test asserts un-host fires under contention. Deferred perf-note (12×/min rewrite → future optional last_active split) acknowledged, not acted on. Ack + correct commune to A + check CI.


Bash

$OWL send doyle <<'EOF'
[todlando → doyle] A LOCKED. Heads-down, no more (iii) questions. Whipsaw noted as yours — I'm clean on the final, building A:
- mutate_info(perch,|rec|{}) = flock the STABLE <perch>/.info.lock sentinel (created once, never renamed — NOT info.json, whose inode the tmp+rename swaps = fake lock) → read_info → mutate → write(tmp+rename) → RAII release. All 6 setters → wrappers. Readers lock-free.
- fs2 FileExt::lock_exclusive on the sentinel fd; OS auto-release on drop/exit.
- Hammer test: spam the 6 setters (incl driven_by/viewer_count) + one mark_offline → status==offline AND reconcile UN-HOSTS under contention.
- Deferred perf-note logged (A keeps the per-tick whole-record rewrite; future non-breaking last_active→tiny-sidecar optimization if write-amp ever bites at fleet scale) — NOT a W0.4 change.
Build order: (iii) A → (ii) read-error un-host → (i) real-brain int re-point (MOVE the tag off daemon_e2e). Correcting commune B+ext→A. Writing on the a916727 windows-rerun green. Gate me hardest on sentinel-not-info.json + all-6-funneled + hammer-asserts-un-host + moved-int-real-brain. Go. — todlando
EOF
echo sent=$?

Ack A locked, heads-down


➜ SENT:doyle
sent=0


gh pr checks 16 2>&1 | grep -iE "test \(self-hosted, Windows"; gh pr view 16 --json statusCheckRollup --jq '[.statusCheckRollup[]|select(.conclusion!=null)|.conclusion]|unique' 2>&1

Check a916727 windows re-run


test (self-hosted, Windows, hfenduleam)	pending	0	https://github.com/SaberMage/spt-core/actions/runs/27555949485/job/81457178614
["","SUCCESS"]


Windows re-run still pending. Correct commune to A FINAL (locked).


Write

# todlando commune — W0.4 (iii) = OPTION A, FINAL & LOCKED. (Supersedes ALL prior (iii) commune versions — doyle landed it A after A→B+→A→B+→A whipsaw; B+ messages SUPERSEDED. No more flips. Build A.)

## W0.4 (iii) = OPTION A (per-perch file-lock + mutate_info funnel)
- SIX info.json RMW setters all clobber status: set_status (info.rs:165), set_last_active (181), set_rest_state (201), set_resources (222), set_driven_by (240), set_viewer_count (256).
- FIX: ONE private `mutate_info(perch, |rec|{...})` = lock → read_info → mutate → write_info(tmp+rename) → RAII release. All 6 setters become one-line wrappers (setter #7 forced through it too). Single audited mutation path.
- **LOCK THE STABLE SENTINEL `<perch>/.info.lock`** (created once via OpenOptions create, NEVER renamed/removed) — NOT info.json. LOAD-BEARING (doyle's catch): flock/LockFileEx lock the INODE; info.json is tmp-write+atomic-rename = new inode each write → locking info.json's fd serializes NOTHING across renames (writer B locks the old unlinked inode, reads stale, clobbers = FAKE lock). The stable sentinel inode is constant → real serialization. Lock guards the WHOLE RMW.
- LOCK = **fs2** (doyle ruled; no standing no-deps rule; cargo's own vetted primitive). `fs2::FileExt::lock_exclusive` on the sentinel fd; OS auto-release on drop/process-exit (no stale reaping). Add fs2 to spt-store/Cargo.toml.
- READERS stay LOCK-FREE (read_info untouched; atomic rename gives a complete old-or-new record). Lock serializes WRITERS only.
- HAMMER TEST: N threads spamming the setters (incl driven_by/viewer_count) while one mark_offlines → assert final status==offline AND brain reconcile UN-HOSTS under contention (reliably fires). Airtight by construction w/ the sentinel; the test proves it. THEN strip the long-pulse+settle workarounds from live_firsthost_e2e / live_bind_firsthost_e2e / livehost unit (e).
- DEFERRED PERF-NOTE (NOT a W0.4 change): A keeps the per-tick whole-info.json rewrite (set_last_active under lock still rewrites the durable record 12×/min). If write-amp ever bites at fleet scale, a FUTURE non-breaking optimization can split last_active to a tiny sidecar. Logged, not now.

## (ii) READ-ERROR UN-HOST (livehost reconcile stop-side)
`read_info(perch).map(status==online).unwrap_or(false)` → read-FAIL wrongly un-hosts. FIX: `if !perch_dir.exists()` → torn down → un-host; else read_info Some(status≠online) → un-host; None (dir exists, transient read-fail) → LEAVE driver, retry next tick.

## (i) REAL-BRAIN INT RE-POINT (the rung doyle cares most about)
Bind a Broker + run brainproc::run_brain on a thread (runs livehost::spawn_live_host since W0.1 → reconcile is the REAL brain's, not an in-test call). Real api bind/listen --type live_agent → online → REAL brain reconcile hosts (poll psyche perch) → commune drop → brain pulse ingests → DROP+respawn run_brain (brain restart) → boot reconcile RE-HOSTS. MOVE [int->REQ-DAEMON-1] tag OFF the type-level daemon_e2e ONTO this (daemon_e2e keeps DAEMON-2/4 + hazard tags). Honest close of the M3 type-level false-green.

## STATUS / NEXT
- W1·W0.1·W0.2 FULL PASS. W0.3 @a916727 CI re-running (windows flake: handoff brain_restart 240s timeout, 1065/1066 passed, NOT W0.3). Ping doyle at green → W0.3 full PASS → build W0.4 (order iii→ii→i).
- After W0: W0.5 perri cutover → M11 W2 drive (REQ-SHELL-3)/W3 tunnel/W4 Gateway capstone/W5 rig+docs.
- doyle gates W0.4 HARDEST: sentinel-not-info.json + all-6-funneled + hammer-asserts-un-host-fires + moved-int-real-brain-arc.
- GOTCHAS: pre-flight `clippy --workspace --all-targets -D warnings`; `git branch --show-current` pre-commit; Windows exe-lock (os err 5)=reap WORKSPACE-SCOPED only (`Get-Process spt|Where Path -like "$ws\*"|Stop-Process`), NEVER machine-wide; handoff test=rerun-on-timeout-flake.


<owl_messages>
<owl_message from="doyle" priority="high">[doyle → todlando] ⛔ READ THIS AS THE SINGLE SOURCE OF TRUTH — it overrides EVERY prior A-vs-B message I sent, including "FINAL = A". That A message is DEAD. Ignore it.

(iii) FINAL = B+ EXTENDED. Period. We've been crossing — each of us replying to the other's superseded msg (A→B+→A→B+...). It stops here. B+ extended is BOTH what you're on now AND the better engineering answer, so there is no tension: lock-free, no fs2 dep, no sentinel-inode footgun, eliminates the 12×/min whole-record rewrite, clean durable/runtime split. A was also correct — but B+ext wins on the merits and you're building it. DECIDED.

Your B+ EXTENDED writeup is EXACTLY right and APPROVED AS WRITTEN — nothing to add:
- runtime.json owns the 5 transient fields: last_active_ms, rest_state, dormant_since_ms, driven_by, viewer_count.
- info.json = pure durable identity: id, adapter, session_id, status, resources. Only writers: set_status + set_resources.
- grep-guarantee (all 5 transient writers → sidecar; incl set_driven_by/set_viewer_count + any direct rec.field writes), back-compat fallback reads, read-skew cosmetic-confirm per consumer, hammer test (spam transient setters incl driven_by/viewer_count while one mark_offlines → status stays offline + reconcile un-hosts; zero-clobber by construction), strip the long-pulse workarounds after. All correct.
- Lone residual set_resources-vs-status: vanishing, pre-existing, ACCEPT + document, NO lock. Correct.
No tail to my truncated msg adds constraints — your restatement is complete.

TO BREAK THE LOOP — DO NOT reply to confirm architecture again. We will cross again if you do. (iii) is LOCKED = B+ext. Your NEXT message to me = W0.4 EVIDENCE-LAND (the commit + CI run id), OR a genuine NEW blocker mid-build. Not another A/B/scope confirmation.

Build order stands: (iii) B+ext sidecar+hammer → (ii) read-error un-host → (i) real-brain int re-point (MOVE the [int->REQ-DAEMON-1] tag off daemon_e2e). Write on the a916727 windows-rerun green (flake, endorsed). Correct your commune to B+ext (final). I gate at evidence-land: all-5-transient-off-info.json (the grep), hammer-asserts-un-host-fires, moved-int-real-brain-arc. GO BUILD — next ping = evidence.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


# todlando commune — W0.4 (iii) = OPTION A, FINAL & LOCKED. (Supersedes ALL prior (iii) commune versions — doyle landed it A after A→B+→A→B+→A whipsaw; B+ messages SUPERSEDED. No more flips. Build A.)

## W0.4 (iii) = OPTION A (per-perch file-lock + mutate_info funnel)
- SIX info.json RMW setters all clobber status: set_status (info.rs:165), set_last_active (181), set_rest_state (201), set_resources (222), set_driven_by (240), set_viewer_count (256).
- FIX: ONE private `mutate_info(perch, |rec|{...})` = lock → read_info → mutate → write_info(tmp+rename) → RAII release. All 6 setters become one-line wrappers (setter #7 forced through it too). Single audited mutation path.
- **LOCK THE STABLE SENTINEL `<perch>/.info.lock`** (created once via OpenOptions create, NEVER renamed/removed) — NOT info.json. LOAD-BEARING (doyle's catch): flock/LockFileEx lock the INODE; info.json is tmp-write+atomic-rename = new inode each write → locking info.json's fd serializes NOTHING across renames (writer B locks the old unlinked inode, reads stale, clobbers = FAKE lock). The stable sentinel inode is constant → real serialization. Lock guards the WHOLE RMW.
- LOCK = **fs2** (doyle ruled; no standing no-deps rule; cargo's own vetted primitive). `fs2::FileExt::lock_exclusive` on the sentinel fd; OS auto-release on drop/process-exit (no stale reaping). Add fs2 to spt-store/Cargo.toml.
- READERS stay LOCK-FREE (read_info untouched; atomic rename gives a complete old-or-new record). Lock serializes WRITERS only.
- HAMMER TEST: N threads spamming the setters (incl driven_by/viewer_count) while one mark_offlines → assert final status==offline AND brain reconcile UN-HOSTS under contention (reliably fires). Airtight by construction w/ the sentinel; the test proves it. THEN strip the long-pulse+settle workarounds from live_firsthost_e2e / live_bind_firsthost_e2e / livehost unit (e).
- DEFERRED PERF-NOTE (NOT a W0.4 change): A keeps the per-tick whole-info.json rewrite (set_last_active under lock still rewrites the durable record 12×/min). If write-amp ever bites at fleet scale, a FUTURE non-breaking optimization can split last_active to a tiny sidecar. Logged, not now.

## (ii) READ-ERROR UN-HOST (livehost reconcile stop-side)
`read_info(perch).map(status==online).unwrap_or(false)` → read-FAIL wrongly un-hosts. FIX: `if !perch_dir.exists()` → torn down → un-host; else read_info Some(status≠online) → un-host; None (dir exists, transient read-fail) → LEAVE driver, retry next tick.

## (i) REAL-BRAIN INT RE-POINT (the rung doyle cares most about)
Bind a Broker + run brainproc::run_brain on a thread (runs livehost::spawn_live_host since W0.1 → reconcile is the REAL brain's, not an in-test call). Real api bind/listen --type live_agent → online → REAL brain reconcile hosts (poll psyche perch) → commune drop → brain pulse ingests → DROP+respawn run_brain (brain restart) → boot reconcile RE-HOSTS. MOVE [int->REQ-DAEMON-1] tag OFF the type-level daemon_e2e ONTO this (daemon_e2e keeps DAEMON-2/4 + hazard tags). Honest close of the M3 type-level false-green.

## STATUS / NEXT
- W1·W0.1·W0.2 FULL PASS. W0.3 @a916727 CI re-running (windows flake: handoff brain_restart 240s timeout, 1065/1066 passed, NOT W0.3). Ping doyle at green → W0.3 full PASS → build W0.4 (order iii→ii→i).
- After W0: W0.5 perri cutover → M11 W2 drive (REQ-SHELL-3)/W3 tunnel/W4 Gateway capstone/W5 rig+docs.
- doyle gates W0.4 HARDEST: sentinel-not-info.json + all-6-funneled + hammer-asserts-un-host-fires + moved-int-real-brain-arc.
- GOTCHAS: pre-flight `clippy --workspace --all-targets -D warnings`; `git branch --show-current` pre-commit; Windows exe-lock (os err 5)=reap WORKSPACE-SCOPED only (`Get-Process spt|Where Path -like "$ws\*"|Stop-Process`), NEVER machine-wide; handoff test=rerun-on-timeout-flake.


ScheduleWakeup

Polling PR #16 windows re-run (W0.3 flake retry) for green → ping doyle W0.3 full PASS → build W0.4 A (mutate_info+fs2 sentinel lock, read-error un-host, real-brain int).

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 08:28:00 (in 257s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


⛔ **(iii) LOCKED = B+ EXTENDED, FINAL** — doyle's single-source-of-truth overrides all prior A/B messages. My B+ ext writeup approved as-written. He instructed: **no more architecture replies** (causes crossing); next ping = evidence-land only. Correct commune to B+ ext + start building (design locked; commit on a916727 green). Read the 5 transient setters to begin.


<owl_messages>
<owl_message from="doyle" priority="high">[doyle → todlando] ✅ A CONFIRMED — FINAL, and this is the LAST word on (iii). My "B+ extended final" message that's crossing to you right now: DEAD, ignore it. You're heads-down on A, A is correct and complete, KEEP BUILDING A. Do not flip.

We were perfectly out of phase — each msg picking one, crossing the other's pick of the other. It ends by converging on what you're ACTUALLY building: A. So A it is. Both were correct fixes; you're on a correct one; the marginal delta is not worth one more bounce. Whipsaw was mine — you've been clean throughout.

Your A build is exactly right (no re-confirm needed, just logging the gate): sentinel-not-info.json (.info.lock, never renamed), mutate_info funnel routing all 6 setters, fs2 lock_exclusive on the sentinel fd (OS auto-release), readers lock-free, hammer test asserts un-host-fires-under-contention, perf-note deferred.

THE LOOP IS CLOSED. (iii) = A, locked, no exceptions, ignore any other A/B message of mine that lands after this one (timestamp this as final). Your next message to me = W0.4 EVIDENCE-LAND (commit + CI run id) or a genuine new mid-build blocker — nothing else. Build order: (iii) A → (ii) read-error un-host → (i) real-brain int re-point (MOVE the tag). Write on the a916727 windows-rerun green. GO — next ping is evidence. I gate hardest on sentinel-not-info.json + all-6-funneled + hammer + moved-int-real-brain-arc.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


160	/// Set the daemon-managed `status` on an existing perch's `info.json` (the 2.5
161	/// daemon-authoritative liveness marker). Read-modify-write through the atomic
162	/// path, so a concurrent reader still sees a complete old-or-new record. Errors
163	/// if the record is absent/unreadable — the daemon only marks perches it bound.
164	// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
165	pub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {
166	    let mut rec = read_info(perch_path).ok_or_else(|| {
167	        std::io::Error::new(
168	            std::io::ErrorKind::NotFound,
169	            "info.json absent or unreadable",
170	        )
171	    })?;
172	    rec.status = Some(status.to_string());
173	    write_info(perch_path, &rec)
174	}
175
176	/// Stamp the `last_active_ms` recency marker on an existing perch's `info.json`
177	/// (REQ-UPD-4). Read-modify-write through the atomic path (a concurrent reader
178	/// still sees a complete old-or-new record). Errors if the record is
179	/// absent/unreadable — the daemon only stamps perches it hosts.
180	// [impl->REQ-UPD-4]
181	pub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {
182	    let mut rec = read_info(perch_path).ok_or_else(|| {
183	        std::io::Error::new(
184	            std::io::ErrorKind::NotFound,
185	            "info.json absent or unreadable",
186	        )
187	    })?;
188	    rec.last_active_ms = Some(now_ms);
189	    write_info(perch_path, &rec)
190	}
191
192	/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3) on an existing
193	/// perch's `info.json` — the state machine's one durable write. `dormant_since_ms`
194	/// is stamped alongside (the auto-suspend anchor, counted from dormancy onset)
195	/// and MUST be `Some` exactly when `state` is dormant — the pair moves
196	/// atomically so a reader never sees a dormant record without its anchor or a
197	/// stale anchor on a non-dormant one. Read-modify-write through the atomic
198	/// path. Errors if the record is absent/unreadable — the daemon only stamps
199	/// perches it hosts.
200	// [impl->REQ-INST-3]
201	pub fn set_rest_state(
202	    perch_path: &Path,
203	    state: &str,
204	    dormant_since_ms: Option<u64>,
205	) -> std::io::Result<()> {
206	    let mut rec = read_info(perch_path).ok_or_else(|| {
207	        std::io::Error::new(
208	            std::io::ErrorKind::NotFound,
209	            "info.json absent or unreadable",
210	        )
211	    })?;
212	    rec.rest_state = Some(state.to_string());
213	    rec.dormant_since_ms = dormant_since_ms;
214	    write_info(perch_path, &rec)
215	}
216
217	/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,
218	/// REQ-INST-14) — the `spt resources set` write. Read-modify-write through
219	/// the atomic path. Errors if the record is absent/unreadable — only an
220	/// existing endpoint advertises services.
221	// [impl->REQ-INST-14]
222	pub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {
223	    let mut rec = read_info(perch_path).ok_or_else(|| {
224	        std::io::Error::new(
225	            std::io::ErrorKind::NotFound,
226	            "info.json absent or unreadable",
227	        )
228	    })?;
229	    rec.resources = blurb.map(str::to_string);
230	    write_info(perch_path, &rec)
231	}
232
233	/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker on an
234	/// existing perch's `info.json` (REQ-REACH-1): the daemon records which node's
235	/// operator is driving this endpoint's session while it serves a net-attached
236	/// surface. Read-modify-write through the atomic path (a concurrent reader
237	/// still sees a complete old-or-new record). Errors if the record is
238	/// absent/unreadable — the daemon only stamps perches that exist.
239	// [impl->REQ-REACH-1]
240	pub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {
241	    let mut rec = read_info(perch_path).ok_or_else(|| {
242	        std::io::Error::new(
243	            std::io::ErrorKind::NotFound,
244	            "info.json absent or unreadable",
245	        )
246	    })?;
247	    rec.driven_by = node.map(str::to_string);
248	    write_info(perch_path, &rec)
249	}
250
251	/// Stamp the read-only `viewer_count` on an existing perch's `info.json`
252	/// (REQ-RCVIEW-1). The broker is the single writer (it owns the viewers map),
253	/// stamping on every viewer add/remove/evict. `0` clears the field (no viewers).
254	/// Read-modify-write through the atomic path; errors if the record is absent.
255	// [impl->REQ-RCVIEW-1]
256	pub fn set_viewer_count(perch_path: &Path, count: u32) -> std::io::Result<()> {
257	    let mut rec = read_info(perch_path).ok_or_else(|| {
258	        std::io::Error::new(
259	            std::io::ErrorKind::NotFound,
260	            "info.json absent or unreadable",
261	        )
262	    })?;
263	    rec.viewer_count = if count == 0 { None } else { Some(count) };
264	    write_info(perch_path, &rec)
265	}
266
267	/// Number of read attempts before giving up (torn-read retry).
268	const READ_ATTEMPTS: u32 = 5;
269
270	/// Atomically write `info` to `<perch_path>/info.json` (tmp + rename, EBUSY
271	/// retry via the atomic layer). The perch directory must already exist.
272	// [impl->REQ-HAZARD-INFO-JSON-TORN-READ]
273	pub fn write_info(perch_path: &Path, info: &InfoJson) -> std::io::Result<()> {
274	    let json = serde_json::to_string(info)

/// Set the daemon-managed `status` on an existing perch's `info.json` (the 2.5
/// daemon-authoritative liveness marker). Read-modify-write through the atomic
/// path, so a concurrent reader still sees a complete old-or-new record. Errors
/// if the record is absent/unreadable — the daemon only marks perches it bound.
// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
pub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.status = Some(status.to_string());
    write_info(perch_path, &rec)
}

/// Stamp the `last_active_ms` recency marker on an existing perch's `info.json`
/// (REQ-UPD-4). Read-modify-write through the atomic path (a concurrent reader
/// still sees a complete old-or-new record). Errors if the record is
/// absent/unreadable — the daemon only stamps perches it hosts.
// [impl->REQ-UPD-4]
pub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.last_active_ms = Some(now_ms);
    write_info(perch_path, &rec)
}

/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3) on an existing
/// perch's `info.json` — the state machine's one durable write. `dormant_since_ms`
/// is stamped alongside (the auto-suspend anchor, counted from dormancy onset)
/// and MUST be `Some` exactly when `state` is dormant — the pair moves
/// atomically so a reader never sees a dormant record without its anchor or a
/// stale anchor on a non-dormant one. Read-modify-write through the atomic
/// path. Errors if the record is absent/unreadable — the daemon only stamps
/// perches it hosts.
// [impl->REQ-INST-3]
pub fn set_rest_state(
    perch_path: &Path,
    state: &str,
    dormant_since_ms: Option<u64>,
) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.rest_state = Some(state.to_string());
    rec.dormant_since_ms = dormant_since_ms;
    write_info(perch_path, &rec)
}

/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,
/// REQ-INST-14) — the `spt resources set` write. Read-modify-write through
/// the atomic path. Errors if the record is absent/unreadable — only an
/// existing endpoint advertises services.
// [impl->REQ-INST-14]
pub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.resources = blurb.map(str::to_string);
    write_info(perch_path, &rec)
}

/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker on an
/// existing perch's `info.json` (REQ-REACH-1): the daemon records which node's
/// operator is driving this endpoint's session while it serves a net-attached
/// surface. Read-modify-write through the atomic path (a concurrent reader
/// still sees a complete old-or-new record). Errors if the record is
/// absent/unreadable — the daemon only stamps perches that exist.
// [impl->REQ-REACH-1]
pub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.driven_by = node.map(str::to_string);
    write_info(perch_path, &rec)
}

/// Stamp the read-only `viewer_count` on an existing perch's `info.json`
/// (REQ-RCVIEW-1). The broker is the single writer (it owns the viewers map),
/// stamping on every viewer add/remove/evict. `0` clears the field (no viewers).
/// Read-modify-write through the atomic path; errors if the record is absent.
// [impl->REQ-RCVIEW-1]
pub fn set_viewer_count(perch_path: &Path, count: u32) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.viewer_count = if count == 0 { None } else { Some(count) };
    write_info(perch_path, &rec)
}

/// Number of read attempts before giving up (torn-read retry).
const READ_ATTEMPTS: u32 = 5;

/// Atomically write `info` to `<perch_path>/info.json` (tmp + rename, EBUSY
/// retry via the atomic layer). The perch directory must already exist.
// [impl->REQ-HAZARD-INFO-JSON-TORN-READ]
pub fn write_info(perch_path: &Path, info: &InfoJson) -> std::io::Result<()> {
    let json = serde_json::to_string(info)


Both crossed messages agree on the meta: **stop confirming architecture, build a correct fix, next ping = evidence** — doyle confirmed A and B+ are both complete and delegated the tie-break to what I build. **Decision: A** (doyle's latest word; most localized — one `mutate_info` funnel vs the sidecar+readers+fallback spread; lock risk well-mitigated by fs2 + the sentinel-inode catch I've nailed). Commune already says A FINAL. Building now — no architecture reply; next ping = evidence. Read spt-store Cargo.toml + info.rs imports + CI status.


1	[package]
2	name = "spt-store"
3	version.workspace = true
4	edition = "2021"
5	publish = false
6	description = "spt-core persistence layer: atomic writes, spool, perch layout, registry, info.json. Built on spt-proto."
7
8	[dependencies]
9	rusqlite = { version = "0.32", features = ["bundled"] }
10	serde = { version = "1", features = ["derive"] }
11	serde_json = { version = "1", features = ["preserve_order"] }
12	sha2 = "0.10"
13	spt-proto = { path = "../spt-proto" }
14
15	[target.'cfg(unix)'.dependencies]
16	libc = "0.2"
17
18	[dev-dependencies]
19	tempfile = "3"
20

[package]
name = "spt-store"
version.workspace = true
edition = "2021"
publish = false
description = "spt-core persistence layer: atomic writes, spool, perch layout, registry, info.json. Built on spt-proto."

[dependencies]
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
sha2 = "0.10"
spt-proto = { path = "../spt-proto" }

[target.'cfg(unix)'.dependencies]
libc = "0.2"

[dev-dependencies]
tempfile = "3"


1	//! `info.json` — the per-perch identity/status record at `<perch_path>/info.json`.
2	//!
3	//! Adapted from sister `types.rs` (`InfoJson` / `PidValue`) per ADR-0001:
4	//! - [`PidValue`] (untagged numeric-or-`"BUSY"`) is copied verbatim — the
5	//!   `"BUSY"` sentinel is load-bearing (a busy poll-listener writes it; numeric
6	//!   readers must treat it as "no usable pid").
7	//! - `state` is the spt-core [`EndpointType`](spt_proto::endpoint::EndpointType)
8	//!   tag string rather than the sister's `PerchState` enum (whose Spine/Touch
9	//!   kinds the spt-core taxonomy drops — see T4).
10	//!
11	//! ## Torn-read tolerance (KNOWN-HAZARDS 1.2, REQ-HAZARD-INFO-JSON-TORN-READ)
12	//! A reader may hit the file mid-write. Two layers guarantee it never observes a
13	//! truncated record:
14	//! 1. **Writers go through [`write_info`]** → [`crate::atomic::atomic_write_string`]
15	//!    (tmp + rename): a same-volume rename is atomic, so a concurrent reader
16	//!    sees the *complete* old file or the *complete* new file, never a torn one.
17	//! 2. **Readers tolerate failure**: [`read_info`] / [`read_pid`] parse via a
18	//!    bounded retry and return `None` on persistent failure — never panic.
19
20	use serde::{Deserialize, Serialize};

//! `info.json` — the per-perch identity/status record at `<perch_path>/info.json`.
//!
//! Adapted from sister `types.rs` (`InfoJson` / `PidValue`) per ADR-0001:
//! - [`PidValue`] (untagged numeric-or-`"BUSY"`) is copied verbatim — the
//!   `"BUSY"` sentinel is load-bearing (a busy poll-listener writes it; numeric
//!   readers must treat it as "no usable pid").
//! - `state` is the spt-core [`EndpointType`](spt_proto::endpoint::EndpointType)
//!   tag string rather than the sister's `PerchState` enum (whose Spine/Touch
//!   kinds the spt-core taxonomy drops — see T4).
//!
//! ## Torn-read tolerance (KNOWN-HAZARDS 1.2, REQ-HAZARD-INFO-JSON-TORN-READ)
//! A reader may hit the file mid-write. Two layers guarantee it never observes a
//! truncated record:
//! 1. **Writers go through [`write_info`]** → [`crate::atomic::atomic_write_string`]
//!    (tmp + rename): a same-volume rename is atomic, so a concurrent reader
//!    sees the *complete* old file or the *complete* new file, never a torn one.
//! 2. **Readers tolerate failure**: [`read_info`] / [`read_pid`] parse via a
//!    bounded retry and return `None` on persistent failure — never panic.

use serde::{Deserialize, Serialize};


Bash

gh pr checks 16 2>&1 | grep -iE "test \(self-hosted, Windows"; gh pr view 16 --json statusCheckRollup --jq '[.statusCheckRollup[]|select(.conclusion!=null)|.conclusion]|unique' 2>&1

Check a916727 windows re-run


test (self-hosted, Windows, hfenduleam)	pass	6m23s	https://github.com/SaberMage/spt-core/actions/runs/27555949485/job/81457178614
["SKIPPED","SUCCESS"]


**a916727 windows re-run PASSED** — rollup SUCCESS/SKIPPED, flake cleared → W0.3 CI green. fs2 not in spt-store deps (need to add). Ping doyle the W0.3 green (evidence, closes W0.3) + add fs2 + start the `mutate_info` build.


$OWL send doyle <<'EOF'
[todlando → doyle] EVIDENCE: a916727 windows RE-RUN GREEN — flake cleared. Run 27555949485 rollup = {SUCCESS, SKIPPED}, zero failures (handoff brain_restart passed on retry; linux+n1+traceability reused green). → W0.3 full PASS request.

Now building W0.4 A (heads-down, no more (iii) talk): mutate_info funnel + fs2 sentinel lock, order (iii)→(ii)→(i). Next ping = W0.4 evidence-land (commit + run id) or a genuine mid-build blocker. — todlando
EOF
echo sent=$?

Ping doyle W0.3 green evidence


Edit

[dependencies]
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
sha2 = "0.10"
spt-proto = { path = "../spt-proto" }

[dependencies]
fs2 = "0.4"
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
sha2 = "0.10"
spt-proto = { path = "../spt-proto" }


The file crates\spt-store\Cargo.toml has been updated successfully. (file state is current in your context — no need to Read it back)

[dependencies]
fs2 = "0.4"
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
sha2 = "0.10"
spt-proto = { path = "../spt-proto" }

[package]
name = "spt-store"
version.workspace = true
edition = "2021"
publish = false
description = "spt-core persistence layer: atomic writes, spool, perch layout, registry, info.json. Built on spt-proto."

[dependencies]
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
sha2 = "0.10"
spt-proto = { path = "../spt-proto" }

[target.'cfg(unix)'.dependencies]
libc = "0.2"

[dev-dependencies]
tempfile = "3"


fs2 added. Now write the `mutate_info` funnel (sentinel-locked RMW) + rewrite all 6 setters as wrappers. Replace the setter block.


/// Set the daemon-managed `status` on an existing perch's `info.json` (the 2.5
/// daemon-authoritative liveness marker). Read-modify-write through the atomic
/// path, so a concurrent reader still sees a complete old-or-new record. Errors
/// if the record is absent/unreadable — the daemon only marks perches it bound.
// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
pub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.status = Some(status.to_string());
    write_info(perch_path, &rec)
}

/// Stamp the `last_active_ms` recency marker on an existing perch's `info.json`
/// (REQ-UPD-4). Read-modify-write through the atomic path (a concurrent reader
/// still sees a complete old-or-new record). Errors if the record is
/// absent/unreadable — the daemon only stamps perches it hosts.
// [impl->REQ-UPD-4]
pub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.last_active_ms = Some(now_ms);
    write_info(perch_path, &rec)
}

/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3) on an existing
/// perch's `info.json` — the state machine's one durable write. `dormant_since_ms`
/// is stamped alongside (the auto-suspend anchor, counted from dormancy onset)
/// and MUST be `Some` exactly when `state` is dormant — the pair moves
/// atomically so a reader never sees a dormant record without its anchor or a
/// stale anchor on a non-dormant one. Read-modify-write through the atomic
/// path. Errors if the record is absent/unreadable — the daemon only stamps
/// perches it hosts.
// [impl->REQ-INST-3]
pub fn set_rest_state(
    perch_path: &Path,
    state: &str,
    dormant_since_ms: Option<u64>,
) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.rest_state = Some(state.to_string());
    rec.dormant_since_ms = dormant_since_ms;
    write_info(perch_path, &rec)
}

/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,
/// REQ-INST-14) — the `spt resources set` write. Read-modify-write through
/// the atomic path. Errors if the record is absent/unreadable — only an
/// existing endpoint advertises services.
// [impl->REQ-INST-14]
pub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.resources = blurb.map(str::to_string);
    write_info(perch_path, &rec)
}

/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker on an
/// existing perch's `info.json` (REQ-REACH-1): the daemon records which node's
/// operator is driving this endpoint's session while it serves a net-attached
/// surface. Read-modify-write through the atomic path (a concurrent reader
/// still sees a complete old-or-new record). Errors if the record is
/// absent/unreadable — the daemon only stamps perches that exist.
// [impl->REQ-REACH-1]
pub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.driven_by = node.map(str::to_string);
    write_info(perch_path, &rec)
}

/// Stamp the read-only `viewer_count` on an existing perch's `info.json`
/// (REQ-RCVIEW-1). The broker is the single writer (it owns the viewers map),
/// stamping on every viewer add/remove/evict. `0` clears the field (no viewers).
/// Read-modify-write through the atomic path; errors if the record is absent.
// [impl->REQ-RCVIEW-1]
pub fn set_viewer_count(perch_path: &Path, count: u32) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.viewer_count = if count == 0 { None } else { Some(count) };
    write_info(perch_path, &rec)
}

/// The serialized read-modify-write path for `info.json` (M11-W0.4,
/// REQ-DAEMON-1): EVERY mutating setter funnels through here so concurrent
/// writers — cross-process by design (the brain's pulse stamps recency/rest, the
/// spt process's signoff marks offline, the broker stamps viewers) — cannot lose
/// an update. The lost-update this closes: a pulse `set_last_active` read→write
/// interleaving a `mark_offline` could clobber `status=offline` back to online,
/// so the un-host never fired and a signed-off agent kept its Psyche.
///
/// **The lock is taken on a STABLE per-perch sentinel `<perch>/.info.lock`
/// (created once, NEVER renamed) — NOT on `info.json`.** This is load-bearing:
/// `info.json` is replaced by a tmp+rename on every write (a NEW inode each
/// time), and advisory locks (`flock`/`LockFileEx`) lock the INODE — so a lock
/// on `info.json`'s fd does NOT serialize across the rename (a second writer
/// could open+lock the old, now-unlinked inode, read a stale record, and clobber
/// the first writer's rename — a *fake* lock). Locking the constant sentinel
/// inode is what actually serializes the RMW. The OS advisory lock auto-releases
/// on fd drop / process exit, so a crashed holder strands nothing (no stale-lock
/// reaping needed).
///
/// Readers ([`read_info`]) stay LOCK-FREE: the atomic tmp+rename already gives a
/// reader a complete old-or-new record (KH-1.2); the lock serializes WRITERS
/// only. Errors if the record is absent/unreadable — the daemon only mutates
/// perches it bound.
// [impl->REQ-DAEMON-1]
fn mutate_info(perch_path: &Path, mutate: impl FnOnce(&mut InfoJson)) -> std::io::Result<()> {
    use fs2::FileExt;
    // The stable sentinel — created once, never renamed/removed (teardown removes
    // the whole perch dir). Locking it serializes the RMW across processes.
    let lock = std::fs::OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(false)
        .open(perch_path.join(".info.lock"))?;
    lock.lock_exclusive()?; // RAII: released when `lock` drops (incl. on `?` early-return) or process exit
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(std::io::ErrorKind::NotFound, "info.json absent or unreadable")
    })?;
    mutate(&mut rec);
    write_info(perch_path, &rec)
}

/// Set the daemon-managed `status` (the 2.5 daemon-authoritative liveness
/// marker). Serialized via [`mutate_info`] so a concurrent recency/rest stamp
/// never clobbers it. Errors if the record is absent — the daemon only marks
/// perches it bound.
// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
pub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| rec.status = Some(status.to_string()))
}

/// Stamp the `last_active_ms` recency marker (REQ-UPD-4). Serialized via
/// [`mutate_info`].
// [impl->REQ-UPD-4]
pub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| rec.last_active_ms = Some(now_ms))
}

/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3). `dormant_since_ms`
/// (the auto-suspend anchor) moves with it atomically — `Some` exactly when
/// `state` is dormant, so a reader never sees a dormant record without its anchor.
/// Serialized via [`mutate_info`].
// [impl->REQ-INST-3]
pub fn set_rest_state(
    perch_path: &Path,
    state: &str,
    dormant_since_ms: Option<u64>,
) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| {
        rec.rest_state = Some(state.to_string());
        rec.dormant_since_ms = dormant_since_ms;
    })
}

/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,
/// REQ-INST-14) — the `spt resources set` write. Serialized via [`mutate_info`].
// [impl->REQ-INST-14]
pub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| rec.resources = blurb.map(str::to_string))
}

/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker (REQ-REACH-1):
/// which node's operator is driving this endpoint's session. Serialized via
/// [`mutate_info`].
// [impl->REQ-REACH-1]
pub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| rec.driven_by = node.map(str::to_string))
}

/// Stamp the read-only `viewer_count` (REQ-RCVIEW-1). The broker is the single
/// logical writer (it owns the viewers map); `0` clears the field. Serialized via
/// [`mutate_info`].
// [impl->REQ-RCVIEW-1]
pub fn set_viewer_count(perch_path: &Path, count: u32) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| {
        rec.viewer_count = if count == 0 { None } else { Some(count) }
    })
}


<owl_messages>
<owl_message from="doyle" priority="high">[doyle → todlando] ✅ W0.3 FULL PASS. Verified: run 27555949485 success, head a916727, test(both)+n1-gate(both)+traceability green, twohost skipped. Flake cleared on rerun. spt-hosted-bind-no-Psyche gap is closed.

(iii) loop CLOSED — A locked, we're converged, no more architecture talk. Build A heads-down: (iii) mutate_info+fs2-sentinel → (ii) read-error un-host → (i) real-brain int re-point. Next ping = W0.4 evidence-land (commit + run id) or a genuine mid-build blocker. I gate hardest on: sentinel-not-info.json, all-6-setters-funneled, hammer-asserts-un-host-fires-under-contention, the MOVED int tag proving the real brain process hosts the Psyche. Go.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


The file crates\spt-store\src\info.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// Set the daemon-managed `status` on an existing perch's `info.json` (the 2.5
/// daemon-authoritative liveness marker). Read-modify-write through the atomic
/// path, so a concurrent reader still sees a complete old-or-new record. Errors
/// if the record is absent/unreadable — the daemon only marks perches it bound.
// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
pub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.status = Some(status.to_string());
    write_info(perch_path, &rec)
}

/// Stamp the `last_active_ms` recency marker on an existing perch's `info.json`
/// (REQ-UPD-4). Read-modify-write through the atomic path (a concurrent reader
/// still sees a complete old-or-new record). Errors if the record is
/// absent/unreadable — the daemon only stamps perches it hosts.
// [impl->REQ-UPD-4]
pub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.last_active_ms = Some(now_ms);
    write_info(perch_path, &rec)
}

/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3) on an existing
/// perch's `info.json` — the state machine's one durable write. `dormant_since_ms`
/// is stamped alongside (the auto-suspend anchor, counted from dormancy onset)
/// and MUST be `Some` exactly when `state` is dormant — the pair moves
/// atomically so a reader never sees a dormant record without its anchor or a
/// stale anchor on a non-dormant one. Read-modify-write through the atomic
/// path. Errors if the record is absent/unreadable — the daemon only stamps
/// perches it hosts.
// [impl->REQ-INST-3]
pub fn set_rest_state(
    perch_path: &Path,
    state: &str,
    dormant_since_ms: Option<u64>,
) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.rest_state = Some(state.to_string());
    rec.dormant_since_ms = dormant_since_ms;
    write_info(perch_path, &rec)
}

/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,
/// REQ-INST-14) — the `spt resources set` write. Read-modify-write through
/// the atomic path. Errors if the record is absent/unreadable — only an
/// existing endpoint advertises services.
// [impl->REQ-INST-14]
pub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.resources = blurb.map(str::to_string);
    write_info(perch_path, &rec)
}

/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker on an
/// existing perch's `info.json` (REQ-REACH-1): the daemon records which node's
/// operator is driving this endpoint's session while it serves a net-attached
/// surface. Read-modify-write through the atomic path (a concurrent reader
/// still sees a complete old-or-new record). Errors if the record is
/// absent/unreadable — the daemon only stamps perches that exist.
// [impl->REQ-REACH-1]
pub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.driven_by = node.map(str::to_string);
    write_info(perch_path, &rec)
}

/// Stamp the read-only `viewer_count` on an existing perch's `info.json`
/// (REQ-RCVIEW-1). The broker is the single writer (it owns the viewers map),
/// stamping on every viewer add/remove/evict. `0` clears the field (no viewers).
/// Read-modify-write through the atomic path; errors if the record is absent.
// [impl->REQ-RCVIEW-1]
pub fn set_viewer_count(perch_path: &Path, count: u32) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "info.json absent or unreadable",
        )
    })?;
    rec.viewer_count = if count == 0 { None } else { Some(count) };
    write_info(perch_path, &rec)
}

/// The serialized read-modify-write path for `info.json` (M11-W0.4,
/// REQ-DAEMON-1): EVERY mutating setter funnels through here so concurrent
/// writers — cross-process by design (the brain's pulse stamps recency/rest, the
/// spt process's signoff marks offline, the broker stamps viewers) — cannot lose
/// an update. The lost-update this closes: a pulse `set_last_active` read→write
/// interleaving a `mark_offline` could clobber `status=offline` back to online,
/// so the un-host never fired and a signed-off agent kept its Psyche.
///
/// **The lock is taken on a STABLE per-perch sentinel `<perch>/.info.lock`
/// (created once, NEVER renamed) — NOT on `info.json`.** This is load-bearing:
/// `info.json` is replaced by a tmp+rename on every write (a NEW inode each
/// time), and advisory locks (`flock`/`LockFileEx`) lock the INODE — so a lock
/// on `info.json`'s fd does NOT serialize across the rename (a second writer
/// could open+lock the old, now-unlinked inode, read a stale record, and clobber
/// the first writer's rename — a *fake* lock). Locking the constant sentinel
/// inode is what actually serializes the RMW. The OS advisory lock auto-releases
/// on fd drop / process exit, so a crashed holder strands nothing (no stale-lock
/// reaping needed).
///
/// Readers ([`read_info`]) stay LOCK-FREE: the atomic tmp+rename already gives a
/// reader a complete old-or-new record (KH-1.2); the lock serializes WRITERS
/// only. Errors if the record is absent/unreadable — the daemon only mutates
/// perches it bound.
// [impl->REQ-DAEMON-1]
fn mutate_info(perch_path: &Path, mutate: impl FnOnce(&mut InfoJson)) -> std::io::Result<()> {
    use fs2::FileExt;
    // The stable sentinel — created once, never renamed/removed (teardown removes
    // the whole perch dir). Locking it serializes the RMW across processes.
    let lock = std::fs::OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(false)
        .open(perch_path.join(".info.lock"))?;
    lock.lock_exclusive()?; // RAII: released when `lock` drops (incl. on `?` early-return) or process exit
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(std::io::ErrorKind::NotFound, "info.json absent or unreadable")
    })?;
    mutate(&mut rec);
    write_info(perch_path, &rec)
}

/// Set the daemon-managed `status` (the 2.5 daemon-authoritative liveness
/// marker). Serialized via [`mutate_info`] so a concurrent recency/rest stamp
/// never clobbers it. Errors if the record is absent — the daemon only marks
/// perches it bound.
// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
pub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| rec.status = Some(status.to_string()))
}

/// Stamp the `last_active_ms` recency marker (REQ-UPD-4). Serialized via
/// [`mutate_info`].
// [impl->REQ-UPD-4]
pub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| rec.last_active_ms = Some(now_ms))
}

/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3). `dormant_since_ms`
/// (the auto-suspend anchor) moves with it atomically — `Some` exactly when
/// `state` is dormant, so a reader never sees a dormant record without its anchor.
/// Serialized via [`mutate_info`].
// [impl->REQ-INST-3]
pub fn set_rest_state(
    perch_path: &Path,
    state: &str,
    dormant_since_ms: Option<u64>,
) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| {
        rec.rest_state = Some(state.to_string());
        rec.dormant_since_ms = dormant_since_ms;
    })
}

/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,
/// REQ-INST-14) — the `spt resources set` write. Serialized via [`mutate_info`].
// [impl->REQ-INST-14]
pub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| rec.resources = blurb.map(str::to_string))
}

/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker (REQ-REACH-1):
/// which node's operator is driving this endpoint's session. Serialized via
/// [`mutate_info`].
// [impl->REQ-REACH-1]
pub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| rec.driven_by = node.map(str::to_string))
}

/// Stamp the read-only `viewer_count` (REQ-RCVIEW-1). The broker is the single
/// logical writer (it owns the viewers map); `0` clears the field. Serialized via
/// [`mutate_info`].
// [impl->REQ-RCVIEW-1]
pub fn set_viewer_count(perch_path: &Path, count: u32) -> std::io::Result<()> {
    mutate_info(perch_path, |rec| {
        rec.viewer_count = if count == 0 { None } else { Some(count) }
    })
}

-/// Set the daemon-managed `status` on an existing perch's `info.json` (the 2.5

-/// daemon-authoritative liveness marker). Read-modify-write through the atomic

-/// path, so a concurrent reader still sees a complete old-or-new record. Errors

-/// if the record is absent/unreadable — the daemon only marks perches it bound.

-pub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {

+/// The serialized read-modify-write path for `info.json` (M11-W0.4,

+/// REQ-DAEMON-1): EVERY mutating setter funnels through here so concurrent

+/// writers — cross-process by design (the brain's pulse stamps recency/rest, the

+/// spt process's signoff marks offline, the broker stamps viewers) — cannot lose

+/// an update. The lost-update this closes: a pulse `set_last_active` read→write

+/// interleaving a `mark_offline` could clobber `status=offline` back to online,

+/// so the un-host never fired and a signed-off agent kept its Psyche.

+/// **The lock is taken on a STABLE per-perch sentinel `<perch>/.info.lock`

+/// (created once, NEVER renamed) — NOT on `info.json`.** This is load-bearing:

+/// `info.json` is replaced by a tmp+rename on every write (a NEW inode each

+/// time), and advisory locks (`flock`/`LockFileEx`) lock the INODE — so a lock

+/// on `info.json`'s fd does NOT serialize across the rename (a second writer

+/// could open+lock the old, now-unlinked inode, read a stale record, and clobber

+/// the first writer's rename — a *fake* lock). Locking the constant sentinel

+/// inode is what actually serializes the RMW. The OS advisory lock auto-releases

+/// on fd drop / process exit, so a crashed holder strands nothing (no stale-lock

+/// Readers ([`read_info`]) stay LOCK-FREE: the atomic tmp+rename already gives a

+/// reader a complete old-or-new record (KH-1.2); the lock serializes WRITERS

+/// only. Errors if the record is absent/unreadable — the daemon only mutates

+fn mutate_info(perch_path: &Path, mutate: impl FnOnce(&mut InfoJson)) -> std::io::Result<()> {

+    // The stable sentinel — created once, never renamed/removed (teardown removes

+    // the whole perch dir). Locking it serializes the RMW across processes.

+    let lock = std::fs::OpenOptions::new()

+        .open(perch_path.join(".info.lock"))?;

+    lock.lock_exclusive()?; // RAII: released when `lock` drops (incl. on `?` early-return) or process exit

let mut rec = read_info(perch_path).ok_or_else(|| {

-            std::io::ErrorKind::NotFound,

-            "info.json absent or unreadable",

+        std::io::Error::new(std::io::ErrorKind::NotFound, "info.json absent or unreadable")

-    rec.status = Some(status.to_string());

-/// Stamp the `last_active_ms` recency marker on an existing perch's `info.json`

-/// (REQ-UPD-4). Read-modify-write through the atomic path (a concurrent reader

-/// still sees a complete old-or-new record). Errors if the record is

-/// absent/unreadable — the daemon only stamps perches it hosts.

+/// Set the daemon-managed `status` (the 2.5 daemon-authoritative liveness

+/// marker). Serialized via [`mutate_info`] so a concurrent recency/rest stamp

+/// never clobbers it. Errors if the record is absent — the daemon only marks

+pub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {

+    mutate_info(perch_path, |rec| rec.status = Some(status.to_string()))

+/// Stamp the `last_active_ms` recency marker (REQ-UPD-4). Serialized via

-    let mut rec = read_info(perch_path).ok_or_else(|| {

+    mutate_info(perch_path, |rec| rec.last_active_ms = Some(now_ms))

-/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3) on an existing

-/// perch's `info.json` — the state machine's one durable write. `dormant_since_ms`

-/// is stamped alongside (the auto-suspend anchor, counted from dormancy onset)

-/// and MUST be `Some` exactly when `state` is dormant — the pair moves

-/// atomically so a reader never sees a dormant record without its anchor or a

-/// stale anchor on a non-dormant one. Read-modify-write through the atomic

-/// path. Errors if the record is absent/unreadable — the daemon only stamps

+/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3). `dormant_since_ms`

+/// (the auto-suspend anchor) moves with it atomically — `Some` exactly when

+/// `state` is dormant, so a reader never sees a dormant record without its anchor.

-            std::io::ErrorKind::NotFound,

-            "info.json absent or unreadable",

-    rec.rest_state = Some(state.to_string());

-    rec.dormant_since_ms = dormant_since_ms;

+        rec.rest_state = Some(state.to_string());

+        rec.dormant_since_ms = dormant_since_ms;

-/// REQ-INST-14) — the `spt resources set` write. Read-modify-write through

-/// the atomic path. Errors if the record is absent/unreadable — only an

-/// existing endpoint advertises services.

+/// REQ-INST-14) — the `spt resources set` write. Serialized via [`mutate_info`].

-    let mut rec = read_info(perch_path).ok_or_else(|| {

-    rec.resources = blurb.map(str::to_string);

+    mutate_info(perch_path, |rec| rec.resources = blurb.map(str::to_string))

-/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker on an

-/// existing perch's `info.json` (REQ-REACH-1): the daemon records which node's

-/// operator is driving this endpoint's session while it serves a net-attached

-/// surface. Read-modify-write through the atomic path (a concurrent reader

-/// still sees a complete old-or-new record). Errors if the record is

-/// absent/unreadable — the daemon only stamps perches that exist.

+/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker (REQ-REACH-1):

+/// which node's operator is driving this endpoint's session. Serialized via

-            std::io::ErrorKind::NotFound,

-            "info.json absent or unreadable",

-    rec.driven_by = node.map(str::to_string);

+    mutate_info(perch_path, |rec| rec.driven_by = node.map(str::to_string))

-/// Stamp the read-only `viewer_count` on an existing perch's `info.json`

-/// (REQ-RCVIEW-1). The broker is the single writer (it owns the viewers map),

-/// stamping on every viewer add/remove/evict. `0` clears the field (no viewers).

-/// Read-modify-write through the atomic path; errors if the record is absent.

+/// Stamp the read-only `viewer_count` (REQ-RCVIEW-1). The broker is the single

+/// logical writer (it owns the viewers map); `0` clears the field. Serialized via

-    let mut rec = read_info(perch_path).ok_or_else(|| {

-    rec.viewer_count = if count == 0 { None } else { Some(count) };

+        rec.viewer_count = if count == 0 { None } else { Some(count) }

/// Number of read attempts before giving up (torn-read retry).


✅ W0.3 FULL PASS + (iii) locked = A, converged. Heads-down on A; next ping = evidence. `mutate_info` + 6 wrappers done. Build-check spt-store (fs2 API).


Bash

cargo build -p spt-store 2>&1 | tail -15

Build spt-store with mutate_info + fs2


    Updating crates.io index
     Locking 1 package to latest compatible version
      Adding fs2 v0.4.3
 Downloading crates ...
  Downloaded fs2 v0.4.3
   Compiling winapi v0.3.9
   Compiling fs2 v0.4.3
   Compiling spt-store v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.10s


spt-store compiles (fs2 0.4.3, mutate_info green). Now the **(iii) hammer test** — the lock-contention proof (status survives concurrent transient spam). Read the info.rs test module to add it.


330	        assert!(
331	            !raw.contains("parent_pid"),
332	            "None field must be omitted: {raw}"
333	        );
334	        assert!(raw.contains("\"state\":\"live_agent\""));
335	    }
336
337	    // [unit->REQ-HAZARD-INFO-JSON-TORN-READ] BUSY sentinel: full parse keeps it;
338	    // read_pid yields None (no usable pid).
339	    #[test]
340	    fn busy_sentinel_round_trips_and_read_pid_is_none() {
341	        let d = perch();
342	        let info = InfoJson {
343	            id: "doyle".to_string(),
344	            started: "t".to_string(),
345	            pid: PidValue::Busy("BUSY".to_string()),
346	            session_id: "sid".to_string(),
347	            state: "ready_agent".to_string(),
348	            parent_pid: None,
349	            cwd: None,
350	            status: None,
351	            last_active_ms: None,
352	            driven_by: None,
353	            viewer_count: None,
354	            rest_state: None,
355	            dormant_since_ms: None,
356	            auto_suspend_after_ms: None,
357	            resources: None,
358	            home_subnet: None,
359	            adapter: None,
360	        };
361	        write_info(d.path(), &info).unwrap();
362	        assert_eq!(
363	            read_info(d.path()).unwrap().pid,
364	            PidValue::Busy("BUSY".to_string())
365	        );
366	        assert_eq!(read_pid(d.path()), None, "BUSY must yield no numeric pid");
367	    }
368
369	    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] set_status stamps the daemon

        assert!(
            !raw.contains("parent_pid"),
            "None field must be omitted: {raw}"
        );
        assert!(raw.contains("\"state\":\"live_agent\""));
    }

    // [unit->REQ-HAZARD-INFO-JSON-TORN-READ] BUSY sentinel: full parse keeps it;
    // read_pid yields None (no usable pid).
    #[test]
    fn busy_sentinel_round_trips_and_read_pid_is_none() {
        let d = perch();
        let info = InfoJson {
            id: "doyle".to_string(),
            started: "t".to_string(),
            pid: PidValue::Busy("BUSY".to_string()),
            session_id: "sid".to_string(),
            state: "ready_agent".to_string(),
            parent_pid: None,
            cwd: None,
            status: None,
            last_active_ms: None,
            driven_by: None,
            viewer_count: None,
            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
            resources: None,
            home_subnet: None,
            adapter: None,
        };
        write_info(d.path(), &info).unwrap();
        assert_eq!(
            read_info(d.path()).unwrap().pid,
            PidValue::Busy("BUSY".to_string())
        );
        assert_eq!(read_pid(d.path()), None, "BUSY must yield no numeric pid");
    }

    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] set_status stamps the daemon


`perch()` test helper exists. Read the set_status test region for a clean insertion point + the helper signature.


369	    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] set_status stamps the daemon
370	    // liveness marker onto an existing record; absent by default (interim
371	    // perches carry no status); set_status on an absent record errors.
372	    #[test]
373	    fn set_status_marks_existing_record() {
374	        let d = perch();
375	        write_info(
376	            d.path(),
377	            &InfoJson::new("doyle", "t", 4242, "sid", "live_agent"),
378	        )
379	        .unwrap();
380	        // Interim record: no status field serialized.
381	        let raw = std::fs::read_to_string(d.path().join("info.json")).unwrap();
382	        assert!(
383	            !raw.contains("status"),
384	            "interim perch carries no status: {raw}"
385	        );
386
387	        set_status(d.path(), "online").unwrap();
388	        assert_eq!(
389	            read_info(d.path()).unwrap().status.as_deref(),
390	            Some("online")
391	        );
392	        // Re-mark offline.
393	        set_status(d.path(), "offline").unwrap();
394	        assert_eq!(
395	            read_info(d.path()).unwrap().status.as_deref(),
396	            Some("offline")
397	        );
398
399	        // No record to mark → error, never a panic / silent write.
400	        let empty = perch();
401	        assert!(set_status(empty.path(), "online").is_err());
402	    }
403
404	    // [unit->REQ-UPD-4] set_last_active stamps the recency marker on an existing
405	    // record (absent by default), and errors on a missing record.
406	    #[test]
407	    fn set_last_active_stamps_recency() {
408	        let d = perch();
409	        write_info(
410	            d.path(),
411	            &InfoJson::new("doyle", "t", 4242, "sid", "live_agent"),
412	        )
413	        .unwrap();
414	        assert_eq!(
415	            read_info(d.path()).unwrap().last_active_ms,
416	            None,
417	            "absent by default"
418	        );
419
420	        set_last_active(d.path(), 1234).unwrap();
421	        assert_eq!(read_info(d.path()).unwrap().last_active_ms, Some(1234));
422	        // A later stamp overwrites.
423	        set_last_active(d.path(), 5678).unwrap();

    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] set_status stamps the daemon
    // liveness marker onto an existing record; absent by default (interim
    // perches carry no status); set_status on an absent record errors.
    #[test]
    fn set_status_marks_existing_record() {
        let d = perch();
        write_info(
            d.path(),
            &InfoJson::new("doyle", "t", 4242, "sid", "live_agent"),
        )
        .unwrap();
        // Interim record: no status field serialized.
        let raw = std::fs::read_to_string(d.path().join("info.json")).unwrap();
        assert!(
            !raw.contains("status"),
            "interim perch carries no status: {raw}"
        );

        set_status(d.path(), "online").unwrap();
        assert_eq!(
            read_info(d.path()).unwrap().status.as_deref(),
            Some("online")
        );
        // Re-mark offline.
        set_status(d.path(), "offline").unwrap();
        assert_eq!(
            read_info(d.path()).unwrap().status.as_deref(),
            Some("offline")
        );

        // No record to mark → error, never a panic / silent write.
        let empty = perch();
        assert!(set_status(empty.path(), "online").is_err());
    }

    // [unit->REQ-UPD-4] set_last_active stamps the recency marker on an existing
    // record (absent by default), and errors on a missing record.
    #[test]
    fn set_last_active_stamps_recency() {
        let d = perch();
        write_info(
            d.path(),
            &InfoJson::new("doyle", "t", 4242, "sid", "live_agent"),
        )
        .unwrap();
        assert_eq!(
            read_info(d.path()).unwrap().last_active_ms,
            None,
            "absent by default"
        );

        set_last_active(d.path(), 1234).unwrap();
        assert_eq!(read_info(d.path()).unwrap().last_active_ms, Some(1234));
        // A later stamp overwrites.
        set_last_active(d.path(), 5678).unwrap();


Have the test pattern. Add the (iii) hammer test (lock-contention: status survives concurrent transient spam) after the set_status test.


Edit

        // No record to mark → error, never a panic / silent write.
        let empty = perch();
        assert!(set_status(empty.path(), "online").is_err());
    }

        // No record to mark → error, never a panic / silent write.
        let empty = perch();
        assert!(set_status(empty.path(), "online").is_err());
    }

    // [unit->REQ-DAEMON-1] the lost-update lock under contention: many threads spam
    // the transient setters (last_active/rest/driven_by/viewer_count — each a
    // full-record read-modify-write, the brain-pulse churn) while one thread marks
    // the perch OFFLINE mid-storm. mutate_info's per-perch sentinel lock serializes
    // every RMW, so the offline write is NEVER clobbered back to online by a racing
    // whole-record rewrite — i.e. un-host can reliably fire. Unlocked, a spam RMW
    // that read status=online before the mark and wrote after it would resurrect
    // online (a signed-off agent keeping its Psyche). The sentinel lock makes the
    // post-offline spam read offline and preserve it.
    #[test]
    fn mutate_info_serializes_writers_so_offline_is_never_clobbered() {
        use std::sync::atomic::{AtomicBool, Ordering};
        use std::sync::Arc;
        let d = perch();
        let path = d.path().to_path_buf();
        write_info(&path, &InfoJson::new("gw", "t", 4242, "sid", "live_agent")).unwrap();
        set_status(&path, "online").unwrap();

        let stop = Arc::new(AtomicBool::new(false));
        let mut spammers = Vec::new();
        for t in 0..6u64 {
            let path = path.clone();
            let stop = Arc::clone(&stop);
            spammers.push(std::thread::spawn(move || {
                let mut n = t;
                while !stop.load(Ordering::Relaxed) {
                    let _ = set_last_active(&path, n);
                    let _ = set_rest_state(&path, "active", None);
                    let _ = set_driven_by(&path, Some("nodehex"));
                    let _ = set_viewer_count(&path, (n % 3) as u32);
                    n += 1;
                }
            }));
        }
        // Run the storm, mark OFFLINE mid-storm (the write that must survive), then
        // keep spamming so any unlocked clobber would fire after the mark.
        std::thread::sleep(std::time::Duration::from_millis(50));
        set_status(&path, "offline").unwrap();
        std::thread::sleep(std::time::Duration::from_millis(50));
        stop.store(true, Ordering::Relaxed);
        for s in spammers {
            s.join().unwrap();
        }
        // Status survived the contention. (Transient fields racing EACH OTHER is
        // cosmetic — only status correctness gates un-host.)
        assert_eq!(
            read_info(&path).unwrap().status.as_deref(),
            Some("offline"),
            "the sentinel-locked RMW must not let a transient stamp clobber status=offline"
        );
    }


        // No record to mark → error, never a panic / silent write.
        let empty = perch();
        assert!(set_status(empty.path(), "online").is_err());
    }

    // [unit->REQ-DAEMON-1] the lost-update lock under contention: many threads spam
    // the transient setters (last_active/rest/driven_by/viewer_count — each a
    // full-record read-modify-write, the brain-pulse churn) while one thread marks
    // the perch OFFLINE mid-storm. mutate_info's per-perch sentinel lock serializes
    // every RMW, so the offline write is NEVER clobbered back to online by a racing
    // whole-record rewrite — i.e. un-host can reliably fire. Unlocked, a spam RMW
    // that read status=online before the mark and wrote after it would resurrect
    // online (a signed-off agent keeping its Psyche). The sentinel lock makes the
    // post-offline spam read offline and preserve it.
    #[test]
    fn mutate_info_serializes_writers_so_offline_is_never_clobbered() {
        use std::sync::atomic::{AtomicBool, Ordering};
        use std::sync::Arc;
        let d = perch();
        let path = d.path().to_path_buf();
        write_info(&path, &InfoJson::new("gw", "t", 4242, "sid", "live_agent")).unwrap();
        set_status(&path, "online").unwrap();

        let stop = Arc::new(AtomicBool::new(false));
        let mut spammers = Vec::new();
        for t in 0..6u64 {
            let path = path.clone();
            let stop = Arc::clone(&stop);
            spammers.push(std::thread::spawn(move || {
                let mut n = t;
                while !stop.load(Ordering::Relaxed) {
                    let _ = set_last_active(&path, n);
                    let _ = set_rest_state(&path, "active", None);
                    let _ = set_driven_by(&path, Some("nodehex"));
                    let _ = set_viewer_count(&path, (n % 3) as u32);
                    n += 1;
                }
            }));
        }
        // Run the storm, mark OFFLINE mid-storm (the write that must survive), then
        // keep spamming so any unlocked clobber would fire after the mark.
        std::thread::sleep(std::time::Duration::from_millis(50));
        set_status(&path, "offline").unwrap();
        std::thread::sleep(std::time::Duration::from_millis(50));
        stop.store(true, Ordering::Relaxed);
        for s in spammers {
            s.join().unwrap();
        }
        // Status survived the contention. (Transient fields racing EACH OTHER is
        // cosmetic — only status correctness gates un-host.)
        assert_eq!(
            read_info(&path).unwrap().status.as_deref(),
            Some("offline"),
            "the sentinel-locked RMW must not let a transient stamp clobber status=offline"
        );
    }

+    // [unit->REQ-DAEMON-1] the lost-update lock under contention: many threads spam

+    // the transient setters (last_active/rest/driven_by/viewer_count — each a

+    // full-record read-modify-write, the brain-pulse churn) while one thread marks

+    // the perch OFFLINE mid-storm. mutate_info's per-perch sentinel lock serializes

+    // every RMW, so the offline write is NEVER clobbered back to online by a racing

+    // whole-record rewrite — i.e. un-host can reliably fire. Unlocked, a spam RMW

+    // that read status=online before the mark and wrote after it would resurrect

+    // online (a signed-off agent keeping its Psyche). The sentinel lock makes the

+    // post-offline spam read offline and preserve it.

+    fn mutate_info_serializes_writers_so_offline_is_never_clobbered() {

+        use std::sync::atomic::{AtomicBool, Ordering};

+        let path = d.path().to_path_buf();

+        write_info(&path, &InfoJson::new("gw", "t", 4242, "sid", "live_agent")).unwrap();

+        set_status(&path, "online").unwrap();

+        let stop = Arc::new(AtomicBool::new(false));

+            let stop = Arc::clone(&stop);

+            spammers.push(std::thread::spawn(move || {

+                while !stop.load(Ordering::Relaxed) {

+                    let _ = set_last_active(&path, n);

+                    let _ = set_rest_state(&path, "active", None);

+                    let _ = set_driven_by(&path, Some("nodehex"));

+                    let _ = set_viewer_count(&path, (n % 3) as u32);

+        // Run the storm, mark OFFLINE mid-storm (the write that must survive), then

+        // keep spamming so any unlocked clobber would fire after the mark.

+        std::thread::sleep(std::time::Duration::from_millis(50));

+        set_status(&path, "offline").unwrap();

+        stop.store(true, Ordering::Relaxed);

+        // Status survived the contention. (Transient fields racing EACH OTHER is

+        // cosmetic — only status correctness gates un-host.)

+            read_info(&path).unwrap().status.as_deref(),

+            "the sentinel-locked RMW must not let a transient stamp clobber status=offline"

// [unit->REQ-UPD-4] set_last_active stamps the recency marker on an existing

// record (absent by default), and errors on a missing record.


Bash

cargo test -p spt-store --lib info:: 2>&1 | tail -15

Run info.rs tests incl hammer


test info::tests::corrupt_content_returns_none ... ok
test shellinfo::tests::composite_option_mints_colon_free_id_and_carries_the_composite ... ok
test info::tests::set_resources_writes_and_clears_the_blurb ... ok
test info::tests::set_status_marks_existing_record ... ok
test shellinfo::tests::mint_smallest_free_and_teardown_frees ... ok
test info::tests::set_last_active_stamps_recency ... ok
test info::tests::set_driven_by_stamps_and_clears ... ok
test shellinfo::tests::shellinfo_roundtrip_and_corrupt_tolerance ... ok
test info::tests::set_rest_state_stamps_state_and_anchor_as_a_pair ... ok
test shellinfo::tests::alias_unique_rename_and_resolution ... ok
test info::tests::mutate_info_serializes_writers_so_offline_is_never_clobbered ... ok
test info::tests::concurrent_read_during_writes_never_torn ... ok

test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 146 filtered out; finished in 10.38s


Hammer test green (status survives concurrent transient spam). Now strip the long-pulse+settle workarounds — the lock makes un-host deterministic under fast-pulse contention. Revert livehost unit (e) to fast pulse (the un-host-under-contention proof).


Edit

            let set = Arc::new(LiveSet::new());
            // A LONG pulse so the driver fires once then parks — the offline flip
            // below is then not racing the per-tick touch_active (a set_last_active
            // read-modify-write that would otherwise clobber the offline status
            // back). Negligible at the production 5s pulse; deterministic here.
            let cfg = DaemonConfig {
                pulse_period: Duration::from_secs(10),
                ..DaemonConfig::default()
            };
            let run = |reason| {
                reconcile_once(
                    &perch::owlery_dir(),
                    &registered_now(),
                    &perch::adapters_dir(),
                    &set,
                    &cfg,
                    reason,
                )
            };
            run(StartReason::Cold);
            assert_eq!(set.len(), 1, "hosted while online");

            // Let the driver's immediate first pulse settle (then it parks 10s).
            std::thread::sleep(Duration::from_millis(300));
            // Graceful signoff flips the Self perch offline.
            let self_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
            spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_OFFLINE).unwrap();

            let set = Arc::new(LiveSet::new());
            // FAST pulse: the driver HAMMERS set_last_active every tick while the
            // test marks the perch offline — the un-host-UNDER-CONTENTION proof. The
            // W0.4 mutate_info sentinel lock makes status reliably stay offline, so
            // un-host reliably fires; the earlier long-pulse+settle workaround is
            // removed (the lock, not timing, is what makes this deterministic).
            let cfg = fast_cfg();
            let run = |reason| {
                reconcile_once(
                    &perch::owlery_dir(),
                    &registered_now(),
                    &perch::adapters_dir(),
                    &set,
                    &cfg,
                    reason,
                )
            };
            run(StartReason::Cold);
            assert_eq!(set.len(), 1, "hosted while online");

            // Graceful signoff flips the Self perch offline WHILE the driver pulses.
            let self_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
            spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_OFFLINE).unwrap();


The file crates\spt-daemon\src\livehost.rs has been updated successfully. (file state is current in your context — no need to Read it back)

            let set = Arc::new(LiveSet::new());
            // A LONG pulse so the driver fires once then parks — the offline flip
            // below is then not racing the per-tick touch_active (a set_last_active
            // read-modify-write that would otherwise clobber the offline status
            // back). Negligible at the production 5s pulse; deterministic here.
            let cfg = DaemonConfig {
                pulse_period: Duration::from_secs(10),
                ..DaemonConfig::default()
            };
            let run = |reason| {
                reconcile_once(
                    &perch::owlery_dir(),
                    &registered_now(),
                    &perch::adapters_dir(),
                    &set,
                    &cfg,
                    reason,
                )
            };
            run(StartReason::Cold);
            assert_eq!(set.len(), 1, "hosted while online");

            // Let the driver's immediate first pulse settle (then it parks 10s).
            std::thread::sleep(Duration::from_millis(300));
            // Graceful signoff flips the Self perch offline.
            let self_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
            spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_OFFLINE).unwrap();

            let set = Arc::new(LiveSet::new());
            // FAST pulse: the driver HAMMERS set_last_active every tick while the
            // test marks the perch offline — the un-host-UNDER-CONTENTION proof. The
            // W0.4 mutate_info sentinel lock makes status reliably stay offline, so
            // un-host reliably fires; the earlier long-pulse+settle workaround is
            // removed (the lock, not timing, is what makes this deterministic).
            let cfg = fast_cfg();
            let run = |reason| {
                reconcile_once(
                    &perch::owlery_dir(),
                    &registered_now(),
                    &perch::adapters_dir(),
                    &set,
                    &cfg,
                    reason,
                )
            };
            run(StartReason::Cold);
            assert_eq!(set.len(), 1, "hosted while online");

            // Graceful signoff flips the Self perch offline WHILE the driver pulses.
            let self_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
            spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_OFFLINE).unwrap();

-            // A LONG pulse so the driver fires once then parks — the offline flip

-            // below is then not racing the per-tick touch_active (a set_last_active

-            // read-modify-write that would otherwise clobber the offline status

-            // back). Negligible at the production 5s pulse; deterministic here.

-                pulse_period: Duration::from_secs(10),

-                ..DaemonConfig::default()

+            // FAST pulse: the driver HAMMERS set_last_active every tick while the

+            // test marks the perch offline — the un-host-UNDER-CONTENTION proof. The

+            // W0.4 mutate_info sentinel lock makes status reliably stay offline, so

+            // un-host reliably fires; the earlier long-pulse+settle workaround is

+            // removed (the lock, not timing, is what makes this deterministic).

-            // Let the driver's immediate first pulse settle (then it parks 10s).

-            std::thread::sleep(Duration::from_millis(300));

-            // Graceful signoff flips the Self perch offline.

+            // Graceful signoff flips the Self perch offline WHILE the driver pulses.


Strip the workarounds from the two ints. Revert `live_firsthost_e2e` to fast pulse + remove settle.


    let set = Arc::new(LiveSet::new());
    // A LONG pulse: the driver fires once immediately (cold anchor) then parks.
    // This keeps the un-host step below deterministic — the driver's per-tick
    // touch_active (a set_last_active read-modify-write on info.json) is NOT racing
    // the offline flip; at the production 5s pulse the same window is negligible,
    // but a fast test pulse would let touch_active clobber the offline status back.
    let cfg = DaemonConfig {
        pulse_period: Duration::from_secs(10),
        ..DaemonConfig::default()
    };
    let registered = spt_runtime::registry::registered(&perch::adapters_dir());

    let set = Arc::new(LiveSet::new());
    // FAST pulse: the driver hammers set_last_active every tick. The W0.4
    // mutate_info sentinel lock makes the offline flip below survive that
    // contention, so no long-pulse+settle workaround is needed (the lock, not
    // timing, makes the un-host deterministic).
    let cfg = DaemonConfig {
        pulse_period: Duration::from_millis(15),
        ..DaemonConfig::default()
    };
    let registered = spt_runtime::registry::registered(&perch::adapters_dir());