---
name: v0150-w1-activity-gated-delivery
description: "v0.15.0 W1 (ACTIVE assignment, todlando) — activity-gated delivery substrate + fault-transient re-spool. Branch v0.15.0-messaging @8fd2e51. Authoritative plan = V0.15.0-PLAN.md + ADR-0028."
metadata: 
  node_type: memory
  type: project
  originSessionId: 34b6e19b-97c5-41c1-b0df-d79dbfd01fd7
---

**v0.15.0 messaging milestone — W1 KICKOFF (doyle assigned, operator GO 2026-06-23).** Follows v0.14.3 (raw-inject removal [[v0143-raw-inject-removal]]). Branch **`v0.15.0-messaging` @8fd2e51** (off main 90852bf = v0.14.3 published counter 33). **AUTHORITATIVE TASK DOC = `V0.15.0-PLAN.md` (repo root) + `docs/adr/0028-activity-gated-delivery-and-send-modifier-axes.md` — READ BOTH FIRST.** doyle gates, perri validates real CC at publish. doyle re-runs ALL gates on the real commit (never trusts the green report — v0.14.3 discipline).

**MILESTONE (4 waves):** close the legacy-SPT parity gap = wire the scaffolded-but-DEAD activity-gated inbound routing (2 windows: ACTIVE=spool for hook-poll / IDLE=deliver+wake via binary→relay-poll→spool), then expose 3 orthogonal `spt send` axes — window (`--idle-only`/`--active-only`[renames `--deferred`]/default) × channel (`--prefer-native`/`--force-native`/unrestricted) × persistence (`--ephemeral`/durable) — + `--json-payload` (opaque `json=""` envelope attr). W1=substrate+fault-transient, W2=window+persistence axes (CLI+spool schema), W3=channel axis, W4=metadata+console-window fix+docs+final traceable.

