Skip to content

notifications: critical-urgency toasts never expire and resurrect on every reboot #12911

Description

@PrashPoorai

Critical-urgency toasts never expire and resurrect on every reboot

Summary

A notification sent with critical urgency and a non-zero expireTimeout is
treated as immortal by the shell's notification plugin: it never expires on
screen, its state file is never archived, and it is re-shown (with a fresh
lifetime) after every reboot — indefinitely, until manually dismissed.

Version / system

  • Omarchy 4.0.4-1
  • Kernel 7.2.5-3-omarchy, Arch base
  • Lenovo Legion 5 (i9-13900H, RTX 4070 laptop), Hyprland

What happened

A "Time to recharge!" low-battery toast (sent by omarchy battery low, which
uses omarchy-notification-send -u critical -t 30000) appeared on the evening
of Sep 21. The machine was rebooted several times over the next ~18 hours;
the toast reappeared on every boot, still in the top-right corner, never
expiring. It could only be removed with omarchy notification dismiss
(the summary substring had to match exactly) or by deleting its state file.

Root cause (traced in shell source)

The three pieces that combine into the bug:

  1. shell/plugins/notifications/Service.qmldurationFor():

    switch (urgency) {
    case NotificationUrgency.Critical:
      return 0        // critical => duration 0, the sender's expireTimeout is ignored
  2. shell/plugins/notifications/NotificationLogic.jspopupExpired():

    var lifetime = Number(duration || 0)
    if (!isFinite(lifetime) || lifetime <= 0) return false   // duration 0 => never expires
  3. Service.qmlrestorePopups(): entries that "haven't expired" are
    restored at startup, and survivors "restart with a full lifetime on
    purpose". With duration 0 there is no deadline at all, so the toast is
    re-shown on every boot with no expiry — permanently.

Per the FreeDesktop notification spec, expireTimeout == 0 means "never
expire"; a non-zero expireTimeout should be honoured regardless of
urgency. The shell currently maps critical → 0 unconditionally, so the
sender's explicit 30000 ms request is discarded.

Note the design comment in restorePopups() shows the intent — critical
alerts must not be silently dropped across restarts. The bug is that a
critical toast that did request a finite timeout gets an infinite one.

Repro

omarchy notification send -u critical -t 30000 "Repro" "critical + 30s timeout"
reboot   # or: omarchy restart shell
  • The toast reappears after the restart and never expires.
  • The state file persists in
    ~/.local/state/omarchy/notifications/<timestamp>-<id>.json and is
    re-restored on every subsequent boot.

Minimal persisted file from the real incident (18 h old, still live):

{"id":1,"originalId":1,"app":"omarchy-action","appIcon":"battery-caution","summary":"Time to recharge!","body":"Battery is down to 10%","image":"","glyph":"󱐋","execArgv":"","urgency":2,"expireTimeout":30000,"timestamp":1790021406967}

Expected

Either:

  • honour the sender's non-zero expireTimeout even for critical urgency
    (spec-compliant), or
  • at minimum, don't restore a persisted critical toast whose original
    lifetime has clearly already elapsed (e.g. judge it against
    timestamp + expireTimeout when no reset deadline is present).

Impact

  • Stale toasts accumulate across reboots and keep occupying the top-right
    corner indefinitely (the incident toast blocked part of the bar area for
    ~18 h across 3+ boots).
  • Critical toasts also bypass DND, so a stuck one can't be suppressed that
    way either.

Attachments

Screenshot of a repro toast (critical urgency + 30s timeout) on screen —
attached below.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions