Skip to content

Event record + standing conditions: a badge on the identity circle, a list behind it #132

Description

@lannbot

Design memo — recorded 2026-08-26 from design discussion (announcements → notification-list thread). Ruling set; account scope implemented with this issue, surface scope specified and dormant. Related: #22 (the strip, announce discipline, three voices), the transparency notes' "detection requires a response path".

Motivation

The strip's bottom-line announcement is one transient line multiplexing three different jobs, and the scar tissue shows it:

  • the priority patch — sticky windows and dropped ambient lines (the revocation-note-erased-by-a-stats-tick lesson) are contention management over a single line;
  • the pulseContext retreat — the storage-page arrival announcement was retired because the line an announcement spends is the line the context lives on;
  • the spoken FIFO queue — two live-region writes in one synchronous block destroy each other;
  • the standing-condition hacksyncFailureAnnounced in the solo host is a state ("sync is down") hand-implemented on a transient medium, with edge-triggering and manual re-arming.

And the deepest problem: announced-never-silent is hollow when nobody is looking. A remote device revocation gets 12 seconds on a strip the user may not be watching; multi-device means consequential events happen while you're away by construction. The transparency roadmap sharpens this: fork/rollback alarms are named as consequential announcements, and "detection requires a response path, or the log is a diary" — a timed line is not a response path.

The split (ruling)

Announcements were three jobs. Each gets its own mechanism:

job mechanism example
moment cues stays push: announce(), pulseContext(), speak() — record-less the fresh-colour teach, the adoption beat
standing conditions new: visor-held keyed conditions; badge lit while standing, clears on resolution sync failing
event records new: persistent, acknowledgeable list device added/revoked, remote renames, reconciliation diffs

The mechanical v1 rule: every consequential announcement leaves a record. One integration point per host (the announce sink / wrapper already carries the consequential flag); moment cues stay record-less because they are not flagged. Arrival push is unchanged — the badge is the memory, not the alarm.

The badge

  • A dot (never a count) on #visor-settings, the identity circle — absolutely positioned, zero layout shift (the strip's geometry is a measured property), framework voice by construction (no words on screen).
  • Lit = unseen records ∪ standing conditions.
  • The symmetry rule: the badge sits on the anchor of whoever the news is about. Identity circle = news about me/my system (this issue). Pet icon = news about it, this surface (dormant, below).
  • The author rule: only visor/engine-authored events light it. An app must never make the user's own identity circle demand attention — a self-badging primitive is "look at me!" handed to components. The subject of a record may be an app; the author is always the system. (Holds today — us-events are engine-authored; recorded so it survives future event sources.)

The list

  • A drawer sheet ("recent events", light tenant), reached from a visor-owned row on the settings sheet, using the same suspend/resume motion as the erase entry: settings slides out, the list slides in, closing resumes settings.
  • Standing conditions render first, then records newest-first with coarse relative times.
  • Opening marks records seen (v1 acknowledgment model). The badge un-lights unless a condition still stands. Per-entry dismissal is deferred until entries carry actions.
  • Entries are flat framework-voice strings under exactly announce()'s three-voices policy (user-voice words inline, app voice never). The growth path is typed slots (framework sentence + petname slot + plated foreign slot), which is what unlocks app-voice content — required for the surface scope, not needed for account-scope events, which have no app-voice content by policy already.
  • Records persist in visor-realm storage under a consumer key; conditions are session-live (a persisted condition could outlive its cause with nothing left to clear it — pollers re-assert them). erase() wipes the record with everything else.

Rules that keep the badge meaningful

  1. Entry gating: consequential only. Ambient telemetry never enters the list, or it becomes junk mail and the one alarm that matters drowns.
  2. A notification never grants; it points at a ceremony. "New version requests new grants" is an entry whose action opens the consent surface; the grant path stays the powerbox.
  3. Publisher blink-spam: a publisher can cause legitimate system-authored events at will (release, release, release). Per-surface entries coalesce to the latest standing offer, not an event-per-release feed. (Surface scope; recorded now.)
  4. The badge is the memory, not the alarm: consequential arrivals still owe the spoken sentence and the strip line/pulse. Announced-never-silent becomes spoken at arrival, standing until seen.

Surface scope (specified, dormant)

System-authored events about one surface — an update landed, a pending upgrade requests new grants, a new version nominates a glyph — badge the pet icon and list in the App settings sheet (which already dresses the provenance key through foreignToken, so version strings and keys can be plated there). Wrinkles needing rulings before this wakes: unnamed surfaces have no glyph to badge (nothing renders in the visor's voice before the user speaks); and news about a backgrounded app in a multi-app world needs a rollup (switcher badges or a per-surface digest in the account list) — parked with the launcher/switcher pass.

v1 scope (this issue)

  • visor/ui: records + conditions on the Visor interface, optional eventsKey persistence, the badge render, the events tenant + settings-sheet row, erase() coverage.
  • Demo wiring: the consequential path of the announce sinks writes records; the solo host's sync watch becomes a condition (the edge return replaces syncFailureAnnounced).
  • E2E: a scenario driving a remote us-event through the mock pairing driver into badge → list → seen → persistence; condition assertions on the store-outage scenario; strip-geometry unchanged.

Open questions

  • Severity tiers: a future fork/rollback alarm shouldn't have "small dot on a small circle" as its only trace — stronger arrival treatment, same record surface.
  • Read-state sync: v1 is per-device (the list is this device's drain), matching how announced-never-silent already works. Account-synced seen-state only if a real annoyance shows up.
  • Whether standing conditions should leave records on crossing beyond what the mechanical rule already captures (v1: the failure announce is consequential, so the crossing leaves a record; the recovery announce is ambient and does not).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions