fix(idle): #127 — input wakes the screen compositor-side; revert the clamshell skip
All checks were successful
Check / eval (push) Successful in 3m33s

The idle brick was never DPMS. Hyprland ships misc:key_press_enables_dpms
and misc:mouse_move_enables_dpms OFF, so hypridle's on-resume was the ONLY
caller of `dpms on` in the session — an idle daemon with the display held
hostage behind it. hypridle then deadlocks on an unfixed upstream bug
(hyprwm/hypridle#171, "Disconnected from pollfd id 1", open since 2025-09,
absent from 0.1.7 and main) and hangs WITHOUT exiting, so Restart=always
never fires and systemd still reports it active (running). Blank + deadlock
= a black seat no keypress can escape. That was the brick: two mundane bugs,
not one exotic DRM state. 19 disconnects in 11 days on the dev box, incl.
the incident day; the compositor is healthy through all of them.

Both options on, so the wake lives in the compositor where no daemon can
lose it — a dead hypridle now costs auto-lock, not the machine. The #127
clamshell DPMS-off skip (060bf52) is therefore reverted: idle blanks again
in every dock/lid state, and the 27" panels stop being held lit all night
under a static lock screen. #135 (an option to opt back in) is retired
unbuilt — there is nothing left to opt into.

TEST B's first run was invalid and nearly bought the wrong answer: it ran
`hyprctl dispatch dpms off` by hand, which no wake path watches, so "input
did not wake it" was guaranteed on any hardware. The undocked control
returning the SAME result — where the theory demanded a difference — is what
exposed it. Driven through hypridle instead, input wakes it in all three
configurations, including clamshell-docked on the sole live output.

Filed from the diagnosis: #146 (hypridle dies silently, systemd calls it
healthy — the surviving bug) and #147 (a re-login leaves session units dead;
graphical-session.target never re-enters, which is why Waybar already runs
from exec-once).

V3 on the incident hardware (AMD dev box, clamshell): hypridle deliberately
stopped, `dpms off` -> keypress woke it in 6s; the identical test before the
options stayed black the full 45s. V1: nix flake check, checks.option-docs,
checks.clamshell-logind, checks.docking-ux, checks.display-profiles; read the
emitted hyprland.conf/hypridle.conf rather than the source.

V3 pending: the fix is proven via runtime `hyprctl keyword`, not yet from our
own Nix — HARDWARE-QUEUE re-checks it after nomarchy-home + relogin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 18:48:22 +01:00
parent c32b51897e
commit a2151f424b
7 changed files with 187 additions and 238 deletions

View File

