# SYMMETRIC-EVENT-PLAN (v0.25.0) — JIT plan

Operator sprint (2026-07-18). Three asks, all against the published public surface (translate
protocol + stub park + poll drain). Floor unchanged (0.27.0) — no new spt-core seam.

## Item 1 — translate: +50ms after each `enter` key event
**Why:** a terminal `enter` immediately followed by `{commit}` (or the next keystroke) can release
the InjectFloor / flush buffered controller input before CC has fully registered the submit —
"leftover messages" residue. A short post-enter settle lets the submit land first.
**Change:** `const POST_ENTER_MS: u64 = 50;` inserted after each TERMINAL `enter` (the one before a
`{commit}` or sequence end) in `commands_for_clear`, `commands_for_msg_stub`, `commands_for_event`,
and the final half of `commands_for_boundary`. The mid-sequence rename `enter` (when a wake half
follows) keeps relying on the existing 150ms `BOUNDARY_BRIDGE_MS` — do NOT double it.
**REQ:** REQ-DIST-IDLE-POST-ENTER-SETTLE (impl+unit).

## Item 2 — notifs + gateway (user-msg) handled like peer msgs: stub + additionalContext, generally
**doyle envelope taxonomy (source-grounded):** ONE grammar `<EVENT type="…" from="…" [attrs]>body</EVENT>`;
parser reads `type` generically (N-1 safe). Types on an agent perch: `msg`, `user-msg` (gateway/local
CLI user-authority; e.g. from="mobile-gw"/"playdate-gw"), `notify` (from/notif_id/subnet;
from="spt-update" etc.), `alarm`, `commune`, `echo_commune`, `reply`, `file_drop` (path attr, EMPTY
body). Shell channel (`api poll <shell-id> --link`) is a DISTINCT transport — carved out, never
reaches this translate.
**Change:**
- `translate.rs::stub_delivery` — drop the `type=="msg"` gate; stub ANY well-formed EVENT
  (valid `from`, closing `</EVENT>`, NON-EMPTY body). Non-empty-body guard EXCLUDES `file_drop` and
  other empty-body envelopes → they full-type so their attrs (e.g. `path`) stay visible (render_frames
  keeps only from+body, so a parked empty-body loses the attr). Control envelopes
  (wake/rename/role/fire) are intercepted earlier in `dispatch_event` and never reach here. Stub shape
  stays `<msg from="…"/>` for every type (recognizer/drain unchanged — "the same way as messages").
- `hook.rs` update-nudge — a spt-update notify can now arrive via the IDLE route → parked (not in the
  busy `frames`). Parse `parse_update_notify_version` over the PARKED bodies **and** the poll frames so
  the nudge still fires. (This reverses the old rationale that kept notify full-typing.)
**REQ:** REQ-STUB-GENERAL-EVENT (impl+unit). Touches REQ-UPDATE-NUDGE-VERSION-GATED (nudge now reads
park+poll) — extend its evidence.

## Item 3 — SessionStart must NOT drain messages
**Finding: already satisfied — no code change.** `handle_session_start` has no `api poll` / no
`drain_msg_park`; it only marks `state idle` (bind:913, clear:950). Setting idle while spooled unreads
exist is exactly the idle route the operator wants: the daemon drains the spool through the translate
binary → stub+park → next UPS drain. Verified: the only `api poll` sites are UPS (1279) and PreToolUse
(1483); the brief path (`perch_brief`, `psyche_download`) pulls static briefs + resume context, never
the message spool. Documented here; nothing to build.

## Gate
`cargo test` green + `traceable-reqs check` green. Then bump manifest 0.24.1→0.25.0, CHANGELOG,
package fat .spt, tag v0.25.0, GH release. Field-verify (idle notify/gateway → stub → body drains;
post-enter settle) owed at a live window — never cut on unit green alone for the delivery pipeline.
