Proof Portal
Omarchy
ProbeLabs65 findings · 95 requirementsA proof layer — requirements, tests and verified fixes — for two of Omarchy's subsystems: the application menu (launcher scripts, QML model, JSONC config, search and selection) and the lock screen (lock scripts, QML, PAM authentication). Scope is deliberately limited to those components of omacom/omarchy; the rest of the distribution is not covered.
`pending-charge` treated as a charge-limit hold without checking the level: a failed battery reports "Holding at 75-80%" at 0%
Reported by xeeg · filed Aug 22, 2026 · open at source
At a glance
- 1 · What is claimed?
- Only the report text below — no validator has restated it as checkable claims yet.
- 2 · What does it affect?
- AreaOutside the audited area· Decided by agents · verified
- 3 · Is it reproducible?
- No validation has been run yet.
- 4 · What remains unclear?
- Not validated yet: no claims, no reproducer.
- 5 · What happens next?
- Decided by agents and verified by a second, independent agent — see the decision.
Original report
System details
Lenovo ThinkPad X1 Yoga Gen 6 (20XY0024US), BIOS N32ET75W 1.51, Omarchy 4.0.0-1, kernel 7.1.8-arch1-3, quickshell-git 0.3.0.r20.g28771c7-2, upower 1.91.3-1. Battery: SMP 5B10W13973, 4 cycles, electrically dead (2.296 V on a 15.44 V pack).
What's wrong?
A battery the EC is refusing to charge because it is electrically dead is reported by both the power panel and omarchy-battery-status as a user-configured charge-limit hold:
$ omarchy-battery-status
Battery 0% · Holding at 75-80% · 0W / 58Wh"Holding at 75-80%" while sitting at 0%. The pack is at 2.296 V against a 15.44 V design — a 4S pack at ~0.57 V/cell, far below the ~2.5 V/cell precharge floor — so the EC declines to charge it:
$ cd /sys/class/power_supply/BAT0 && grep . status capacity voltage_now voltage_min_design \
power_now charge_control_start_threshold charge_control_end_threshold charge_behaviour
status:Not charging
capacity:0
voltage_now:2296000
voltage_min_design:15440000
power_now:0
charge_control_start_threshold:0
charge_control_end_threshold:100
charge_behaviour:[auto] inhibit-charge force-dischargeNo charge limit is configured — sysfs reads 0/100, charge_behaviour is auto, TLP is not installed, and nothing in /etc writes a threshold. The panel nevertheless labels the field "Charge limit" with value "Holding", and the mode label reads "Threshold".
Root cause. The kernel's Not charging (UPower pending-charge) means only "AC present, EC is not charging the pack" — for any reason: a threshold hold, a battery fault, an insufficient PD supply, or charge_behaviour=inhibit-charge. Two independent implementations collapse that to "a charge limit is holding it", without checking whether the charge level is anywhere near a limit.
1. `shell/plugins/panels/power/Model.js:59` — no threshold check at all:
function chargeThresholdActive(device, onBattery, states) {
...
if (d.state === s.PendingCharge) return true // <-- unconditional
if (d.state === s.FullyCharged && fraction < 0.99) return true
if (d.state !== s.Charging || fraction >= 0.99) return false
return Number(d.changeRate || 0) <= 0.2 || Number(d.timeToFull || 0) >= 8 * 60 * 60
}This one predicate drives the Charge limit / Holding pair (panels/power/Panel.qml:441-446), the Threshold mode label (Model.js:86), and the battery icon (Model.js:75). Note it never reads a threshold value.
2. `bin/omarchy-battery-status` — guards on a threshold existing, but not on the level:
if [[ $ac_online == "true" && -n $threshold_end ]]; then
if [[ $state == "pending-charge" ]]; then
charge_holding=true # <-- no percentage check
elif [[ $state == "fully-charged" ]] && (( percentage < 99 )); then
charge_holding=true
elif [[ $state == "charging" && $charge_idle == "true" ]] && (( threshold_end < 99 && percentage >= threshold_end )); then
charge_holding=true # <-- this branch gets it right
fi
fiThe third branch already applies the correct discriminator (percentage >= threshold_end). The pending-charge branch doesn't.
Relationship to #7374
Adjacent but distinct — and #7374's fix alone does not cure this; it makes the label worse.
#7374 is a data-source bug: UPower's udev hwdb default CHARGE_LIMIT=75,80 is preferred over sysfs, so the wrong threshold value is displayed. (That default is also where the bogus 75-80% above comes from — this machine has no limit set.)
Here the threshold value is irrelevant, because the classification never consults it. Applying #7374's proposed sysfs preference to this machine yields threshold_end=100, which is still non-empty, so -n $threshold_end still passes and the output becomes:
Battery 0% · Holding at 0-100% · 0W / 58WhThe ThinkPad T14 Gen 6 report in #7374's comments — "there is no charge limit enabled at all, but the power panel still reports one", with sysfs 0/100 and ChargeThresholdEnabled: false — is the fully-charged face of this same classification gap.
Expected
Claim a hold only when a real limit exists and the battery is actually in the holding band. Otherwise pending-charge should surface honestly as "Not charging".
Proposed fix
Gate both implementations on two conditions:
- a real limit is configured —
threshold_end < 100, ideally UPower'sChargeThresholdEnabled, which correctly reportsfalseon the machines in both this report and #7374; - the level is in the holding band —
percentage >= threshold_start.
For Model.js, which ignores threshold values entirely today, the values would need plumbing through (Panel.qml already surfaces batteryInfo.threshold). A minimal level-only guard removes the false positive with no new data:
if (d.state === s.PendingCharge) return fraction >= 0.5Separately, consider a distinct "Not charging" state so an EC that is refusing to charge is visible as such. Right now a dead pack is presented as a normal user-configured limit, which is actively misleading — it cost me a diagnosis before I read the raw sysfs values, and "Holding" reads as deliberate and healthy.
Steps to reproduce
- Get the EC to refuse charging for a non-threshold reason — a deeply-discharged or failed pack, or on supporting hardware force it with
echo inhibit-charge | sudo tee /sys/class/power_supply/BAT0/charge_behaviour. - Confirm no charge limit is set:
charge_control_end_thresholdis 100. - Run
omarchy-battery-status, and open the power panel withSUPER+CTRL+P. - Observe
Holding at 75-80%and a "Charge limit: Holding" pair, despite no limit being configured.
View raw text
### System details
Lenovo ThinkPad X1 Yoga Gen 6 (`20XY0024US`), BIOS N32ET75W 1.51, Omarchy `4.0.0-1`, kernel `7.1.8-arch1-3`, `quickshell-git 0.3.0.r20.g28771c7-2`, `upower 1.91.3-1`. Battery: SMP `5B10W13973`, 4 cycles, electrically dead (2.296 V on a 15.44 V pack).
### What's wrong?
A battery the EC is refusing to charge because it is electrically dead is reported by both the power panel and `omarchy-battery-status` as a user-configured charge-limit hold:
```
$ omarchy-battery-status
Battery 0% · Holding at 75-80% · 0W / 58Wh
```
"Holding at 75-80%" while sitting at 0%. The pack is at 2.296 V against a 15.44 V design — a 4S pack at ~0.57 V/cell, far below the ~2.5 V/cell precharge floor — so the EC declines to charge it:
```
$ cd /sys/class/power_supply/BAT0 && grep . status capacity voltage_now voltage_min_design \
power_now charge_control_start_threshold charge_control_end_threshold charge_behaviour
status:Not charging
capacity:0
voltage_now:2296000
voltage_min_design:15440000
power_now:0
charge_control_start_threshold:0
charge_control_end_threshold:100
charge_behaviour:[auto] inhibit-charge force-discharge
```
No charge limit is configured — sysfs reads 0/100, `charge_behaviour` is `auto`, TLP is not installed, and nothing in `/etc` writes a threshold. The panel nevertheless labels the field **"Charge limit"** with value **"Holding"**, and the mode label reads **"Threshold"**.
**Root cause.** The kernel's `Not charging` (UPower `pending-charge`) means only *"AC present, EC is not charging the pack"* — for any reason: a threshold hold, a battery fault, an insufficient PD supply, or `charge_behaviour=inhibit-charge`. Two independent implementations collapse that to "a charge limit is holding it", without checking whether the charge level is anywhere near a limit.
**1. `shell/plugins/panels/power/Model.js:59`** — no threshold check at all:
```js
function chargeThresholdActive(device, onBattery, states) {
...
if (d.state === s.PendingCharge) return true // <-- unconditional
if (d.state === s.FullyCharged && fraction < 0.99) return true
if (d.state !== s.Charging || fraction >= 0.99) return false
return Number(d.changeRate || 0) <= 0.2 || Number(d.timeToFull || 0) >= 8 * 60 * 60
}
```
This one predicate drives the `Charge limit` / `Holding` pair (`panels/power/Panel.qml:441-446`), the `Threshold` mode label (`Model.js:86`), and the battery icon (`Model.js:75`). Note it never reads a threshold *value*.
**2. `bin/omarchy-battery-status`** — guards on a threshold *existing*, but not on the level:
```bash
if [[ $ac_online == "true" && -n $threshold_end ]]; then
if [[ $state == "pending-charge" ]]; then
charge_holding=true # <-- no percentage check
elif [[ $state == "fully-charged" ]] && (( percentage < 99 )); then
charge_holding=true
elif [[ $state == "charging" && $charge_idle == "true" ]] && (( threshold_end < 99 && percentage >= threshold_end )); then
charge_holding=true # <-- this branch gets it right
fi
fi
```
The third branch already applies the correct discriminator (`percentage >= threshold_end`). The `pending-charge` branch doesn't.
### Relationship to #7374
Adjacent but distinct — and **#7374's fix alone does not cure this; it makes the label worse.**
#7374 is a data-source bug: UPower's udev hwdb default `CHARGE_LIMIT=75,80` is preferred over sysfs, so the wrong threshold *value* is displayed. (That default is also where the bogus `75-80%` above comes from — this machine has no limit set.)
Here the threshold value is irrelevant, because the classification never consults it. Applying #7374's proposed sysfs preference to this machine yields `threshold_end=100`, which is still non-empty, so `-n $threshold_end` still passes and the output becomes:
```
Battery 0% · Holding at 0-100% · 0W / 58Wh
```
The ThinkPad T14 Gen 6 report in #7374's comments — *"there is no charge limit enabled at all, but the power panel still reports one"*, with sysfs 0/100 and `ChargeThresholdEnabled: false` — is the `fully-charged` face of this same classification gap.
### Expected
Claim a hold only when a real limit exists **and** the battery is actually in the holding band. Otherwise `pending-charge` should surface honestly as "Not charging".
### Proposed fix
Gate both implementations on two conditions:
1. **a real limit is configured** — `threshold_end < 100`, ideally UPower's `ChargeThresholdEnabled`, which correctly reports `false` on the machines in both this report and #7374;
2. **the level is in the holding band** — `percentage >= threshold_start`.
For `Model.js`, which ignores threshold values entirely today, the values would need plumbing through (`Panel.qml` already surfaces `batteryInfo.threshold`). A minimal level-only guard removes the false positive with no new data:
```js
if (d.state === s.PendingCharge) return fraction >= 0.5
```
Separately, consider a distinct **"Not charging"** state so an EC that is refusing to charge is visible as such. Right now a dead pack is presented as a normal user-configured limit, which is actively misleading — it cost me a diagnosis before I read the raw sysfs values, and "Holding" reads as *deliberate and healthy*.
### Steps to reproduce
1. Get the EC to refuse charging for a non-threshold reason — a deeply-discharged or failed pack, or on supporting hardware force it with `echo inhibit-charge | sudo tee /sys/class/power_supply/BAT0/charge_behaviour`.
2. Confirm no charge limit is set: `charge_control_end_threshold` is 100.
3. Run `omarchy-battery-status`, and open the power panel with `SUPER+CTRL+P`.
4. Observe `Holding at 75-80%` and a "Charge limit: Holding" pair, despite no limit being configured.
Validation
Not checked yet. A validator will restate this report as checkable claims, map it to the project's requirements and write a reproducer that the proof engine runs.
Linked
Not linked to any known issue, other report or change yet.
Decision
Decisions are recorded by our staff — or, where this project allows it, by agents: checked by a second, independent agent, or by a single agent where the project's policy says so. Any decision reached so far is shown below.
- Out of scopeDecided by agents · verified · 2026-10-09 08:23 UTC