# CHECKPOINT-CLEAR-RACE — JIT plan

## Problem

The `!!checkpoint!!` clear+wake macro submits `/clear` then the wake in ONE inject sequence with a
fixed `CLEAR_DELAY_MS = 500ms` gap (`translate.rs:142`). Two failures:

1. **Blind delay.** CC's `/clear` re-runs SessionStart (whoami, psyche-download, brief assembly —
   network I/O, seconds). 500ms routinely too short → the wake is typed/submitted before clear
   settles → it lands in the OLD session (or ordering inverts — "followup hits first"). Field-observed.
2. **Hard ceiling.** The whole inject sequence must `{commit}` within `INJECT_COMMIT_DEADLINE` (5s,
   `translate.rs:56`). So the delay CANNOT be widened to cover a slow clear — a single inject
   sequence is architecturally incapable of straddling a `/clear`.

Separately observed on ENLYZEAM (F-027-adjacent): the checkpoint self-send is a PLAIN `spt send`
(`hook.rs:646`) with no native-routing flag → it spooled to the target's active-poll channel and was
delivered as a plain message (CC read+replied to the envelope) instead of firing the macro through
the translation binary.

## Approach — memory-armed split, fired across the SessionStart-on-clear boundary

Split the one sequence into two, synchronized by CC's own "clear finished" signal (SessionStart
firing with `source == clear`). State lives ONLY in translate process memory; no marker file.

- **Arm (translate):** on a `{"checkpoint":"v1",…}` envelope, stash `pending_wake: Option<String>`
  in the loop state and emit **clear-only** (`ctrl+s → delay → /clear → enter → commit`) — NOT the wake.
- **Fire (hook, stateless):** SessionStart with `source == clear` UNCONDITIONALLY self-sends a
  content-free `{"checkpoint_fire":"v1"}` (no pending-check in the hook). Mark the perch idle first
  (mirrors the arm path, `hook.rs:642`) so the wake lands on an idle input box.
- **Emit-or-no-op (translate):** on a `{"checkpoint_fire":"v1"}` envelope → if `pending_wake` is
  `Some`, emit **wake-only** (`ctrl+s → delay → wake → enter → commit`) into the clean session and set
  `None`; if `None`, no-op.

Ordering guaranteed by construction: the wake can only emit after SessionStart, which only fires after
`/clear` completes. No delay guessing; no 5s straddle; no disk artifact.

**Native routing:** BOTH self-sends (the checkpoint arm in `handle_post_tool_use`, and the new fire in
the SessionStart-clear path) use `--force-native` — deliver ONLY through the translation binary's
stdin (where the markers are parsed), no spool / no active-poll fallback. Closes the ENLYZEAM
plain-text-misdelivery mode. (If translate isn't running, the signal drops rather than spools —
correct: a checkpoint/wake is meaningless without live translate.)

## Tasks

1. **traceable-reqs first.** Mint `REQ-HAZARD-CHECKPOINT-CLEAR-RACE` in `traceable-reqs.toml`
   (activate real stages `[doc, impl, unit, int]`). It is the KNOWN-HAZARDS entry for the
   clear/wake ordering race.
2. **translate.rs:**
   - Split `commands_for_checkpoint` → `commands_for_clear` (ctrl+s, delay, /clear, enter, commit) +
     `commands_for_wake` (ctrl+s, delay, wake, enter, commit).
   - Add `is_checkpoint_fire(envelope)` parsing `{"checkpoint_fire":"v1"}` (parallel to
     `checkpoint_wake`).
   - Thread `pending_wake: Option<String>` through the main loop: checkpoint envelope → store wake +
     emit clear-only; fire envelope → drain wake + emit wake-only (or no-op); every other envelope
     unchanged.
   - Retire `CLEAR_DELAY_MS` (no longer a cross-command straddle).
   - Unit tests: arm stores + emits clear-only; fire with armed wake emits wake-only + disarms; fire
     with no wake is a no-op; a normal envelope still delivers; malformed/unknown still degrades.
3. **hook.rs:**
   - `handle_post_tool_use`: add `--force-native` to the checkpoint self-send.
   - `handle_session_start`: when `register_verb(source, …) == "boundary"` on a `clear` source (NOT
     compact — decide, see open q), mark idle + self-send `{"checkpoint_fire":"v1"}` with
     `--force-native`. Add a `checkpoint_fire_payload()` helper.
   - Unit tests: clear-source SessionStart fires the signal; bind/seed/resume/compact do NOT (per the
     open-q ruling); the self-send carries `--force-native`.
4. **KNOWN-HAZARDS.md:** add the race + the orphan-wake edge (open q) as `REQ-HAZARD-CHECKPOINT-CLEAR-RACE`.
5. **int:** extend the checkpoint-commune int (`ci/idle-translate/…` or the live E2E) to assert the
   wake arrives AFTER the clear (ordering), not before.
6. **Docs:** update the live-ops brief / commune skill only if the agent-facing contract changes (it
   does not — the agent still writes `<id>-commune.md` with `!!checkpoint!!`; the split is internal).

## Open design questions

- **(Q1) Orphan-wake guard.** If the checkpoint's own clear fails to fire the signal (hook error), the
  armed wake fires on the NEXT clear — possibly a slightly-wrong context. The every-clear-fire model
  self-limits it (won't sit forever). Options: (a) ACCEPT for v1 (simplest, self-correcting; document
  in KNOWN-HAZARDS) — RECOMMENDED; (b) bound staleness with a translate-side generation/timeout on the
  armed wake. Decide before impl.
- **(Q2) `compact` source.** `register_verb` maps both `clear` AND `compact` → `boundary`. Should a
  `/compact` also fire a pending wake? A checkpoint arms via `/clear` specifically; firing on a
  `compact` boundary could emit into an unintended reset. Lean: fire ONLY on `clear`. Confirm.

## Gate

`cargo build` + `cargo test` (translate + hook unit suites) green; `traceable-reqs check` exit-0 with
`REQ-HAZARD-CHECKPOINT-CLEAR-RACE` fully evidenced; the ordering int asserts wake-after-clear.
