# W1 #348 — census + the two picks (todlando → doyle, 2026-09-25)

Lane `feat/351-w1-axes` @a170e03c, no impl yet. Raw census with every file:line: `.spt/preserved/351/w1-census-notes.md`.

## Census: what differs from the brief's picture (5 points)
1. **USER_INPUT at broker.rs:6400 is the wrong tap.** The CLI pre-filters before calling the broker: `nowsignal.rs:1189` returns early unless the payload quotes a file path, so the broker only sees a subset of turns. `dispatch_user_input_report` (broker.rs:9060) serves only the file-access helper. The trigger belongs where every report arrives: `api state busy` carrying IO_KIND_USER_INPUT (`api/mod.rs:785-793`, has `proof.session_id`) and `api now-signal --user-input` (`mod.rs:665`), before the filter. Same ruled trigger, different line.
2. **Auto-suspend ticks only for hosted live endpoints.** Its only feed is `lifecycle.rs:987 pulse_tick` (5 s, one `run_pulse_loop` per `livehost.rs:654 host_one`). The new condition needs "uncontrolled since", but `info.json` has `controlled: bool` with no timestamp (info.rs:197, stamped by the broker). "Idle since" does exist (`perch.rs:832 read_activity_at`). Plan: add `controlled_changed_ms` next to `controlled` in the broker stamp. The timer anchor becomes max(uncontrolled-since, idle-since, [dormant-since]), so any break restarts it. Perches with no pulse loop never auto-suspend, same as today.
3. **Siblings are always on other nodes** (one perch per id per node). The only path into dormant is the registry feed (`dispatch.rs:1390-1418` ← `registryhost.rs:380-387` flip). That flip misses remote rows that arrive already Active (Inserted) and Active→Active re-claims, and has no tie-break. Pick (b) replaces it.
4. **shellwake `resolve_wake` (:403) leaves a Dormant owner dormant.** The post-grill confirmation makes the watcher an explicit wake to ACTIVE when no instance is active anywhere. That branch changes in W1 (trigger 5).
5. **`api presence`** (`reporting.rs:444`) writes a global `presence.json` that nothing reads. Its only caller is an e2e auth probe (`bind_honest_cross_perch_e2e.rs:155`). It appears in the harness-contract docs (`api.md:410`, `integration-checklist.md:90`), and out-of-repo adapters may call it. **Pick: KEEP the verb and document it as "not an activation trigger, no state effect"**, because retiring it is a break in the public contract that adapters could hit. Say RETIRE if you want it gone.

There is no daemon.json/info.json schema version. The migration copies the `full_auto_update` read-once precedent (config.rs:211-215): the old `auto_suspend_after_ms` is read, never written back, and the value resolves to the new default `{dormant-enable, 900000}`. The `spt daemon config` and `spt endpoint auto-suspend` verbs do not exist yet and are new. Stale docs: instances/, messaging/, lifecycle/ and concepts/ overviews, DORMANCY-BUDGET.md:90-97, PRD.md:214, ADR-0003:50. About 20 tests pin the old Detach/AttentionShift/knob behaviour; the list is in the notes.

## Pick (a): where "stealing message" is classified — at ADMISSION on the recipient's node
One pure classifier, `resting::message_steals(target, sender, window) -> bool`: true when window != active_only AND sender non-empty AND sender != target. It is called at the two points where a message is admitted to a local recipient, and it feeds `RestEvent::Activate`:
- **local** `cmd_send`, right after the window is chosen (cli.rs:12986): the window and sender are known there.
- **WAN** `receive_wan` after the access decision (wan.rs ~1160 → `deliver_admitted` :1413). The sender is `sender_proven` when stamped, otherwise the `from` label. A WAN message's window is always default, because busy-only never crosses the WAN (cli.rs:13073).

**Why not the delivery edge (delivery.rs/worker.rs poll drain):**
1. `DrainedMsg{from,body}` drops the window class (spool.rs:278), so the drain cannot tell busy-only rows from the rest.
2. It only runs when a warm hook polls, so a message to a SUSPENDED instance would never steal. That breaks S7/Q1(d) and R3-1 (a message wakes it).
3. Four of the nine inbound paths never pass through it: inject, TCP relay, WAN, ring.

**Why not the transition host alone:** it never sees the message. The classifier lives in resting.rs as one rule. What the admission points pass it are observations, not decisions.

Two consequences:
- **Shell→owner** (`reporting.rs:565 deliver_tcp`, `from=""`) bypasses both admission points and has an empty sender, so it never steals twice over. That is what R2-1 requires.
- **A spooled message steals at admission**, not when it is read. The steal is about which instance the other endpoint addressed, not about when the message gets read.

⚠ Flag: a local sender is the caller-supplied `--from` (hazard 7.5). A forged `from` could at most suppress a steal (same id) or cause one (any other id is already a steal by the rule), so no new authority is created.

## Pick (b): how a node learns a remote sibling went active, and what the int asserts
- **Counter:** `Instance.activation: u64` (serde default 0, skip-if-zero, the `controlled` pattern at registry.rs:228) plus `info.json activation`. Activating sets it to max(own, every `instances_of(id)` row) + 1, persists it, and calls `request_advertise_now()` (already called after every rest edge, resting.rs:441). The pump then pushes to every roster peer within about one 200 ms tick plus dial time, instead of waiting for the 30 s period.
- **Receive rule** (replaces the flip at registryhost.rs:380-387): after the merge, for each id hosted locally as Active, if any remote row for that id is Active with `(activation, node)` greater than the local `(activation, node)`, feed `RestEvent::SiblingActivated` (renamed from AttentionShift) and go Dormant. This fires on Inserted, Updated, and Active→Active alike, which closes all three gaps. The losing side yields and re-advertises Dormant; the winner ignores a lower Active row. Two simultaneous activations with the same counter resolve by node id.
- **Both-active window** = one advertise-now push. If the push fails because the peer is in backoff, the fallback is the next periodic round, bounded by the registry period.

**The int asserts (two-host, kitsubito rung):**
1. **Convergence:** activate on A while B is active. Within the deadline B reads Dormant, A reads Active, and exactly one instance in both registries reads Active, the one with the higher `(activation, node)`.
2. **The push is what does it, not the period:** both daemons run with `registry_pump_period_ms` = 600000, so only the advertise-now push can meet a 15 s deadline. **Negative control:** the same run with the push call disabled must time out, which proves the assertion can fail.
3. **Tie:** both instances are given equal counters and activated together. The one with the higher node id stays Active on both sides.
4. **Vacate:** suspend A while active. B stays Dormant (no auto-pass) and no instance reads Active.

What the int does NOT assert: a both-active window of zero. Between A's activation and B's receipt both can read Active, and that window is the push latency by design.

Awaiting your ruling on (a), (b) and the presence keep/retire before I write any impl.
