Skip to content

feat(capture): OS notifications on critical live failures#69

Merged
sepehr-safari merged 1 commit into
mainfrom
feat/live-notifications
Jul 12, 2026
Merged

feat(capture): OS notifications on critical live failures#69
sepehr-safari merged 1 commit into
mainfrom
feat/live-notifications

Conversation

@sepehr-safari

Copy link
Copy Markdown
Member

The last S5 issue: OS notifications when live detection raises a critical failure, so an operator watching a session gets pinged without staring at the timeline (macOS-first, ADR-0002). Depends on the effects channel (#58) and the live failure stream (#56/#59).

Spike: reaching the notification service (ADR-0011)

The issue flagged a contingency — "if no notification API is reachable in the zero-config runner, move to the #33 runner-eject." The spike found a seam, so this lands in-runner:

  • The effects channel (fx) exposes spawn / fetch / timers / … but no notification verb, and PlatformServices is constructed inside the runner, never handed to main.
  • But Effects binds the platform services as a public field — fx.services: ?*const PlatformServices, set on the loop thread before dispatch. Zig has no private fields, so update_fx reaches fx.services.?.showNotification(...) directly — on the loop thread, where services are meant to be called. Guarded, so a null-services build (tests / null platform) is a silent no-op.

Decision logic (pure, headless-tested)

Lives in workspace.zig, no platform involved: LiveCapture tracks the critical failure codes already notified this session (std.EnumSet), and each reload enqueues one Notification per new code (title/body copied into fixed buffers so nothing dangles into the live arena; deduped per code; reset on start). update_fx fires the pending set through fx.services and clears it each turn. Read/clear are split (pendingNotifications const / clearNotifications void) because the runtime reflects value-returning model accessors and requires them *const.

Notify on explicit faults only

A live session is detected on a growing prefix, and two of the four critical rules are inferred from a not-yet-arrived message: unresponsive_csms (a Call awaiting its response) and station_offline_during_session (a StartTransaction with no StopTransaction). Both are the normal mid-session state — every in-flight request and open transaction trips them, then they resolve. So the notifier fires only on criticals the station explicitly reports: connector_fault (a "Faulted" status) and diagnostics_failure (a failed upload). The absence-based ones still show in the live failure panel; they just don't ping (Model.isLiveNotifiable).

Permission

Declares notifications (security.permission_notifications) in the runtime permission set and app.zon — no notification is delivered without it.

Tests (165/165, +4)

  • A critical fault (Faulted during a transaction → CONNECTOR_FAULT) queues exactly one notification, titled with the code; duplicates within the session are suppressed.
  • Non-critical severities don't notify.
  • Prefix-transient criticals don't notify though they're present as failures (locks the policy).
  • A new capture session resets the dedup — the same fault notifies again.

Verification

  • native test -Dplatform=null165/165.
  • native build (ReleaseFast) → clean (the fx.services firing compiles into the app, not just tests).
  • zig fmt --check clean; native checkapp.zon is valid; studio ci exit 0; the GUI launches without a startup crash.

S5 complete

This closes the flagship milestone — the WS proxy, the studio capture CLI, and the live inspector surface with notifications (#54#60). Next is S6 — public release & launch (0.5.0).

Closes #60.

The last S5 issue. When live detection raises a critical failure, fire an
OS notification so an operator watching a session gets pinged without
staring at the timeline (macOS-first, ADR-0002).

Reaching the notification service (spike, ADR-0011): the effects channel
(fx) exposes no notification verb, and PlatformServices is built inside the
runner, never handed to main. But Effects binds the platform services as a
public field (`fx.services`), set on the loop thread before dispatch — so
update_fx reaches `fx.services.?.showNotification(...)` directly, on the
loop thread where services are meant to be called. Guarded, so a null-
services build (tests / null platform) is a silent no-op.

Decision logic is pure and headless-tested (workspace.zig): LiveCapture
tracks the critical codes already notified this session (std.EnumSet),
enqueues one notification per new code (title/body copied into fixed
buffers, deduped per code, reset on start); update_fx fires and clears the
queue each turn. Split pendingNotifications (const) / clearNotifications
(void) so the value-returning accessor stays const — the runtime reflects
model accessors and requires it.

Notify on explicit faults only: connector_fault (a "Faulted" status) and
diagnostics_failure (a failed upload). The absence-based criticals —
unresponsive_csms (an in-flight Call) and station_offline_during_session
(an unclosed transaction) — are the normal mid-session state of a growing
live prefix and would ping on nearly every session before resolving, so
they stay in the failure panel but don't notify.

Declares the `notifications` permission (app.zon + the runtime permission
set); no notification is delivered without it.

Tests: a critical fault queues one notification (deduped per code);
non-critical severities don't notify; prefix-transient criticals don't
notify though they show as failures; a new session resets the dedup. 165/165
green; ReleaseFast build clean; app.zon valid.

Closes #60.
@sepehr-safari sepehr-safari merged commit 7d47fc0 into main Jul 12, 2026
3 checks passed
@sepehr-safari sepehr-safari deleted the feat/live-notifications branch July 12, 2026 15:29
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.

feat(capture): OS notifications on critical failures

1 participant