Skip to content

v1.5.2: calendar escalation ladder, start-of-event chirp, alert snooze - #12

Merged
sumitake merged 4 commits into
mainfrom
dev/claude/urgent-ambient-v1.5.2
Aug 4, 2026
Merged

v1.5.2: calendar escalation ladder, start-of-event chirp, alert snooze#12
sumitake merged 4 commits into
mainfrom
dev/claude/urgent-ambient-v1.5.2

Conversation

@sumitake

@sumitake sumitake commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Operator-designed escalation and acknowledgment features:

  • Escalation ladder: approach (30m, priority 25 — outranks CI rotation) → notice (15m, 65 — outranks alerts, amber) → warn (5m, red) → imminent (1m, LED blinking); new framework tiers RAISED=25/URGENT=65 with contracts
  • Start-of-event chirp: one edge-detected chirp precisely at T-0 (sleep-shortening lands within ~1s) using the firmware's stock calendar_event_starts.wav; LED extinguishing is now explicit and guaranteed (tracked state, LED_OFF_COLOR, vanish-path flush)
  • Alert snooze: starting a BUSY session while an alert shows snoozes that exact failure fingerprint for snooze_minutes (30) past session end; any new failure re-alerts immediately. Safety property review-hardened: sessions predating an alert never auto-snooze (polled-state gating, reviewer-reproduced scenario as regression test)
  • Review round fixed one Critical + two Important findings; scoped re-review clean

