Skip to content
Gary Wu
Go back

Notification System Design Patterns

Edit page

Org Status: 🟒 Active Cloudflare: N/A Last Audited: 2026-04-28


Every notification system faces the same fundamental tension: sources want attention, users want peace. iOS solves this with interruption levels and Focus modes. Android solves it with notification channels and importance levels. Linux solves it with D-Bus urgency hints. Car dashboards solve it with NHTSA-compliant priority preemption. Smart displays solve it with proximity-aware rendering.

But what happens when you’re building a tiny ambient display β€” a macOS notch HUD where multiple AI agents, system monitors, and user scripts all compete for a single line of text? None of these systems map directly. You need to understand all of them, extract the patterns that work, and design something new.

What you’ll learn:


You have a programmable status bar embedded in a macOS notch. The display area is roughly 600 pixels wide and 28 pixels tall β€” enough for one line of text, maybe a small icon. Multiple sources want to use it:

These sources don’t coordinate with each other. They don’t know what else is trying to display. They each think their content is important. Without a policy layer, the display becomes a seizure-inducing mess of flickering text.

The existing approaches all fall short for this use case:

ApproachWhy it fails for a tiny HUD
iOS notificationsDesigned for a large screen with a notification drawer; assumes notifications stack vertically
Android channelsAssumes the user configures per-app settings through a full settings UI
Linux D-BusFire-and-forget; no concept of display ownership or preemption policy
Dashboard toolsDesigned for multi-widget layouts with ample screen space
Alert systems (PagerDuty)Designed for human escalation, not ambient display management

What changes if you get this right:


1. Interruption Levels

Every notification system, regardless of platform, implicitly or explicitly defines interruption levels. The concept determines whether a notification can break through the user’s current context.

/**
 * Universal interruption level taxonomy derived from iOS, Android,
 * and Linux notification systems.
 *
 * iOS: passive | active | timeSensitive | critical
 * Android: MIN | LOW | DEFAULT | HIGH (maps to importance)
 * Linux: low | normal | critical
 * Pushover: -2 | -1 | 0 | 1 | 2
 */
enum InterruptionLevel {
  /** Silent. No visual change. Logged only.
   *  iOS: passive. Android: IMPORTANCE_MIN. Linux: low. */
  SILENT = 0,

  /** Ambient. Updates display if nothing else is showing.
   *  iOS: passive. Android: IMPORTANCE_LOW. Linux: low. */
  AMBIENT = 1,

  /** Standard. Shown at next opportunity (after current content expires).
   *  iOS: active. Android: IMPORTANCE_DEFAULT. Linux: normal. */
  STANDARD = 2,

  /** Elevated. Preempts ambient/standard content within 2 seconds.
   *  iOS: active (with sound). Android: IMPORTANCE_HIGH. Linux: normal. */
  ELEVATED = 3,

  /** Urgent. Immediately preempts everything except critical.
   *  iOS: timeSensitive. Android: IMPORTANCE_HIGH (heads-up). Linux: critical. */
  URGENT = 4,

  /** Critical. Bypasses all filters, quiet hours, focus modes.
   *  iOS: critical (requires Apple entitlement). Android: foreground service.
   *  Linux: critical. Pushover: emergency (2). */
  CRITICAL = 5,
}

Key insight: iOS nailed the taxonomy with four levels, but for an ambient display you need six. The gap between β€œpassive” and β€œactive” is too wide β€” you need AMBIENT (update if idle) and STANDARD (show at next opportunity) as distinct behaviors.

2. Notification Channels

Android introduced notification channels in API 26 (Oreo) as a way to give users per-category control over notification behavior. The concept is powerful: instead of per-app settings, you have per-purpose settings.

/**
 * A channel defines a category of notifications with shared behavior.
 * Users can override the default importance of any channel.
 *
 * Android requires channels since API 26.
 * iOS achieves similar grouping via notification categories.
 * Our HUD uses channels to partition display time.
 */
interface NotificationChannel {
  /** Unique identifier. Once created, the ID cannot change. */
  id: string;

  /** Human-readable name shown in settings UI. */
  name: string;

  /** Description of what this channel carries. */
  description: string;

  /** Default interruption level for notifications in this channel. */
  defaultLevel: InterruptionLevel;

  /** User override. If set, takes precedence over defaultLevel. */
  userOverride?: InterruptionLevel;

  /** Whether the user has muted this channel entirely. */
  muted: boolean;

  /** Maximum notifications per minute from this channel. */
  rateLimit: number;

  /** How to handle multiple notifications: replace, queue, or summarize. */
  collapseStrategy: "replace" | "queue" | "summarize";

  /** Display duration in milliseconds. null = until displaced. */
  defaultTTL: number | null;

  /** Visual style hint. */
  style?: "text" | "metric" | "progress" | "alert";
}

Key insight: Android’s fatal design flaw is that channel importance cannot be changed programmatically after creation. This is a feature for user trust β€” it prevents apps from escalating their own importance. Our HUD should adopt this: sources declare channels, but only the user (or the policy engine) can change their effective importance.

3. Notification Identity and Lifecycle

Every notification needs a lifecycle β€” it’s created, displayed, potentially updated, and eventually expires or is dismissed. The freedesktop.org D-Bus specification handles this elegantly with replaces_id.

/**
 * A notification is a discrete unit of information with an identity
 * and a lifecycle.
 *
 * Mirrors the freedesktop.org Notify method signature:
 * UINT32 Notify(app_name, replaces_id, icon, summary, body, actions, hints, expire_timeout)
 *
 * And Firebase Cloud Messaging's collapse_key concept:
 * Messages with the same collapse_key replace each other.
 */
interface Notification {
  /** Unique ID assigned by the notification system. */
  id: string;

  /** Source identifier (app, agent, service). */
  source: string;

  /** Channel this notification belongs to. */
  channel: string;

  /** If set, this notification replaces an existing one with this ID.
   *  Equivalent to freedesktop replaces_id or FCM collapse_key. */
  replacesId?: string;

  /** The content to display. */
  content: NotificationContent;

  /** Interruption level. Defaults to channel's default if not set. */
  level?: InterruptionLevel;

  /** When this notification was created. */
  createdAt: number;

  /** When this notification expires. null = no expiry. */
  expiresAt: number | null;

  /** Current state in the lifecycle. */
  state: "pending" | "active" | "displayed" | "expired" | "dismissed";

  /** Number of times this notification has been updated via replacesId. */
  updateCount: number;

  /** Relevance score for summary ordering (0-1).
   *  iOS uses this for notification summary ranking. */
  relevanceScore?: number;
}

interface NotificationContent {
  /** Short text for the status bar (max ~60 chars). */
  summary: string;

  /** Longer text for hover/expanded view. */
  body?: string;

  /** Structured data for metric display. */
  metric?: { label: string; value: string; trend?: "up" | "down" | "flat" };

  /** Icon identifier or emoji. */
  icon?: string;

  /** Severity coloring. */
  severity?: "green" | "yellow" | "red";

  /** Actions the user can take (like iOS actionable notifications). */
  actions?: NotificationAction[];
}

interface NotificationAction {
  /** Unique action identifier. */
  id: string;

  /** Display label. */
  label: string;

  /** URL or callback identifier to invoke. */
  handler: string;

  /** Whether this action is destructive (shown in red). */
  destructive?: boolean;
}

Key insight: The replacesId pattern from freedesktop.org is essential for an ambient display. Without it, a CPU monitor sending updates every second would flood the queue. With it, each update replaces the previous one β€” the display always shows the latest value. FCM calls this β€œcollapsible messages” and limits you to 4 simultaneous collapse keys per device.

4. Focus Modes

iOS 15 introduced Focus modes as a generalization of Do Not Disturb. Android has a simpler DND with priority exceptions. Both recognize that notification filtering should be contextual β€” different rules for different situations.

/**
 * A focus mode defines a notification filtering policy that activates
 * based on time, location, or manual toggle.
 *
 * iOS Focus modes: Work, Personal, Sleep, Driving, custom
 * Android: Do Not Disturb with priority exceptions
 * Grafana: Mute timings (recurring) vs silences (one-time)
 */
interface FocusMode {
  /** Unique identifier. */
  id: string;

  /** Human-readable name. */
  name: string;

  /** Whether this focus mode is currently active. */
  active: boolean;

  /** Channels that are allowed through during this focus mode. */
  allowedChannels: string[];

  /** Sources that are allowed through regardless of channel. */
  allowedSources: string[];

  /** Minimum interruption level that can break through.
   *  Everything below this level is suppressed. */
  minimumLevel: InterruptionLevel;

  /** Schedule for automatic activation. */
  schedule?: FocusModeSchedule;

  /** What to show instead of suppressed notifications. */
  fallback: "nothing" | "count" | "summary";
}

interface FocusModeSchedule {
  /** Days of the week (0=Sunday). */
  days: number[];

  /** Start time in HH:MM format. */
  startTime: string;

  /** End time in HH:MM format. */
  endTime: string;

  /** Timezone. */
  timezone: string;
}

Key insight: Grafana’s distinction between β€œmute timings” (recurring, like weekends) and β€œsilences” (one-time, like a maintenance window) is more useful than iOS’s Focus model for a HUD. You want both: scheduled quiet hours AND the ability to say β€œmute everything for 30 minutes while I present.”


Pattern 1: Priority Queue with Preemption

The most fundamental pattern. Every notification enters a priority queue. The display always shows the highest-priority item. When a higher-priority notification arrives, it preempts what’s currently showing.

When to use it: As the base layer of any notification display system. Every other pattern builds on this.

/**
 * Priority queue with preemption for a single-line display.
 *
 * Implements the core display scheduling algorithm:
 * 1. Notifications enter the queue sorted by priority
 * 2. The display always shows the head of the queue
 * 3. Higher-priority items preempt lower-priority ones
 * 4. Items expire based on TTL and are removed
 * 5. When the current item expires, the next one shows
 *
 * This mirrors how car dashboards work: navigation directions
 * preempt music info, incoming calls preempt navigation,
 * collision warnings preempt everything.
 */
class NotificationPriorityQueue {
  private queue: QueueEntry[] = [];
  private currentDisplay: QueueEntry | null = null;
  private displayCallback: (entry: QueueEntry | null) => void;
  private expiryTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();

  constructor(onDisplayChange: (entry: QueueEntry | null) => void) {
    this.displayCallback = onDisplayChange;
  }

  /**
   * Submit a notification to the queue.
   * Returns the assigned queue position.
   */
  submit(notification: Notification): number {
    const entry: QueueEntry = {
      notification,
      effectivePriority: this.calculatePriority(notification),
      enqueuedAt: Date.now(),
      displayedAt: null,
    };

    // Check if this replaces an existing notification
    if (notification.replacesId) {
      const existingIndex = this.queue.findIndex(
        (e) => e.notification.id === notification.replacesId
      );
      if (existingIndex !== -1) {
        const existing = this.queue[existingIndex];
        entry.notification.updateCount = existing.notification.updateCount + 1;
        this.queue.splice(existingIndex, 1);
        this.clearExpiryTimer(notification.replacesId);
      }
    }

    // Insert in priority order (highest first)
    const insertIndex = this.queue.findIndex(
      (e) => e.effectivePriority < entry.effectivePriority
    );
    if (insertIndex === -1) {
      this.queue.push(entry);
    } else {
      this.queue.splice(insertIndex, 0, entry);
    }

    // Set expiry timer
    if (notification.expiresAt) {
      const delay = notification.expiresAt - Date.now();
      if (delay > 0) {
        const timer = setTimeout(() => {
          this.expire(notification.id);
        }, delay);
        this.expiryTimers.set(notification.id, timer);
      } else {
        // Already expired, don't even queue it
        return -1;
      }
    }

    // Check if this should preempt the current display
    this.evaluateDisplay();

    return this.queue.findIndex(
      (e) => e.notification.id === notification.id
    );
  }

  /**
   * Calculate effective priority from notification properties.
   *
   * Priority is a composite score:
   * - Base: interruption level (0-5) * 1000
   * - Boost: relevance score * 100
   * - Decay: time-based decay for aging notifications
   * - Escalation: +500 if notification has been waiting too long
   */
  private calculatePriority(notification: Notification): number {
    const level = notification.level ?? InterruptionLevel.STANDARD;
    let priority = level * 1000;

    // Relevance score boost (0-100 points)
    if (notification.relevanceScore !== undefined) {
      priority += notification.relevanceScore * 100;
    }

    return priority;
  }

  /**
   * Evaluate whether the current display should change.
   * Called after every queue mutation.
   */
  private evaluateDisplay(): void {
    if (this.queue.length === 0) {
      if (this.currentDisplay !== null) {
        this.currentDisplay = null;
        this.displayCallback(null);
      }
      return;
    }

    const topEntry = this.queue[0];

    // Nothing currently displayed β€” show the top item
    if (!this.currentDisplay) {
      this.showEntry(topEntry);
      return;
    }

    // Something is displayed β€” check if the top item should preempt it
    const currentPriority = this.currentDisplay.effectivePriority;
    const newPriority = topEntry.effectivePriority;

    if (topEntry.notification.id === this.currentDisplay.notification.id) {
      // Same notification (possibly updated) β€” refresh display
      this.showEntry(topEntry);
      return;
    }

    if (newPriority > currentPriority) {
      // Higher priority β€” preempt
      this.showEntry(topEntry);
      return;
    }

    // Lower or equal priority β€” wait for current to expire
  }

