# Busy race (#32) — measure first, then decide

> JIT plan (AGENTS.md §Plans). Successor to `RECHARGE-MILESTONE-PLAN.md`, which shipped as v0.39.0
> with #32 deliberately unbuilt. Gate for every task, as always: `sh ci/run-gates.sh` PASS **and**
> `traceable-reqs check` exit 0.
>
> Mirror-excluded by the root JIT-plan predicate (`:(glob)*-PLAN.md`) on its name alone — no
> `ci/publish/mirror-public.sh` edit is needed or wanted.

## The state this inherits

**The Request's stated root cause is REFUTED.** `UserPromptSubmit` *does* mark busy —
`report_busy_with_input` (`hook.rs`, the UPS handler), and a live trace on this endpoint carried a
real `busy=36ms` stage. "UPS doesn't mark busy" is not the defect, and no fix may be built on it.

**Nothing has replaced it yet.** Two candidates, and the distinction between what is *reachable* and
what *fires* is the whole reason this is a measurement task rather than a build task:

- **(a) A race, not an absence.** The busy mark lands one binary spawn plus ~36 ms after Enter.
  Anything core pushes inside that window is PTY-injected into a turn that is already running —
  which fits the report's wording ("directly after a UserPromptSubmit") exactly. The ~36 ms is
  measured; the *consequence* is not.
- **(b) A silent hole.** The busy/poll block is guarded by `if !id.is_empty()`. An identity
  resolution that returns empty marks **nothing**, and from outside reads exactly like a healthy
  turn. **DEMONSTRATED REACHABLE** by driving the binary directly from a scratch cwd with
  `env -u SPT_ENDPOINT_ID`: it logged `TRACE UserPromptSubmit id=- …` with no `busy=` stage at all.
  **NOT demonstrated: that it ever fires in the field.** Those are different claims and only the
  second one justifies a fix.

## Why this waits on a release rather than starting now

v0.39.0 ships #34, which is the instrument this needs: before it, `hook-trace.log` filled its 512KB
cap in 101 minutes (measured 87 B/s, 95% of bytes stale-frame replay) and the roll *emptied the file
in place*, performed by whichever endpoint's hook crossed the cap. The `TRACE UserPromptSubmit` line
this measurement depends on was written and destroyed roughly two minutes later — that is what filed
#34 in the first place. The fix is in the binary, not on the node, until the release is installed.

**So step 0 is: install v0.39.0 on this node and confirm the instrument.** Do not skip this and do
not trust a zero hit before it — the last "the TRACE lines are missing" turned out to be a wiped log
and a perfectly healthy emitter. Confirm by reading a rolled generation (`hook-trace.log.1` exists,
the live file is fresh) and by seeing one summary refusal line per poll rather than one per frame.

## The measurement

1. **Size (a)'s window.** Stamp the wall-clock gap from UPS hook entry to the `state busy` call
   returning, on a real endpoint, across a spread of turns — not one sample. `busy=36ms` is one
   observation on one endpoint; the number that matters is the distribution, including a cold
   binary spawn.
2. **Show whether anything lands in it.** Replay the io funnel across a turn where a message
   arrived near a prompt submit and read whether the inbound was PUSHED or SPOOLED. A window that
   nothing ever lands in is a window, not a defect.
3. **Count (b) in the field.** Grep the retained trace for UPS lines carrying no `busy=` stage.
   Zero over a real multi-day window means (b) is reachable-but-unfired, and the honest outcome is
   to record that and close it — not to harden it anyway.

Read the trace with python, never grep: it is UTF-8-lossy, and it lives at
`%LOCALAPPDATA%\spt-core\adapters\_github\SaberMage-claude-spt\hook-trace.log` (the pointer-mode
`source_dir`, **not** `adapters/claude-spt/`).

## Only then, the fix

If the measurement shows the window is real and things land in it, the direction is to move the busy
mark to the earliest point in the hook that has an id, ahead of every other stage. **That is a
direction, not a decision** — this plan has not earned it. If (b) is what fires, the fix is a
different one (an identity that resolves empty should be loud, not silently unmarked). Mint the
requirement **with the fix**, once the measurement names which defect is being fixed; the id and its
title should state what was measured, not what was suspected.

## Also outstanding

- **`#1 Spill-to-file tax`** — SHELVED by operator ruling, not closed. Still ours, still untyped
  (Type is set once and never changed, so it is filled deliberately or not at all). Its own finding
  ("cap tuning recovers only 6%") implies a redesign rather than a tweak, so it wants a bag of its
  own rather than a corner of this one.
- **Retiring `--across` / "commune across"** — stage 2 of the v0.39.0 rename, a later release, once
  peers' durable context has rotated. Before retiring it, grep `ci/` and `tests/` for the token, not
  just `src/`: the wake rename shipped its retirement forty releases late for exactly that reason.
  `tests/recharge-vocabulary.sh` is where the retirement's guard belongs when it comes.
