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:
-
shell/plugins/notifications/Service.qml — durationFor():
switch (urgency) {
case NotificationUrgency.Critical:
return 0 // critical => duration 0, the sender's expireTimeout is ignored
-
shell/plugins/notifications/NotificationLogic.js — popupExpired():
var lifetime = Number(duration || 0)
if (!isFinite(lifetime) || lifetime <= 0) return false // duration 0 => never expires
-
Service.qml — restorePopups(): 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.
Critical-urgency toasts never expire and resurrect on every reboot
Summary
A notification sent with critical urgency and a non-zero
expireTimeoutistreated 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
What happened
A "Time to recharge!" low-battery toast (sent by
omarchy battery low, whichuses
omarchy-notification-send -u critical -t 30000) appeared on the eveningof 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:
shell/plugins/notifications/Service.qml—durationFor():shell/plugins/notifications/NotificationLogic.js—popupExpired():Service.qml—restorePopups(): entries that "haven't expired" arerestored 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 == 0means "neverexpire"; a non-zero
expireTimeoutshould be honoured regardless ofurgency. 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 — criticalalerts 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
~/.local/state/omarchy/notifications/<timestamp>-<id>.jsonand isre-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:
expireTimeouteven for critical urgency(spec-compliant), or
lifetime has clearly already elapsed (e.g. judge it against
timestamp + expireTimeoutwhen no reset deadline is present).Impact
corner indefinitely (the incident toast blocked part of the bar area for
~18 h across 3+ boots).
way either.
Attachments
Screenshot of a repro toast (critical urgency + 30s timeout) on screen —
attached below.