Proof Portal

Project overview

Omarchy

ProbeLabs65 findings · 95 requirements

A proof layer — requirements, tests and verified fixes — for two of Omarchy's subsystems: the application menu (launcher scripts, QML model, JSONC config, search and selection) and the lock screen (lock scripts, QML, PAM authentication). Scope is deliberately limited to those components of omacom/omarchy; the rest of the distribution is not covered.

Inbox
ReportR-655GitHub issuePublicValidated· Reproduced

Lock screen blanks ~5 seconds after resume: suspend-gap guard re-arms the blank timer instead of holding the display

Reported by callumau · filed Sep 12, 2026 · open at source

Validation verdict: Reproduced. An engine-executed run exited 1 and showed the defect signal. Awaiting a human decision.

At a glance

1 · What is claimed?
  • ConfirmedReproducible: after a resume the frozen idle countdown re-arms and the lock screen blanks about 5 s later with no input; a resume 5-7 s after arming blanks at once
  • Not confirmedAffects the lock component
2 · What does it affect?
AreaIn the audited arealock· Decided by agents · verified
3 · Is it reproducible?
An engine-executed run exited 1 and showed the defect signal. See reproducer
4 · What remains unclear?
  • Open question for maintainers: Open decision: after a resume from suspend, what should the lock screen do with the displays? Options: (A) keep the current behaviour (re-arm, blank one 5 s interval after the resume; SW-REQ-260912-ND55 as written); (B) stay lit until the next user input (the report's proposal); (C) stay lit for a grace period chosen by the maintainers, then blank.
  • Open question for maintainers: Related, independent of the option above: should a resume 5-7 s after the countdown was armed (neither branch of the guard today; blanks at once) count as a resume? Approving an option amends the suspend clause of SW-REQ-260912-ND55.
  • Open question for maintainers: The decision is the Omarchy maintainers'; a draft requirement listing these options sits on this report's branch (report/cmulr8v3s0je91gw4wt4k3bat), and approving one option = merging that draft into the baseline with the chosen text.
  • Affects the lock component — no requirement of the lock component is confirmed as violated
5 · What happens next?
Our staff decide whether the report is valid. Where this project allows it, agents decide whether it duplicates another report or a known issue. So far: in the audited area (lock) — decided by agents · verified; not a duplicate of another report — decided by agents · verified; intended behaviour — a design question, not a defect — decided by agents · verified. The outcome (valid, duplicate, not a bug, …) is not decided yet. See the decision.

Open question — draft spec awaiting a decision

Draft — awaiting a decisionSW-REQ-261005-4S1Mcomponent lockrefines When the user locks the session, the system shall engage… SYS-REQ-260912-T0XP

After a resume from suspend, what shall the lock screen do with the displays?

What the code does today

(upstream quattro 035ce29f) idleBlankTimer (Service.qml:730-748, interval 5000 ms) re-arms when wall-clock time shows the countdown froze through a suspend (Service.qml:739-742), so with no input the displays blank 5 s after the resume; a resume between 5 s and 7 s after arming takes neither branch and blanks at once. This is what SW-REQ-260912-ND55 says today.

Options (listed neutrally — the choice is the maintainers')
  1. AKeep the current behaviour
  2. BAfter a resume the displays shall stay lit until the next user input
  3. CAfter a resume the displays shall stay lit for a grace period chosen by the maintainers, then blank

Separately: whether a resume 5-7 s after arming counts as a resume. Approving this draft means choosing one option; it then amends the suspend clause of SW-REQ-260912-ND55.

Why this is an open question

Raised by omacom/omarchy#11411 (proof inbox R-655). The validation (report/cmulr8v3s0je91gw4wt4k3bat, rval-1de6fd92d7df97879d5e2230af3730de) reproduced the blank 5.0 s after the resume-time fire and the immediate blank in the 5-7 s window with the real Service.qml timer. SW-REQ-260912-ND55 specifies the re-arm, while its own rationale states the intent not to blank the freshly woken unlock screen: the requirement and its intent disagree. The reporter's follow-up in the issue tested a 60 s blank interval on a Thunderbolt dock.

Raised inomacom/omarchy#11411 (upstream) R-655 (this report)

Proposed on this report's branch @ 31eeed97ea · specs/software/requirements/SW-REQ-261005-4S1M.req.yaml · not part of the audited spec until approved and merged

Written after the validation was sealed at 790596e81f: the seal covers the reproducer and the observed behaviour, not this proposed text.

Original report

Lock screen blanks ~5 seconds after resume: suspend-gap guard re-arms the blank timer instead of holding the displayrevision 1 · captured 2026-09-28 21:23 UTC via poll

System details

Framework Laptop 13 (AMD Ryzen 5 7640U, Radeon 760M, amdgpu), Thunderbolt dock with 2x DELL S2721DGF over DP, Omarchy 4.0.3-1, Hyprland 0.56.2-2, quickshell 0.3.1-1, kernel 7.2.3-arch1-3.

(omarchy debug is not an available command in 4.0.3, so no debug log; journal timeline included below.)

What's wrong?

After waking the laptop from suspend, the lock screen is visible for about five seconds, then all displays blank (DPMS off). Typing the password still works — each keypress wakes the displays again (submitPassword() -> runWake()) — but the freshly woken lock screen goes dark under the user.

The Thunderbolt dock makes this easy to hit: on resume the dock's DP outputs re-enumerate and a recreated output comes back with DPMS on (the same mechanism documented in #11330's root-cause analysis), so the lock screen lights up on its own without any input — and then the blank timer turns it off again.

Steps to reproduce

  1. Dock the laptop, lock the session, close the lid / let it suspend.
  2. Wake the machine.
  3. The lock screen appears, then blanks ~5 seconds after resume.

Root cause

shell/plugins/lock/Service.qml arms a 5-second blank timer at lock and re-arms it on every wake:

  • idleBlankTimer (interval: 5000) -> blankProcess -> omarchy-brightness-display off -> hyprctl dispatch 'hl.dsp.dpms({ action = "disable" })'
  • runWake() re-arms it on every input while locked

QML timers freeze across suspend and fire right after resume. The wall-clock guard in idleBlankTimer.onTriggered exists for exactly this case, but it re-arms the timer instead of cancelling it:

onTriggered: {
  // A countdown frozen by suspend fires right after resume, which would
  // blank the freshly woken unlock screen under the user. Wall-clock time
  // exposes the gap: take a fresh run-up instead of blanking.
  if (Date.now() - armedAt > interval + 2000) {
    root.armBlankTimer()
    return
  }
  ...
}

The comment states the intent — don't blank the freshly woken unlock screen under the user — but re-arming guarantees exactly that blank, 5 seconds after resume. The guard postpones the blank by one interval; it does not prevent it.

Journal timeline from one cycle (lock ~64s before suspend, so the timer had already fired and the displays were DPMS-off before sleep; the dock's output re-enumeration at resume is what lit them again):

TimeEvent
12:28:11lock-requested / secure=true (blank timer armed, fires at 12:28:16, displays DPMS-off)
12:29:15PM: suspend entry (s2idle)
12:34:08PM: suspend exit — dock outputs re-enumerate, panels light with DPMS reset to on
12:34:13 (approx)lock screen blanks again
12:34:16user types password -> runWake() -> displays on -> unlocked

The same blank also lands when the lock happens seconds before suspend (lid-close -> omarchy-sleep-lock -> logind suspends right after secure=true): the timer is frozen mid-countdown and blanks 5s after resume even without the output-recreation path.

Expected

After waking the machine, the lock screen stays visible until the user interacts. Any input already re-arms the normal 5s countdown via runWake().

Actual

The lock screen goes dark ~5 seconds after resume; the user must type blind or press a key to bring it back.

Proposed fix

On the suspend-gap branch, hold the display instead of scheduling a blank:

if (Date.now() - armedAt > interval + 2000) {
  // Freshly resumed: the user woke this machine to unlock it. Hold the
  // display up; their next input re-arms the normal countdown via runWake().
  idleBlankTimer.stop()
  return
}

Trade-off: an unattended wake (WoL, rtcwake) leaves the lock screen lit until input. That is cosmetic, and the same is already true today whenever an output is recreated with DPMS reset — which the quattro branch already acknowledges ("A panel coming back is a display turning on that runWake did not ask for"). A more conservative variant would re-arm with a longer post-resume grace (30-60s).

Still present on the quattro branch as of this report.

Related issues

  • #11330 — output recreation resets DPMS to on (why the lock screen lights up on its own at resume)
  • #8361 — lock screen loses keyboard focus after resume (compounds this: after the blank, a click may be needed before typing works)
View raw text
### System details

Framework Laptop 13 (AMD Ryzen 5 7640U, Radeon 760M, amdgpu), Thunderbolt dock with 2x DELL S2721DGF over DP, Omarchy 4.0.3-1, Hyprland 0.56.2-2, quickshell 0.3.1-1, kernel 7.2.3-arch1-3.

(`omarchy debug` is not an available command in 4.0.3, so no debug log; journal timeline included below.)

### What's wrong?

After waking the laptop from suspend, the lock screen is visible for about five seconds, then all displays blank (DPMS off). Typing the password still works — each keypress wakes the displays again (`submitPassword()` -> `runWake()`) — but the freshly woken lock screen goes dark under the user.

The Thunderbolt dock makes this easy to hit: on resume the dock's DP outputs re-enumerate and a recreated output comes back with DPMS **on** (the same mechanism documented in #11330's root-cause analysis), so the lock screen lights up on its own without any input — and then the blank timer turns it off again.

**Steps to reproduce**

1. Dock the laptop, lock the session, close the lid / let it suspend.
2. Wake the machine.
3. The lock screen appears, then blanks ~5 seconds after resume.

**Root cause**

`shell/plugins/lock/Service.qml` arms a 5-second blank timer at lock and re-arms it on every wake:

- `idleBlankTimer` (`interval: 5000`) -> `blankProcess` -> `omarchy-brightness-display off` -> `hyprctl dispatch 'hl.dsp.dpms({ action = "disable" })'`
- `runWake()` re-arms it on every input while locked

QML timers freeze across suspend and fire right after resume. The wall-clock guard in `idleBlankTimer.onTriggered` exists for exactly this case, but it **re-arms** the timer instead of cancelling it:

```qml
onTriggered: {
  // A countdown frozen by suspend fires right after resume, which would
  // blank the freshly woken unlock screen under the user. Wall-clock time
  // exposes the gap: take a fresh run-up instead of blanking.
  if (Date.now() - armedAt > interval + 2000) {
    root.armBlankTimer()
    return
  }
  ...
}
```

The comment states the intent — don't blank the freshly woken unlock screen under the user — but re-arming guarantees exactly that blank, 5 seconds after resume. The guard postpones the blank by one interval; it does not prevent it.

Journal timeline from one cycle (lock ~64s before suspend, so the timer had already fired and the displays were DPMS-off before sleep; the dock's output re-enumeration at resume is what lit them again):

| Time | Event |
|---|---|
| 12:28:11 | `lock-requested` / `secure=true` (blank timer armed, fires at 12:28:16, displays DPMS-off) |
| 12:29:15 | `PM: suspend entry (s2idle)` |
| 12:34:08 | `PM: suspend exit` — dock outputs re-enumerate, panels light with DPMS reset to on |
| 12:34:13 (approx) | lock screen blanks again |
| 12:34:16 | user types password -> `runWake()` -> displays on -> `unlocked` |

The same blank also lands when the lock happens seconds *before* suspend (lid-close -> `omarchy-sleep-lock` -> logind suspends right after `secure=true`): the timer is frozen mid-countdown and blanks 5s after resume even without the output-recreation path.

**Expected**

After waking the machine, the lock screen stays visible until the user interacts. Any input already re-arms the normal 5s countdown via `runWake()`.

**Actual**

The lock screen goes dark ~5 seconds after resume; the user must type blind or press a key to bring it back.

**Proposed fix**

On the suspend-gap branch, hold the display instead of scheduling a blank:

```qml
if (Date.now() - armedAt > interval + 2000) {
  // Freshly resumed: the user woke this machine to unlock it. Hold the
  // display up; their next input re-arms the normal countdown via runWake().
  idleBlankTimer.stop()
  return
}
```

Trade-off: an unattended wake (WoL, rtcwake) leaves the lock screen lit until input. That is cosmetic, and the same is already true today whenever an output is recreated with DPMS reset — which the quattro branch already acknowledges ("A panel coming back is a display turning on that runWake did not ask for"). A more conservative variant would re-arm with a longer post-resume grace (30-60s).

Still present on the `quattro` branch as of this report.

**Related issues**

- #11330 — output recreation resets DPMS to on (why the lock screen lights up on its own at resume)
- #8361 — lock screen loses keyboard focus after resume (compounds this: after the blank, a click may be needed before typing works)

Validation

AI validator claude-code (claude-opus-5.5) · evaluated revision 1 at product commit 1e43373287af · sealed 2026-10-05T14:29:55Z

Validator's summary (commentary, not evidence)

Validation of omacom/omarchy#11411 on quattro-proof 1e433732 (upstream quattro 035ce29f product code plus the proof layer). The reported behaviour reproduces with the real idleBlankTimer of Service.qml (Service.qml:730-748 upstream, interval 5000 ms): a countdown that fires after a suspend (armedAt 20 s in the past) takes the wall-clock guard (Service.qml:739-742) and re-arms, and with no input the displays blank 5 s after that resume-time fire. A resume 6 s after arming (between the 5 s interval and the 7 s guard, the case in the thread's second comment) blanks at the fire itself. This is exactly what SW-REQ-260912-ND55 says ('When wall-clock time shows the timer froze through a suspend, the service shall re-arm the timer instead of blanking'), while ND55's own rationale states the intent that the freshly woken unlock screen is not blanked under the user. The requirement and its stated intent disagree, so the report is not a violation of the spec as written; what the lock should do after a resume is an open specification question for the maintainers. KI-LOCK-SCREENS-CHANGE-BLANK-LOST (screens added/removed while locked) was examined: a different path (onScreensChanged), not this report.

  • ConfirmedReproducible

    Reproducible: after a resume the frozen idle countdown re-arms and the lock screen blanks about 5 s later with no input; a resume 5-7 s after arming blanks at once

    Engine: engine-executed run exited 1 and showed the defect signal

    Evidence: file test/reports/report-cmulr8v3s0je91gw4wt4k3bat.sh

    Validator's rationale (commentary): Observed with the real Service.qml idleBlankTimer: displaysBlank flips 5.0 s after the resume-time fire; with armedAt 6 s back it flips at the fire. This conforms to SW-REQ-260912-ND55 as written; the question is whether ND55 states the intended behaviour.

  • Not confirmedAffects a componentlock

    Affects the lock component

    Engine: no requirement of the lock component is confirmed as violated

Engine reference checks at 790596e81f2c (10/10 ok)
  • okcommit790596e81f2c74b5ecc5728977dc29e1c3720c94
  • okbase commit1e43373287afbd6dca4674ad463d5b5db7812358
  • okbase ancestry1e43373287afbd6dca4674ad463d5b5db7812358
  • okproduct code untouchedthis report's reproducer branch
  • okbranchthis report's reproducer branch
  • okfiletest/reports/report-cmulr8v3s0je91gw4wt4k3bat.qml
  • okfiletest/reports/report-cmulr8v3s0je91gw4wt4k3bat.sh
  • okrequirementSW-REQ-260912-ND55
  • okcomponentlock
  • okknown issueKI-LOCK-SCREENS-CHANGE-BLANK-LOST

The engine's own check, at validation time, that the commits, the reproducer branch (touching only reproducer files), the reproducer files and every mapped requirement / component / known issue really exist.

Validator-authored hypothesis. Each claim's verification is engine-computed: confirmed only from an engine-executed reproducer run recorded in this record, refuted when recorded evidence contradicts it. Prose is commentary. The disposition of the report is a human decision.

Reproducer

engine-runexit 1 · expected exit 0 · defect exit 1 · 5636 ms · 2026-10-05T14:29:49Z→ showed the defect
test/reports/report-cmulr8v3s0je91gw4wt4k3bat.shon this report's reproducer branch @ 790596e81f+ test/reports/report-cmulr8v3s0je91gw4wt4k3bat.qml
bash test/reports/report-cmulr8v3s0je91gw4wt4k3bat.sh

Setup failure: exit 2 means the reproducer could not set up (e.g. the product did not build) — such a run decides nothing.

Defect signal: output contains AS-REPORTED:

Matched lines (engine, over the full output)
AS-REPORTED: after a resume the lock screen blanks about 5 s later with no input (the suspend-gap guard re-arms the countdown); a resume 5-7 s after arming blanks at once
Observed output · 647 bytes
Observed on this tree (quickshell 0.3.1, lock plugin shell/plugins/lock):
  - idleBlankTimer interval 5000 ms; the session is locked, no password check in flight
  - resume 20 s after arming: at the frozen timer's fire the displays stay lit; the timer is re-armed for another 5000 ms
  - with no input after that, the displays blank 5 s after the resume-time fire
  - resume 6 s after arming (between the 5 s interval and the 7 s guard): the displays blank at the fire itself
AS-REPORTED: after a resume the lock screen blanks about 5 s later with no input (the suspend-gap guard re-arms the countdown); a resume 5-7 s after arming blanks at once

Needs: linux · quickshell (0.3.x) and jq on PATH; a sway binary for a private headless compositor (PROOF_ENV_DIR/sysroot/usr/bin/sway, else sway on PATH); the script exits 2 with SETUP: otherwise. Nothing to build: the QML is loaded straight from the worktree · ran on linux/amd64

Linked

  • Related PRpr/8773· confirmed
  • Related PRpr/8561· confirmed

Decision

Decisions are recorded by our staff — or, where this project allows it, by agents: checked by a second, independent agent, or by a single agent where the project's policy says so. Any decision reached so far is shown below.

  • Decided by agents · verified: in the audited area (lock)
  • Decided by agents · verified: not a duplicate of another report
  • Decided by agents · verified: intended behaviour — a design question, not a defect

The outcome (valid, duplicate, not a bug, …) is not decided yet.