Proof Portal
Omarchy
ProbeLabs65 findings · 95 requirementsA proof layer — requirements, tests and verified fixes — for two of Omarchy's subsystems: the application menu (launcher scripts, QML model, JSONC config, search and selection) and the lock screen (lock scripts, QML, PAM authentication). Scope is deliberately limited to those components of omacom/omarchy; the rest of the distribution is not covered.
omarchy-hyprland-monitor-watch: recover_modeless() detects a wedged output but hyprctl reload cannot recover it
Reported by WonkaRed · filed Sep 19, 2026 · open at source
At a glance
- 1 · What is claimed?
- Only the report text below — no validator has restated it as checkable claims yet.
- 2 · What does it affect?
- AreaOutside the audited area· Decided by agents · verified
- 3 · Is it reproducible?
- No validation has been run yet.
- 4 · What remains unclear?
- Not validated yet: no claims, no reproducer.
- 5 · What happens next?
- Decided by agents and verified by a second, independent agent — see the decision.
Original report
System details: AMD Ryzen 7 8700F, dual NVIDIA RTX 3090 (nvidia 610.57.04), Omarchy 4.0.3-1, Hyprland 0.56.2-2, aquamarine 0.15.0-2, kernel 7.2.3-arch1-3. Three monitors, one of them rotated 90 degrees.
recover_modeless() detects a modeless monitor correctly but its only remedy, hyprctl reload, cannot recover one. It retried for hours here and never succeeded.
This is not a report of the aquamarine 0.15.0 regression itself, that is already tracked in #11249 and #11019. It is about Omarchy's own recovery path not being able to recover from it, which I think is worth fixing independently of when 0.15.1 reaches the mirrors, because the same wedge can happen again for other reasons.
What happened
I powered all three monitors off at the switch and back on, no suspend, no reboot. Two of the three came back as enabled outputs reporting size 0x0. omarchy-hyprland-monitor-modeless got this exactly right, it was exiting 0 the whole time. omarchy-hyprland-monitor-watch was running (pid 2196, launched from autostart.lua at boot) and recover_modeless() was doing its job, looping and calling hyprctl reload on the 3s to 60s backoff. I found it sitting at sleep 60, still modeless, hours after the event. Detection worked perfectly. The remedy simply cannot fix this class of wedge.
The reason is mechanical. In this failure the kernel still believes a CRTC is attached to a connector that has gone away, so on reconnect the CRTCs come back swapped between outputs and every atomic commit collides with the stale routing. I counted 492 failed ATOMIC_ALLOW_MODESET ATOMIC_TEST_ONLY commits, at every mode in the list right down to 640x480, which rules out bandwidth. A reload re-reads the EDID and re-applies the rule, but it never releases the CRTC, so it loses the same race every time. There is also hyprwm/Hyprland#14496, where scheduleReload() can silently no-op, which makes reload an even weaker lever than it looks.
What does work, confirmed live on this box, is disabling the output first. That releases the CRTC, and the reload that follows then wins the modeset it had been losing:
hyprctl eval 'hl.monitor({ output = "DP-3", disabled = true })'
sleep 3
hyprctl reloadTwo things worth folding in. hyprctl keyword monitor ... does not work under the Lua config parser, it returns error: keyword can't work with non-legacy parsers. Use eval., the same trap that #11450 hit in the Display panel. And order mattered for me: DP-1 would not take any mode at all until DP-3 had been cycled first, after which DP-1 came back immediately at its full 3440x1440@155. So the escalation needs to cycle every modeless output in a round rather than fixing them one at a time.
I should also flag that the comment above recover_modeless() says "Powering the monitor on fires no DRM hotplug". That was not true in my case, my log has 11 real hotplug events from the one power cycle, which is part of why the CRTCs got reshuffled.
Suggested change
Keep the reload as the cheap first move, since it is enough for the EDID-reread case the function was originally written for, and escalate only if it has not worked after a couple of rounds. Restricting the escalation to the exact documented signature, enabled and zero area, is deliberate: it must never touch an output the user disabled on purpose.
+# Outputs matching the wedge signature: enabled but zero area. The packaged
+# detector answers yes or no for the whole session; this names the outputs.
+modeless_outputs() {
+ hyprctl monitors all -j 2>/dev/null |
+ jq -r '.[] | select(.disabled != true and (.width == 0 or .height == 0)) | .name'
+}
+
+# A reload re-reads the EDID, but it cannot release a CRTC the kernel still
+# believes is attached to a connector that went away, which is what the
+# aquamarine 0.15.0 regression leaves behind (hyprwm/aquamarine#386). Disabling
+# the output frees it; the reload that follows then wins the modeset it kept
+# losing. Cycle every wedged output before reloading, because one output can
+# refuse every mode until another has released its CRTC. `hyprctl keyword` is
+# not usable here, the Lua parser rejects it (see #11450), hence eval.
+cycle_modeless() {
+ local name
+ while read -r name; do
+ hyprctl eval "hl.monitor({ output = \"$name\", disabled = true })" >/dev/null 2>&1 || true
+ done < <(modeless_outputs)
+ sleep 3
+ hyprctl reload >/dev/null 2>&1 || true
+}
+
recover_modeless() {
(
flock -w 1 9 || exit 0
- local delay=3 state reloaded unanswered=0
+ local delay=3 state reloaded unanswered=0 rounds=0
while true; do
omarchy-hyprland-monitor-modeless
state=$?
(( state == 1 )) && break
(( state == 2 )) && (( ++unanswered > 20 )) && break
(( state == 0 )) && unanswered=0
reloaded=0
# Reloading into half-replaced package config is what the guard prevents.
if (( state == 0 )) && ! omarchy-hyprland-reload-guard paused; then
- hyprctl reload >/dev/null 2>&1 || true
+ if (( ++rounds > 2 )); then
+ cycle_modeless
+ else
+ hyprctl reload >/dev/null 2>&1 || true
+ fi
reloaded=1
fi
sleep "$delay"
(( reloaded )) && (( delay = delay * 2 > 60 ? 60 : delay * 2 ))
done
) 9>"$MODELESS_LOCK" &
}I am running the equivalent of this locally as a separate script and it recovered a deliberately disabled output on the first attempt in about four seconds. What I have not been able to test is the real hotplug path end to end, because that needs another physical power cycle, so treat the escalation threshold of two rounds as a guess rather than a tuned value. Happy to open a PR if the approach looks right.
View raw text
**System details:** AMD Ryzen 7 8700F, dual NVIDIA RTX 3090 (nvidia 610.57.04), Omarchy 4.0.3-1, Hyprland 0.56.2-2, aquamarine 0.15.0-2, kernel 7.2.3-arch1-3. Three monitors, one of them rotated 90 degrees.
`recover_modeless()` detects a modeless monitor correctly but its only remedy, `hyprctl reload`, cannot recover one. It retried for hours here and never succeeded.
This is not a report of the aquamarine 0.15.0 regression itself, that is already tracked in #11249 and #11019. It is about Omarchy's own recovery path not being able to recover from it, which I think is worth fixing independently of when 0.15.1 reaches the mirrors, because the same wedge can happen again for other reasons.
### What happened
I powered all three monitors off at the switch and back on, no suspend, no reboot. Two of the three came back as enabled outputs reporting size 0x0. `omarchy-hyprland-monitor-modeless` got this exactly right, it was exiting 0 the whole time. `omarchy-hyprland-monitor-watch` was running (pid 2196, launched from `autostart.lua` at boot) and `recover_modeless()` was doing its job, looping and calling `hyprctl reload` on the 3s to 60s backoff. I found it sitting at `sleep 60`, still modeless, hours after the event. Detection worked perfectly. The remedy simply cannot fix this class of wedge.
The reason is mechanical. In this failure the kernel still believes a CRTC is attached to a connector that has gone away, so on reconnect the CRTCs come back swapped between outputs and every atomic commit collides with the stale routing. I counted 492 failed `ATOMIC_ALLOW_MODESET ATOMIC_TEST_ONLY` commits, at every mode in the list right down to 640x480, which rules out bandwidth. A reload re-reads the EDID and re-applies the rule, but it never releases the CRTC, so it loses the same race every time. There is also hyprwm/Hyprland#14496, where `scheduleReload()` can silently no-op, which makes reload an even weaker lever than it looks.
What does work, confirmed live on this box, is disabling the output first. That releases the CRTC, and the reload that follows then wins the modeset it had been losing:
```
hyprctl eval 'hl.monitor({ output = "DP-3", disabled = true })'
sleep 3
hyprctl reload
```
Two things worth folding in. `hyprctl keyword monitor ...` does not work under the Lua config parser, it returns `error: keyword can't work with non-legacy parsers. Use eval.`, the same trap that #11450 hit in the Display panel. And order mattered for me: DP-1 would not take any mode at all until DP-3 had been cycled first, after which DP-1 came back immediately at its full 3440x1440@155. So the escalation needs to cycle every modeless output in a round rather than fixing them one at a time.
I should also flag that the comment above `recover_modeless()` says "Powering the monitor on fires no DRM hotplug". That was not true in my case, my log has 11 real hotplug events from the one power cycle, which is part of why the CRTCs got reshuffled.
### Suggested change
Keep the reload as the cheap first move, since it is enough for the EDID-reread case the function was originally written for, and escalate only if it has not worked after a couple of rounds. Restricting the escalation to the exact documented signature, enabled and zero area, is deliberate: it must never touch an output the user disabled on purpose.
```diff
+# Outputs matching the wedge signature: enabled but zero area. The packaged
+# detector answers yes or no for the whole session; this names the outputs.
+modeless_outputs() {
+ hyprctl monitors all -j 2>/dev/null |
+ jq -r '.[] | select(.disabled != true and (.width == 0 or .height == 0)) | .name'
+}
+
+# A reload re-reads the EDID, but it cannot release a CRTC the kernel still
+# believes is attached to a connector that went away, which is what the
+# aquamarine 0.15.0 regression leaves behind (hyprwm/aquamarine#386). Disabling
+# the output frees it; the reload that follows then wins the modeset it kept
+# losing. Cycle every wedged output before reloading, because one output can
+# refuse every mode until another has released its CRTC. `hyprctl keyword` is
+# not usable here, the Lua parser rejects it (see #11450), hence eval.
+cycle_modeless() {
+ local name
+ while read -r name; do
+ hyprctl eval "hl.monitor({ output = \"$name\", disabled = true })" >/dev/null 2>&1 || true
+ done < <(modeless_outputs)
+ sleep 3
+ hyprctl reload >/dev/null 2>&1 || true
+}
+
recover_modeless() {
(
flock -w 1 9 || exit 0
- local delay=3 state reloaded unanswered=0
+ local delay=3 state reloaded unanswered=0 rounds=0
while true; do
omarchy-hyprland-monitor-modeless
state=$?
(( state == 1 )) && break
(( state == 2 )) && (( ++unanswered > 20 )) && break
(( state == 0 )) && unanswered=0
reloaded=0
# Reloading into half-replaced package config is what the guard prevents.
if (( state == 0 )) && ! omarchy-hyprland-reload-guard paused; then
- hyprctl reload >/dev/null 2>&1 || true
+ if (( ++rounds > 2 )); then
+ cycle_modeless
+ else
+ hyprctl reload >/dev/null 2>&1 || true
+ fi
reloaded=1
fi
sleep "$delay"
(( reloaded )) && (( delay = delay * 2 > 60 ? 60 : delay * 2 ))
done
) 9>"$MODELESS_LOCK" &
}
```
I am running the equivalent of this locally as a separate script and it recovered a deliberately disabled output on the first attempt in about four seconds. What I have not been able to test is the real hotplug path end to end, because that needs another physical power cycle, so treat the escalation threshold of two rounds as a guess rather than a tuned value. Happy to open a PR if the approach looks right.
Validation
Not checked yet. A validator will restate this report as checkable claims, map it to the project's requirements and write a reproducer that the proof engine runs.
Linked
Not linked to any known issue, other report or change yet.
Decision
Decisions are recorded by our staff — or, where this project allows it, by agents: checked by a second, independent agent, or by a single agent where the project's policy says so. Any decision reached so far is shown below.
- Out of scopeDecided by agents · verified · 2026-10-09 15:46 UTC