  private showEntry(entry: QueueEntry): void {
    entry.displayedAt = Date.now();
    entry.notification.state = "displayed";
    this.currentDisplay = entry;
    this.displayCallback(entry);
  }

  /**
   * Remove an expired notification from the queue.
   */
  private expire(notificationId: string): void {
    const index = this.queue.findIndex(
      (e) => e.notification.id === notificationId
    );
    if (index !== -1) {
      this.queue[index].notification.state = "expired";
      this.queue.splice(index, 1);
    }
    this.clearExpiryTimer(notificationId);

    // If the expired item was being displayed, show the next one
    if (this.currentDisplay?.notification.id === notificationId) {
      this.currentDisplay = null;
      this.evaluateDisplay();
    }
  }

  private clearExpiryTimer(notificationId: string): void {
    const timer = this.expiryTimers.get(notificationId);
    if (timer) {
      clearTimeout(timer);
      this.expiryTimers.delete(notificationId);
    }
  }

  /**
   * Dismiss a notification (user action).
   */
  dismiss(notificationId: string): void {
    const index = this.queue.findIndex(
      (e) => e.notification.id === notificationId
    );
    if (index !== -1) {
      this.queue[index].notification.state = "dismissed";
      this.queue.splice(index, 1);
    }
    this.clearExpiryTimer(notificationId);

    if (this.currentDisplay?.notification.id === notificationId) {
      this.currentDisplay = null;
      this.evaluateDisplay();
    }
  }

  /** Get queue state for debugging/UI. */
  getState(): { current: QueueEntry | null; queued: QueueEntry[] } {
    return {
      current: this.currentDisplay,
      queued: [...this.queue],
    };
  }
}

interface QueueEntry {
  notification: Notification;
  effectivePriority: number;
  enqueuedAt: number;
  displayedAt: number | null;
}

Gotchas:

Connection to other patterns: This is the foundation. Pattern 2 (Rate Limiting) prevents queue flooding. Pattern 3 (Time-Sharing) prevents starvation. Pattern 4 (Channel Policies) maps source behavior to queue priorities.


Pattern 2: Rate Limiting and Notification Budget

iOS throttles notifications from noisy apps. Android rate-limits notification updates. Firebase Cloud Messaging has a per-device notification budget. Pushover limits emergency retries. Every system needs rate limiting to prevent abuse.

When to use it: Always. Without rate limiting, a single misbehaving source can monopolize the display.

/**
 * Rate limiter for notification sources.
 *
 * Implements three strategies:
 * 1. Token bucket: allows bursts but limits sustained rate
 * 2. Sliding window: hard limit per time window
 * 3. Adaptive: adjusts limits based on user engagement
 *
 * iOS budget system: undocumented per-device budget, progressive
 * throttling for background execution.
 * Android: automatically groups when an app sends 4+ notifications.
 * FCM: can store max 4 collapsible messages per device per collapse key.
 * Pushover emergency: retry every 30+ seconds, expire after N seconds.
 */
class NotificationRateLimiter {
  private buckets: Map<string, TokenBucket> = new Map();
  private windowCounters: Map<string, SlidingWindowCounter> = new Map();
  private channelLimits: Map<string, ChannelRateConfig> = new Map();

  constructor(private defaultConfig: RateLimitConfig) {}

  /**
   * Register rate limit configuration for a channel.
   */
  configureChannel(channelId: string, config: ChannelRateConfig): void {
    this.channelLimits.set(channelId, config);
  }

  /**
   * Check if a notification from a source/channel is allowed.
   * Returns { allowed, retryAfterMs, reason }.
   */
  check(
    source: string,
    channel: string,
    level: InterruptionLevel
  ): RateLimitResult {
    // Critical notifications bypass rate limiting entirely
    // This mirrors iOS critical alerts that bypass all controls
    if (level >= InterruptionLevel.CRITICAL) {
      return { allowed: true };
    }

    const channelConfig = this.channelLimits.get(channel);
    const config = channelConfig ?? this.defaultConfig;

    // Check source-level rate limit (token bucket)
    const sourceKey = `${source}:${channel}`;
    let bucket = this.buckets.get(sourceKey);
    if (!bucket) {
      bucket = new TokenBucket(
        config.maxBurst,
        config.refillRate,
        config.refillIntervalMs
      );
      this.buckets.set(sourceKey, bucket);
    }

    if (!bucket.tryConsume()) {
      return {
        allowed: false,
        retryAfterMs: bucket.msUntilNextToken(),
        reason: `Source ${source} exceeded burst limit for channel ${channel}`,
      };
    }

    // Check global per-source limit (sliding window)
    let counter = this.windowCounters.get(source);
    if (!counter) {
      counter = new SlidingWindowCounter(
        config.windowMs,
        config.maxPerWindow
      );
      this.windowCounters.set(source, counter);
    }

    if (!counter.tryIncrement()) {
      return {
        allowed: false,
        retryAfterMs: counter.msUntilWindowSlides(),
        reason: `Source ${source} exceeded ${config.maxPerWindow} notifications per ${config.windowMs}ms`,
      };
    }

    return { allowed: true };
  }
}

/**
 * Token bucket algorithm for burst-tolerant rate limiting.
 * Allows short bursts while limiting sustained throughput.
 */
class TokenBucket {
  private tokens: number;
  private lastRefill: number;

  constructor(
    private maxTokens: number,
    private refillRate: number,
    private refillIntervalMs: number
  ) {
    this.tokens = maxTokens;
    this.lastRefill = Date.now();
  }

  tryConsume(): boolean {
    this.refill();
    if (this.tokens >= 1) {
      this.tokens -= 1;
      return true;
    }
    return false;
  }

  msUntilNextToken(): number {
    this.refill();
    if (this.tokens >= 1) return 0;
    return this.refillIntervalMs;
  }

  private refill(): void {
    const now = Date.now();
    const elapsed = now - this.lastRefill;
    const tokensToAdd = Math.floor(
      (elapsed / this.refillIntervalMs) * this.refillRate
    );
    if (tokensToAdd > 0) {
      this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
      this.lastRefill = now;
    }
  }
}

/**
 * Sliding window counter for hard rate limits.
 */
class SlidingWindowCounter {
  private timestamps: number[] = [];

  constructor(
    private windowMs: number,
    private maxCount: number
  ) {}

  tryIncrement(): boolean {
    const now = Date.now();
    const windowStart = now - this.windowMs;

    // Remove expired timestamps
    this.timestamps = this.timestamps.filter((t) => t > windowStart);

    if (this.timestamps.length >= this.maxCount) {
      return false;
    }

    this.timestamps.push(now);
    return true;
  }

  msUntilWindowSlides(): number {
    if (this.timestamps.length === 0) return 0;
    const oldest = this.timestamps[0];
    return Math.max(0, oldest + this.windowMs - Date.now());
  }
}

interface RateLimitConfig {
  /** Maximum burst size (tokens). */
  maxBurst: number;
  /** Tokens refilled per interval. */
  refillRate: number;
  /** Interval for token refill in ms. */
  refillIntervalMs: number;
  /** Sliding window size in ms. */
  windowMs: number;
  /** Max notifications per window. */
  maxPerWindow: number;
}

interface ChannelRateConfig extends RateLimitConfig {
  /** Whether to auto-summarize when rate-limited. */
  summarizeOnLimit: boolean;
}

interface RateLimitResult {
  allowed: boolean;
  retryAfterMs?: number;
  reason?: string;
}

Gotchas:


Pattern 3: Time-Sharing Rotation

When multiple notifications have the same priority and all deserve display time, rotate between them. This is how smartwatch complications work β€” each complication gets a slot on the face, and the watch rotates through them when the user taps.

When to use it: When the display is idle (no urgent content) and multiple ambient sources want visibility.

/**
 * Time-sharing rotation for ambient content.
 *
 * When the display has no urgent notifications, it rotates
 * through ambient content sources on a configurable interval.
 *
 * Inspired by:
 * - Apple Watch complications: each gets a fixed position,
 *   data rotates on interaction
 * - Google Nest Hub: ambient mode cycles through photos,
 *   weather, calendar
 * - Car instrument clusters: info display cycles between
 *   odometer, range, average MPG
 * - RSVP reading: words rotate at a fixed interval
 */
class TimeShareRotation {
  private sources: RotationSource[] = [];
  private currentIndex: number = 0;
  private timer: ReturnType<typeof setInterval> | null = null;
  private paused: boolean = false;
  private displayCallback: (content: NotificationContent | null) => void;

  constructor(
    private intervalMs: number,
    onContentChange: (content: NotificationContent | null) => void
  ) {
    this.displayCallback = onContentChange;
  }

  /**
   * Register a content source for rotation.
   * The source provides a function that returns current content.
   */
  addSource(source: RotationSource): void {
    this.sources.push(source);
    if (this.sources.length === 1 && !this.paused) {
      this.start();
    }
  }

  removeSource(sourceId: string): void {
    this.sources = this.sources.filter((s) => s.id !== sourceId);
    if (this.currentIndex >= this.sources.length) {
      this.currentIndex = 0;
    }
    if (this.sources.length === 0) {
      this.stop();
    }
  }

  /**
   * Start rotation. Called when the priority queue is empty
   * and the display enters ambient mode.
   */
  start(): void {
    if (this.timer) return;
    this.paused = false;

    // Show first source immediately
    this.showCurrent();

    this.timer = setInterval(() => {
      this.advance();
    }, this.intervalMs);
  }

  /**
   * Pause rotation. Called when a priority notification
   * preempts the rotation.
   */
  pause(): void {
    this.paused = true;
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }

  /**
   * Resume rotation from where it left off.
   */
  resume(): void {
    if (!this.paused) return;
    this.paused = false;
    this.start();
  }

  private advance(): void {
    if (this.sources.length === 0) return;

    // Find the next source with content
    let attempts = 0;
    do {
      this.currentIndex = (this.currentIndex + 1) % this.sources.length;
      attempts++;
    } while (
      !this.sources[this.currentIndex].hasContent() &&
      attempts < this.sources.length
    );

    this.showCurrent();
  }

  private showCurrent(): void {
    if (this.sources.length === 0) {
      this.displayCallback(null);
      return;
    }

    const source = this.sources[this.currentIndex];
    const content = source.getContent();
    this.displayCallback(content);
  }

  /** Update rotation interval dynamically. */
  setInterval(ms: number): void {
    this.intervalMs = ms;
    if (this.timer) {
      this.stop();
      this.start();
    }
  }
}

interface RotationSource {
  /** Unique identifier for this source. */
  id: string;

  /** Human-readable name. */
  name: string;

  /** Weight for weighted rotation (higher = more frequent). */
  weight: number;

  /** Whether this source currently has content to display. */
  hasContent(): boolean;

  /** Get the current content to display. */
  getContent(): NotificationContent;
}

Gotchas:


Pattern 4: Channel Policy Engine

This is where it all comes together. A policy engine that evaluates incoming notifications against channels, focus modes, rate limits, and the current display state to make a decision: display, queue, suppress, or summarize.

When to use it: This is the brain of the notification system. Every notification passes through it.

/**
 * The policy engine evaluates every incoming notification and decides
 * what to do with it.
 *
 * Decision flow:
 * 1. Validate the notification (required fields, known channel)
 * 2. Check rate limits
 * 3. Check focus mode filters
 * 4. Apply channel policies (collapse, summarize)
 * 5. Calculate effective priority
 * 6. Submit to the priority queue or rotation
 *
 * This mirrors the layered policy approach used by:
 * - iOS: interruption level β†’ Focus mode β†’ notification summary
 * - Grafana: alert rules β†’ notification policies β†’ silences β†’ mute timings
 * - PagerDuty: event rules β†’ services β†’ escalation policies β†’ user notification rules
 */
class PolicyEngine {
  private channels: Map<string, NotificationChannel> = new Map();
  private focusModes: Map<string, FocusMode> = new Map();
  private rateLimiter: NotificationRateLimiter;
  private queue: NotificationPriorityQueue;
  private rotation: TimeShareRotation;
  private history: NotificationHistoryLog;
  private suppressionRules: SuppressionRule[] = [];

  constructor(config: PolicyEngineConfig) {
    this.rateLimiter = new NotificationRateLimiter(config.defaultRateLimit);
    this.queue = new NotificationPriorityQueue((entry) => {
      if (entry) {
        // Priority notification: pause rotation and show it
        this.rotation.pause();
        this.onDisplay(entry);
      } else {
        // Queue empty: resume rotation
        this.rotation.resume();
      }
    });
    this.rotation = new TimeShareRotation(
      config.rotationIntervalMs,
      (content) => {
        this.onRotationDisplay(content);
      }
    );
    this.history = new NotificationHistoryLog(config.historySize);
  }