Tests: 319. Note: base includes the parallel set_busy fix (#11) — no file overlap.

🤖 Generated with Claude Code

sumitake and others added 4 commits August 4, 2026 00:06
…alerts

Operator-reported UX gap: a persistent CI failure alert (PRIORITY_ALERT,
60) permanently evicted the calendar, hiding an imminent event with no
way for the ambient tier to ever reclaim the screen -- unlike the
overlay tier, ambient has no dwell/silence contract of its own to fall
back on. Fixes this with a state-dependent draw priority: as an
upcoming event gets closer, the calendar climbs two new shared tiers.

busybar/display.py gains PRIORITY_AMBIENT_RAISED (25, strictly between
the overlay tier and PRIORITY_ALERT -- "near-term, may outrank overlays,
never alerts") and PRIORITY_AMBIENT_URGENT (65, strictly between
PRIORITY_ALERT and PRIORITY_SESSION -- "imminent, may outrank alerts,
never sessions"), each fully documented with their contract text.
led_notification_color is called out explicitly as the one channel NOT
subject to this same priority arbitration -- it gets through even when
a BUSY/CUSTOM session owns the whole panel, making it the session-safe
signal for the final-minute LED below. Ladder-ordering test extended:
20 < 21 < 25 < 60 < 65 < 90, plus dedicated boundary tests for each new
tier.

calendar_countdown/logic.py gains select_priority (in_progress -> 20;
<= notice_minutes -> 65, covering both the NOTICE and WARNING visual
states unchanged; <= approach_minutes -> 25; else 20 -- deliberately a
separate ladder from _state_for's palette selection, not a 1:1 mapping:
approach changes priority without changing the palette at all) and
select_led (LED on every draw from imminent_minutes before start until
the event starts, off the instant in_progress is true). New config
keys: approach_minutes=30, imminent_minutes=1 (LED window only).
in_progress deliberately stays at the baseline: once a meeting has
started you already know about it, so the elevation exists to catch
your attention before it starts, not to keep fighting for the screen
once it has.

Priority changes for the same app_name need no clear() of their own
(same app upserting the same ids at a new priority always succeeds
regardless of the number); ci_status's own alert redraws while the
calendar holds the higher tier get a 409 (REJECTED), which its existing
unified shape-tracking mechanism (from the v1.5 revision round) already
handles cleanly with zero new code -- only commits state on a confirmed
DRAWN result.

Stage 4 -- one-time chirp at event start (T-0), not during the
countdown: should_chirp/commit_chirped do edge-detected (not level-
detected) once-per-event firing, tracked by event start timestamp in a
caller-owned chirp_state dict, pruned after 24h. commit is separate from
the fire decision so a failed play_audio retries next poll rather than
silently skipping forever (same DRAWN-gated-commit discipline used
throughout this codebase, applied to an audio action). A restart during
an event's final minute/after it started never observed the event as
upcoming, so it correctly never fires -- documented tradeoff versus the
alternative (level-detection) risking a spurious chirp on every restart.
next_sleep_seconds sleeps exactly until a sooner-than-usual event start
instead of the full poll interval, landing the transition-detecting poll
within about a second of T-0.

Audio: probed the firmware's own stock sounds first (per instruction --
found assets/shared/sounds/calendar_event_starts.wav, an exact semantic
match) before generating anything; confirmed live (POST /api/audio/play
with that stock_path returned 200). No asset generation, upload, or
repo-committed binary needed. BusyBarClient.play_audio (src/busybar/
client.py) supports both stock_path and path per the device's PlayAudio
schema but this feature only uses stock_path; it has no volume
parameter at all, deliberately, so playback always uses whatever volume
is currently configured -- /api/audio/volume is never touched. New
config key chirp=true (false disables audio entirely).

TZ=UTC uv run pytest -q: 272 passed in this commit's isolated state
(verified via git stash --keep-index before committing).

On-device: full eviction/409/recovery cycle confirmed against the REAL
live ci_status and calendar_countdown LaunchAgents (not a simulation) --
a genuine live CI failure alert visibly evicted by a calendar draw at
priority 65, the live agent's own next redraw attempt logged "-> rejected"
(409) exactly as designed, and "-> drawn" resumed once the calendar
dropped back to priority 20. All four escalation stages (approach/
notice/warn/imminent) captured individually with matching palettes; the
imminent stage's led_notification_color field confirmed present in the
actual payload sent (a frame capture can't show the LED itself). One
real, audible chirp fired via the actual should_chirp/play_audio path
against a synthetic event, landing 0.01s after the event's start
(2026-08-04T06:54:16.622887+00:00 UTC) -- confirms both the T-0 timing
precision and the once-per-event edge detection (fired exactly once
across a 10-poll window spanning the transition). Live LaunchAgents
paused (bootout) for the duration of the eviction/stage captures to
avoid interference and restarted afterward, confirmed healthy.

Discovered and worked around (not fixed, out of scope for this commit,
flagged separately): BusyBarClient.set_busy_simple sends a request body
shape the device's PUT /api/busy/snapshot actually rejects (confirmed
against the real device) -- the correct shape wraps the fields under a
"snapshot" key plus a top-level "snapshot_timestamp_ms".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Additive feature riding on the v1.5.2 branch. Raw physical button
events aren't API-observable (confirmed: the status WebSocket only
reports what's currently on screen, not button events), but the BUSY/
CUSTOM session the button starts is, via client.get_busy(). The snooze
rule rides on that: an alert showing at the moment a session starts is
treated as "the operator saw it and pressed the button."

compute_alert_fingerprint (ci_status/logic.py): a frozenset of (repo,
workflow, category) triples, category being "failing" or "stuck" so a
pair moving between categories counts as a change, not the same alert
continuing.

update_snooze: the full state machine, in-memory (restart clears --
documented). Pending starts only on a genuine inactive -> active
transition (edge, not level) while an alert is showing -- a session
that predates the alert, or predates a fingerprint change mid-session,
must not be misattributed as "you just pressed the button for this."
Detecting the edge needs the previous poll's observation
(session_was_active), which defaults to True (not False) whenever
unobserved -- on the very first poll of a fresh process, or after a gap
from the deliberately-gated get_busy() polling (only while an alert is
showing or a snooze is pending/active, never on a fully idle poll) --
biasing against a false-positive auto-snooze at the cost of
occasionally requiring the operator to press the button again. Once
pending, the session ending starts a timed snooze
(snooze_until = now + snooze_minutes); any fingerprint change at any
point clears the snooze and re-alerts immediately; expiry resumes
alerting if still failing. New config key snooze_minutes=30 (0
disables).

build_ci_payload gains suppress_alert (skip failure/stuck entirely,
falling through to overlay/quiet-green/nothing -- "running/quota
rotation and green behavior unaffected" is explicit) and suppress_led
(blank the failure branch's LED specifically without suppressing the
draw -- used during pending, since the alert's own element draw still
proceeds as normal and gets naturally rejected by the session's higher
priority anyway, but its LED bypasses that same arbitration and would
otherwise keep blinking through a session the operator just
acknowledged). main.run_once computes effective_has_alert = has_alert
and not suppress_alert and uses it for the overlay rotation's own "an
alert takes precedence" check too, not just the alert badge's own
rendering.

TZ=UTC uv run pytest -q: 299 passed in this commit's isolated state
(verified via git stash --keep-index before committing). New coverage:
compute_alert_fingerprint, the full update_snooze state machine
(pending -> timed -> suppression -> expiry, fingerprint-change re-alert
at every stage including mid-pending and mid-timed, snooze_minutes=0
disables, the conservative session_was_active default), and the loop-
level end-to-end sequence through run_once (busy-poll gating -- never
called when idle -- LED suppressed during pending, alert suppressed
during timed, running/quota rotation unaffected while suppressed,
full backward compatibility when snooze_state is omitted).

On-device: every actual display action (draw/clear, the LED field,
re-alert) verified against the real device through the real
ci_status.main.run_once, including a genuinely hardware-triggered BUSY
session via POST /api/input?key=busy (the documented remote button-
emulation endpoint) and key=off to end it -- the closest possible
reproduction of the actual operator flow short of a physical press.
Captured: alert visible, panel dark during the timed-snooze suppression
window, and the alert reappearing on a new failure fingerprint. The
live ci_status LaunchAgent was paused (bootout) for the duration to
avoid interference and restarted afterward, confirmed healthy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New "v1.5.2 urgent-ambient escalation, stage-4 chirp, and CI-alert
snooze" section covering the final converged design across the round's
several in-flight operator refinements (a two-stage proposal superseded
by three-stage, then four-stage, then a stage-4 timing split, then the
additive snooze feature) -- documents the final state, not the
intermediate ones. Covers: the two new priority tiers and their
contracts, the escalation ladder table, the eviction/409 interplay
(verified against the real live agents, not just asserted), stage 4's
LED/chirp timing split and the firmware stock-sound probe (with its
exact timestamp), the once-per-event edge-detection design and its
documented restart tradeoff, and the full snooze state machine
including the conservative session_was_active default and its
reasoning. Verification totals and on-device summary; full captures and
transcripts live in the implementation report for this round.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ings

Final-gate review on the v1.5.2 round found one Critical (reproduced by
the reviewer), two Important, and three Minor issues. All addressed
here.

Critical -- snooze auto-suppressed unacknowledged failures:
update_snooze unconditionally wrote session_was_active every poll,
including a dummy False for idle (unpolled) cycles. A run of idle polls
would stamp session_was_active=False regardless of the device's real
state; if a BUSY session then started unobserved and only afterward did
an alert appear (triggering the first real get_busy() poll, correctly
observing busy_active=True), the stale False read as a fresh transition
and silently snoozed a failure the operator never acknowledged. Fixed:
busy_active is now bool | None, with None meaning "not polled this
cycle" -- session_was_active is committed only when busy_active is not
None, so an unpolled period can no longer corrupt it. (Also fixed an
ordering bug introduced while writing this fix: was_active must be read
BEFORE the new value is committed, not after -- caught immediately by
8 failing pre-existing tests, corrected, re-verified.) New tests
reproduce the reviewer's exact scenario and its mirror, at both the
pure-function and full-loop (through the real run_once) level.

Important -- LED stuck-on path: no code path guaranteed the LED turns
off when an imminent event vanishes without passing through
in_progress (all-day filtering, or an event shorter than one poll
interval) -- the "no upcoming event" path never touched the LED field
at all. Probed what's actually checkable (no status endpoint exposes
LED state anywhere in the API; a 409 response carries no LED info) --
unresolvable from this codebase, so the fix is hypothesis-agnostic
instead of depending on an answer: select_led now returns a bool
("should be on"), and a new resolve_led_value combines that with a
newly-tracked state["led_on"] (DRAWN-gated commit, same discipline as
every other piece of device state here) to send an EXPLICIT
LED_OFF_COLOR ("#00000000") on every on->off transition rather than
omitting the field and hoping. The "no upcoming event" path now checks
led_on before its clear() and, if lit, sends a minimal 1x1 transparent
placeholder (LED_OFF_ELEMENTS, self-expiring) carrying the explicit off
value first -- clear() is a bare DELETE with no body and can't carry
it, and the draw endpoint requires at least one element. Tests cover
both repro shapes (normal in_progress transition, and the vanishing-
event edge case) plus no-spurious-flush and retry-on-failure. On-device
(preview app): confirmed the device accepts both new request shapes
(200) and the placeholder renders as a true visual no-op.

Missing on-device evidence: the initial round's completion message
cited specific results (chirp timing, stage captures, the eviction/409
cycle) with no persistent report file recording them --
.superpowers/sdd/display-v1.5.2-report.md did not exist. Written now,
reconstructed from saved scratchpad artifacts (frame captures, scripts)
and an accurate transcription of terminal output actually observed
during the initial round -- not regenerated or fabricated. One fresh
on-device check included for the LED-off mechanism specifically, since
it's genuinely new request shapes the initial round never exercised.
Flagged the same gap for the still-missing v1.5.1 report as a separate
follow-up task, out of scope for this branch.

Minor -- config sanity: check_threshold_ordering warns once at startup
if approach_minutes/notice_minutes/warn_minutes/imminent_minutes
violate their assumed nesting; documented in config.example.toml and
the README.

Minor -- chirp keying: should_chirp/commit_chirped now key by
(start, ascii-safe title) via _chirp_key, not start alone, so two
events sharing an exact start timestamp (two all-day events, two
calendars firing simultaneously) can't collide on one tracking entry.

Minor -- unverified LED claim: busybar/display.py's
PRIORITY_AMBIENT_URGENT docstring asserted the LED survives a session's
panel eviction as settled fact. Confirmed unverifiable through this
API (no LED-state-observable endpoint exists anywhere in the OpenAPI
spec) -- softened to an explicit "assumed, unverified" framing
everywhere it's referenced (display.py, calendar_countdown/logic.py,
the README), and added a concrete operator-verification recipe to
calendar_countdown/README.md ("Verifying the LED assumption") for a
post-deploy live check.

