---
status: accepted
---

# Owner activity gates the tag scanner's poll: the idle edge scans once and then quiesces, going busy resumes it

<!-- [doc->REQ-TAG-SCANNER] --><!-- [doc->REQ-TAG-SCAN-GATE] -->

Supersedes [ADR-0010](0010-tag-scanner-polls-on-a-tick-activity-frames-never-drive-it.md).
Accepted 2026-07-25 (Operator ruling). Implementation tracked as Request
[#19](https://github.com/BigscreenVR/spt-progress-tool/issues/19) — this ADR records
the decision, not a shipped mechanism.

## Context

ADR-0010 rejected using spt-core 0.42.0's owner `activity` frame to **drive** the
scanner, and that reasoning stands: a turn seals when the owner goes idle *or* when
the next user-input arrives, so an owner messaged while still working seals the
previous turn with no idle edge anywhere. A scanner that only woke on idle edges would
hold an already-visible tag until the next idle, which a long tool sprint can defer
arbitrarily. Waking *on* idle is incomplete.

ADR-0010 then over-reached. It concluded that activity frames "must never be able to
replace, throttle, or **gate** the tick." Gating is a different proposal from driving,
and it does not inherit the hole:

- Tags only ever appear in the owner's **own turns**. An idle owner cannot produce a
  new one, so a poll running against an idle owner is asking a question whose answer
  cannot have changed.
- The case that defeats idle-driving is the case gating handles for free: the
  user-input that seals a turn without an idle edge is also what makes the owner
  **busy** — which is precisely the re-enable trigger. The seal and the resume are the
  same event.

So the tick remains the authority while the owner is working; what changes is that it
stops running when there is provably nothing to find.

## Decision

**Owner activity gates the poll.** On busy→idle, scan **once**, then quiesce. On
idle→busy, resume polling at the normal interval.

Two constraints are load-bearing, not refinements:

1. **The idle-edge scan happens before quiescing.** Going idle is the very event that
   seals the trailing turn and makes its tags visible. Quiescing without that final
   scan would leave the last turn's tags unfired until the owner was prompted again —
   reintroducing exactly the mint-then-idle deadlock spt-core 0.42.0 removed, and
   which cost a Liaison bringup to diagnose.
2. **A lost busy-frame must degrade to latency, never to silence.** If the poll is
   gated off and the frame that would re-enable it never arrives, the scanner goes
   permanently deaf — tags lost rather than delayed, which is the one failure shape
   this system does not accept. The gate needs a floor: a slow heartbeat scan that
   runs even while quiesced, or resumption on any inbound link activity. Whichever is
   chosen, no single dropped message may gate the poll off indefinitely.

Still rejected, unchanged from ADR-0010: driving scan scheduling off the idle edge, or
letting an activity frame substitute for the tick while the owner is busy.

## Consequences

- No polling against an idle owner — the digest pull stops when it cannot find
  anything, rather than running flat out.
- Worst-case tag latency while the owner is **working** stays bounded by the tick, as
  before. The new exposure is a gate-off bug, which is why constraint 2 is mandatory
  rather than advisory.
- The scanner acquires a dependency on frame delivery it did not have. Replay safety
  is unaffected — it stays keyed solely on the digest cursor, so
  `REQ-HAZARD-TAG-SEQ-DRIFT` reasoning is untouched — but liveness now depends on the
  gate, and the heartbeat floor is what keeps that from becoming a correctness
  dependency.
- ~~**Open, to be decided when built:**~~ the parked-confirm expiry sweep. **Decided
  when built (W18): the sweep does not ride the gate at all.** It was only ever coupled
  to the scan tick by implementation accident — expiry is a wall-clock comparison over
  an in-memory parking lot and needs no digest pull, so it now runs on the *ungated*
  drain tick. That is strictly better than the "likely acceptable" latency the question
  contemplated: the notice is prompt rather than interval-rounded, ADR-0010's fourth
  reason (expiry must not wait on an idle that may never come) is preserved verbatim
  under gating, and nothing about it depends on frame delivery.
- The heartbeat floor did land as its own mechanism, so per the rule below it mints
  requirements: **`REQ-TAG-SCAN-GATE`** (the gate) and **`REQ-HAZARD-SCAN-GATE-DEAF`**
  (constraint 2 as an invariant with a test). Constraint 2 is satisfied by three
  independent floors, any one of which suffices alone: the heartbeat scan while
  quiesced, resumption on any inbound owner frame, and never quiescing at all until an
  activity frame has actually been seen — so an spt-core too old to push them, or a
  drain that answers nothing, degrades to exactly the pre-ADR unconditional poll.
- One thing gating bought that was not argued for: the activity frame carries the
  owner's id and is re-pushed on every link, so a freshly spawned shell now learns its
  owner from it rather than waiting to be driven. The first turn's tags can fire before
  the owner has ever issued a command.