  /**
   * Register a notification channel.
   */
  registerChannel(channel: NotificationChannel): void {
    this.channels.set(channel.id, channel);
    this.rateLimiter.configureChannel(channel.id, {
      maxBurst: channel.rateLimit,
      refillRate: 1,
      refillIntervalMs: 60_000 / channel.rateLimit,
      windowMs: 60_000,
      maxPerWindow: channel.rateLimit,
      summarizeOnLimit: channel.collapseStrategy === "summarize",
    });
  }

  /**
   * Set a focus mode. Only one can be active at a time.
   */
  activateFocusMode(modeId: string): void {
    // Deactivate all others
    for (const mode of this.focusModes.values()) {
      mode.active = false;
    }
    const mode = this.focusModes.get(modeId);
    if (mode) {
      mode.active = true;
    }
  }

  /**
   * Process an incoming notification.
   * Returns the decision made by the policy engine.
   */
  process(notification: Notification): PolicyDecision {
    // Step 1: Validate
    const channel = this.channels.get(notification.channel);
    if (!channel) {
      return {
        action: "reject",
        reason: `Unknown channel: ${notification.channel}`,
      };
    }

    // Apply channel defaults
    if (notification.level === undefined) {
      notification.level = channel.userOverride ?? channel.defaultLevel;
    }

    // Step 2: Check suppression rules
    for (const rule of this.suppressionRules) {
      if (rule.matches(notification)) {
        this.history.log(notification, "suppressed", rule.reason);
        return {
          action: "suppress",
          reason: rule.reason,
        };
      }
    }

    // Step 3: Check if channel is muted
    if (
      channel.muted &&
      notification.level! < InterruptionLevel.CRITICAL
    ) {
      this.history.log(notification, "muted", "Channel is muted");
      return {
        action: "suppress",
        reason: `Channel ${channel.id} is muted`,
      };
    }

    // Step 4: Check focus mode
    const activeFocus = this.getActiveFocusMode();
    if (activeFocus) {
      const allowed = this.checkFocusMode(activeFocus, notification);
      if (!allowed) {
        this.history.log(
          notification,
          "filtered",
          `Blocked by focus mode: ${activeFocus.name}`
        );

        // Still log for summary display if configured
        if (activeFocus.fallback === "summary") {
          return {
            action: "defer",
            reason: `Deferred by focus mode: ${activeFocus.name}`,
          };
        }

        return {
          action: "suppress",
          reason: `Blocked by focus mode: ${activeFocus.name}`,
        };
      }
    }

    // Step 5: Check rate limits
    const rateResult = this.rateLimiter.check(
      notification.source,
      notification.channel,
      notification.level!
    );
    if (!rateResult.allowed) {
      this.history.log(
        notification,
        "rate-limited",
        rateResult.reason!
      );
      return {
        action: "rate-limited",
        reason: rateResult.reason!,
        retryAfterMs: rateResult.retryAfterMs,
      };
    }

    // Step 6: Apply collapse strategy
    if (channel.collapseStrategy === "replace" && notification.replacesId) {
      // The queue handles replacement via replacesId
    } else if (channel.collapseStrategy === "summarize") {
      // Check if we should summarize instead of showing individually
      const pendingCount = this.countPendingForChannel(
        notification.channel
      );
      if (pendingCount >= 3) {
        return this.summarize(notification);
      }
    }

    // Step 7: Set TTL from channel default if not specified
    if (notification.expiresAt === null && channel.defaultTTL !== null) {
      notification.expiresAt = Date.now() + channel.defaultTTL;
    }

    // Step 8: Submit to priority queue
    const position = this.queue.submit(notification);
    this.history.log(notification, "queued", `Position: ${position}`);

    return {
      action: "queued",
      position,
    };
  }

  private checkFocusMode(
    mode: FocusMode,
    notification: Notification
  ): boolean {
    // Critical always breaks through (iOS behavior)
    if (notification.level! >= InterruptionLevel.CRITICAL) {
      return true;
    }

    // Check minimum level
    if (notification.level! < mode.minimumLevel) {
      return false;
    }

    // Check allowed channels
    if (mode.allowedChannels.includes(notification.channel)) {
      return true;
    }

    // Check allowed sources
    if (mode.allowedSources.includes(notification.source)) {
      return true;
    }

    return false;
  }

  private getActiveFocusMode(): FocusMode | null {
    for (const mode of this.focusModes.values()) {
      if (mode.active) return mode;
    }

    // Check scheduled modes
    const now = new Date();
    for (const mode of this.focusModes.values()) {
      if (mode.schedule && this.isScheduleActive(mode.schedule, now)) {
        mode.active = true;
        return mode;
      }
    }

    return null;
  }

  private isScheduleActive(
    schedule: FocusModeSchedule,
    now: Date
  ): boolean {
    const day = now.getDay();
    if (!schedule.days.includes(day)) return false;

    const timeStr = now.toLocaleTimeString("en-US", {
      hour12: false,
      hour: "2-digit",
      minute: "2-digit",
      timeZone: schedule.timezone,
    });

    return timeStr >= schedule.startTime && timeStr <= schedule.endTime;
  }

  private countPendingForChannel(channelId: string): number {
    return this.queue
      .getState()
      .queued.filter(
        (e) => e.notification.channel === channelId
      ).length;
  }

  private summarize(notification: Notification): PolicyDecision {
    // Aggregate pending notifications for this channel into a summary
    const pending = this.queue
      .getState()
      .queued.filter(
        (e) => e.notification.channel === notification.channel
      );

    const summaryContent: NotificationContent = {
      summary: `${pending.length + 1} ${notification.channel} notifications`,
      body: pending
        .map((e) => e.notification.content.summary)
        .concat(notification.content.summary)
        .join("\n"),
      icon: "stack",
    };

    // Replace all pending with a single summary
    for (const entry of pending) {
      this.queue.dismiss(entry.notification.id);
    }

    const summaryNotification: Notification = {
      ...notification,
      id: `summary:${notification.channel}:${Date.now()}`,
      content: summaryContent,
      relevanceScore: 0.5,
    };

    this.queue.submit(summaryNotification);

    return {
      action: "summarized",
      reason: `Aggregated with ${pending.length} pending notifications`,
    };
  }

  private onDisplay(entry: QueueEntry): void {
    // Hook for external display rendering
  }

  private onRotationDisplay(content: NotificationContent | null): void {
    // Hook for ambient rotation rendering
  }
}

interface PolicyDecision {
  action:
    | "queued"
    | "reject"
    | "suppress"
    | "rate-limited"
    | "defer"
    | "summarized";
  reason?: string;
  position?: number;
  retryAfterMs?: number;
}

interface PolicyEngineConfig {
  defaultRateLimit: RateLimitConfig;
  rotationIntervalMs: number;
  historySize: number;
}

interface SuppressionRule {
  reason: string;
  matches(notification: Notification): boolean;
}

Gotchas:


Pattern 5: RSVP Interruption Protocol

Rapid Serial Visual Presentation (RSVP) displays words one at a time at high speed. This is a special case because the display is β€œoccupied” β€” showing content that the user is actively reading. Interrupting RSVP is like interrupting someone mid-sentence.

When to use it: When the HUD has a speed-reading mode that cycles through text word-by-word.

/**
 * RSVP-aware notification delivery.
 *
 * When the display is in RSVP mode (showing words at 300+ WPM),
 * interruptions must be handled carefully:
 *
 * - SILENT/AMBIENT: queue silently, show after RSVP completes
 * - STANDARD: queue, show subtle indicator (dot color change)
 * - ELEVATED: pause RSVP, show notification, resume RSVP
 * - URGENT: immediately stop RSVP, show notification
 * - CRITICAL: immediately stop RSVP, show notification, cannot dismiss
 *
 * This mirrors how car dashboards handle navigation + calls:
 * - Music info: hidden during turn-by-turn
 * - Phone call: pauses navigation, shows caller
 * - Collision warning: overrides everything, cannot be dismissed
 */
class RSVPInterruptionHandler {
  private rsvpState: RSVPState | null = null;
  private pendingDuringRSVP: Notification[] = [];

  constructor(
    private policyEngine: PolicyEngine,
    private displayController: DisplayController
  ) {}

  /**
   * Called when RSVP mode starts.
   */
  startRSVP(text: string, wpm: number): void {
    this.rsvpState = {
      text,
      words: text.split(/\s+/),
      currentWordIndex: 0,
      wpm,
      startedAt: Date.now(),
      pausedAt: null,
      intervalMs: 60_000 / wpm,
    };
    this.pendingDuringRSVP = [];
    this.displayController.setMode("rsvp");
    this.advanceRSVP();
  }

  /**
   * Handle a notification arriving during RSVP.
   */
  handleDuringRSVP(notification: Notification): RSVPInterruptAction {
    if (!this.rsvpState) {
      // Not in RSVP mode β€” process normally
      return { action: "normal" };
    }

    const level = notification.level ?? InterruptionLevel.STANDARD;

    switch (level) {
      case InterruptionLevel.SILENT:
      case InterruptionLevel.AMBIENT:
        // Queue silently β€” don't disturb the reader
        this.pendingDuringRSVP.push(notification);
        return {
          action: "queued-silent",
          resumeAfterRSVP: true,
        };

      case InterruptionLevel.STANDARD:
        // Queue but show subtle indicator
        this.pendingDuringRSVP.push(notification);
        this.displayController.showIndicator("pending", {
          count: this.pendingDuringRSVP.length,
        });
        return {
          action: "queued-indicated",
          resumeAfterRSVP: true,
        };

      case InterruptionLevel.ELEVATED:
        // Pause RSVP, show notification, then resume
        this.pauseRSVP();
        this.displayController.showNotification(notification);

        // Auto-resume after notification TTL or 5 seconds
        const resumeDelay = notification.expiresAt
          ? notification.expiresAt - Date.now()
          : 5_000;

        setTimeout(() => {
          this.resumeRSVP();
        }, resumeDelay);

        return {
          action: "pause-show-resume",
          resumeAfterMs: resumeDelay,
        };

      case InterruptionLevel.URGENT:
      case InterruptionLevel.CRITICAL:
        // Stop RSVP entirely
        this.stopRSVP();
        this.displayController.showNotification(notification);
        return {
          action: "stop-rsvp",
          rsvpCancelled: true,
        };

      default:
        return { action: "normal" };
    }
  }

  private pauseRSVP(): void {
    if (!this.rsvpState) return;
    this.rsvpState.pausedAt = Date.now();
    this.displayController.setMode("notification");
  }

  private resumeRSVP(): void {
    if (!this.rsvpState || !this.rsvpState.pausedAt) return;
    this.rsvpState.pausedAt = null;
    this.displayController.setMode("rsvp");
    this.advanceRSVP();
  }

  private stopRSVP(): void {
    this.rsvpState = null;

    // Process any notifications that accumulated during RSVP
    for (const pending of this.pendingDuringRSVP) {
      this.policyEngine.process(pending);
    }
    this.pendingDuringRSVP = [];
  }

  private advanceRSVP(): void {
    if (!this.rsvpState || this.rsvpState.pausedAt) return;

    const state = this.rsvpState;
    if (state.currentWordIndex >= state.words.length) {
      // RSVP complete
      this.stopRSVP();
      return;
    }

    const word = state.words[state.currentWordIndex];
    this.displayController.showWord(word, state.currentWordIndex, state.words.length);
    state.currentWordIndex++;

    setTimeout(() => {
      this.advanceRSVP();
    }, state.intervalMs);
  }
}

interface RSVPState {
  text: string;
  words: string[];
  currentWordIndex: number;
  wpm: number;
  startedAt: number;
  pausedAt: number | null;
  intervalMs: number;
}

interface RSVPInterruptAction {
  action:
    | "normal"
    | "queued-silent"
    | "queued-indicated"
    | "pause-show-resume"
    | "stop-rsvp";
  resumeAfterRSVP?: boolean;
  resumeAfterMs?: number;
  rsvpCancelled?: boolean;
}

interface DisplayController {
  setMode(mode: "rsvp" | "notification" | "ambient" | "idle"): void;
  showWord(word: string, index: number, total: number): void;
  showNotification(notification: Notification): void;
  showIndicator(
    type: string,
    data: Record<string, unknown>
  ): void;
}

Gotchas:


Pattern 6: Escalation and Acknowledgment

Some notifications become more important over time. PagerDuty’s entire business model is built on escalation: if the first responder doesn’t acknowledge in N minutes, escalate to the next level. Pushover’s emergency priority retries every 30+ seconds until acknowledged.

When to use it: For notifications that require human awareness β€” not just display, but confirmation that the human saw it.