TZ=UTC uv run pytest -q: 319 passed (299 before this fix round).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sumitake
sumitake merged commit 18ba1ed into main Aug 4, 2026
4 checks passed
@sumitake
sumitake deleted the dev/claude/urgent-ambient-v1.5.2 branch August 4, 2026 07:48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 492b4f2ea6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +207 to +208
busy = client.get_busy() or {}
busy_active = busy.get("type") not in (None, "NOT_STARTED")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve unknown busy snapshots during snooze

If /api/busy/snapshot is temporarily unreachable or returns a non-200 while a snooze is pending, get_busy() returns None, but or {} converts that unknown result into busy_active=False. update_snooze consequently treats the session as ended and starts the timed snooze early, even though the session may still be active. Pass None through on a failed snapshot so the existing defensive pending-state branch can retry without committing a false transition.

Useful? React with 👍 / 👎.

Comment on lines +695 to +699
if failures and not suppress_alert:
text = "CI FAIL " + " ".join(f"{repo}:{name}" for repo, name in failures)
led = None if suppress_led else "#FF0000FF"
return {"elements": _badge_elements(text, "#A32D2DFF", "#FFFFFFFF", timeout_s),
"priority": PRIORITY_ALERT, "led": "#FF0000FF"}
if stuck:
"priority": PRIORITY_ALERT, "led": led}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Explicitly turn off the acknowledged alert LED

When a failure alert has already sent the red LED color and a BUSY session starts, suppress_led=True changes the next payload to None, which only omits led_notification_color from the request. On firmware where the previous LED setting remains sticky—the exact ambiguity handled by the new calendar LED transition code—the red alert continues blinking throughout the supposedly acknowledged session. Send an explicit transparent/off color on this transition rather than omitting the field.

Useful? React with 👍 / 👎.

Comment on lines +121 to +124
if state is not None:
if should_chirp(event, in_progress, now, state, c["chirp"]):
if client.play_audio(APP, stock_path=CHIRP_STOCK_PATH):
commit_chirped(event, state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track upcoming starts while another event is active

When event A remains in progress while event B approaches and starts, the loop selects A exclusively and this is the only should_chirp call, while next_start is also cleared for an in-progress selection. Event B is therefore never recorded in seen_upcoming and the loop never wakes at B's start; once A ends and B is selected as active, B looks like a restart-mid-event and never chirps. Track the next upcoming event independently of the event chosen for display so overlapping meetings still receive their start chirp.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant