0217: Select one fresh client for each attention event
Date: 2026-10-18 Status: Withdrawn 2026-10-19. Reverted: every tab wrote a lease file every 2 seconds that was never pruned, and a failed routing request silenced every tab. Each tab alerts on its own again.
Context
Section titled “Context”Before this change, each dashboard tab started its own title flash, favicon and chime for the same actionable event. A browser’s focus flag can survive after its user leaves. Local engagement suppression helps that tab, but cannot choose between tabs or devices viewing the same dashboard server.
Decision
Section titled “Decision”Keep the existing semantic AttentionRequest.key as the event identity, scoped
to the dashboard’s configured project root. Every mounted page continues to
show its in-page attention state. Only a server grant starts automatic title,
favicon or audio channels. The CLI and agent coordination need no browser or
presence service. Browser clients on different devices coordinate when they
reach the same dashboard server; independent dashboard servers do not share
browser presence in this change.
Each provider mount creates a random client ID in memory. It observes its
current event every two seconds and on visibility, focus and interaction
changes, with at most one observation request in flight. The server assigns
observedAt from its own clock. A client is fresh for six seconds, exclusive, only while
its current event key matches. Nonfinite, negative and future stored server
timestamps are ineligible. Clients send a monotonically increasing interaction
counter, never their wall clock. Only a greater counter establishes a new
interaction time on the server; unchanged polls cannot renew engagement.
Interaction must be within eight seconds to establish engagement. A stale focus
flag cannot suppress another client’s alert.
The pure selector ranks fresh visible, focused and recently engaged clients first, then visible clients, then other fresh clients. Within a rank, the most recent server observation wins, with ascending client ID as the final tie breaker. Presence is an observation, never a delivery receipt or an approval. An engaged winner receives the existing in-page channels and suppresses its disruptive channels for that event. Other clients cannot use their own stale focus flag to absorb it.
Observation and claim are separate operations. An event keeps its selected
recipient while that recipient is fresh and its lease remains valid. This
prevents ordinary heartbeat ordering from moving the alert between tabs.
Otherwise the server selects one fresh successor or returns no recipient when
none exists. The event key survives reassignment; a random generation identifies
each selected recipient episode. A selected client requests a claim for that
exact generation. Only its first successful claim returns granted: true.
Retries return claimed without another grant. The claim’s server engagement
value controls suppression; the client does not substitute its own stale flag.
Event selection and claim changes append numbered immutable revisions under
.harnery/attention/events/<sha256(key)>/. The writer flushes a temporary file,
then publishes it through an exclusive hard link to the next revision. Competing
workers retry after an existing revision without polling sleeps or stale locks.
A crash before publication leaves no revision, and a crash after
publication leaves a complete record. Every read validates the original event
key, client ID, generation and revision. Unreadable records fail closed.
Presence rows use atomic replacement under .harnery/attention/clients.
Storage errors grant no channels.
The selection lease lasts six seconds. A claimed client renews it by observing with its current generation only after receiving the grant. An unconfirmed grant therefore cannot retain a recipient forever. The client starts channels only if the response still names its mounted event, client and generation. It stops at the lease deadline, on acknowledgment, event replacement or disposal. It subtracts the full monotonic request round trip from the server’s remaining lease duration, so a delayed response cannot extend the lease. Renewals retain the generation and never replay its initial chime. A heartbeat cannot revive an expired generation. A lost grant response can leave that episode silent until its lease expires; the next observation then selects a fresh recipient even if it is the same client.
If the recipient disappears, a fresh successor can claim a new generation for the same event after expiry. No fresh client means no disruptive alert; the inline event remains visible and a later observation selects a fresh client. The claim proves authorization only, not display, sound or human receipt. There is one current recipient and one grant per generation. A crash or blocked browser thread can leave an old effect’s outcome unknown; a successor generation may therefore repeat an alert. Exactly-once physical delivery and guaranteed successor availability cannot both be established across that boundary. Fresh successor availability takes precedence, while event and attempt identities make the distinction explicit.
Deliberate interaction acknowledges the current event separately through the server and the existing tab store. The acknowledgment is an immutable, exclusively created event marker. It suppresses future channels and stops active channels on the next observation. Neither selection nor acknowledgment changes an approval, task or workflow record. Acknowledgment can precede a claim and then prevents it. An acknowledgment racing a claim can leave a granted alert briefly active until its next observation; it never creates a second recipient. Manual replay remains an explicit local user action and creates no automatic claim or approval transition.
Acknowledgments do not expire automatically. Presence and recipient leases expire for selection after restart using the same server clock checks. Claim history remains inspectable across generations. No cross-host transport is introduced.
Interfaces
Section titled “Interfaces”selectAttentionClient(clients, eventKey, now) is pure and returns one client
or null. observeAttention(root, observation, now) atomically publishes
presence and returns { recipientId, state, granted: false }.
The response also carries generation, expiresAt, serverNow and engaged.
claimAttention(root, eventKey, clientId, generation, now) revalidates presence
and returns the same shape with granted: true only for the exclusive claim
creator in that generation.
acknowledgeAttention(root, eventKey, clientId, now) records only attention
acknowledgment. POST /api/attention validates and dispatches these three
operations. Client orchestration takes an injected transport and clock and
discards responses for a replaced or unmounted event. Grant and acknowledgment
callbacks carry the exact event key; the provider checks its synchronous slot
reference as well as the client’s committed event, including during a pending
React transition.
Alternatives
Section titled “Alternatives”Session storage cannot coordinate tabs. BroadcastChannel and browser locks do not coordinate devices. A permanent grant cannot satisfy successor availability after client death. Reassignment cannot distinguish a missing alert from a lost response after the alert began, so it is recorded as another attempt rather than exactly-once delivery. Recording delivery only after starting channels leaves the same crash window. A global presence relay is unnecessary for clients sharing one dashboard.
Verification
Section titled “Verification”Fake-clock tests cover stale focus, future values, two tabs and a phone, ties, missing presence, pre-claim disappearance, and event replacement. Durable store tests cover competing workers, a lost response, a restarted reader, partial claims, acknowledgment separation and storage refusal. A rendered two-tab test requires one automatic title alert while both retain their in-page event. Private guard-removal runs must fail the relevant invariant.