/**
 * Escalation engine for notifications that require acknowledgment.
 *
 * Implements PagerDuty-style escalation:
 * 1. Show notification at initial level
 * 2. If not acknowledged within timeout, escalate to next level
 * 3. Repeat until acknowledged or escalation chain exhausted
 * 4. If chain exhausted, mark as unacknowledged and log
 *
 * Also implements Pushover-style retry:
 * - Retry display every N seconds
 * - Expire after M seconds
 * - Cancel retries on acknowledgment
 *
 * Grafana uses similar concepts: group_wait, group_interval, repeat_interval.
 */
class EscalationEngine {
  private escalations: Map<string, EscalationState> = new Map();
  private policyEngine: PolicyEngine;

  constructor(policyEngine: PolicyEngine) {
    this.policyEngine = policyEngine;
  }

  /**
   * Submit a notification with an escalation policy.
   */
  submitWithEscalation(
    notification: Notification,
    policy: EscalationPolicy
  ): void {
    const state: EscalationState = {
      notification,
      policy,
      currentStep: 0,
      startedAt: Date.now(),
      acknowledged: false,
      retryCount: 0,
      timers: [],
    };

    this.escalations.set(notification.id, state);

    // Submit at initial level
    this.policyEngine.process(notification);

    // Set up escalation timer
    this.scheduleEscalation(state);

    // Set up retry timer if configured
    if (policy.retryIntervalMs) {
      this.scheduleRetry(state);
    }

    // Set up expiry timer
    if (policy.expireAfterMs) {
      const expiryTimer = setTimeout(() => {
        this.expire(notification.id);
      }, policy.expireAfterMs);
      state.timers.push(expiryTimer);
    }
  }

  /**
   * Acknowledge a notification, stopping escalation and retries.
   */
  acknowledge(notificationId: string): AcknowledgmentResult {
    const state = this.escalations.get(notificationId);
    if (!state) {
      return { success: false, reason: "Notification not found" };
    }

    state.acknowledged = true;

    // Clear all timers
    for (const timer of state.timers) {
      clearTimeout(timer);
    }
    state.timers = [];

    const elapsed = Date.now() - state.startedAt;

    return {
      success: true,
      acknowledgedAfterMs: elapsed,
      escalationLevel: state.currentStep,
      retryCount: state.retryCount,
    };
  }

  private scheduleEscalation(state: EscalationState): void {
    const step = state.policy.steps[state.currentStep];
    if (!step) return;

    const timer = setTimeout(() => {
      if (state.acknowledged) return;

      // Move to next escalation step
      state.currentStep++;
      const nextStep = state.policy.steps[state.currentStep];

      if (nextStep) {
        // Re-submit at higher level
        const escalated: Notification = {
          ...state.notification,
          level: nextStep.level,
          content: {
            ...state.notification.content,
            summary: `[ESCALATED] ${state.notification.content.summary}`,
          },
          replacesId: state.notification.id,
        };
        this.policyEngine.process(escalated);

        // Execute escalation action (e.g., send to additional channels)
        if (nextStep.action) {
          nextStep.action(state.notification);
        }

        // Schedule next escalation
        this.scheduleEscalation(state);
      } else {
        // Escalation chain exhausted
        this.onEscalationExhausted(state);
      }
    }, step.timeoutMs);

    state.timers.push(timer);
  }

  private scheduleRetry(state: EscalationState): void {
    if (!state.policy.retryIntervalMs) return;

    const timer = setTimeout(() => {
      if (state.acknowledged) return;

      state.retryCount++;

      // Re-display the notification
      const retry: Notification = {
        ...state.notification,
        id: `${state.notification.id}:retry:${state.retryCount}`,
        replacesId: state.notification.id,
        content: {
          ...state.notification.content,
          summary: `[${state.retryCount}x] ${state.notification.content.summary}`,
        },
      };
      this.policyEngine.process(retry);

      // Schedule next retry
      this.scheduleRetry(state);
    }, state.policy.retryIntervalMs);

    state.timers.push(timer);
  }

  private expire(notificationId: string): void {
    const state = this.escalations.get(notificationId);
    if (!state || state.acknowledged) return;

    // Clear all timers
    for (const timer of state.timers) {
      clearTimeout(timer);
    }

    this.escalations.delete(notificationId);
  }

  private onEscalationExhausted(state: EscalationState): void {
    // All escalation steps exhausted without acknowledgment
    // This is the "nobody is responding" case
    const finalNotification: Notification = {
      ...state.notification,
      level: InterruptionLevel.CRITICAL,
      content: {
        ...state.notification.content,
        summary: `[UNACK] ${state.notification.content.summary}`,
        severity: "red",
      },
      replacesId: state.notification.id,
    };
    this.policyEngine.process(finalNotification);
  }
}

interface EscalationPolicy {
  /** Ordered escalation steps. */
  steps: EscalationStep[];

  /** Retry interval in ms (Pushover-style). Min 30000. */
  retryIntervalMs?: number;

  /** Total time before escalation expires in ms. */
  expireAfterMs?: number;
}

interface EscalationStep {
  /** Interruption level at this step. */
  level: InterruptionLevel;

  /** Time to wait before escalating to next step (ms). */
  timeoutMs: number;

  /** Optional action to execute at this step. */
  action?: (notification: Notification) => void;
}

interface EscalationState {
  notification: Notification;
  policy: EscalationPolicy;
  currentStep: number;
  startedAt: number;
  acknowledged: boolean;
  retryCount: number;
  timers: ReturnType<typeof setTimeout>[];
}

interface AcknowledgmentResult {
  success: boolean;
  reason?: string;
  acknowledgedAfterMs?: number;
  escalationLevel?: number;
  retryCount?: number;
}

Example 1: Registering Channels for a HUD

const engine = new PolicyEngine({
  defaultRateLimit: {
    maxBurst: 5,
    refillRate: 1,
    refillIntervalMs: 10_000,
    windowMs: 60_000,
    maxPerWindow: 30,
  },
  rotationIntervalMs: 8_000,
  historySize: 500,
});

// AI agent status channel β€” replace old status with new
engine.registerChannel({
  id: "agent-status",
  name: "Agent Status",
  description: "Athena and Jane operational status",
  defaultLevel: InterruptionLevel.AMBIENT,
  muted: false,
  rateLimit: 12, // max 12/minute (one every 5 seconds)
  collapseStrategy: "replace",
  defaultTTL: 30_000, // 30 seconds
  style: "metric",
});

// System alerts β€” queue multiple, don't replace
engine.registerChannel({
  id: "system-alerts",
  name: "System Alerts",
  description: "CPU, memory, disk, network anomalies",
  defaultLevel: InterruptionLevel.ELEVATED,
  muted: false,
  rateLimit: 6,
  collapseStrategy: "summarize",
  defaultTTL: 60_000,
  style: "alert",
});

// User timers β€” high priority, replace on update
engine.registerChannel({
  id: "timers",
  name: "Timers & Reminders",
  description: "User-set timers, calendar reminders",
  defaultLevel: InterruptionLevel.URGENT,
  muted: false,
  rateLimit: 10,
  collapseStrategy: "replace",
  defaultTTL: null, // persist until dismissed
  style: "text",
});

// Weather β€” ambient, very low rate
engine.registerChannel({
  id: "weather",
  name: "Weather",
  description: "Current conditions and alerts",
  defaultLevel: InterruptionLevel.SILENT,
  muted: false,
  rateLimit: 2, // max 2/minute
  collapseStrategy: "replace",
  defaultTTL: 300_000, // 5 minutes
  style: "metric",
});

Example 2: Sending a Notification from an AI Agent

function agentStatusUpdate(
  agentName: string,
  status: string,
  severity: "green" | "yellow" | "red"
): Notification {
  const level =
    severity === "red"
      ? InterruptionLevel.URGENT
      : severity === "yellow"
        ? InterruptionLevel.STANDARD
        : InterruptionLevel.AMBIENT;

  return {
    id: `agent:${agentName}:${Date.now()}`,
    source: agentName,
    channel: "agent-status",
    replacesId: `agent:${agentName}:latest`,
    content: {
      summary: `${agentName}: ${status}`,
      severity,
      icon: agentName === "athena" ? "owl" : "sparkle",
      metric: severity === "yellow" || severity === "red"
        ? { label: agentName, value: status, trend: "down" }
        : undefined,
    },
    level,
    createdAt: Date.now(),
    expiresAt: Date.now() + 30_000,
    state: "pending",
    updateCount: 0,
    relevanceScore: severity === "red" ? 1.0 : severity === "yellow" ? 0.7 : 0.3,
  };
}

// Normal operation
engine.process(agentStatusUpdate("athena", "nominal", "green"));

// Something unusual
engine.process(agentStatusUpdate("athena", "traffic spike 3x", "yellow"));

// System down
engine.process(agentStatusUpdate("athena", "UNREACHABLE", "red"));

Example 3: Focus Mode for Deep Work

const deepWorkMode: FocusMode = {
  id: "deep-work",
  name: "Deep Work",
  active: false,
  // Only agent alerts and timers break through
  allowedChannels: ["timers"],
  // Only Athena (the system) can interrupt, not individual agents
  allowedSources: ["athena"],
  // Only URGENT and above can break through
  minimumLevel: InterruptionLevel.URGENT,
  schedule: {
    days: [1, 2, 3, 4, 5], // weekdays
    startTime: "09:00",
    endTime: "12:00",
    timezone: "America/Chicago",
  },
  fallback: "summary", // show count of suppressed notifications
};

// Manual activation
engine.activateFocusMode("deep-work");

Example 4: System Monitor as a Rotation Source

class CPUMonitorSource implements RotationSource {
  id = "cpu-monitor";
  name = "CPU Usage";
  weight = 1;
  private lastReading: number = 0;

  hasContent(): boolean {
    return true; // always has something to show
  }

  getContent(): NotificationContent {
    const usage = this.lastReading;
    const trend =
      usage > 80 ? "up" : usage < 20 ? "down" : ("flat" as const);

    return {
      summary: `CPU ${usage}%`,
      metric: {
        label: "CPU",
        value: `${usage}%`,
        trend,
      },
      severity: usage > 90 ? "red" : usage > 70 ? "yellow" : "green",
      icon: "cpu",
    };
  }

  update(usage: number): void {
    this.lastReading = usage;

    // If CPU is critically high, send as a priority notification
    // instead of waiting for rotation
    if (usage > 95) {
      // This would be submitted to the policy engine directly
    }
  }
}

class MemoryMonitorSource implements RotationSource {
  id = "memory-monitor";
  name = "Memory Usage";
  weight = 1;
  private usedGB: number = 0;
  private totalGB: number = 16;

  hasContent(): boolean {
    return true;
  }

  getContent(): NotificationContent {
    const pct = Math.round((this.usedGB / this.totalGB) * 100);
    return {
      summary: `RAM ${this.usedGB.toFixed(1)}/${this.totalGB}GB`,
      metric: {
        label: "RAM",
        value: `${pct}%`,
        trend: pct > 80 ? "up" : "flat",
      },
      severity: pct > 90 ? "red" : pct > 75 ? "yellow" : "green",
      icon: "memory",
    };
  }

  update(usedGB: number): void {
    this.usedGB = usedGB;
  }
}

Example 5: Quiet Hours with Grafana-Style Mute Timing

/**
 * Mute timing: suppress notifications during configured periods.
 * Modeled after Grafana's mute timings, which are distinct from silences.
 *
 * Mute timing = recurring (every weeknight, every weekend)
 * Silence = one-time (mute for next 2 hours during this deploy)
 */
class MuteTimingManager {
  private muteTimings: MuteTiming[] = [];
  private silences: Silence[] = [];

  addMuteTiming(timing: MuteTiming): void {
    this.muteTimings.push(timing);
  }

  addSilence(silence: Silence): void {
    this.silences.push(silence);
  }

  isCurrentlyMuted(channelId?: string, source?: string): MuteResult {
    const now = new Date();

    // Check one-time silences first
    for (const silence of this.silences) {
      if (now >= silence.startsAt && now <= silence.endsAt) {
        if (!silence.channelFilter || silence.channelFilter.includes(channelId ?? "")) {
          return { muted: true, reason: silence.reason, type: "silence" };
        }
      }
    }

    // Check recurring mute timings
    for (const timing of this.muteTimings) {
      if (this.isTimingActive(timing, now)) {
        return { muted: true, reason: timing.name, type: "mute-timing" };
      }
    }

    return { muted: false };
  }

  private isTimingActive(timing: MuteTiming, now: Date): boolean {
    const day = now.getDay();
    if (!timing.days.includes(day)) return false;

    const hour = now.getHours();
    const minute = now.getMinutes();
    const currentMinutes = hour * 60 + minute;

    const [startH, startM] = timing.startTime.split(":").map(Number);
    const [endH, endM] = timing.endTime.split(":").map(Number);
    const startMinutes = startH * 60 + startM;
    const endMinutes = endH * 60 + endM;

    // Handle overnight ranges (e.g., 22:00 - 07:00)
    if (endMinutes < startMinutes) {
      return currentMinutes >= startMinutes || currentMinutes <= endMinutes;
    }

    return currentMinutes >= startMinutes && currentMinutes <= endMinutes;
  }
}

