diff --git a/README.md b/README.md index 9364e5d..83517d8 100644 --- a/README.md +++ b/README.md @@ -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.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.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.yazi.enable` | `true` | yazi TUI file manager, themed + curated plugins | | `nomarchy.osd.enable` | `true` | swayosd on-screen display for volume/brightness/mute | diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index dccbfe3..c737fee 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -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 -~60–100W 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 diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index de9394c..83afe7b 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -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 diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index 229f64f..129d9c6 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 2376509..b061d8a 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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 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. + **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 `state.json` and still accepts the legacy `theme-state.json`, because 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 encrypted hibernate phase so resume is LUKS-only (not hyprlock twice). Menu row self-gates on CanHibernate. V3: HARDWARE-QUEUE bag-carry test. -- **Clamshell idle DPMS (#127 mitigation):** after an idle brick (alive - machine, black external, undock no eDP, no usable Ctrl+Alt+F3), hypridle - **skips DPMS-off** when any laptop internal is present but not enabled. - Lock at 5 min unchanged; undocked still blanks at 10 min. - **Say clamshell, not "docked" (#136, settled 2026-07-16):** the gate is - "an internal exists and is switched off" — the lid shut — and Bernardo - confirms the incident was exactly that, laptop closed. Docked with the lid - **open** has every output enabled, falls straight through, and still blanks; - whether *that* can brick is untested and not claimed either way. The earlier - wording ("skip when docked") promised twice the coverage the code has, which - is how a recurrence gets misread as a regression. - Intentional DPMS-when-clamshell is LATER (BACKLOG) once wake is proven. - Tooling kept: `nomarchy-display-wake` / `nomarchy-display-dump`. +- ✓ **#127 solved — the idle brick was never DPMS (2026-07-16, AMD dev box):** + the clamshell DPMS-off skip (060bf52, and its #136 wording fix) is + **reverted**; idle blanks again in every dock/lid state. Hyprland ships + `misc:key_press_enables_dpms` and `misc:mouse_move_enables_dpms` **off**, and + nothing else in the session calls `dpms on` — so hypridle's `on-resume` was + the *only* thing that could un-blank a screen: 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 is the brick: two mundane bugs, not one exotic DRM state — the same + 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): 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 diff --git a/modules/home/hyprland.nix b/modules/home/hyprland.nix index 6a14fee..c0902da 100644 --- a/modules/home/hyprland.nix +++ b/modules/home/hyprland.nix @@ -1097,6 +1097,20 @@ in # instead of compositor black — on light themes the black flash # reads as a glitch (item 28c, seen in the capture harness). 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 diff --git a/modules/home/idle.nix b/modules/home/idle.nix index 2cf34ff..eecae92 100644 --- a/modules/home/idle.nix +++ b/modules/home/idle.nix @@ -51,34 +51,6 @@ let inherit pkgs; displayTransition = miniTransition; }).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 { config = lib.mkIf cfg.idle.enable { @@ -182,8 +154,14 @@ in { timeout = 300; on-timeout = "loginctl lock-session"; } { timeout = 600; - # Docked: skip blanking sole output (#127). Undocked: DPMS off. - on-timeout = "${dpmsOff}"; + # Blanks in every dock/lid state — the #127 clamshell skip is + # 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}"; } # Suspend only on battery, and sooner than the old fixed 30 min