# spt-pacer-tool

A `kind = "shell"` [spt-core] adapter that keeps an autonomous agent from
silently stalling. pacer is a **pure inward nudge**: while its **pacer
latch** is raised, an idle **bound endpoint** is pinged back into motion after a
quiet interval — unless the agent has declared a bounded or open-ended wait. It
pokes *inward* and reports nothing outward (deliberately distinct from a
conventional heartbeat). Formerly *rebound* / `spt-rebound-tool`; renamed in
0.3.0 (`docs/adr/0002`).

Like [spt-shell-notify], the glue is **the manifest + one binary** — no spt-core
crates, no source integration. The binary speaks the public `spt api` /
`spt send` / `spt endpoint digest` surface and copies spt-core's EVENT envelope
wire format verbatim.

## What it does

- Watches the bound endpoint's **busy↔idle** activity. A busy→idle edge (latch
  raised) arms the **pacer timer**; if it elapses while still idle, pacer
  fires a **ping** — an `spt send --idle-only --ephemeral` back to the endpoint
  that reappears in its inbound and re-provokes a turn.
- An unanswered ping re-arms on a **widening backoff** (60s → 2m → 5m → 15m
  cap); any idle→busy edge resets it to base. No give-up, no escalation.
- Every message it sends — ping, report, status answer, education — is signed
  `--from` its canonical instance id (`PACER-0`), so the agent always sees the
  same non-peer sender, and that sender byte-matches the ref the owner types.
- The agent steers it with **pacer codes** it reads from the endpoint's
  session digest — `!!done!!` (**pause**: blocked open-endedly → lower the
  latch) and `!!wait=m!!` (**delay**: ETA-bounded wait → push the next ping out
  m minutes) — or the equivalent `pause` / `delay` capability verbs. `!!done!!`
  covers **the current idle stretch only**: going back to work re-arms the
  pacing, so a still-blocked agent re-emits it.
- When an idle stretch of **≥ 60s** ends, pacer sends a **stretch report**: the
  stretch's final duration (second precision — `8m20s`) and a short **stretch
  id**. The agent can **classify** a recurring kind of wait —
  `!!classify=<id>:<name>!!` or `spt shell cmd PACER-0 classify <id> <name>` —
  and pacer folds the duration into that class's running average, kept durably
  per endpoint in `state/<owner>.json` under the adapter's install dir.
- `spt shell cmd PACER-0 status` asks a running pacer what state it is in —
  latch, activity and idle duration, the timer countdown, the backoff step, the
  last code it acted on, the pending unclassified stretch ids, and the registry
  class count. Read-only, and the answer comes back as a message to the
  endpoint it paces.
- Re-educates the agent (`spt send --active-only`) on first online and on every
  context boundary (`clear` | `compact` | `boot`) — post-reset amnesia is
  exactly when the reminder is needed.

## Install

<!-- [doc->REQ-PACER-INSTALL] -->
One command registers the adapter, and the release archive carries the
`pacer-shell` binary for every supported platform (Windows x86_64, Linux
x86_64 glibc and musl) — spt-core extracts your platform's binary into the
adapter's own install dir and resolves the spawn from there. Needs spt-core
**0.44.0 or newer** (the release whose shell spawn resolves install-dir-first)
and no Rust toolchain:

```sh
spt adapter add --release BigscreenVR/spt-pacer-tool --gh
spt shell spawn PACER     # mints an instance and launches it -> online
```

`--gh` because the home repo is private, so the plain-HTTPS fetch cannot see
it. `spt shell list` should show it `online`. From then on it paces the
endpoint that owns it; `spt shell teardown PACER-0` stops it.
`spt adapter update PACER` keeps the whole adapter — manifest and binary —
current from this repo's releases, without disturbing a running instance;
restart the instance (teardown + spawn) to pick a new binary up.

> **Register from the release, not from a directory you might move.** An update
> rewrites the directory you originally registered from, in place — it does not
> relocate the adapter somewhere durable. Registering with `--release` puts it in
> spt's own adapter home and updates land there; registering a local path means
> every future update rewrites *that* path, so a scratch or temporary directory
> leaves the registration pointing at something that may not survive.

### Known issue: quiet delivery needs spt-core ≥ 0.45.0

<!-- [doc->REQ-PACER-QUIET-DELIVERY-NOTE] -->
Everything pacer sends besides the ping — stretch reports, the onboarding
reminder, `status` answers, refusals — is delivered "quietly": it surfaces on
the agent's own next poll instead of waking it. That quietness is honored by
the delivery layer only on **spt-core 0.45.0 or newer**: on 0.44.0 (pacer's
minimum), a quiet send to an endpoint that has *already gone idle* can arrive
as a fresh wake — the report itself acts as a nudge (spt-core #104, fixed in
0.45.0). Harmless but noisy; if agents on your node wake to stretch reports,
update spt-core and restart the daemon.

### From source (development)

With a Rust toolchain, `cargo build --release`, then `spt adapter add .` and
`spt shell spawn PACER` — the spawn falls back to PATH when the install dir
holds no binary, so `cargo install --path .` also works for a dev loop.

### Upgrading from rebound 0.2.x

The adapter name changed, so there is no in-place update path — re-register
once by hand: `spt shell teardown rebound-0`, `spt adapter remove rebound`,
then the one-command install above. Your old binary on PATH can be deleted;
nothing references it.

## Status

**The loop is complete and verified live**, and since 0.3.0 the install is a
single command on spt-core ≥ 0.44.0. The full non-core surface (latch / timer /
backoff / ping / stretch report / classify / digest-code parse / capability
verbs / education / durable state) runs against the **real** spt-core activity
seam (published 0.42.0; field-verified 2026-07-25,
`docs/FIELD-VERIFICATION-0.42.0.md`). `docs/adr/0001` is the (now-satisfied)
core dependency; `docs/adr/0002` is the rename.

## Layout

- `manifest.toml` — the spt-core contract: the `spawn` template, `pause` /
  `delay` / `classify` / `status` capabilities, the `gh_release` `[update]`
  avenue, owner-private binding (`broadcast = "none"`,
  `max_instances_per_owner = 1`).
- `src/` — `machine` (latch/timer/backoff state machine), `digest` (code parser:
  committed-seq dedup, quotation-guard, clamp), `frame` (capability EVENT
  decode), `ping`, `report` (stretch reports: floor, id mint, rendering),
  `state` (durable per-owner registry + pending window + watermark rider),
  `education`, `activity` (the swappable seam), `main` (the resident bind +
  drain loop).
- `ci/publish/package-adapter.sh` — packs the fat `adapter.spt` (three
  target-triple roots + the shared manifest).

## Testing

`cargo test` covers the whole state-machine truth table, digest-code parsing
(including the quotation-guard and seq-replay hazards), the capability frames,
the stretch-report floor and bodies, the durable-state round trip and
atomicity, and the ping/education bodies (including the no-signoff hazard).
The live idle-edge→ping leg runs env-gated against the M1 rig.

Every requirement is traced (`traceable-reqs check`) — see `docs/TRACEABILITY.md`.

[spt-core]: https://github.com/SaberMage/spt-core
[spt-shell-notify]: https://github.com/BigscreenVR/spt-shell-notify