interface MuteTiming {
  name: string;
  days: number[];
  startTime: string; // HH:MM
  endTime: string;   // HH:MM
}

interface Silence {
  reason: string;
  startsAt: Date;
  endsAt: Date;
  channelFilter?: string[];
}

interface MuteResult {
  muted: boolean;
  reason?: string;
  type?: "silence" | "mute-timing";
}

// Usage
const muteManager = new MuteTimingManager();

// Quiet hours every night
muteManager.addMuteTiming({
  name: "Sleep hours",
  days: [0, 1, 2, 3, 4, 5, 6],
  startTime: "22:00",
  endTime: "07:00",
});

// Weekend mornings are quieter
muteManager.addMuteTiming({
  name: "Weekend mornings",
  days: [0, 6],
  startTime: "07:00",
  endTime: "10:00",
});

// One-time silence for a presentation
muteManager.addSilence({
  reason: "Presenting to client",
  startsAt: new Date("2026-03-28T14:00:00"),
  endsAt: new Date("2026-03-28T15:30:00"),
});

Example 6: Notification History Log

/**
 * Ring buffer log of all notification decisions.
 * Critical for debugging "why didn't I see that notification?"
 *
 * Every notification system needs this β€” iOS has Notification Center,
 * Android has the notification log (Settings > Notifications > History),
 * dunst has notification history accessible via keyboard shortcut.
 */
class NotificationHistoryLog {
  private entries: HistoryEntry[] = [];
  private maxSize: number;

  constructor(maxSize: number = 500) {
    this.maxSize = maxSize;
  }

  log(
    notification: Notification,
    decision: string,
    reason: string
  ): void {
    const entry: HistoryEntry = {
      timestamp: Date.now(),
      notificationId: notification.id,
      source: notification.source,
      channel: notification.channel,
      level: notification.level ?? InterruptionLevel.STANDARD,
      summary: notification.content.summary,
      decision,
      reason,
    };

    this.entries.push(entry);

    // Ring buffer: drop oldest when full
    if (this.entries.length > this.maxSize) {
      this.entries.shift();
    }
  }

  /**
   * Query history by source, channel, or time range.
   */
  query(filter: HistoryFilter): HistoryEntry[] {
    return this.entries.filter((entry) => {
      if (filter.source && entry.source !== filter.source) return false;
      if (filter.channel && entry.channel !== filter.channel) return false;
      if (filter.decision && entry.decision !== filter.decision)
        return false;
      if (filter.since && entry.timestamp < filter.since) return false;
      if (filter.until && entry.timestamp > filter.until) return false;
      return true;
    });
  }

  /** Get suppressed notifications β€” the ones the user didn't see. */
  getSuppressed(since?: number): HistoryEntry[] {
    return this.query({
      decision: "suppressed",
      since,
    }).concat(
      this.query({ decision: "rate-limited", since }),
      this.query({ decision: "filtered", since }),
      this.query({ decision: "muted", since })
    );
  }

  /** Export as JSON for debugging. */
  export(): string {
    return JSON.stringify(this.entries, null, 2);
  }
}

interface HistoryEntry {
  timestamp: number;
  notificationId: string;
  source: string;
  channel: string;
  level: InterruptionLevel;
  summary: string;
  decision: string;
  reason: string;
}

interface HistoryFilter {
  source?: string;
  channel?: string;
  decision?: string;
  since?: number;
  until?: number;
}

Example 7: SSE-Based Notification Server

/**
 * Server-Sent Events endpoint for real-time notification delivery.
 *
 * SSE is preferred over WebSocket for notifications because:
 * - Notifications are unidirectional (server β†’ client)
 * - SSE auto-reconnects with Last-Event-ID
 * - SSE works through HTTP proxies without special configuration
 * - SSE is simpler to implement and debug
 *
 * WebSocket is only needed if the client needs to send data back
 * (e.g., acknowledgments, dismiss actions). In that case, use a
 * hybrid: SSE for notification delivery, REST for actions.
 */

// Hono server example (runs on Cloudflare Workers or Node)
import { Hono } from "hono";
import { streamSSE } from "hono/streaming";

const app = new Hono();

// In-memory notification bus (replace with Redis/D1 in production)
const subscribers: Map<string, WritableStreamDefaultWriter> = new Map();

app.get("/notifications/stream", async (c) => {
  const lastEventId = c.req.header("Last-Event-ID");

  return streamSSE(c, async (stream) => {
    const clientId = crypto.randomUUID();

    // Send any missed notifications since lastEventId
    if (lastEventId) {
      const missed = getMissedNotifications(lastEventId);
      for (const notification of missed) {
        await stream.writeSSE({
          event: "notification",
          data: JSON.stringify(notification),
          id: notification.id,
        });
      }
    }

    // Keep connection alive with heartbeat
    const heartbeat = setInterval(async () => {
      await stream.writeSSE({
        event: "heartbeat",
        data: JSON.stringify({ timestamp: Date.now() }),
      });
    }, 30_000);

    // Listen for new notifications
    const handler = async (notification: Notification) => {
      await stream.writeSSE({
        event: "notification",
        data: JSON.stringify(notification),
        id: notification.id,
      });
    };

    subscribe(clientId, handler);

    stream.onAbort(() => {
      clearInterval(heartbeat);
      unsubscribe(clientId);
    });
  });
});

// REST endpoint for submitting notifications
app.post("/notifications", async (c) => {
  const body = await c.req.json();

  const notification: Notification = {
    id: crypto.randomUUID(),
    source: body.source,
    channel: body.channel,
    replacesId: body.replacesId,
    content: body.content,
    level: body.level,
    createdAt: Date.now(),
    expiresAt: body.ttl ? Date.now() + body.ttl * 1000 : null,
    state: "pending",
    updateCount: 0,
    relevanceScore: body.relevanceScore,
  };

  // Process through policy engine
  const decision = engine.process(notification);

  // Broadcast to all connected clients
  broadcast(notification);

  return c.json({ id: notification.id, decision });
});

// REST endpoint for acknowledging notifications
app.post("/notifications/:id/acknowledge", async (c) => {
  const id = c.req.param("id");
  const result = escalationEngine.acknowledge(id);
  return c.json(result);
});

// REST endpoint for dismissing notifications
app.post("/notifications/:id/dismiss", async (c) => {
  const id = c.req.param("id");
  queue.dismiss(id);
  return c.json({ dismissed: true });
});

function getMissedNotifications(sinceId: string): Notification[] {
  // Implementation: query history log for notifications after sinceId
  return [];
}

function subscribe(
  clientId: string,
  handler: (n: Notification) => Promise<void>
): void {
  // Implementation: add to subscriber list
}

function unsubscribe(clientId: string): void {
  // Implementation: remove from subscriber list
}

function broadcast(notification: Notification): void {
  // Implementation: send to all subscribers
}

Example 8: File-Based Status Protocol (Local HUD)

/**
 * For a local macOS HUD that reads from a file, the simplest
 * protocol is a JSON status file watched via FSEvents.
 *
 * This is what our Atlas HUD uses: ~/.atlas/status.json
 * Any process can write to it. The HUD reads it.
 *
 * Advantages over a server:
 * - No daemon needed β€” any script can write a JSON file
 * - No network β€” works offline
 * - No authentication β€” filesystem permissions are sufficient
 * - Simple debugging β€” cat the file
 *
 * Disadvantages:
 * - No queuing β€” last writer wins
 * - No history β€” overwritten data is lost
 * - No multi-client β€” only one reader
 * - Race conditions with multiple writers
 */
import { watch } from "fs";
import { readFile, writeFile } from "fs/promises";

const STATUS_PATH = `${process.env.HOME}/.atlas/status.json`;

interface StatusFile {
  /** Current entries, keyed by source */
  entries: Record<string, StatusEntry>;
  /** Timestamp of last modification */
  lastModified: string;
}

interface StatusEntry {
  source: string;
  severity: "green" | "yellow" | "red";
  message: string;
  channel: string;
  level: InterruptionLevel;
  updated: string;
  ttl?: number;
  replacesId?: string;
}

/**
 * Write a status entry. Uses read-modify-write to support
 * multiple sources without clobbering each other.
 */
async function writeStatus(entry: StatusEntry): Promise<void> {
  let current: StatusFile;
  try {
    const raw = await readFile(STATUS_PATH, "utf-8");
    current = JSON.parse(raw);
  } catch {
    current = { entries: {}, lastModified: new Date().toISOString() };
  }

  current.entries[entry.source] = entry;
  current.lastModified = new Date().toISOString();

  // Clean expired entries
  const now = Date.now();
  for (const [key, e] of Object.entries(current.entries)) {
    if (e.ttl) {
      const entryTime = new Date(e.updated).getTime();
      if (now - entryTime > e.ttl * 1000) {
        delete current.entries[key];
      }
    }
  }

  await writeFile(STATUS_PATH, JSON.stringify(current, null, 2));
}

/**
 * Watch the status file and feed changes into the policy engine.
 */
function watchStatus(engine: PolicyEngine): void {
  let lastContent = "";

  watch(STATUS_PATH, async () => {
    try {
      const raw = await readFile(STATUS_PATH, "utf-8");
      if (raw === lastContent) return;
      lastContent = raw;

      const status: StatusFile = JSON.parse(raw);

      // Convert each entry to a notification and process
      for (const entry of Object.values(status.entries)) {
        const notification: Notification = {
          id: `file:${entry.source}:${Date.now()}`,
          source: entry.source,
          channel: entry.channel,
          replacesId: entry.replacesId ?? `file:${entry.source}:latest`,
          content: {
            summary: entry.message,
            severity: entry.severity,
          },
          level: entry.level,
          createdAt: Date.now(),
          expiresAt: entry.ttl ? Date.now() + entry.ttl * 1000 : null,
          state: "pending",
          updateCount: 0,
        };

        engine.process(notification);
      }
    } catch (err) {
      // File might be partially written β€” retry on next change
    }
  });
}

Example 9: Notification with Escalation Chain

// An agent detects a critical issue and submits it with escalation
const criticalAlert: Notification = {
  id: `athena:unreachable:${Date.now()}`,
  source: "jane",
  channel: "system-alerts",
  content: {
    summary: "Athena unreachable for 2 cycles",
    body: "Jane cannot reach Athena. Last heartbeat was 10 minutes ago. Checking DO state via API.",
    severity: "red",
    icon: "alert",
    actions: [
      { id: "ack", label: "Acknowledge", handler: "ack://jane:unreachable" },
      { id: "restart", label: "Restart Athena", handler: "action://athena:restart", destructive: true },
    ],
  },
  level: InterruptionLevel.ELEVATED,
  createdAt: Date.now(),
  expiresAt: null,
  state: "pending",
  updateCount: 0,
  relevanceScore: 1.0,
};

escalationEngine.submitWithEscalation(criticalAlert, {
  steps: [
    // Step 1: Show as ELEVATED for 2 minutes
    {
      level: InterruptionLevel.ELEVATED,
      timeoutMs: 120_000,
    },
    // Step 2: Escalate to URGENT after 2 minutes
    {
      level: InterruptionLevel.URGENT,
      timeoutMs: 300_000,
      action: (n) => {
        // Also send to Telegram
        sendTelegram(`URGENT: ${n.content.summary}`);
      },
    },
    // Step 3: Escalate to CRITICAL after 5 more minutes
    {
      level: InterruptionLevel.CRITICAL,
      timeoutMs: 600_000,
      action: (n) => {
        // Play sound, send SMS
        playSound("alarm");
        sendSMS(`CRITICAL: ${n.content.summary}`);
      },
    },
  ],
  retryIntervalMs: 60_000, // Retry display every minute
  expireAfterMs: 1_800_000, // Give up after 30 minutes
});

declare function sendTelegram(msg: string): void;
declare function playSound(name: string): void;
declare function sendSMS(msg: string): void;

Example 10: Complete HUD Initialization

/**
 * Full initialization of the HUD notification system.
 * This wires together all the patterns.
 */
