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

@@ -258,7 +258,7 @@ two tables below are split along exactly that line.
| `nomarchy.batteryNotify.enable` | `true` | Low-battery toasts at the bar's thresholds — 25% low, 10% critical (stays up until dismissed); silent no-op on machines without a battery | | `nomarchy.batteryNotify.enable` | `true` | Low-battery toasts at the bar's thresholds — 25% low, 10% critical (stays up until dismissed); silent no-op on machines without a battery |
| `nomarchy.dockAudio.enable` | `true` | On a fresh external-monitor plug, PipeWire/WirePlumber are reprobed after hardware settles and the highest-priority available HDMI/DP/USB sink becomes default, with a toast and journal result. A later manual speaker choice sticks until the next physical plug; unplug falls back to built-in. Manual route: System Audio | | `nomarchy.dockAudio.enable` | `true` | On a fresh external-monitor plug, PipeWire/WirePlumber are reprobed after hardware settles and the highest-priority available HDMI/DP/USB sink becomes default, with a toast and journal result. A later manual speaker choice sticks until the next physical plug; unplug falls back to built-in. Manual route: System Audio |
| `nomarchy.firstBootWelcome.enable` | `true` | One dismissible “you're set” toast on the first session (SUPER+M / SUPER+T / SUPER+? + network pointer); marker is `settings.firstBootShown` in the flake checkout | | `nomarchy.firstBootWelcome.enable` | `true` | One dismissible “you're set” toast on the first session (SUPER+M / SUPER+T / SUPER+? + network pointer); marker is `settings.firstBootShown` in the flake checkout |
| `nomarchy.idle.enable` | `true` | hyprlock + hypridle (lock 5 min; display off 10 min **except when docked** — laptop internal present but disabled skips DPMS, #127; suspend 15 min battery-only via `nomarchy-suspend`) | | `nomarchy.idle.enable` | `true` | hyprlock + hypridle (lock 5 min; display off 10 min in every dock/lid state — input wakes it compositor-side, #127; suspend 15 min battery-only via `nomarchy-suspend`) |
| `nomarchy.idle.fingerprint` | `false` | Unlock the lock screen with a fingerprint as well as the password, and say so on the input field. Set it alongside `nomarchy.hardware.fingerprint.pam` in system.nix: hyprlock is configured from Home Manager, which cannot read the NixOS option — and hyprlock does **not** take a fingerprint through PAM at all (its PAM stack runs only on submit), so it uses its own fprintd backend that this switch turns on | | `nomarchy.idle.fingerprint` | `false` | Unlock the lock screen with a fingerprint as well as the password, and say so on the input field. Set it alongside `nomarchy.hardware.fingerprint.pam` in system.nix: hyprlock is configured from Home Manager, which cannot read the NixOS option — and hyprlock does **not** take a fingerprint through PAM at all (its PAM stack runs only on submit), so it uses its own fprintd backend that this switch turns on |
| `nomarchy.yazi.enable` | `true` | yazi TUI file manager, themed + curated plugins | | `nomarchy.yazi.enable` | `true` | yazi TUI file manager, themed + curated plugins |
| `nomarchy.osd.enable` | `true` | swayosd on-screen display for volume/brightness/mute | | `nomarchy.osd.enable` | `true` | swayosd on-screen display for volume/brightness/mute |

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. flake pin are different root causes even when they show up on the same machine.
(Terminal / Ghostty-on-Acer → shipped as Kitty-only, #95.) (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 ## 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 ### 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 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 ## 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, - **Wallpapers artifact split** (ROADMAP § Faster switches — decided,
deferred): pinned `Nomarchy-wallpapers` input so a state write stops deferred): pinned `Nomarchy-wallpapers` input so a state write stops
re-copying 86 MB. Follow-on: pre-built theme variants if switches are 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 ### 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 ### 120. A netinstall ISO, next to the fat offline one
**Deferred to PROPOSED 2026-07-16 (Bernardo): not now.** Nothing below is **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 | | # | Item | Why first |
|---|------|-----------| |---|------|-----------|
| 1 | **#115** suspend-then-hibernate | Just shipped; quick path is minutes (battery vs AC + `nomarchy-suspend`) | | 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 | | 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` | | 4 | **#104** airplane mode | Fast radio smoke after `nomarchy-home` |
| 5 | **#101** charge-limit USB-C burst | Dock plug/unplug storm; pairs with #3 | | 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 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 off, it must stay off after disengage. Glyph must **not** appear when
airplane is off. airplane is off.
- [ ] **#127 clamshell idle — TEST A, the mitigation holds (AMD dev box)** — - [x] **#127 TESTS A + B — ANSWERED 2026-07-16 (AMD dev box), both closed.**
**Context:** original brick = clamshell-docked idle → black, no input TEST B ran and DPMS-off is **not** the trigger: with hypridle driving the
wake, undock no eDP, Caps Lock alive; Ctrl+Alt+F3 gave no usable TTY. blank, input woke it in all three configurations including clamshell-docked
**Shipped mitigation:** skip DPMS-off while clamshell (internal on the sole live output. The mitigation was reverted and #135 retired
present-but-disabled); lock at 5m still. unbuilt, which also voids TEST A (its premise — the skip — no longer
**After `nomarchy-home` + relogin:** exists). Real cause: Hyprland's input→DPMS options ship off, so hypridle's
1. Dock, **lid closed** (eDP in `hyprctl monitors all -j`, absent from `on-resume` was the only caller of `dpms on`, and hypridle deadlocks
`hyprctl monitors`). (hyprwm/hypridle#171) without exiting. Full record: ROADMAP § "#127
2. Leave it entirely alone past 10+ min. solved"; the surviving bug is BACKLOG #146. **Method note worth keeping:**
3. **Pass:** external still shows hyprlock (not black power-save); the *first* TEST B was invalid — it ran `dpms off` by hand, which no wake
`journalctl --user -t nomarchy-idle --since '-20 min'` has path watches, so its "input did not wake it" was guaranteed regardless of
`dpms-off skipped: clamshell`. hardware. Drive the real code path or measure nothing.
4. Undocked control: eDP only → at 10 min it *does* blank; input wakes. - [ ] **#127 fix from config, not a runtime keyword (AMD dev box)** —
Optional: `nomarchy-display-dump` / SUPER+SHIFT+D for other display faults. the fix was proven with `hyprctl keyword` (runtime-only); this checks our
- [ ] **#127 TEST B — was DPMS-off the trigger at all? (AMD dev box)** — Nix actually emits it and that it survives a session. **After
**This one outranks #135**: the mitigation is a cure for an *inferred* `nomarchy-home` + relogin:**
cause, and if the inference is wrong we are paying for it forever while 1. `hyprctl getoption misc:key_press_enables_dpms` and
the brick stays live. Bounded and self-recovering (there is no sshd on `misc:mouse_move_enables_dpms` → both `int: 1` with **no** manual
that box). Full protocol + how to read each outcome: **BACKLOG #127 § keyword set.
"The two tests, exactly"**. In short — docked, lid closed: 2. Docked, **lid closed**, idle 10+ min → the external **blanks** (it no
```sh longer stays lit; that is the revert working).
( sleep 45; hyprctl dispatch dpms on; hyprctl reload ) & 3. Press a key → it comes back. `journalctl --user -t
hyprctl dispatch dpms off nomarchy-display-wake --since '-15 min'` shows a `done enabled=…` line
``` (hypridle's on-resume also ran).
then (1) try to wake it with input in the first ~40s, (2) let the 45s 4. The one that matters — the brick condition on purpose:
recovery run, (3) open the lid if still dark. **Input wakes it** → the `systemctl --user stop hypridle`, then
mitigation is pointless, revert it and #135 never exists. **Dark until `( sleep 45; hyprctl dispatch dpms on ) &` and
recovery** → mitigation confirmed, #135 is the opt-in. **Dark after `hyprctl dispatch dpms off`. **Pass:** a keypress wakes it with
recovery** → the brick, now reproducible on demand, which is the best hypridle stopped (it did in 6s on 2026-07-16 via runtime keyword).
outcome available. Then `systemctl --user start hypridle`.
- [ ] **#96 battery-limit row on threshold-less firmware (Acer M5-481T)** — - [ ] **#96 battery-limit row on threshold-less firmware (Acer M5-481T)** —
on the Acer (live or installed session with the commit carrying this on the Acer (live or installed session with the commit carrying this
entry): open System (`SUPER+CTRL+I`). **Pass:** a "Battery limit" row 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 ## 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, - **Task:** Bernardo triaged the queue (#136 settled, #130 decided, #143 → wait,
#120 → PROPOSED), then #133, #132, the codename drop, and #129+#130. #120 → PROPOSED), then #133, #132, the codename drop, and #129+#130.

View File

@@ -622,6 +622,10 @@ Design/decision records and a running log of shipped work (items marked
that could not run — two mundane bugs, not one exotic DRM state. The shipped that could not run — two mundane bugs, not one exotic DRM state. The shipped
DPMS mitigation may therefore be treating the wrong cause; #127's TEST B is DPMS mitigation may therefore be treating the wrong cause; #127's TEST B is
what decides, and it is far safer now that an undock works again. what decides, and it is far safer now that an undock works again.
**It did decide, the same day: the hunch was right on both counts** — the
mitigation *was* treating the wrong cause and is now reverted, and the brick
*was* two mundane bugs (an ordinary blank + a wake path that had died). See
"#127 solved" below.
- ✓ **The #107 compat shim is pinned (#133, 2026-07-16):** `mkFlake` prefers - ✓ **The #107 compat shim is pinned (#133, 2026-07-16):** `mkFlake` prefers
`state.json` and still accepts the legacy `theme-state.json`, because a `state.json` and still accepts the legacy `theme-state.json`, because a
machine only migrates on its next menu write — but nothing evaluated a machine only migrates on its next menu write — but nothing evaluated a
@@ -723,19 +727,44 @@ Design/decision records and a running log of shipped work (items marked
`nomarchy-suspend` (hypridle + Power menu). LUKS: unlock session before `nomarchy-suspend` (hypridle + Power menu). LUKS: unlock session before
encrypted hibernate phase so resume is LUKS-only (not hyprlock twice). encrypted hibernate phase so resume is LUKS-only (not hyprlock twice).
Menu row self-gates on CanHibernate. V3: HARDWARE-QUEUE bag-carry test. Menu row self-gates on CanHibernate. V3: HARDWARE-QUEUE bag-carry test.
- **Clamshell idle DPMS (#127 mitigation):** after an idle brick (alive - ✓ **#127 solved — the idle brick was never DPMS (2026-07-16, AMD dev box):**
machine, black external, undock no eDP, no usable Ctrl+Alt+F3), hypridle the clamshell DPMS-off skip (060bf52, and its #136 wording fix) is
**skips DPMS-off** when any laptop internal is present but not enabled. **reverted**; idle blanks again in every dock/lid state. Hyprland ships
Lock at 5 min unchanged; undocked still blanks at 10 min. `misc:key_press_enables_dpms` and `misc:mouse_move_enables_dpms` **off**, and
**Say clamshell, not "docked" (#136, settled 2026-07-16):** the gate is nothing else in the session calls `dpms on` — so hypridle's `on-resume` was
"an internal exists and is switched off" — the lid shut — and Bernardo the *only* thing that could un-blank a screen: an idle daemon with the
confirms the incident was exactly that, laptop closed. Docked with the lid display held hostage behind it. hypridle then deadlocks on an unfixed
**open** has every output enabled, falls straight through, and still blanks; upstream bug (hyprwm/hypridle#171, "Disconnected from pollfd id 1", open
whether *that* can brick is untested and not claimed either way. The earlier since 2025-09, absent from 0.1.7 *and* main) and hangs **without exiting**,
wording ("skip when docked") promised twice the coverage the code has, which so `Restart=always` never fires and systemd still reports it
is how a recurrence gets misread as a regression. `active (running)`. Blank + deadlock = a black seat no keypress can escape.
Intentional DPMS-when-clamshell is LATER (BACKLOG) once wake is proven. That is the brick: two mundane bugs, not one exotic DRM state — the same
Tooling kept: `nomarchy-display-wake` / `nomarchy-display-dump`. shape as the #100 latch above.
**Evidence, on the incident hardware.** With hypridle driving the blank,
input woke it in all three configurations — including clamshell-docked on the
sole live output (`nomarchy-display-wake done enabled=1 first=DP-2`), which
is the incident's exact geometry. So blanking-while-clamshell is innocent and
the gate guarded nothing. Then the decisive run: same box, still clamshell,
hypridle **deliberately stopped** (i.e. the brick condition manufactured on
purpose), `dpms off` → a keypress woke it in **6 s** with the two options on.
The identical test before them stayed black the full 45 s.
**Why the first TEST B said the opposite, and the lesson.** It ran `hyprctl
dispatch dpms off` **by hand**, which no wake path watches, so "input did not
wake it" was guaranteed on any hardware, in any dock state. The tell was the
undocked control returning the *same* result where the theory demanded a
different one. A test must put the mechanism under test inside the circuit —
this one measured a wake path that was never armed, and nearly bought a
permanent behaviour cost with the answer.
**Why the fix is structural.** The wake now lives in the compositor, where no
daemon can lose it; a dead hypridle costs auto-lock, not the machine. #135
(an option to opt back into DPMS-when-clamshell) is therefore **retired
unbuilt** — there is nothing left to opt into, and the 27" panels stop being
held lit all night under a static lock screen.
**Still open:** hypridle's silent death is its own bug (~2×/week on this box;
no auto-lock is a security cost even when the screen is recoverable), and the
incident's dead Ctrl+Alt+F3 remains unexplained — a known co-symptom upstream
(hyprwm/hypridle#145) but not accounted for here. Tooling kept:
`nomarchy-display-wake` / `nomarchy-display-dump`.
- ✓ **Generation prune (#128):** weekly `nomarchy-gen-prune` (system + HM): - ✓ **Generation prune (#128):** weekly `nomarchy-gen-prune` (system + HM):
delete only gens older than **14 days** that are also beyond the **3 most delete only gens older than **14 days** that are also beyond the **3 most
recent past** gens (current always kept). Stock `nix.gc` no longer uses recent past** gens (current always kept). Stock `nix.gc` no longer uses

View File

@@ -1097,6 +1097,20 @@ in
# instead of compositor black — on light themes the black flash # instead of compositor black — on light themes the black flash
# reads as a glitch (item 28c, seen in the capture harness). # reads as a glitch (item 28c, seen in the capture harness).
background_color = rgb c.base; background_color = rgb c.base;
# #127, and the whole answer to it. Hyprland ships both of these
# OFF, so nothing in the compositor turned a blanked screen back
# on: hypridle's on-resume was the ONLY caller of `dpms on` in the
# session — an idle daemon as a single point of failure with the
# display behind it. hypridle then deadlocks on an unfixed upstream
# bug (hyprwm/hypridle#171: "Disconnected from pollfd id 1", open,
# not in 0.1.7 or main) and hangs WITHOUT exiting, so Restart=always
# never fires and systemd still reports it active. Blank + deadlock
# = a black seat no keypress could escape. That was the brick.
# These put the wake in the compositor, where no daemon can lose it;
# a dead hypridle now costs auto-lock, not the machine.
key_press_enables_dpms = lib.mkDefault true;
mouse_move_enables_dpms = lib.mkDefault true;
}; };
# First boot of every installed image was showing "Hyprland updated to # First boot of every installed image was showing "Hyprland updated to

View File

@@ -51,34 +51,6 @@ let
inherit pkgs; inherit pkgs;
displayTransition = miniTransition; displayTransition = miniTransition;
}).displayWakeTool; }).displayWakeTool;
# #127 mitigation (2026-07-15): blanking the *only* live output with the
# lid shut (eDP disabled) left a black seat that sometimes would not
# recover (and even VT switch looked dead). Skip DPMS-off when any laptop
# internal is present but not enabled — i.e. CLAMSHELL, which is narrower
# than "docked" and is deliberately what the incident was (#136, Bernardo:
# laptop closed). Docked with the lid OPEN keeps every output enabled and
# still blanks; that case is untested, not exempted. Lock still runs at 5 min.
# Revisit: intentional DPMS-on-dock once wake is trustworthy — see
# BACKLOG #127 and LATER "DPMS when docked".
dpmsOff = pkgs.writeShellScript "nomarchy-dpms-off" ''
set -euo pipefail
jq=${pkgs.jq}/bin/jq
internals=$(hyprctl monitors all -j 2>/dev/null \
| $jq -r '.[] | select(.name | test("^(eDP|LVDS|DSI)")) | .name' \
2>/dev/null || true)
if [ -n "$internals" ]; then
for m in $internals; do
if ! hyprctl monitors -j 2>/dev/null \
| $jq -e --arg m "$m" 'any(.[]; .name == $m)' >/dev/null 2>&1; then
${pkgs.util-linux}/bin/logger -t nomarchy-idle -- \
"dpms-off skipped: clamshell (internal $m present but off)"
exit 0
fi
done
fi
hyprctl dispatch dpms off
'';
in in
{ {
config = lib.mkIf cfg.idle.enable { config = lib.mkIf cfg.idle.enable {
@@ -182,8 +154,14 @@ in
{ timeout = 300; on-timeout = "loginctl lock-session"; } { timeout = 300; on-timeout = "loginctl lock-session"; }
{ {
timeout = 600; timeout = 600;
# Docked: skip blanking sole output (#127). Undocked: DPMS off. # Blanks in every dock/lid state — the #127 clamshell skip is
on-timeout = "${dpmsOff}"; # gone: it was a cure for a cause that does not exist (proven on
# hardware 2026-07-16, see ROADMAP). The wake no longer depends
# on this daemon surviving — misc:{key_press,mouse_move}_enables_dpms
# in hyprland.nix means input wakes the screen compositor-side.
on-timeout = "hyprctl dispatch dpms off";
# Same rescue path as after_sleep (#127): re-enables a disabled
# internal, which plain `dpms on` cannot do.
on-resume = "${lib.getExe displayWake}"; on-resume = "${lib.getExe displayWake}";
} }
# Suspend only on battery, and sooner than the old fixed 30 min # Suspend only on battery, and sooner than the old fixed 30 min