@@ -31,163 +31,8 @@ post-boot sessions). Preserve separation: the installer bake failure and the
flake pin are different root causes even when they show up on the same machine.
(Terminal / Ghostty-on-Acer → shipped as Kitty-only, #95.)
### 127. `[blocked:hw]` Docked idle: black screens, no wake, undock does not restore panel
Bernardo 2026-07-15 (AMD dev box, docked): left the laptop docked; after a
few minutes of idle it appeared to "sleep". Then:
1. External keyboard/mouse did **not** wake any display.
2. Unplugging the external monitor did **not** turn the laptop panel back
on (dock mode has eDP disabled — undock *should* re-enable it).
3. The machine was still alive: Caps Lock LED toggled on the keyboard.
This also killed a long agent session (looked like a "crash" from the
outside). Separate from awake undock recovery (HARDWARE-QUEUE round 8):
the failure is **idle while docked**, then no path back to a usable panel.
**Likely shape (code-side, unconfirmed on the incident):** on AC, hypridle
does **not** call `systemctl suspend` (`onAc || suspend` — only battery
suspends at 15 min). Idle path is lock @5 min + `dpms off` @10 min
(`modules/home/idle.nix`). Caps Lock working fits **DPMS/lock blackout**
better than deep S3. Dock mode has already disabled eDP; recovery then
needs either (a) input → hypridle `on-resume` / `dpms on` on the external,
or (b) undock → `nomarchy-display-transition undock` re-enabling the
panel. Both failed. `after_sleep_cmd` only does `hyprctl dispatch dpms on`
and never re-enables a disabled internal.
**Progress 2026-07-15:**
- Recovery tooling: undock ends with `dpms on`; `nomarchy-display-wake` /
`nomarchy-display-dump` (SUPER+SHIFT+D; TTY if it works).
- **Mitigation (shipped):** do **not** DPMS-off when **clamshell** — if any
laptop internal (eDP/LVDS/DSI) exists in `monitors all` but is not
enabled, the 10 min listener skips blanking. Lock at 5 min still runs;
undocked laptops still DPMS-off as before. Bernardo: Ctrl+Alt+F3 did
not yield a usable TTY on the original brick, so prevention > dump path.
- **Scope settled (#136, 2026-07-16):** the gate is *clamshell*, not
"docked" — and Bernardo confirms the incident was exactly that, **laptop
closed**. So the code matched the evidence and only the wording
overclaimed; that is fixed (idle.nix, ROADMAP, and the journal line, now
`dpms-off skipped: clamshell`). Docked with the lid **open** still blanks
and is untested — not exempted, not claimed safe.
- **Revisit later (LATER):** intentional DPMS-when-clamshell once wake is
proven trustworthy (monitor power-save without a brick). Tracked as
**#135** (state key + the surface to delete this by), downstream of the
hardware test below.
- **Caveat the test must settle:** the mitigation is a cure for an
**unconfirmed cause**. "Blanking the only live output caused the brick"
is inference from one incident, shipped at V1. If DPMS-off is not the
trigger, we are paying a permanent behaviour cost (docked externals never
sleep) and the brick is still live — so confirming the cause outranks
#135/#136.
- **Symptom 2 is SOLVED, and it was not DPMS (ff5017e, 2026-07-16).** Bernardo
reproduced "unplugging did not turn the laptop panel back on" live, minutes
after TEST A — with **no blanking involved at all** (the mitigation had kept
the external lit). Cause: `awaiting_lid_open` is set by every successful
undock and cleared only when the external is gone **and** the lid is open, so
a clamshell undock **latches** it; a re-dock clears its sibling
`undock_pending` but not the latch, and the next departure is then silently
dropped by the `[ -z "$awaiting_lid_open" ]` guard. His journal shows the
first undock logging *nothing*, a release the instant he lifted the lid, then
a clean undock. Replaying the sequence with the lid never opened: 1 of 3
undocks ran; fixed, 3 of 3. **What this does to the theory:** the original
brick may simply have been an ordinary blank plus a recovery that could not
run — two mundane bugs, not one exotic DRM state. TEST B is now the only
thing standing between that hypothesis and the truth, and it is a much safer
test with the latch fixed (an undock is a real escape hatch again).
**Pass (current mitigation):** clamshell-docked on AC, idle past 10+ min →
external stays lit under hyprlock (journal: `dpms-off skipped: clamshell`);
undocked still blanks at 10 min and wakes on input. Full original pass (wake
after DPMS-while-clamshell) deferred to the LATER revisit.
---
### The two tests, exactly (2026-07-16)
**There is no SSH lifeline on this box** (`sshd` inactive), which is why test B
self-recovers instead of relying on one. If you would rather have the net, turn
`services.openssh.enable = true` on for the day and keep a phone SSH client
open — then a brick is a nuisance, not a reboot.
**Test A — does the mitigation hold? (safe, ~15 min, unattended)**
1. Docked, **lid closed**, on AC. Leave it completely alone for 12 minutes.
2. Expect: at 5 min the external shows hyprlock; at 10 min it **stays lit**.
3. Then: `journalctl --user -b -t nomarchy-idle --since '-15 min'`
→ one `dpms-off skipped: clamshell (internal eDP-1 present but off)`.
4. Now open the lid / undock, idle 11 min → the panel **should** blank, and
wake on a keypress. That half must still work; the mitigation is not
supposed to have disabled blanking everywhere.
**Test B — was DPMS-off actually the trigger? (the decisive one; bounded)**
This is the test that outranks everything else here, because the mitigation is
a cure for an *inferred* cause. Docked, **lid closed**, from a terminal on the
external:
```sh
( sleep 45
hyprctl dispatch dpms on
hyprctl reload # re-enables every output (the #142 hammer)
) >/tmp/dpms-test.log 2>&1 &
hyprctl dispatch dpms off
```
The screen goes black. Then, in order:
1. **Press keys / move the mouse** in the first ~40s. Does it come back?
(In the incident it did not — this is the exact step that failed.)
2. **Wait for the 45s auto-recovery.** Does the external return by itself?
3. If it is still dark, **open the lid** — the `reload` should have re-enabled
eDP even if the external stayed dead.
4. Afterwards:
`journalctl --user -b -t nomarchy-idle -t nomarchy-display-watch --since '-5 min'`
**What each outcome means — all three are useful:**
- **Input wakes it** → DPMS-off is **not** the trigger. The mitigation is a
permanent behaviour cost buying nothing, the brick is still live and
un-diagnosed → revert the skip, **#135 never exists**, and hunt the real
cause (hyprlock? hypridle's own resume path? the dock's DP link?).
- **Dark until the 45s recovery, then fine** → the compositor was alive and
`dpms on` works, so blanking-while-clamshell *is* the trigger and the
mitigation is right → **#135 becomes the way to opt back in**.
- **Dark even after recovery** (lid needed) → the original brick, reproduced
**deterministically and on demand**. That is the best outcome: it turns a
once-off incident into something bisectable (try it without hyprlock, with
the lid open, on the internal only …).
Note the deliberate omission: this runs `dpms off` **by hand** rather than
waiting for hypridle, precisely because the shipped mitigation would otherwise
skip it. Same command, same state, no rebuild.
**Fallback if the cause resists (Bernardo 2026-07-16):** diagnosing stays the
preference — DPMS when docked is the outcome we want. But the mitigation as
shipped holds a near-static lock screen on the externals indefinitely, so if
the cause cannot be pinned down, a **screensaver while docked** is the
consolation prize: it protects the panels (burn-in — the same cost #135
names) without re-entering the blanking path that bricked the session. Not a
new item: it is the "if diagnosis fails" branch of this one, and #135's state
key is where it would land.
## NEXT
### 135. Give the #127 DPMS mitigation a state key (and a way back)
The shipped mitigation is hardcoded policy: docked (clamshell) externals now
**never** blank, and hyprlock has no timeout of its own, so they stay lit
indefinitely. Two real costs land on the user — a couple of 27" panels at
~60100W all night, and **OLED burn-in** from a near-static lock screen held
for hours. Today there is no way to opt back in, which also breaks the
in-flake-state rule that settings are menu-writable.
Add `settings.idle.dpmsWhenDocked` (default **false** = today's safe
behaviour), read by `modules/home/idle.nix`'s `dpmsOff` gate, with a
Preferences row. The real point is not the option: it gives the mitigation a
**named surface to delete** once wake is proven, instead of an unowned LATER
note that quietly becomes the product. Whoever removes the mitigation removes
the key in the same commit.
**Order:** after the #127 hardware test. If DPMS-off turns out not to be the
trigger, the mitigation is reverted wholesale and this key never exists —
building it first risks shipping an option for a policy we withdraw.
### 143. Rofi keeps the selected *row number* when the search changes, so the highlight lands on an unrelated entry
Bernardo 2026-07-16, and his repro is exact: open the app menu, type `ca`, move
@@ -246,14 +91,6 @@ the next lock bump, that is fine — nothing here rots meanwhile.
## LATER
- **DPMS when docked (revisit #127):** today we **skip** display blanking
while any laptop internal is present-but-disabled (dock/clamshell), so
idle only locks. That avoids blanking the sole external and the brick
where wake/TTY failed (2026-07-15). Revisit when we want monitor
power-save on dock again: re-enable DPMS-off for that mode only if
`nomarchy-display-wake` (or better) reliably restores the external and
undock-while-black restores eDP on the AMD dock setup — without
depending on SSH. Until then, leave the skip in `modules/home/idle.nix`.
- **Wallpapers artifact split** (ROADMAP § Faster switches — decided,
deferred): pinned `Nomarchy-wallpapers` input so a state write stops
re-copying 86 MB. Follow-on: pre-built theme variants if switches are
@@ -299,6 +136,64 @@ high-ROI, etc.) live in the journal + ROADMAP — not here.*
### Product / day-2
### 146. hypridle dies silently and systemd calls it healthy
Fell out of the #127 diagnosis (2026-07-16, ROADMAP § "#127 solved"), and it is
the bug that was actually behind the brick — the DPMS gate we just reverted was
only ever a symptom shield. hypridle hits an unfixed upstream deadlock
(hyprwm/hypridle#171, "Disconnected from pollfd id 1" — open since 2025-09, in
neither 0.1.7 nor main; upstream's own reporter saw 10-hour hangs) and then
**hangs without exiting**, so `Restart=always` never fires and `systemctl
--user is-active` still says `active (running)`. On the dev box: **19 times in
11 days**, including the incident day. Jul 14 is the clean example — locking
normally at 13:02, `CRITICAL` at 13:08:17, then eleven hours of silence, with
D-Bus receive queues visibly backing up (48 KB unread) because nothing was
reading them. The compositor is *fine* through all of it (no other session unit
dies in the same second), so this is not the compositor's death taking it down.
Now that input wakes the screen compositor-side this is no longer brick-class,
but it is not cosmetic either: a silently dead hypridle means **no auto-lock**
(Bernardo sat unlocked for 25 min on 2026-07-16 without knowing), no blank, no
idle suspend. Cost: systemd can't see it — `WatchdogSec` needs `sd_notify`
hypridle doesn't implement, and the process never exits — so detection has to be
external: a guard watching for the `CRITICAL` line, or a liveness probe on its
Wayland fd, then `systemctl --user restart hypridle`. **Sequence it after #147**:
today's disconnect was the compositor dying, and restarting hypridle into a dead
compositor just fail-loops into `start-limit-hit` exactly as cliphist does. The
trigger is still unknown — *not* dock/undock (no display-watch activity in the
3 min before any of them) and *not* a D-Bus quota disconnect (no broker errors).
Worth a reproducer + an upstream comment on #171: it has sat ten months on
vaxerski's "that will cause a segfault no?", and we have better data than the
reporter did.
### 147. A re-login leaves the session's user units dead, and nothing brings them back
Bernardo, 2026-07-16: after re-logging in, Waybar took 5+ seconds and
nomarchy-doctor reported cliphist, network-manager-applet and udiskie failing.
Root cause is the session lifecycle, not those apps. Hyprland exited at
17:25:39 and the replacement came up at **17:25:52** — a 13-second gap — but
`graphical-session.target` never re-entered (its `ActiveEnterTimestamp` still
read 08:03:32, from the *morning's* session). So: units with a restart policy
burn their start limit inside that gap against a Wayland socket that does not
exist yet (cliphist's journal is explicit — `wl-paste: Failed to connect to a
Wayland server`, 5 restarts in one second, `start-limit-hit` at 17:25:40,
twelve seconds before the new compositor existed); units without one
(nm-applet, udiskie) simply die once and stay dead forever. Nothing re-enters
the target, so nothing recovers.
We have already paid for this once without naming it: `modules/home/hyprland.nix`
launches Waybar from `exec-once` rather than a unit, and the comment there says
why — "bound to graphical-session.target the unit raced Hyprland's IPC on
relogin". That is this bug, worked around for one service; the 5-second Waybar
start is the workaround being slow instead of failing. Fix is the standard
wiring the repo never adopted: the compositor imports `WAYLAND_DISPLAY` +
`HYPRLAND_INSTANCE_SIGNATURE` into the user manager and starts/stops a
`hyprland-session.target` on entry/exit (Home Manager's
`wayland.windowManager.hyprland.systemd.enable`, or uwsm). Cost: touches how
every session service is bound, so it wants a VM pass and a real relogin on
hardware — but it would let Waybar go back to being a unit and unblock #144's
guard.
### 120. A netinstall ISO, next to the fat offline one
**Deferred to PROPOSED 2026-07-16 (Bernardo): not now.** Nothing below is

View File

@@ -16,7 +16,7 @@ Run these first when you have a laptop session (AMD dev box unless noted):
| # | Item | Why first |
|---|------|-----------|
| 1 | **#115** suspend-then-hibernate | Just shipped; quick path is minutes (battery vs AC + `nomarchy-suspend`) |
| 2 | **#127** docked idle blackout | Real incident; capture path before it bit-rots |
| 2 | **#127** fix from config (not runtime keyword) | Cause found + fixed 2026-07-16; confirm our Nix emits it and it survives a relogin |
| 3 | **Docking recovery round 8** + headphone jack-follow | Same dock session; relogin once, then undock ×5 + jack |
| 4 | **#104** airplane mode | Fast radio smoke after `nomarchy-home` |
| 5 | **#101** charge-limit USB-C burst | Dock plug/unplug storm; pairs with #3 |
@@ -49,36 +49,36 @@ Everything else below stays open; order is convenience, not a gate.
restores **only** the radios that were on before. If Wi-Fi was already
off, it must stay off after disengage. Glyph must **not** appear when
airplane is off.
- [ ] **#127 clamshell idle — TEST A, the mitigation holds (AMD dev box)** —
**Context:** original brick = clamshell-docked idle → black, no input
wake, undock no eDP, Caps Lock alive; Ctrl+Alt+F3 gave no usable TTY.
**Shipped mitigation:** skip DPMS-off while clamshell (internal
present-but-disabled); lock at 5m still.
**After `nomarchy-home` + relogin:**
1. Dock, **lid closed** (eDP in `hyprctl monitors all -j`, absent from
`hyprctl monitors`).
2. Leave it entirely alone past 10+ min.
3. **Pass:** external still shows hyprlock (not black power-save);
`journalctl --user -t nomarchy-idle --since '-20 min'` has
`dpms-off skipped: clamshell`.
4. Undocked control: eDP only → at 10 min it *does* blank; input wakes.
Optional: `nomarchy-display-dump` / SUPER+SHIFT+D for other display faults.
- [ ] **#127 TEST B — was DPMS-off the trigger at all? (AMD dev box)** —
**This one outranks #135**: the mitigation is a cure for an *inferred*
cause, and if the inference is wrong we are paying for it forever while
the brick stays live. Bounded and self-recovering (there is no sshd on
that box). Full protocol + how to read each outcome: **BACKLOG #127 §
"The two tests, exactly"**. In short — docked, lid closed:
```sh
( sleep 45; hyprctl dispatch dpms on; hyprctl reload ) &
hyprctl dispatch dpms off
```
then (1) try to wake it with input in the first ~40s, (2) let the 45s
recovery run, (3) open the lid if still dark. **Input wakes it** → the
mitigation is pointless, revert it and #135 never exists. **Dark until
recovery** → mitigation confirmed, #135 is the opt-in. **Dark after
recovery** → the brick, now reproducible on demand, which is the best
outcome available.
- [x] **#127 TESTS A + B — ANSWERED 2026-07-16 (AMD dev box), both closed.**
TEST B ran and DPMS-off is **not** the trigger: with hypridle driving the
blank, input woke it in all three configurations including clamshell-docked
on the sole live output. The mitigation was reverted and #135 retired
unbuilt, which also voids TEST A (its premise — the skip — no longer
exists). Real cause: Hyprland's input→DPMS options ship off, so hypridle's
`on-resume` was the only caller of `dpms on`, and hypridle deadlocks
(hyprwm/hypridle#171) without exiting. Full record: ROADMAP § "#127
solved"; the surviving bug is BACKLOG #146. **Method note worth keeping:**
the *first* TEST B was invalid — it ran `dpms off` by hand, which no wake
path watches, so its "input did not wake it" was guaranteed regardless of
hardware. Drive the real code path or measure nothing.
- [ ] **#127 fix from config, not a runtime keyword (AMD dev box)** —
the fix was proven with `hyprctl keyword` (runtime-only); this checks our
Nix actually emits it and that it survives a session. **After
`nomarchy-home` + relogin:**
1. `hyprctl getoption misc:key_press_enables_dpms` and
`misc:mouse_move_enables_dpms` → both `int: 1` with **no** manual
keyword set.
2. Docked, **lid closed**, idle 10+ min → the external **blanks** (it no
longer stays lit; that is the revert working).
3. Press a key → it comes back. `journalctl --user -t
nomarchy-display-wake --since '-15 min'` shows a `done enabled=…` line
(hypridle's on-resume also ran).
4. The one that matters — the brick condition on purpose:
`systemctl --user stop hypridle`, then
`( sleep 45; hyprctl dispatch dpms on ) &` and
`hyprctl dispatch dpms off`. **Pass:** a keypress wakes it with
hypridle stopped (it did in 6s on 2026-07-16 via runtime keyword).
Then `systemctl --user start hypridle`.
- [ ] **#96 battery-limit row on threshold-less firmware (Acer M5-481T)** —
on the Acer (live or installed session with the commit carrying this
entry): open System (`SUPER+CTRL+I`). **Pass:** a "Battery limit" row

View File

@@ -19,6 +19,39 @@ Template:
---
## 2026-07-16 — #127 SOLVED: the brick was never DPMS, and TEST B's first run lied
- **Task:** #127 TEST B (Bernardo ran it), then the whole sweep his result forced.
- **Read this if nothing else — the test measured nothing, and said so.** His run
reported "input never wakes it" **docked *and* undocked alike**, and that tie is
the tell: the theory demanded those differ. It ran `dpms off` **by hand**, which
no wake path watches, so "input didn't wake it" was guaranteed on any hardware
in any dock state. Re-run *through* hypridle so `on-resume` is actually armed:
input woke it in all 3 configs, **including clamshell on the sole live output**
(`done enabled=1 first=DP-2`) — the incident's exact geometry. Put the mechanism
under test inside the circuit, or you are measuring your own scaffolding.
- **Real cause:** Hyprland ships `misc:{key_press,mouse_move}_enables_dpms` **off**,
so hypridle's `on-resume` was the ONLY caller of `dpms on` anywhere in the session
— and hypridle deadlocks (hyprwm/hypridle#171, open since 2025-09, in neither
0.1.7 nor main) **without exiting**, so `Restart=always` never fires and systemd
still reports it active. Blank + deadlock = a black seat no key escapes. **19× in
11 days** on his box, incl. the incident day. Two mundane bugs, not one exotic one.
- **Did:** both options on — structural: the wake moves into the compositor where no
daemon can lose it, so a dead hypridle costs auto-lock, not the machine. Reverted
the clamshell skip; **#135 retired unbuilt**; ROADMAP § "#127 solved"; filed #146
(hypridle mortality) + #147 (relogin leaves session units dead) → PROPOSED.
- **Verified: V3 on the incident hardware.** Clamshell, hypridle **deliberately
stopped** (brick condition on purpose), `dpms off` → keypress woke it in **6s**;
the identical test before the options stayed black the full 45s. V1: `flake check`
+ `option-docs`/`clamshell-logind`/`docking-ux`/`display-profiles` pass, and I read
the **emitted** configs, not the source (`key_press_enables_dpms=true`;
`on-timeout=hyprctl dispatch dpms off`, skip script gone).
- **Pending:** V3 — proven via runtime `hyprctl keyword`, NOT from our Nix yet;
HARDWARE-QUEUE entry re-checks after `nomarchy-home` + relogin. Ctrl+Alt+F3 dead
during the incident is still unexplained (upstream co-symptom, hypridle#145).
- **Next suggestion:** #147 before #146 — a guard that restarts hypridle into a dead
compositor just fail-loops into `start-limit-hit` exactly like cliphist does. Both
are PROPOSED; Bernardo triages.
## 2026-07-16 — #129/#130 buttons; #133; #132; branding; and a latch bug from TEST A
- **Task:** Bernardo triaged the queue (#136 settled, #130 decided, #143 → wait,
#120 → PROPOSED), then #133, #132, the codename drop, and #129+#130.