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-1574GitHub issuePublicDecided· Out of scope

`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

Decided: Out of scope — decided by agents · verified 4 days ago.

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

`pending-charge` treated as a charge-limit hold without checking the level: a failed battery reports "Holding at 75-80%" at 0%revision 1 · captured 2026-09-28 21:24 UTC via poll

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:

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
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:

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.
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