function initializeHUD(): {
  engine: PolicyEngine;
  escalation: EscalationEngine;
  rsvp: RSVPInterruptionHandler;
  mute: MuteTimingManager;
} {
  // 1. Create the policy engine
  const engine = new PolicyEngine({
    defaultRateLimit: {
      maxBurst: 5,
      refillRate: 1,
      refillIntervalMs: 12_000,
      windowMs: 60_000,
      maxPerWindow: 30,
    },
    rotationIntervalMs: 8_000,
    historySize: 1000,
  });

  // 2. Register all channels
  const channels: NotificationChannel[] = [
    {
      id: "agent-status",
      name: "Agent Status",
      description: "AI agent operational status",
      defaultLevel: InterruptionLevel.AMBIENT,
      muted: false,
      rateLimit: 12,
      collapseStrategy: "replace",
      defaultTTL: 30_000,
      style: "metric",
    },
    {
      id: "system-alerts",
      name: "System Alerts",
      description: "Infrastructure and service alerts",
      defaultLevel: InterruptionLevel.ELEVATED,
      muted: false,
      rateLimit: 6,
      collapseStrategy: "summarize",
      defaultTTL: 120_000,
      style: "alert",
    },
    {
      id: "system-metrics",
      name: "System Metrics",
      description: "CPU, memory, disk, network",
      defaultLevel: InterruptionLevel.SILENT,
      muted: false,
      rateLimit: 60,
      collapseStrategy: "replace",
      defaultTTL: 5_000,
      style: "metric",
    },
    {
      id: "user-messages",
      name: "User Messages",
      description: "Chat messages, emails, social",
      defaultLevel: InterruptionLevel.STANDARD,
      muted: false,
      rateLimit: 10,
      collapseStrategy: "queue",
      defaultTTL: 60_000,
      style: "text",
    },
    {
      id: "timers",
      name: "Timers & Reminders",
      description: "Countdown timers, calendar alerts",
      defaultLevel: InterruptionLevel.URGENT,
      muted: false,
      rateLimit: 10,
      collapseStrategy: "replace",
      defaultTTL: null,
      style: "text",
    },
    {
      id: "ambient",
      name: "Ambient Info",
      description: "Weather, stocks, news headlines",
      defaultLevel: InterruptionLevel.SILENT,
      muted: false,
      rateLimit: 2,
      collapseStrategy: "replace",
      defaultTTL: 300_000,
      style: "text",
    },
  ];

  for (const channel of channels) {
    engine.registerChannel(channel);
  }

  // 3. Set up focus modes
  // (registered on the engine β€” see Pattern 4)

  // 4. Create escalation engine
  const escalation = new EscalationEngine(engine);

  // 5. Create RSVP handler
  const displayController: DisplayController = {
    setMode: (mode) => {
      // Bridge to SwiftUI notch renderer
    },
    showWord: (word, index, total) => {
      // Render single word in notch
    },
    showNotification: (notification) => {
      // Render notification in notch
    },
    showIndicator: (type, data) => {
      // Show subtle indicator dot
    },
  };
  const rsvp = new RSVPInterruptionHandler(engine, displayController);

  // 6. Create mute timing manager
  const mute = new MuteTimingManager();
  mute.addMuteTiming({
    name: "Sleep",
    days: [0, 1, 2, 3, 4, 5, 6],
    startTime: "22:00",
    endTime: "07:00",
  });

  // 7. Start watching the status file
  watchStatus(engine);

  return { engine, escalation, rsvp, mute };
}

Notification Systems Across Platforms