**W1 SCOPE (NO send flags — make DEFAULT routing correct):**
1. ACTIVATE `REQ-MSG-DELIVERY-AXES` `+impl` (was doc-only seed). int at final wave (W4) per [[traceable-per-wave-activation]].
2. WIRE THE DEAD ROUTER: `crates/spt/src/api/delivery.rs` `is_idle(id)` (:97), `resolve_inject_methods(manifest, idle)` (:112), the discarded `let _methods = resolve_inject_methods(…)` (:147) → thread idle sentinel + methods into broker `dispatch_endpoint_input` (broker.rs ~:1875, the v0.14.3 spine). Route by `.idle`: ACTIVE → spool (delivered=false, hook-poll surfaces); IDLE (or idle-transition) → deliver immediately (translation-binary → relay-poll → spool fallback order).
3. IDLE-TRANSITION DRAIN (the WAKE, spt-core-driven via relay-poll per operator ruling — NOT adapter-mediated): when `api state idle` flips the `.idle` sentinel (`delivery.rs cmd_state` :57, perch.rs IDLE_SENTINEL), daemon DRAINS idle-window-eligible spooled inbound for the endpoint + delivers to binary/relay.
4. FAULT-TRANSIENT RE-SPOOL (closes the REQ-HAZARD-IDLE-SILENT-NONDELIVERY transient — Design-RESOLVED #2 (a)): `broker.rs run_inject_worker` fault path (~:1099, faults on no-commit/death + RETURNS dropping queued events) → RE-SPOOL queued-undelivered events to the endpoint's PERCH SPOOL (broker-side `spt_store::spool` write, delivered=false-equiv, poll-fed, NEVER lost). KEEP current caller-spool + delivered-bool model — NOT the full ack-on-spool relocation (later milestone, OUT OF SCOPE).

**W1 TESTS:** unit = routing decision-table (active→spool / idle→inject / idle-transition→drain) INJECTED sentinel, NO wall-clock. int (real broker+PTY, NEXTEST) = active→spool-not-inject · idle→reaches-PTY-via-binary · active→idle-transition drains+delivers · fault-mid-sequence re-spools (delivered=false, not lost). GATE = two-window routing correct + fault-transient closed.

**KEY ANCHORS (V0.15.0-PLAN.md §22-30):** delivery.rs (is_idle:97 / resolve_inject_methods:112 / dead let_methods:147 / cmd_state:57); broker.rs (dispatch_endpoint_input ~:1875 / run_inject_worker ~:1099); translation.rs TranslationChild::spawn:240 (W4 console-window CREATE_NO_WINDOW site); cli.rs Send clap ~:62 / --deferred :71-73 / cmd_send:3522 / try_broker_inject:3510; brain.rs inject_endpoint:1189; spool.rs (deferred col :59, spool_message_at/_deferred_at, drain_non_deferred_at/drain_all_at, ttl_seconds — W2 schema ext); event.rs compose_typed_event:189 (W4 json attr).

**GATE DISCIPLINE (MANDATORY, v0.14.3-paid, SAME broker seam):** `cargo nextest run -p spt-daemon` FULL crate (NOT bare cargo test = env-race; NOT just edited file = sibling escape [[shared-seam-change-run-all-seam-tests]]) · `cargo clippy --workspace --all-targets`=0 (incl `///` doc-comments, EVERY amend [[ci-clippy-preflight-workspace]]) · traceable EXIT0 · xtask gen/check docs-drift · rebuild bin · grep tests for OLD behavior before any rename ([[behavior-change-grep-tests-not-comments]]). everyone on LEGACY owl — `$OWL send doyle` ([[owl-send-not-legacy-spt-send]]).

**OUT OF SCOPE (doyle):** backlog-drain-on-binary-up (idle-transition drain + subsequent sends cover it; separate change); full ack-on-spool spool-relocation redesign (ADR-0028 end-state, later milestone). W4 also: AMEND REQ-HAZARD-IDLE-SILENT-NONDELIVERY title w/ `--ephemeral` carve-out (sole sender-opted-in silent-drop); EXTEND REQ-HAZARD-CHILD-CONSOLE-FLASH to translation-binary spawn.

**STATUS: ✅ W1 COMPLETE + GATED + COMMITTED @844b3f2 (branch v0.15.0-messaging, 2026-06-24). Surfaced GREEN to doyle for his real-commit gate.** Disk had MORE than the prior checkpoint recorded — PART A+B + CLI + unit tests were ALL built pre-session-end; this session verified each vs real code, ran the test sweep, added g5/g6 int gates, full gate PASS. GATES: nextest -p spt-daemon 483/483 · -p spt 291/291 · clippy --workspace --all-targets 0 · traceable EXIT0 (297 complete) · xtask OK · bin rebuilt. SWEEP REALITY: only g1+g2 broke (assert delivered=true via deliver_endpoint_event) → set_endpoint_idle helper (writes .idle via resolve_idle_file, same path broker reads). g3(operator keystrokes only)/g4(no delivery)/large_endpoint+wall_b+endpoint_keyed(assert delivered=false, robust under gate)/p0_paste(operator input) ALL UNAFFECTED. NEW int: g5 active→spool-deferred-not-injected, g6 faulted-inflight-respooled-not-lost (both [int->REQ-MSG-DELIVERY-AXES] over-coverage; traceable tolerates extra-stage tags — required_stages stays [doc,impl]). OPEN (doyle to confirm): cross-crate idle-transition-drain INT (cmd_state+live broker) deferred to W4 (int-stage activation), unit-covered now (peek_non_deferred+mark_delivered+poll-method-honor). GOTCHA RE-LOGGED: dev `target/debug/spt.exe` daemon+brain from e2e (job_escape) survive `daemon stop` (kill-escape test) → lock spt.exe → xtask rebuild os-error-5; kill the 2 workspace-target PIDs (scoped, NOT machine-wide — owl.exe is the live-agent listener). NEXT = W2 (window+persistence axes: --idle-only/--active-only[rename --deferred]/--ephemeral CLI + spool 3-col schema ext).

**✅ W2 COMPLETE + GATED + COMMITTED @c54a59c (2026-06-24). Surfaced GREEN to doyle (3 decisions flagged). Awaiting his real-commit gate.** Delivery-WINDOW + persistence axes. doyle RATIFIED the drain-surface fork (precise ping, his reply @c4fa5ad8): Q1 SPLIT (only ACTIVE-hook re-points; idle/wake ZERO change — KEY IDENTITY deferred=0 == {default,idle_only}); Q2 MIRROR (keep deferred, derive from window at single insert chokepoint + invariant unit test); Q3 no relay .idle self-gate (broker-inject-at-send-time sole idle gate; window orthogonal/caller-side). BUILT: spool +3 cols (`"window"` DOUBLE-QUOTED=SQLite keyword / channel / ephemeral); ONE new `drain_active_window_at(perch,include_deferred)` = include_deferred?window!='idle_only':window='default'; re-pointed poll_drain+cmd_worker_poll (suppress idle_only, resting-gate flows through); deliver::send_windowed; CLI --idle-only/--active-only(rename --deferred, PURE no-alias)/--ephemeral mutually-exclusive group; cmd_send maps flag->window (broker/EndpointInputReq UNTOUCHED — window caller-side). GATES: nextest 483+292 / clippy 0 (fixed 1 doc_lazy_continuation) / traceable EXIT0 / xtask gen+check (reference.md regen for 3 flags). 3 DECISIONS flagged to doyle: (1) EPHEMERAL flag+col STORED, evaporation TRIGGER deferred→W3 (concrete only with channel: force-native+ephemeral); (2) --deferred PURE rename no-alias (offered hidden alias if he wants external back-compat); (3) BEHAVIOR REFINEMENT — default-send-to-ACTIVE now spools window=default (deferred=0, wake-eligible), SUPERSEDING W1's active->spool_deferred=true hint-branch (was deferred=1 hook-only no-wake); ADR tri-state model; g5 still passes (asserts broker reply not caller spool). ALL 3 DECISIONS RATIFIED by doyle @3a5b64c4 (he independently re-confirmed the g5/g6/hazard analysis, traced every arm): (1) ephemeral→W3 confirmed; (2) ADD hidden alias — DONE @6f98694 (`#[arg(long, alias="deferred")]` on active_only, parse test, CONFIRMED reference.md UNCHANGED=hidden renders no flag entry; the 1 `--deferred` in reference.md is W2 migration-note prose "Replaces the old --deferred", kept as DX); (3) decision-3 ratified (default-to-active=window=default=wake-eligible is ADR-correct supersede; the ONLY changed arm = spt-hosted-active broker-miss, drains via broker idle-transition drain AT NEXT IDLE = non-disruptive by construction; relay-wakes-busy = pre-existing Q3 out-of-scope). **✅ W4 GATE PASS @c019610 (doyle blessed; send-modifier set W1-W4 DONE).** **W5 COMMITTED @20bfc1f (on doyle's design @58fd8ac), surfaced — AWAITING gate.** W5 = REQ-RESUME-CONTEXT-PULL Tier-1: `spt api psyche-download <id> [--session-id]` verb (ApiCmd::PsycheDownload + reporting::cmd_psyche_download; durable role/live/project brief from perch cwd via info.cwd→project_id_for_dir; NO-CONTEXT:<id> stderr when empty; auth via gated()+AuthFlags --session-id — DROPPED separate session_id field, clap-collided w/ AuthFlags) + download_psyche_context gains commune_file/signoff_file → appends <pending-commune>/<pending-signoff> AFTER project when drop-file present (=pre-synthesis in sync-ingest; presentation-only/read-only/self-clearing; REQ-HAZARD-DROP-FILE-SINGLE-WRITER). resolve_filedrops reused. Tier-2 (drift-stamp/<current>/memformat/pulse-log) NOT built. ACTIVATED REQ [doc,impl,unit,int] (doc=CONTEXT.md §resume-session note). GATES nextest 485+354/clippy 0/traceable EXIT0/xtask (psyche-download rendered). int=psyche_download_e2e (real binary, --manifest daemon-free, pending+self-clear). OPEN TODO: trigger-sentinel-strip = VERBATIM for now (doyle-ratified); pinged perri for her claude-spt checkpoint convention (strip marker or no-op). GATE GOTCHA PAID: full spt-daemon hit 240s TIMEOUT on handoff::brain_restart_survives_gaplessly = ORPHAN target/debug/spt.exe (from prior spt-suite job_escape e2e) racing the brain-restart broker, NOT regression; killed 3 orphans→0.23s pass→485/485 clean. LESSON: pre-kill workspace-target orphans before full spt-daemon AFTER any spt-suite e2e run. **v0.15.0 messaging milestone W1-W5 BUILT — on W5 gate-pass: deployah cuts release (bump-in-PR), perri validates real CC (3 axes+json+no-window+psyche-download).** Commits: W1 844b3f2 / W2 c54a59c+6f98694 / W3 5c4ae83 / W4 c019610 / W5 20bfc1f (doyle design @58fd8ac).

**W5 TRIGGER-STRIP FOLLOW-UP COMMITTED @27163bb (on @20bfc1f) — AWAITING doyle gate (+ his REQ-title amend for both strip points + docs-site prose authoring).** perri confirmed sentinel=literal `!!checkpoint!!` (@2e291378); doyle ruled durable-ingest strip REQUIRED not optional (@d52ffcac). BUILT: fixed const `CHECKPOINT_SENTINEL` + ONE shared `strip_checkpoint_markers` (spt-live ingest, beside COMMUNE_SUFFIX; remove every token, KEEP inter-marker text, collapse trivial double-space) called at BOTH `resume::append_pending` (pre-synthesis <pending-*>) + `ingest::route_slices` (post-synthesis durable, strip-then-empty-filter — single choke covers route_two_slice+signoff). CONTEXT.md §fixed-const doc+tag. Tests: strip unit + pending-strip + ingested-no-marker. spt-live 63/63 / clippy 0 / traceable EXIT0 / xtask OK. **NEW SHARED-BOX GOTCHA (paid):** `seedmap::request_stop_barrier_holds_until_no_listener` (+ kin daemon-lifecycle stop/start/handoff tests) use a FIXED/global `seed_socket_name()` — they COLLIDE with the LIVE PRODUCTION daemon `C:\Users\decid\AppData\Local\spt-core\bin\spt.exe daemon run --detached` (hosts the live owl.exe agents) → 240s TIMEOUT when live infra is up, pass when down. NOT a code regression; orphan-kill only targets target\debug (never touches the AppData\Local\spt-core\bin production daemon — DO NOT KILL IT, it's the live broker). Full spt-daemon can't go clean locally while live agents run; doyle gates strip with that one test EXCLUDED (`-E 'not test(request_stop_barrier_holds_until_no_listener)'`, backed by W3-W5 daemon-down passes + clean-box CI). **CORRECTED @0b4bea54 (my collision theory REFUTED — doyle wrongly confirmed it first, then grounded the full chain):** with_home (test_home.rs:27-28) sets SPT_HOME to a FRESH tempdir per test; spt_home() is PURE (perch.rs:34-35, live read NO cache) → seed_socket_name()=hash(unique tempdir)=UNIQUE socket, CANNOT reach prod 149588. NO collision, NO request_stop-prod hazard. REAL CAUSE = LOAD-CONTENTION FLAKE: the in-process serve+barrier is timing-sensitive; loaded box (live agents up) starves it→240s backstop; idle box (W3-W5 daemon-down) passes. Non-deterministic, load-dependent. FOLLOW-UP (lower severity = flake not hazard, doyle-owned): harden the barrier/ping timing (generous timeout / load-tolerance, like the SPT_ATTACH_*_MS headroom precedent). Release proceeds; rerun seedmap if it flakes on hfenduleam CI. **LESSON (mine): I over-diagnosed — built a plausible collision mechanism from the process list without grounding the FULL isolation chain (test_home→spt_home purity→socket name); a plausible mechanism ≠ a grounded one. doyle echoed by confirming before verifying. Verify the whole chain before asserting a root.** RELEASE SEQUENCE: doyle strip-gate+REQ-amend → doyle authors docs-site PROSE ([[v0150-public-docs-update]]) → THEN deployah cuts release+docs-publish together; deployah PARKED until doyle docs-ready signal (I never trigger releases). Strip=27163bb.

**(historical) ✅ W2 GATE PASS @6f98694 — doyle BLESSED + CLOSED (re-ran all 5 green; prose "Replaces the old --deferred" KEPT per his ruling).** W2 = spool/CLI @c54a59c + hidden alias @6f98694. **W3 STARTED.** NEW heads-up @9abe8054: parity wave REQ-RESUME-CONTEXT-PULL incoming (doyle spec'ing) — EXPOSE `spt api psyche-download <id>` (NO verb exists today; download_psyche_context is internal, 0 callers, resume.rs:9 anticipated adapter-pull never wired) + APPEND pending commune/signoff as <pending-commune>/<pending-signoff> slices while NOT-YET-SYNTHESIZED (keys on synthesis-state not on-disk; forward-compat async synthesis), trigger-stripped, presentation-only, self-clearing, new public verb→docs-drift gate. SEPARATE, post-W3, DON'T WAIT. **W3 CODE COMPLETE + LOCALLY GATED (nextest spt-daemon 484/spt 292/clippy 0/traceable EXIT0/xtask gen+check OK), COMMIT HELD on a SHARED-TREE collision** (doyle=live agent on SAME box HFENDULEAM, wrote W5 design into the shared working tree: V0.15.0-PLAN.md W5 section + REQ-RESUME-CONTEXT-PULL block in traceable-reqs.toml, interleaved with my W3 ephemeral-note in the SAME traceable.toml; no git add -p here). RESOLVED: doyle `git restore`d BOTH files to HEAD himself (his W5 being re-spec'd, must NOT commit as-is; my redundant 1-line note went with it — CONTEXT.md keeps the doc). Net = option B. **W3 GATE-PASS @5c4ae83 (doyle 5/5).** **W4 COMMITTED @c019610, surfaced — AWAITING gate.** W4 = --json-payload (opaque attr-escaped json="" via compose_typed_event, pre-composed at send→rides verbatim, threaded through apply_user_msg_gate the one compose-point, type mirrors UNCHANGED user-auth gate, collision-proof) + translation console-window fix (TranslationChild::spawn CREATE_NO_WINDOW cfg(windows), brainproc parity, REQ-HAZARD-CHILD-CONSOLE-FLASH +impl+unit) + traceable ACTIVATE REQ-MSG-DELIVERY-AXES +unit+int (evidence g1/g2/g5/g6/g7 + units) + AMEND REQ-HAZARD-IDLE-SILENT-NONDELIVERY title (--ephemeral carve-out). GATES nextest 485+293/clippy 0/traceable EXIT0/xtask. console-flash unit=flag-doesnt-break-spawn (error-87, window-absence untestable=runtime.rs:750 pattern). **v0.15.0 send-modifier feature set W1-W4 DONE pending gate.** SEQUENCING: my W4 traceable edits committed @c019610 → doyle's W5 REQ block + ephemeral-1liner fold rebase on top (no collision). AFTER W5: deployah cuts release (bump-in-PR), perri validates 3 axes+json+no-window real CC. Commits: W1 844b3f2 / W2 c54a59c+6f98694 / W3 5c4ae83 / W4 c019610. LESSON: shared working tree w/ doyle — concurrent edits to the same file (traceable-reqs.toml) collide; coordinate commit boundaries. W3 BUILT: EndpointInputReq+native(serde-default), broker inject=working_binary&&(idle||native) [native bypasses idle gate→mid-active inject via InjectFloor], cli --prefer-native/--force-native(mutex)+force-native-early-binary-or-nothing-path(no spool, LOUD delivered=false)+prefer-native-fallback-spool, drain_idle_window native=false + evaporate_ephemeral_non_deferred_at (idle-transition no-carrier drop), CONTEXT.md+traceable ephemeral-PARTIAL doc (spt-hosted+TTL legs only; relay-carrier leg→W4 deferred). Tests: spool evaporate unit + g7 native-injects-mid-active int (delivered=true keystone; payload byte-receipt unix-only/Win-banked like g1) + cli parse. NEXT after commit=W4 (json-payload + console-window fix + final traceable activate). W5 parity (doyle re-spec'ing richer legacy payload) post-W3/W4.

**(old) NEXT=W3** (channel axis --prefer-native/--force-native + ephemeral evaporation). NOTE: operator added AGENTS.md line @296e579 (between W1+W2) reaffirming legacy $OWL send / no --reply-to / $LIVE list — already compliant.

**(historical) W1 PART A SPINE STARTED — compiles, UNCOMMITTED. Checkpointed at deep context.**

**DONE (on disk, compiling):**
- `msg.rs`: `EndpointInjected` gained `#[serde(default)] pub spool_deferred: bool` (the activity-gate hint: delivered=false + spool_deferred=true ⇒ caller spools DEFERRED/active; false ⇒ NON-deferred/idle-relay-wakes; N-1-safe). `endpoint_injected_envelope(endpoint, delivered, spool_deferred)` — 3rd arg added.
- `broker.rs` `dispatch_endpoint_input`: ACTIVITY GATE — reads `spt_store::perch::resolve_idle_file(&req.endpoint, ParentHint::Infer).exists()`. Routes: `!idle`(ACTIVE)→reply(delivered=false, spool_deferred=TRUE), no inject; IDLE+working-binary→inject→reply(true,false); IDLE+no-binary→reply(false,false). None(no session)→reply(false,false). Tagged `[impl->REQ-MSG-DELIVERY-AXES]`.
- `msg.rs` units updated to 3-arg + renamed `endpoint_injected_envelope_carries_delivered_both_ways`→`_carries_delivered_and_spool_hint` (asserts active=deferred / idle=non-deferred / delivered / N-1 serde-default false). Tagged [unit->REQ-MSG-DELIVERY-AXES].

**REMAINING W1 (ordered) — NOT yet done:**
1. **ACTIVATE `REQ-MSG-DELIVERY-AXES` +impl** in traceable-reqs.toml (currently `["doc"]` → `["doc","impl"]`; unit/int at W4). NOT yet done — do before declaring (traceable will FAIL on the [impl->] tags I already added until activated... actually impl tags on a doc-only req = traceable error; ACTIVATE EARLY).
2. **brain.rs `inject_endpoint`** (:1189) returns `Ok(reply.delivered)` (bool) → change to return the hint too (e.g. `EndpointInjected` or `(bool,bool)`) so the caller sees spool_deferred.
3. **cli.rs `try_broker_inject`** (:3510, returns bool) + **`cmd_send`** (:3522): on delivered=false, spool with the broker hint — active→`deliver::send_deferred`, idle→`deliver::send`. (Keep existing `--deferred` flag; W2 renames it.)
4. **broker.rs `run_inject_worker`** fault path (~:1099): on fault (no-commit/death + RETURN), RE-SPOOL the queued-undelivered events to the endpoint's PERCH SPOOL (`spt_store::spool::spool_message_at`, NON-deferred/idle-eligible — doyle: a faulted idle-delivery belongs back in the idle window). Needs the endpoint id + the queued envelopes in the worker (currently the worker only has event_rx of envelopes; may need to thread endpoint id into Translation/the worker). GROUND THIS — the worker may not currently know its endpoint id.
5. **idle-transition DRAIN** (the wake): in `delivery.rs cmd_state` idle (spt crate) — on `.idle` flip, re-attempt delivery of non-deferred idle-spool (spt-hosted: re-inject via brain.inject_endpoint; harness-hosted: relay auto-surfaces). GROUND: does cmd_state have brain access?
6. **PART B**: `delivery.rs cmd_poll` (:147) kill `let _methods` discard → relay/poll honors manifest `[inject]` idle/activity methods (use the resolved methods, not discard).
7. **★ TEST SWEEP (BIG ripple, [[shared-seam-change-run-all-seam-tests]]):** the activity gate makes the DEFAULT (no `.idle`) route to ACTIVE→spool — so EVERY existing test that sends to a hosted binary expecting INJECTION now spools instead. MUST set `.idle` (write IDLE_SENTINEL, or `api state idle`) in: inject_control_wedge g1/g2/g3/g4/large_endpoint/p0_paste (those expecting inject), broker.rs endpoint_keyed_inject + wall_b. GREP all KIND_ENDPOINT_INPUT senders + deliver_endpoint_event/spawn_xlate_session helpers; add idle-set. Consider an idle-set in spawn_xlate_session helper. RE-RUN full `cargo nextest run -p spt-daemon` until green.
8. **NEW W1 tests** (doyle: don't JUST .idle-patch the inject tests — ADD dedicated active-path coverage for the NEW default→spool behavior): unit routing decision-table (injected sentinel: active→spool / idle→inject / idle-transition→drain, no wall-clock) + int (real broker+PTY nextest): **active→spool-NOT-inject (new default, its own test)** · idle→reaches-PTY-via-binary · active→idle-transition drains+delivers · fault-mid-sequence re-spools (delivered=false, not lost).
9. GATE: full nextest -p spt-daemon + clippy --workspace 0 + traceable EXIT0 + xtask check + rebuild bin. Surface green to doyle ($OWL send doyle); he re-runs all on the real commit.

**GROUND-FIRST NOTES for resume:** run_inject_worker may not know its endpoint id (step 4) — check Translation struct / build_translation; thread endpoint id if missing. cmd_state brain access (step 5). The `.idle` default-active ripple (step 7) is the biggest surface.

**(historical) GROUNDING — broker reads sentinel via spt_store::perch::resolve_idle_file (crate-boundary clean); resolve_inject_methods is spt-crate. doyle confirmed.**

**GROUNDING FINDINGS (read delivery.rs + perch.rs + v0.14.3 dispatch spine):**
- CRATE BOUNDARY: `is_idle` + `resolve_inject_methods` are in the **`spt` crate** (`api/delivery.rs`); `dispatch_endpoint_input` is in **spt-daemon** (spt→depends→spt-daemon, NOT vice-versa). Broker CANNOT call `delivery::resolve_inject_methods`.
- BUT the sentinel IS shared: **`spt_store::perch::resolve_idle_file(id, hint)`** (perch.rs:382) + `IDLE_SENTINEL`=".idle" (perch.rs:376) — broker reads idle directly via spt-store, no spt-crate dep. (also `resolve_echo_gate_file`:389, `.more-done` ECHO_GATE_SENTINEL:379.)
- `cmd_state(id,"idle",no_gate)` (delivery.rs:36) writes `.idle` + arms echo-gate; `"busy"` removes `.idle`. `is_idle`=resolve_idle_file.exists() (:97). The discarded `let _methods = resolve_inject_methods(manifest, is_idle(id))` is in `cmd_poll` (delivery.rs:147) — the hook-drain surface.
- spool API (spt-store::spool, broker-accessible): `spool_message_at(perch,from,body)`, `spool_message_deferred_at`, `drain_non_deferred_at`/`drain_all_at`, `pending_count_at`, `ttl_seconds`. `poll_drain` (delivery.rs:196) keys on resting gate.

**★ doyle DESIGN RULING (CONFIRMED, GO 2026-06-23) — W1 SPINE, 2 surfaces same wave:**
1. CRATE BOUNDARY confirmed — broker gate reads `.idle` DIRECTLY via spt_store::perch (the plan's "wire resolve_inject_methods INTO dispatch_endpoint_input" was imprecise about WHERE).
2. **PART A (spt-daemon broker, spt-HOSTED PTY path):** `dispatch_endpoint_input` gates via the sentinel → {IDLE+working-binary → INJECT (delivered=true) | ACTIVE → spool | IDLE+no-binary → spool}. (My route reading CORRECT.)
3. **PART B (spt, api/delivery.rs, HARNESS-HOSTED relay/poll path):** kill the `let _methods` discard in cmd_poll → relay/poll honors manifest `[inject]` idle/activity methods. SEPARATE surface, SAME wave. (So the let_methods fix IS in W1 scope — wired to honor methods, NOT a throwaway.)
4. RELAY-POLL COLLAPSE confirmed: "binary→relay-poll→spool" = binary-inject (spt-hosted idle) ELSE spool, where the spool's DRAINER is the relay/poll (api listen PUSHES for idle = the wake; hook-poll drains for active). idle+no-binary→spool surfaced by the live relay = spt-core-driven wake.
5. **★ THE #4 NUANCE (deferred-column REUSE — the elegant key):** active-spool vs idle-spool map onto the EXISTING `deferred` column — ACTIVE → spool **deferred=1** (relay SKIPS, hook-poll only, NO wake); IDLE+no-binary → spool **deferred=0** (relay PUSHES = wake). Broker replies delivered=false for BOTH active+binary AND idle+no-binary — but the CALLER must spool with the right deferred-ness. **RULING: EXTEND the KIND_ENDPOINT_INJECTED reply payload (EndpointInjected) with the activity/window hint** (additive bool, serde-default false = N-1-safe today's non-deferred; mirrors InputReq.ack v0.13.0 P1b) → cmd_send/deliver::send spools deferred(active)/non-deferred(idle) DETERMINISTICALLY off the broker's ONE sentinel read — NO caller TOCTOU re-read. deferred already MEANS "no live wake, hook-poll only" → clean reuse.
- **cmd_send false-branch:** spool deferred-ness per the broker hint — active→`deliver::send_deferred`, idle→`deliver::send`. (W1 keeps the existing `--deferred` flag; rename→`--active-only` is W2.)
- **idle-transition drain (the WAKE):** hook in `cmd_state` idle (delivery.rs:36, spt crate) — on the `.idle` flip, re-attempt delivery of non-deferred idle-spool (spt-hosted: re-inject via brain; harness-hosted: the relay auto-surfaces non-deferred spool, so the wake is automatic there).
- doyle: "propose back if cleaner seam for #4 — otherwise build." todlando: no cleaner seam; additive reply field = the clean seam. BUILDING.

**(historical) MY READING (proposed to doyle, confirmed above):** (1) activity gate in dispatch_endpoint_input reads sentinel via spt_store::perch::resolve_idle_file — ACTIVE→spool(delivered=false) / IDLE+working-binary→inject / IDLE+no-binary→spool. i.e. v0.14.3 binary-or-spool GATED so ACTIVE always spools. For spt-hosted, relay-poll = the spool feeds api-listen/poll, so binary→relay-poll→spool collapses to binary→spool at this seam. (2) resolve_inject_methods is the api-POLL-surface concern, NOT called in the broker; the `let _methods` fix is a SEPARATE cmd_poll cleanup — UNLESS doyle wants resolve_inject_methods/is_idle MOVED to spt-store/daemon + genuinely called in the broker route. **OPEN: doyle to confirm the resolve_inject_methods disposition (separate cmd_poll cleanup vs move-and-call-in-broker).** Context long post-v0.14.3 — may checkpoint/commune before the build.