FeatureiOSmacOSAndroidLinux (freedesktop)Our HUD
Priority levels4 (passive, active, time-sensitive, critical)2 (banner, alert)5 (MIN through URGENT)3 (low, normal, critical)6 (SILENT through CRITICAL)
Channels/categoriesCategories with actionsCategoriesChannels (required since API 26)None (app-level only)Channels with collapse strategies
GroupingThread identifier + automaticBy appGroup key + automatic (4+)None (daemon-specific)Channel-based + summarization
Focus/DNDFocus modes (Work, Personal, Sleep, custom)Focus (synced with iOS)DND with priority exceptionsNone (daemon-specific)Focus modes + mute timings + silences
Rate limitingUndocumented per-device budgetNone documentedAuto-groups at 4+NoneToken bucket + sliding window per channel
ReplacementVia notification identifierVia identifierVia notification ID + tagreplaces_idreplacesId per notification
TTL/expiryBadge persists, banner auto-dismissesAuto-dismiss (banner) or persist (alert)Per-channel settingsexpire_timeout parameterPer-notification + per-channel default
ActionsCategory-defined actions (up to 4)Same as iOS categoriesUp to 3 action buttonsActions array (key/label pairs)Action array with handler URIs
HistoryNotification CenterNotification CenterNotification log (Settings)Daemon-dependent (dunst: ctrl+shift+`)Ring buffer with query API
Live/persistentLive Activities + Dynamic IslandNoneForeground service notificationsNoneRotation sources + ambient mode
EscalationNone built-inNoneNoneNoneEscalation engine with retry/expire
Display constraintFull screen availableFull screen availableFull screen availableDesktop-dependentSingle line, 600px wide

Notification Delivery Technologies

TechnologyDirectionReconnectionBrowser limitComplexityBest for
Server-Sent Events (SSE)Server to clientAutomatic (Last-Event-ID)6 per domain (HTTP/1.1)LowNotifications, logs, metrics
WebSocketBidirectionalManualNo hard limitMediumChat, gaming, collaborative editing
Long pollingServer to clientManual6 per domainLowFallback when SSE unavailable
Firebase Cloud MessagingServer to deviceManaged by GoogleN/A (push)MediumMobile push notifications
APNsServer to deviceManaged by AppleN/A (push)MediumiOS/macOS push notifications
File watching (FSEvents)Writer to readerAutomaticN/A (local)Very lowLocal single-machine HUD
D-BusApp to daemonSession busN/A (local)LowLinux desktop notifications
GraphQL subscriptionsServer to clientLibrary-dependent1 WebSocketHighApps already using GraphQL

Alert Management Platforms

PlatformPriority modelEscalationAcknowledgmentRoutingRate limiting
PagerDutySeverity (critical/warning/error/info) mapped to urgency (high/low)Multi-step with timeoutRequired for high-urgencyEvent rules + service routingUndocumented
OpsGenieP1-P5 prioritiesTeam-based with schedulesRequired for P1-P3Alert policies + routing rulesAPI rate limits
Grafana AlertingLabels + notification policiesgroup_wait/group_interval/repeat_intervalVia silencesLabel-based policy treeMute timings + silences
Pushover-2 to +2 (5 levels)Emergency retry/expireReceipt-based for emergencyUser/group targeting7,500 messages/month free
Firebase Cloud MessagingNormal/HighNone built-inNoneTopic-based + device targetingPer-device budget (undocumented)
Our HUD6 levels (SILENT-CRITICAL)Multi-step with escalation actionsIn-display + multi-channelChannel + focus mode + mute timingToken bucket + sliding window

Ambient Display Approaches

SystemDisplay areaPriority handlingMultiple sourcesUser control
Apple Watch complications4-8 fixed slots on watch faceTimeline-based relevanceEach complication is a slotChoose which complications to show
Dynamic Island2 pill-shaped areas flanking notchSystem-determined; calls > timers > musicSplit into 2 mini-islandsTap to expand/interact
Google Nest HubFull 7-10” screenProximity-based (ultrasound)Ambient mode cycles through sourcesSettings per notification type
Car instrument cluster2-4 info areas around speedometerNHTSA guidelines: safety > navigation > infotainmentFixed zones per functionSteering wheel buttons
dunstDesktop corner, stackedUrgency-based sorting, configurable per-ruleStack up to N notificationsPer-app rules in config
Our notch HUDSingle line, 600px6-level priority with preemptionTime-sharing rotation for ambient; queue for priorityFocus modes, mute timings, per-channel config

Don’tDo InsteadWhy
Let sources set their own display durationChannel policy sets TTL; sources only suggestA misbehaving source can hold the display forever by setting TTL to infinity
Use a single priority level for all notificationsMap every notification to one of 6 interruption levels via channel defaultsWithout differentiation, everything gets the same treatment and nothing feels important
Skip rate limiting because β€œmy sources are well-behaved”Always rate-limit, even trusted sourcesA bug in a monitoring script can send 1000 notifications per second. Ask me how I know.
Block critical notifications during Focus/DNDCritical (level 5) always breaks through all filtersiOS requires Apple entitlement for critical alerts precisely because they bypass everything β€” that’s the point
Replace notifications without tracking historyLog every notification decision (displayed, suppressed, rate-limited)When the user asks β€œdid Athena alert me about that?” you need to answer definitively
Show notification content during RSVP without pausingPause RSVP, show notification, then resume with backtrackOverlapping content makes both unreadable
Use WebSocket when data flows one directionUse SSE for server-to-client notification deliverySSE auto-reconnects, works through proxies, and is simpler. WebSocket is overkill for push-only.
Hard-code notification importance in the sourceSources declare a channel; channel config determines importanceImportance should be controlled by the display policy, not the sender. This is Android’s key insight.
Treat all time periods the sameImplement mute timings (recurring) and silences (one-time) separatelyGrafana’s distinction is important: weeknight quiet is recurring, deploy freeze is one-time
Escalate immediately to the highest levelUse multi-step escalation with increasing timeoutsPagerDuty’s whole value is in the escalation chain. Jumping to CRITICAL for everything desensitizes the user.
Show a counter like β€œ12 notifications” without any detailShow the most important suppressed notification’s summary with a countA number without context creates anxiety. β€œAthena: nominal (+11 more)” is better than β€œ12 notifications”
Let sources specify pixel coordinates or layout zonesSources declare content and priority; the layout engine decides placementDecoupling content from geometry is the fundamental principle. See the HUD layout engine design.
Use polling to check for new notificationsUse FSEvents (local) or SSE (network) for push-based deliveryPolling wastes CPU and adds latency. FSEvents and SSE are event-driven.
Implement notification channels without letting users override importanceUsers must be able to mute, boost, or change importance of any channelAndroid gets this right: once a channel is created, only the user can change its importance level

Pulling together all the patterns, here is the complete API surface for a notch HUD notification system.

API Endpoint Summary

POST   /notifications                    Submit a notification
GET    /notifications/stream             SSE stream of notification events
POST   /notifications/:id/acknowledge    Acknowledge (stop escalation)
POST   /notifications/:id/dismiss        Dismiss from display
GET    /notifications/history            Query notification history
POST   /channels                         Register a notification channel
PUT    /channels/:id                     Update channel configuration
GET    /channels                         List all channels
POST   /focus-modes                      Create a focus mode
PUT    /focus-modes/:id/activate         Activate a focus mode
DELETE /focus-modes/:id/activate         Deactivate a focus mode
POST   /mute-timings                     Create a recurring mute timing
POST   /silences                         Create a one-time silence
DELETE /silences/:id                     Cancel a silence
GET    /status                           Current display state + queue

Submit Notification Request

/**
 * POST /notifications
 *
 * This is what sources call to send a notification to the HUD.
 * The design is influenced by:
 * - freedesktop.org Notify method (replaces_id, actions, hints, expire_timeout)
 * - FCM message format (collapse_key, priority, ttl, topic)
 * - Pushover API (priority, sound, retry, expire)
 * - iOS UNNotificationContent (interruptionLevel, relevanceScore, threadIdentifier)
 */
interface SubmitNotificationRequest {
  /** Source identifier. Required. */
  source: string;

  /** Channel ID. Must be pre-registered. */
  channel: string;

  /** Content to display. */
  content: {
    /** Short text for the status bar. Max 80 chars. */
    summary: string;

    /** Longer text for hover/expanded view. */
    body?: string;

    /** Structured metric for dashboard-style display. */
    metric?: {
      label: string;
      value: string;
      unit?: string;
      trend?: "up" | "down" | "flat";
    };

    /** Severity affects visual styling (traffic light colors). */
    severity?: "green" | "yellow" | "red";

    /** Icon identifier or emoji. */
    icon?: string;

    /** Actions the user can take. */
    actions?: Array<{
      id: string;
      label: string;
      handler: string;
      destructive?: boolean;
    }>;
  };

  /** Override the channel's default interruption level. */
  level?: InterruptionLevel;

  /** If set, replaces the notification with this ID.
   *  Like freedesktop replaces_id or FCM collapse_key. */
  replacesId?: string;

  /** Time-to-live in seconds. Overrides channel default.
   *  0 = display immediately, discard if display is busy (FCM TTL 0).
   *  null = persist until dismissed. */
  ttl?: number | null;

  /** Relevance score (0-1) for ranking in summaries.
   *  iOS uses this for notification summary ordering. */
  relevanceScore?: number;

  /** Escalation policy. If set, the notification will escalate
   *  through the defined steps until acknowledged. */
  escalation?: {
    steps: Array<{
      level: InterruptionLevel;
      timeoutMs: number;
      /** Additional channels to notify at this step. */
      notifyChannels?: string[];
    }>;
    retryIntervalMs?: number;
    expireAfterMs?: number;
  };

  /** Thread identifier for grouping related notifications.
   *  iOS threadIdentifier / Android group key equivalent. */
  threadId?: string;

  /** Idempotency key. If the same key is submitted twice,
   *  the second submission is silently ignored. */
  idempotencyKey?: string;
}

interface SubmitNotificationResponse {
  /** Assigned notification ID. */
  id: string;

  /** Policy engine decision. */
  decision: {
    action: "queued" | "displayed" | "replaced" | "summarized" | "suppressed" | "rate-limited" | "deferred";
    reason?: string;
    position?: number;
    retryAfterMs?: number;
  };

  /** Current display state after this notification was processed. */
  displayState: {
    currentNotificationId: string | null;
    queueDepth: number;
    mode: "idle" | "notification" | "rotation" | "rsvp";
  };
}

Notification Event Stream

/**
 * GET /notifications/stream
 * Accept: text/event-stream
 *
 * SSE stream of notification lifecycle events.
 * The HUD client connects to this and renders accordingly.
 *
 * Events:
 * - notification:display    β€” show this notification
 * - notification:expire     β€” remove expired notification
 * - notification:dismiss    β€” user dismissed
 * - notification:escalate   β€” notification escalated to higher level
 * - rotation:show           β€” ambient rotation content
 * - mode:change             β€” display mode changed
 * - heartbeat               β€” keep-alive
 */

type NotificationEvent =
  | {
      event: "notification:display";
      data: {
        notification: Notification;
        preempted: string | null; // ID of preempted notification
      };
    }
  | {
      event: "notification:expire";
      data: {
        notificationId: string;
        nextNotificationId: string | null;
      };
    }
  | {
      event: "notification:dismiss";
      data: {
        notificationId: string;
        nextNotificationId: string | null;
      };
    }
  | {
      event: "notification:escalate";
      data: {
        notificationId: string;
        fromLevel: InterruptionLevel;
        toLevel: InterruptionLevel;
        step: number;
      };
    }
  | {
      event: "rotation:show";
      data: {
        sourceId: string;
        content: NotificationContent;
      };
    }
  | {
      event: "mode:change";
      data: {
        from: string;
        to: string;
        reason: string;
      };
    }
  | {
      event: "heartbeat";
      data: {
        timestamp: number;
        queueDepth: number;
        activeMode: string;
        activeFocusMode: string | null;
      };
    };

Configuration Schema

/**
 * Complete configuration for the notification system.
 * Stored at ~/.atlas/notification-config.json
 *
 * Three-layer config (like the HUD layout engine):
 * 1. System defaults (ships with app)
 * 2. User configuration (this file)
 * 3. Runtime overrides (focus modes, silences)
 */
interface NotificationSystemConfig {
  /** Global settings. */
  global: {
    /** Maximum notifications in the priority queue. */
    maxQueueSize: number;

    /** History log size (ring buffer). */
    historySize: number;

    /** Default TTL for notifications without one (ms). */
    defaultTTLMs: number;

    /** Ambient rotation interval (ms). */
    rotationIntervalMs: number;

    /** Whether to show suppressed notification count. */
    showSuppressedCount: boolean;
  };

  /** Default rate limit for channels without specific config. */
  defaultRateLimit: {
    maxBurst: number;
    refillRate: number;
    refillIntervalMs: number;
    windowMs: number;
    maxPerWindow: number;
  };

  /** Channel definitions. */
  channels: NotificationChannel[];

  /** Focus mode definitions. */
  focusModes: FocusMode[];

  /** Recurring mute timings. */
  muteTimings: MuteTiming[];

  /** Source trust levels. */
  sources: Array<{
    /** Source identifier or glob pattern. */
    pattern: string;
    /** Whether this source is trusted (can use URGENT+). */
    trusted: boolean;
    /** Maximum interruption level this source can use. */
    maxLevel: InterruptionLevel;
    /** Override rate limit for this source. */
    rateLimit?: Partial<RateLimitConfig>;
  }>;

  /** RSVP interruption settings. */
  rsvp: {
    /** Minimum level to pause RSVP. */
    pauseLevel: InterruptionLevel;
    /** Minimum level to stop RSVP entirely. */
    stopLevel: InterruptionLevel;
    /** Words to backtrack when resuming after pause. */
    backtrackWords: number;
  };
}

Default Configuration

{
  "global": {
    "maxQueueSize": 100,
    "historySize": 1000,
    "defaultTTLMs": 30000,
    "rotationIntervalMs": 8000,
    "showSuppressedCount": true
  },
  "defaultRateLimit": {
    "maxBurst": 5,
    "refillRate": 1,
    "refillIntervalMs": 12000,
    "windowMs": 60000,
    "maxPerWindow": 30
  },
  "channels": [
    {
      "id": "agent-status",
      "name": "Agent Status",
      "description": "AI agent operational status",
      "defaultLevel": 1,
      "muted": false,
      "rateLimit": 12,
      "collapseStrategy": "replace",
      "defaultTTL": 30000,
      "style": "metric"
    },
    {
      "id": "system-alerts",
      "name": "System Alerts",
      "description": "Infrastructure and service alerts",
      "defaultLevel": 3,
      "muted": false,
      "rateLimit": 6,
      "collapseStrategy": "summarize",
      "defaultTTL": 120000,
      "style": "alert"
    },
    {
      "id": "system-metrics",
      "name": "System Metrics",
      "description": "CPU, memory, disk, network",
      "defaultLevel": 0,
      "muted": false,
      "rateLimit": 60,
      "collapseStrategy": "replace",
      "defaultTTL": 5000,
      "style": "metric"
    },
    {
      "id": "timers",
      "name": "Timers & Reminders",
      "description": "Countdown timers, calendar alerts",
      "defaultLevel": 4,
      "muted": false,
      "rateLimit": 10,
      "collapseStrategy": "replace",
      "defaultTTL": null,
      "style": "text"
    },
    {
      "id": "ambient",
      "name": "Ambient Info",
      "description": "Weather, stocks, news, learning cards",
      "defaultLevel": 0,
      "muted": false,
      "rateLimit": 2,
      "collapseStrategy": "replace",
      "defaultTTL": 300000,
      "style": "text"
    }
  ],
  "focusModes": [
    {
      "id": "deep-work",
      "name": "Deep Work",
      "active": false,
      "allowedChannels": ["timers", "system-alerts"],
      "allowedSources": ["athena", "jane"],
      "minimumLevel": 4,
      "schedule": {
        "days": [1, 2, 3, 4, 5],
        "startTime": "09:00",
        "endTime": "12:00",
        "timezone": "America/Chicago"
      },
      "fallback": "summary"
    },
    {
      "id": "sleep",
      "name": "Sleep",
      "active": false,
      "allowedChannels": [],
      "allowedSources": [],
      "minimumLevel": 5,
      "schedule": {
        "days": [0, 1, 2, 3, 4, 5, 6],
        "startTime": "22:00",
        "endTime": "07:00",
        "timezone": "America/Chicago"
      },
      "fallback": "nothing"
    }
  ],
  "muteTimings": [],
  "sources": [
    {
      "pattern": "athena",
      "trusted": true,
      "maxLevel": 5
    },
    {
      "pattern": "jane",
      "trusted": true,
      "maxLevel": 5
    },
    {
      "pattern": "user:*",
      "trusted": true,
      "maxLevel": 4
    },
    {
      "pattern": "*",
      "trusted": false,
      "maxLevel": 3
    }
  ],
  "rsvp": {
    "pauseLevel": 3,
    "stopLevel": 4,
    "backtrackWords": 3
  }
}

iOS: The Most Sophisticated Consumer Notification System

Apple’s User Notifications framework is the most thoughtfully designed consumer notification system. Key design decisions:

Interruption Levels (iOS 15+)

The UNNotificationInterruptionLevel enum defines four levels:

  1. Passive: Silently added to Notification Center. No sound, no screen wake, no banner. Used for information that can wait (e.g., news updates, promotional content).

  2. Active (default): Standard delivery. Sound plays, screen lights up, banner appears. This is what most notifications use.

  3. Time Sensitive: Breaks through Focus mode and Scheduled Summary. The system shows these immediately even when the user has explicitly asked not to be disturbed. Apps must declare the Time Sensitive capability in their entitlements. Abuse gets your app rejected.

  4. Critical: Bypasses the mute switch. The user’s phone will make noise even if the physical switch is set to silent. Requires explicit entitlement from Apple β€” only granted for health, safety, security, and public safety apps. Think flood warnings and medical alerts.

Focus Modes (iOS 15+)

Focus modes are the evolution of Do Not Disturb. Each Focus mode defines:

Notification Summaries (iOS 15+)

Instead of delivering every notification immediately, iOS can batch them into scheduled summaries. The system uses machine learning to determine which notifications are most relevant and places those at the top of the summary. Apps influence ranking with the relevanceScore property (0.0 to 1.0).

Notification Grouping

iOS groups notifications using threadIdentifier. Notifications with the same thread ID are grouped together in Notification Center. When the user taps a group, it expands to show individual notifications. Apps provide a summaryArgument to customize the group summary text (e.g., β€œ3 messages from Alice”).

Live Activities and Dynamic Island

Live Activities are a fundamentally different concept from notifications. They represent ongoing activities (sports scores, delivery tracking, timers) that update in real-time on the Lock Screen and Dynamic Island.

The Dynamic Island handles competing Live Activities by splitting into two smaller areas, one on each side of the camera cutout. Each responds independently. The system uses relevanceScore to determine which activity gets the prominent position. Active calls typically claim more visual real estate than background music.

ActivityKit imposes a notification budget that restricts the number of push updates per hour. Updates use the apns-priority header (priority 10 for immediate delivery).

Notification Categories

Apps register UNNotificationCategory objects that define available actions. Each category can have up to 4 action buttons. Actions can be typed (text input), destructive (shown in red), or require authentication (unlock to execute).

macOS: iOS Notification System, Desktop Edition

macOS uses the same UNUserNotificationCenter framework as iOS. Key differences:

Notification Styles

Users choose banner vs. alert per app in System Settings > Notifications.

Notification Center The macOS Notification Center (top-right corner, accessed via click on date/time) shows all recent notifications grouped by app. Notifications expire and are removed after a period (not well-documented, appears to be ~24 hours for most apps).

Focus Integration macOS Focus modes sync with iOS. When you set Focus on your iPhone, your Mac follows suit. This is relevant for our HUD: the HUD should query the system Focus state and respect it.

Android: Channels Are the Key Innovation

Android’s notification system went through a major redesign in Android 8.0 (Oreo, API 26) with the introduction of notification channels. This is arguably the most influential notification design decision in the industry.

Notification Channels

Every notification must belong to a channel. Channels define:

The critical design decision: once a channel is created, the app cannot change its importance. Only the user can modify it through Settings. This prevents apps from sneaking higher priority over time.

Notification Grouping

Android supports notification groups via setGroup(). When an app sends 4 or more notifications without specifying a group key, the system automatically groups them. Android 16 will force-group all app notifications by default.

Groups require a summary notification (the β€œparent” that appears when the group is collapsed). This is the notification the user sees first β€” it should meaningfully summarize the group contents.

Priority Conversations (Android 12+)

Users can mark specific conversations as β€œpriority.” Priority conversation notifications get their own section at the top of the notification shade and can break through DND.

Bubbles (Android 11+)

Bubbles are floating circular icons for ongoing conversations. They overlap other apps and persist until dismissed. This is Android’s answer to the β€œpersistent but not intrusive” problem.

Foreground Service Notifications

Android requires apps with foreground services (background work visible to the user) to show a persistent, non-dismissable notification. This ensures users know what’s running in the background. Music players, navigation apps, and VPN apps all use this.

Linux: The Minimal Standard

The freedesktop.org Desktop Notifications Specification defines a D-Bus protocol that any notification daemon can implement.

The Notify Method

The core of the protocol is a single D-Bus method:

UINT32 org.freedesktop.Notifications.Notify(
  STRING app_name,
  UINT32 replaces_id,
  STRING app_icon,
  STRING summary,
  STRING body,
  ARRAY  actions,
  DICT   hints,
  INT32  expire_timeout
)

Parameters:

Urgency Levels

Three levels, passed as a byte in the hints dictionary:

Notification Daemons

The specification only defines the protocol. The actual behavior (queuing, stacking, styling, animations) is up to the daemon:

Key insight for our HUD: The freedesktop.org replaces_id pattern is essential. Our HUD operates like a notification daemon β€” it receives notifications from multiple sources and decides how to display them. The replaces_id concept (called replacesId in our API) allows sources to update in place without flooding the queue.


Google Nest Hub

The Google Nest Hub manages a 7-inch screen that shows ambient information (photos, weather, calendar) and responds to notifications from Google services.

Key patterns:

Car Dashboards

Automotive HMI (Human-Machine Interface) design is governed by NHTSA Visual-Manual Driver Distraction Guidelines. Key principles:

This maps directly to our HUD:

Apple Watch Complications

The Apple Watch complication system is the gold standard for ambient data display on a tiny screen.

Key concepts:

Key insight: The Apple Watch timeline model is highly relevant to our HUD. Instead of every update being a β€œnotification,” sources could provide a timeline of data points. The HUD displays the current entry and can show past/future entries on interaction (hover or click).


Scenario 1: RSVP Interrupted by Urgent Alert

The user is reading text via RSVP at 350 WPM. An urgent alert arrives from Athena.

// The RSVP handler receives the interrupt
const action = rsvpHandler.handleDuringRSVP({
  id: "athena:spike:1234",
  source: "athena",
  channel: "system-alerts",
  content: {
    summary: "Traffic spike: 5x normal on scalable-media",
    severity: "yellow",
    actions: [
      { id: "ack", label: "OK", handler: "ack://athena:spike" },
      { id: "investigate", label: "Details", handler: "open://dashboard" },
    ],
  },
  level: InterruptionLevel.URGENT,
  createdAt: Date.now(),
  expiresAt: Date.now() + 30_000,
  state: "pending",
  updateCount: 0,
  relevanceScore: 0.9,
});

// Result: { action: "stop-rsvp", rsvpCancelled: true }
// RSVP stops, alert is shown, pending RSVP notifications are flushed to queue

The RSVP configuration determines the threshold:

If the alert were ELEVATED instead of URGENT, RSVP would pause for 5 seconds (the notification’s TTL or default), show the alert, then resume reading from 3 words back.

Scenario 2: Multiple Agents Sending Simultaneously

Three agents all report status within 1 second of each other.

// All three arrive within ~1 second
engine.process(agentStatusUpdate("athena", "nominal", "green"));
engine.process(agentStatusUpdate("jane", "watching", "green"));
engine.process(agentStatusUpdate("coo", "queue clear", "green"));

Because all three use channel agent-status with collapseStrategy: "replace", and all use replacesId: "agent:<name>:latest", each agent’s notification replaces its own previous one. The priority queue now has 3 entries at AMBIENT level.

Since no notification is currently displayed (or the current one has a lower/equal priority), the queue shows the first one that arrived. But all three are AMBIENT β€” they don’t preempt anything. They enter the time-sharing rotation instead.

The rotation cycles through them every 8 seconds:

If one of them is YELLOW severity:

engine.process(agentStatusUpdate("athena", "traffic spike 3x", "yellow"));

This notification is now STANDARD level (because severity=yellow maps to STANDARD in our agent status function). It preempts the ambient rotation and shows immediately. The rotation pauses until this notification expires (30 seconds).

Scenario 3: User Wants Quiet Hours

The user wants complete quiet from 10 PM to 7 AM, except for critical alerts from Athena.

// Option 1: Focus mode (recurring, with source exception)
const sleepMode: FocusMode = {
  id: "sleep",
  name: "Sleep",
  active: false,
  allowedChannels: [],
  allowedSources: ["athena"], // only Athena can break through
  minimumLevel: InterruptionLevel.CRITICAL, // only critical from Athena
  schedule: {
    days: [0, 1, 2, 3, 4, 5, 6],
    startTime: "22:00",
    endTime: "07:00",
    timezone: "America/Chicago",
  },
  fallback: "nothing", // don't even show a count
};

// Option 2: Mute timing (recurring, broader)
muteManager.addMuteTiming({
  name: "Sleep hours",
  days: [0, 1, 2, 3, 4, 5, 6],
  startTime: "22:00",
  endTime: "07:00",
});
// Plus: ensure Athena's CRITICAL notifications bypass mute timings
// (which they do by default β€” CRITICAL bypasses everything)

// Option 3: One-time silence for tonight only
muteManager.addSilence({
  reason: "Going to bed early",
  startsAt: new Date("2026-03-28T21:00:00"),
  endsAt: new Date("2026-03-29T08:00:00"),
});

The layered approach means:

  1. Mute timings are checked first (recurring quiet hours)
  2. Silences override mute timings (one-time exceptions)
  3. Focus modes provide source-level filtering within quiet hours
  4. CRITICAL notifications always break through everything

Scenario 4: Thundering Herd After Focus Mode Ends

The user has been in Deep Work mode for 3 hours. During that time, 47 notifications were suppressed. When Deep Work ends, what happens?

Bad approach: flood the display with 47 queued notifications.

Good approach (what our system does):

// When focus mode deactivates, check the deferred queue
function onFocusModeDeactivated(modeId: string): void {
  const deferred = history.query({
    decision: "filtered",
    since: focusModeActivatedAt,
  });

  if (deferred.length === 0) return;

  if (deferred.length <= 3) {
    // Small number: show them one by one with standard TTL
    for (const entry of deferred) {
      engine.process(reconstructNotification(entry));
    }
  } else {
    // Many notifications: show a summary
    const bySeverity = groupBy(deferred, (e) =>
      e.level >= InterruptionLevel.ELEVATED ? "important" : "routine"
    );

    const important = bySeverity.important ?? [];
    const routine = bySeverity.routine ?? [];

    if (important.length > 0) {
      engine.process({
        id: `summary:focus-end:important:${Date.now()}`,
        source: "system",
        channel: "system-alerts",
        content: {
          summary: `${important.length} important alerts while in ${modeId}`,
          body: important.map((e) => `- ${e.summary}`).join("\n"),
          severity: "yellow",
        },
        level: InterruptionLevel.STANDARD,
        createdAt: Date.now(),
        expiresAt: Date.now() + 60_000,
        state: "pending",
        updateCount: 0,
      });
    }

    if (routine.length > 0) {
      // Just show a count for routine notifications
      engine.process({
        id: `summary:focus-end:routine:${Date.now()}`,
        source: "system",
        channel: "ambient",
        content: {
          summary: `${routine.length} notifications while in ${modeId}`,
          severity: "green",
        },
        level: InterruptionLevel.AMBIENT,
        createdAt: Date.now(),
        expiresAt: Date.now() + 30_000,
        state: "pending",
        updateCount: 0,
      });
    }
  }
}

function groupBy<T>(
  array: T[],
  keyFn: (item: T) => string
): Record<string, T[]> {
  return array.reduce(
    (groups, item) => {
      const key = keyFn(item);
      (groups[key] ??= []).push(item);
      return groups;
    },
    {} as Record<string, T[]>
  );
}

function reconstructNotification(entry: HistoryEntry): Notification {
  return {
    id: `deferred:${entry.notificationId}`,
    source: entry.source,
    channel: entry.channel,
    content: { summary: entry.summary },
    level: entry.level,
    createdAt: Date.now(),
    expiresAt: Date.now() + 30_000,
    state: "pending",
    updateCount: 0,
  };
}

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        NOTIFICATION SOURCES                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Athena  β”‚   Jane    β”‚ System   β”‚  User     β”‚  Cloud Services   β”‚
β”‚  (agent) β”‚  (agent)  β”‚ Monitor  β”‚  Scripts  β”‚  (webhooks)       β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚           β”‚          β”‚           β”‚              β”‚
     β–Ό           β–Ό          β–Ό           β–Ό              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      NOTIFICATION INGRESS                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚ REST API     β”‚  β”‚ Status File  β”‚  β”‚ SSE Webhooks       β”‚    β”‚
β”‚  β”‚ POST /notify β”‚  β”‚ ~/.atlas/    β”‚  β”‚ (inbound)          β”‚    β”‚
β”‚  β”‚              β”‚  β”‚ status.json  β”‚  β”‚                    β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚         β”‚                 β”‚                     β”‚              β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
β”‚                  β–Ό                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”‚
β”‚  β”‚              POLICY ENGINE                    β”‚              β”‚
β”‚  β”‚                                              β”‚              β”‚
β”‚  β”‚  1. Validate (known channel, required fields) β”‚              β”‚
β”‚  β”‚  2. Source trust check (max level)            β”‚              β”‚
β”‚  β”‚  3. Rate limit (token bucket + sliding window)β”‚              β”‚
β”‚  β”‚  4. Mute timing / silence check               β”‚              β”‚
β”‚  β”‚  5. Focus mode filter                         β”‚              β”‚
β”‚  β”‚  6. Channel collapse strategy                 β”‚              β”‚
β”‚  β”‚  7. Calculate effective priority              β”‚              β”‚
β”‚  β”‚  8. Route to queue or rotation                β”‚              β”‚
β”‚  β”‚                                              β”‚              β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚              β”‚
β”‚  β”‚  β”‚    History Log          β”‚                  β”‚              β”‚
β”‚  β”‚  β”‚ (every decision logged) β”‚                  β”‚              β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚              β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β–Ό                   β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PRIORITY    β”‚   β”‚  TIME-SHARING    β”‚
β”‚  QUEUE       β”‚   β”‚  ROTATION        β”‚
β”‚              β”‚   β”‚                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚   β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚CRITICALβ”‚  β”‚   β”‚  β”‚ Agent    β”‚   β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€  β”‚   β”‚  β”‚ Status   β”‚   β”‚
β”‚  β”‚URGENT  β”‚  β”‚   β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€   β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€  β”‚   β”‚  β”‚ CPU/RAM  β”‚   β”‚
β”‚  β”‚ELEVATEDβ”‚  β”‚   β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€   β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€  β”‚   β”‚  β”‚ Weather  β”‚   β”‚
β”‚  β”‚STANDARDβ”‚  β”‚   β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚   β”‚  β”‚ Calendar β”‚   β”‚
β”‚              β”‚   β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚  Preemption  β”‚   β”‚  Round-robin    β”‚
β”‚  by priority β”‚   β”‚  every 8s       β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                    β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         DISPLAY SCHEDULER        β”‚
β”‚                                  β”‚
β”‚  If queue non-empty:             β”‚
β”‚    β†’ Show highest priority item  β”‚
β”‚    β†’ Pause rotation              β”‚
β”‚                                  β”‚
β”‚  If queue empty:                 β”‚
β”‚    β†’ Resume rotation             β”‚
β”‚    β†’ Cycle through ambient       β”‚
β”‚                                  β”‚
β”‚  If RSVP active:                 β”‚
β”‚    β†’ Apply RSVP interruption     β”‚
β”‚      protocol                    β”‚
β”‚                                  β”‚
β”‚  Escalation engine runs in       β”‚
β”‚  parallel, re-submitting at      β”‚
β”‚  higher levels on timeout.       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚       DISPLAY OUTPUT             β”‚
β”‚                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ SSE Stream (to HUD client) β”‚  β”‚
β”‚  β”‚ event: notification:displayβ”‚  β”‚
β”‚  β”‚ event: rotation:show       β”‚  β”‚
β”‚  β”‚ event: mode:change         β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Status File                β”‚  β”‚
β”‚  β”‚ ~/.atlas/status.json       β”‚  β”‚
β”‚  β”‚ (for local HUD)            β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Escalation Side-channels   β”‚  β”‚
β”‚  β”‚ - Telegram                 β”‚  β”‚
β”‚  β”‚ - Sound                    β”‚  β”‚
β”‚  β”‚ - SMS (critical only)      β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Start with the File-Based Protocol

For a single-machine macOS HUD, the file-based protocol (~/.atlas/status.json) is the right starting point:

  1. Zero infrastructure: No server, no daemon, no database. Any process that can write a JSON file can send notifications.
  2. FSEvents is fast: macOS FSEvents triggers within milliseconds of a file change.
  3. Race conditions are manageable: Read-modify-write with a file lock (or accept last-writer-wins for non-critical data).
  4. Easy debugging: cat ~/.atlas/status.json | jq tells you exactly what the HUD is seeing.

Graduate to the SSE API when you need:

Channel Taxonomy

Start with 5-6 channels. You can always add more, but you cannot easily remove them (Android learned this: channels persist even if the app removes them in code).

Recommended starter channels:

  1. agent-status β€” AI agent heartbeats and status updates
  2. system-alerts β€” Infrastructure anomalies and failures
  3. system-metrics β€” CPU/RAM/disk/network readings (ambient rotation only)
  4. timers β€” User timers and calendar reminders
  5. ambient β€” Weather, learning cards, news, quotes
  6. user-messages β€” Chat, email, social (if you want the HUD to show these)

Priority Level Mapping

Map your sources to levels conservatively. Most notifications should be AMBIENT or STANDARD. Reserve URGENT for things that genuinely need immediate attention. Reserve CRITICAL for β€œthe building is on fire.”

A practical rule of thumb:

If more than 10% of your notifications are URGENT or above, your priority levels are miscalibrated. Recalibrate.

Testing Your Policy Engine

Write tests for these specific scenarios:

  1. Higher priority preempts lower priority
  2. Same priority respects FIFO
  3. Rate-limited notifications return appropriate retry-after
  4. Focus mode blocks the right notifications and passes the right ones
  5. CRITICAL always breaks through everything (Focus, mute timing, rate limit)
  6. Mute timing suppresses during configured hours
  7. Silence suppresses during configured window and then stops
  8. RSVP interruption protocol works at each level boundary
  9. Escalation fires at configured timeouts
  10. Acknowledgment stops escalation and retry
  11. History log records every decision
  12. Queue doesn’t grow unbounded (expired items are cleaned up)
  13. Rotation pauses when priority notification arrives and resumes when queue empties
  14. Focus mode end doesn’t flood the display
  15. replacesId correctly updates in-place

Official Documentation

Alert Management Platforms

Cloud Messaging APIs

Smart Displays and Automotive

Linux Notification Daemons

Notification Delivery Patterns

iOS Notification Deep Dives

Internal References


Edit page
Share this post on:

Previous Post
Multi-Machine AI Agent Deployment
Next Post
Ghostty, Tmux, and AI Integration