Skip to content

Hibernate laptops on critical battery once hibernation is set up - #13540

Open
stevederico wants to merge 10 commits into
omacom:quattrofrom
stevederico:fix/critical-battery-hibernate
Open

stevederico wants to merge 10 commits into
omacom:quattrofrom
stevederico:fix/critical-battery-hibernate

Conversation

@stevederico

@stevederico stevederico commented Sep 28, 2026 •

Copy link
Copy Markdown

Fixes #13256

UPower's Auto critical action resolves to logind's Sleep(), which is suspend-then-hibernate, and fires at 2%. A laptop can drain in the suspend before the hibernate runs and power off hard, even with hibernation set up.

When omarchy-hibernation-setup runs on a machine with a battery, it now installs a UPower drop-in (/etc/UPower/UPower.conf.d/70-omarchy-critical-hibernate.conf) that hibernates directly at 5%, with critical at 8%. Rerunning setup adds it to machines set up before, and omarchy-hibernation-remove takes it out. Machines without a battery get nothing. If hibernate isn't available, UPower falls back to a clean power off.

Test plan

  • bash test/shell.d/hibernation-setup-test.sh: new cases on its scratch root (/etc/UPower redirected, battery detection and systemctl stubbed) check the drop-in is installed on a laptop, left alone when unchanged, not installed without a battery or without a resume offset, that a failed install fails setup (so the migration retries), and that it is removed with hibernation. They fail with quattro's setup and remove
  • The drop-in name and format follow /etc/UPower/UPower.conf.d/README.md (upower 1.91.3)
  • The drop-in's comment and manual/36-system-sleep.md show how to pick another level with a later drop-in (99-local.conf), as suggested in review
  • On a fresh setup the drop-in is installed only after the UKI rebuild succeeds
  • ./test/shell: same 11 failures with and without this change (unrelated)
  • Not tested with a real battery drain

@llstrk

llstrk commented Sep 29, 2026

Copy link
Copy Markdown

Automated AI review

Community review: Independent automated community review, unaffiliated with the Omarchy team, intended to help prepare PRs for their review.

Outcome: Laptops that already have hibernation keep the old critical battery action after updating.

Existing hibernating laptops keep the 2% default

Only setup installs the drop-in, and nothing in bin/, install/ or migrations/ reruns it after an update. Impact: the laptops in #13256 keep CriticalPowerAction=Auto at 2% unless setup is rerun by hand.

Suggested change: add a migration that installs it when omarchy_resume.conf has HOOKS+=(resume) and omarchy-battery-present succeeds, with setup's cmp check and try-restart.

Reproducer (source search at 28c4d46)

Reproducer: laptops that already have hibernation keep the stock critical action after updating

Reviewed head: 28c4d46.

This is a source-level reproduction: it shows that nothing in the update path installs the new drop-in, so an existing install only gets it if the user reruns omarchy hibernation setup by hand.

The installer sets hibernation up as a fixed install phase: configure_hibernation in omacom/omarchy-iso (listed in orchestrator/main.py, defined in configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py, public branch quattro at 86c07785) runs arch-chroot <target> env OMARCHY_PATH=/usr/share/omarchy ... /usr/bin/omarchy-hibernation-setup --force --no-rebuild (setup itself exits early where the kernel has no hibernation support). Laptops installed this way therefore already have /etc/mkinitcpio.conf.d/omarchy_resume.conf, which sends a rerun down the already-configured branch. Nothing on the update path in bin/, install/ or migrations/ reruns setup or installs the drop-in; the only other caller is factory reset, as the searches below show.

From a checkout of the PR head:

rg -n "omarchy-hibernation-setup" bin install migrations
rg -n "critical-hibernate|UPower.conf.d" bin install migrations

Actual output:

bin/omarchy-system-factory-reset:262:  if [[ -x $next/usr/bin/omarchy-hibernation-setup ]] && grep -q "/swap/swapfile" "$next/etc/fstab"; then
bin/omarchy-system-factory-reset:278:      /usr/bin/omarchy-hibernation-setup --force --no-rebuild >>"$LOG_FILE" 2>&1; then

bin/omarchy-hibernation-setup:32:UPOWER_DROP_IN="/etc/UPower/UPower.conf.d/70-omarchy-critical-hibernate.conf"
bin/omarchy-hibernation-setup:67:  cmp -s "$OMARCHY_PATH/default/upower/omarchy-critical-hibernate.conf" "$UPOWER_DROP_IN" && return 0
bin/omarchy-hibernation-setup:71:  if install_root_file "$OMARCHY_PATH/default/upower/omarchy-critical-hibernate.conf" "$UPOWER_DROP_IN" 0644; then
bin/omarchy-hibernation-remove:48:UPOWER_DROP_IN="/etc/UPower/UPower.conf.d/70-omarchy-critical-hibernate.conf"

The only caller of omarchy-hibernation-setup in shipped code (bin/, install/, migrations/) is factory reset, and no migration mentions the drop-in. On an existing hibernating laptop, UPower keeps the stock CriticalPowerAction=Auto with PercentageAction=2.0 after the update that ships this PR.

Details

Verified (UPower 1.91.4 and systemd 262 source, sandbox runs):

  • 70-omarchy-critical-hibernate.conf matches UPower's conf.d name pattern and loads after UPower.conf, whose stock copy defines all three keys the drop-in overrides.
  • With the stock PercentageLow=20, 20 > 8 > 5 passes UPower's ordering check.
  • When logind's CanHibernate is not yes, UPower falls back to PowerOff.
  • Rerunning setup is idempotent (no restart when the file is unchanged), machines without a battery get nothing, and remove deletes the file and restarts UPower.
  • The PR's test fails at the base commit and catches removal of the rerun call, the battery gate, the cmp guard, the install-path restart and the remove block.

Optional notes:

  • Optional test improvement: the test only reruns setup with the resume marker present, so deleting the first-setup call at bin/omarchy-hibernation-setup:171 (the path new installs take) leaves all five checks passing; the first-setup case in the next block catches it. The test also skips wherever /sys/power/image_size is absent. Redirecting /sys/power/ in redirect(), creating a stand-in image_size in the scratch root and dropping the host skip let all five checks run and pass in a sandbox without it.
  • Open PR Hibernate on critical battery when hibernation is set up #13610 installs the same destination file with the same values, adds a migration and edits the same parts of omarchy-hibernation-setup, so the two PRs conflict there.
  • manual/36-system-sleep.md does not mention that hibernation setup now changes the critical battery action; a sentence there would tell users about it (the drop-in is /etc/UPower/UPower.conf.d/70-omarchy-critical-hibernate.conf).
Reproducer for the first-setup test (fails at 28c4d46 with line 171 removed)

Reproducer: the first-setup path is not covered by the PR's test

Reviewed head: 28c4d46 (base 97a9fce).

New installs reach setup_critical_battery_hibernate through the first-setup path (the installer runs omarchy-hibernation-setup --force --no-rebuild), at bin/omarchy-hibernation-setup line 171. test/shell.d/hibernation-critical-battery-test.sh only runs setup on an already-configured machine, so removing that call leaves the whole file green.

Step 1: remove the first-setup call and run the PR's test

From a checkout of the PR head, on a machine where /sys/power/image_size exists (the test skips otherwise):

sed -i '/^setup_critical_battery_hibernate$/d' bin/omarchy-hibernation-setup
bash test/shell.d/hibernation-critical-battery-test.sh

The edit removes exactly one line:

@@ -168,7 +168,6 @@
   fi
 fi
 
-setup_critical_battery_hibernate
 
 if ! $NO_REBUILD; then

Actual output, exit status 0 (run in a sandbox with a private /sys/power/image_size stand-in):

ok - rerunning setup on a laptop installs the critical battery hibernate action
ok - the critical battery action is Hibernate, not suspend-then-hibernate
ok - an installed action is left alone
ok - a machine without a battery gets no battery action
ok - removing hibernation removes the critical battery action

Step 2: a test case for the first-setup path

Save as test/shell.d/hibernation-critical-battery-first-setup-test.sh (the PR's sudo stub and /etc and /swap redirection, plus btrfs, swaplabel and swapon stubs and a redirected system-sleep directory):

#!/bin/bash
#
# The first hibernation setup on a laptop (the installer runs it with --force
# --no-rebuild) also installs the critical battery action. /etc, /swap and the
# system-sleep hook directory are redirected into a scratch root; sudo runs the
# file operations there and records service restarts.

set -euo pipefail

source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"

[[ -f /sys/power/image_size ]] || { skip "no hibernation support to exercise"; exit 0; }

scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
mkdir -p "$scratch/bin" "$scratch/etc/limine-entry-tool.d" "$scratch/usr/lib/systemd/system-sleep" "$scratch/swap"
export CALL_LOG="$scratch/calls"
export OMARCHY_PATH="$ROOT"
export PATH="$scratch/bin:$ROOT/bin:$PATH"

cat > "$scratch/bin/sudo" <<'STUB'
#!/bin/bash
case "$1" in
  systemctl) echo "systemctl ${*:2}" >> "$CALL_LOG" ;;
  /usr/bin/install)
    args=()
    while (($# > 1)); do
      shift
      case "$1" in -o | -g) shift ;; *) args+=("$1") ;; esac
    done
    exec /usr/bin/install "${args[@]}"
    ;;
  *) exec "$@" ;;
esac
STUB
printf '#!/bin/bash\nexit 0\n' > "$scratch/bin/omarchy-battery-present"
printf '#!/bin/bash\necho "btrfs $*" >> "$CALL_LOG"\n' > "$scratch/bin/btrfs"
printf '#!/bin/bash\nexit 0\n' > "$scratch/bin/swaplabel"
printf '#!/bin/bash\necho "%s"\n' "$scratch/swap/swapfile" > "$scratch/bin/swapon"
chmod +x "$scratch/bin/"*

sed -e "s|/etc/|$scratch/etc/|g" -e "s|\"/swap|\"$scratch/swap|g" \
  -e "s|/usr/lib/systemd/system-sleep/|$scratch/usr/lib/systemd/system-sleep/|g" \
  "$ROOT/bin/omarchy-hibernation-setup" > "$scratch/omarchy-hibernation-setup"

# Swap subvolume, swapfile, fstab entry and resume parameters already exist, so
# the run only writes the resume hook, the sleep hook and the battery action.
printf '\n# Btrfs swapfile for system hibernation\n%s none swap defaults,pri=0 0 0\n' "$scratch/swap/swapfile" > "$scratch/etc/fstab"
echo 'KERNEL_CMDLINE[default]+=" resume=/dev/null resume_offset=1"' > "$scratch/etc/limine-entry-tool.d/resume.conf"

: > "$CALL_LOG"
bash "$scratch/omarchy-hibernation-setup" --force --no-rebuild > "$scratch/out" 2>&1 ||
  fail "first setup on a laptop succeeds" "$(<"$scratch/out")"
grep -qx 'HOOKS+=(resume)' "$scratch/etc/mkinitcpio.conf.d/omarchy_resume.conf" ||
  fail "first setup writes the resume hook" "$(<"$scratch/out")"
cmp -s "$ROOT/default/upower/omarchy-critical-hibernate.conf" \
  "$scratch/etc/UPower/UPower.conf.d/70-omarchy-critical-hibernate.conf" ||
  fail "first setup on a laptop installs the critical battery action" "$(<"$scratch/out")"
grep -qx 'systemctl try-restart upower.service' "$CALL_LOG" || fail "UPower is restarted to read the new action"
pass "first setup on a laptop installs the critical battery action"

Actual results (same sandbox):

At the PR head, unmodified (exit 0):

ok - first setup on a laptop installs the critical battery action

At the PR head with the Step 1 edit (exit 1):

Adding resume hook to <scratch>/etc/mkinitcpio.conf.d/omarchy_resume.conf
not ok - first setup on a laptop installs the critical battery action

At the base commit 97a9fce, with the PR's default/upower/omarchy-critical-hibernate.conf copied in (exit 1):

Adding resume hook to <scratch>/etc/mkinitcpio.conf.d/omarchy_resume.conf
not ok - first setup on a laptop installs the critical battery action

<scratch> replaces the test's own mktemp -d scratch root, redacted from the output; nothing else in the output is changed.


Review information

Test scope: Source review and sandbox tests at 28c4d460; no real battery drain.

AI process: Opus 5.5 Medium coordination and synthesis, Opus 5.5 Xhigh technical review and final fact check, GPT 6 Sol Xhigh search for related issues, Opus 5.5 Medium editorial check.

Opt out: To stop receiving these reviews, reply to this comment saying so.

@Oldrich333

Copy link
Copy Markdown

Reproduced the critical-action issue on an LG Gram running Omarchy 4.0.4-1. Hibernation is available (CanHibernate returns yes). A local UPower drop-in already set PercentageLow=20, PercentageCritical=10, and PercentageAction=5, but left CriticalPowerAction=Auto. GetCriticalAction returned Sleep; the battery then drained to empty and the laptop powered off without hibernating.

This is another case where changing the percentage alone is insufficient. The explicit CriticalPowerAction=Hibernate in this PR addresses the observed configuration. I also did not see a low-battery notification, although I could not recover the previous UPower.onBattery state to establish the notification cause.

@sergihrs

sergihrs commented Oct 2, 2026

Copy link
Copy Markdown

+1. Same setup here on an ASUS ROG Zephyrus G16 (GU605CP), Omarchy 4.0.4-1, upower 1.91.3: hibernation set up by Omarchy, logind CanHibernate and CanSuspendThenHibernate both yes, so the stock CriticalPowerAction=Auto at 2 % ends up as suspend-then-hibernate.

One suggestion: make the level easy to change. 5 % is a sensible default, but people will weigh the margin differently (e.g. 3 % on a new battery). UPower already allows this without a new Omarchy setting: a later drop-in such as /etc/UPower/UPower.conf.d/99-local.conf with

[UPower]
PercentageAction=3.0

overrides the 70- file, and setup's cmp check leaves it alone. A line in the drop-in's comment and in manual/36-system-sleep.md saying so (and that it has to stay below PercentageCritical) would cover it.

@omarchybot omarchybot added the enhancement New feature or request label Oct 3, 2026
@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the latest test changes address all three numbered findings without introducing a new actionable issue.

Summary

Makes configured laptops hibernate directly at 5% battery through a UPower drop-in.

  • Laptops with working hibernation hibernate directly when the battery gets low.

Reviews (6) · Last reviewed commit: "Fold the critical battery checks into th..." · Reviewed by Greptile

Comment thread bin/omarchy-hibernation-setup Resolved
Comment thread bin/omarchy-hibernation-setup Resolved
@omarchybot

Copy link
Copy Markdown
Collaborator

I read this pull request at 6ad6e4c, together with #13256, to decide whether it is a bug fix or a new feature before reviewing it further. The two models that classify items separately did not agree, so it has no class label for now and was not run on a worker.

Both readings hold up, so which it is comes down to whether the maintainer counts the stock critical-battery action as broken on a machine Omarchy set up for hibernation. Nothing here waits on you; this waits on the maintainer to decide that.

@stevederico
stevederico force-pushed the fix/critical-battery-hibernate branch from d28ef2f to d269fea Compare October 30, 2026 15:55
@omarchybot

Copy link
Copy Markdown
Collaborator

I read this pull request again at d269fea, since the head moved after my last note, together with #13256, to decide whether it is a bug fix or a new feature. The two models classifying it separately still do not agree, so it carries no class label and was not run on a worker.

  • Claude Opus 5.5 read it as a bug this time: on a laptop Omarchy has set up for hibernation, the critical-battery action resolves to suspend-then-hibernate at 2% and the machine can power off hard with nothing saved, as reproduced on a MacBook Pro, an LG Gram and an ROG Zephyrus. The critical action exists to prevent exactly that, and here it does not.
  • Codex Medium, reading the item on its own, read it as an enhancement: UPower's stock Auto action is doing what it is configured to do, and this changes that default on purpose by installing a drop-in that hibernates at 5%.

The two readings swapped since the last pass, which says the question really is a judgement call: whether the stock critical-battery action counts as broken on a machine Omarchy set up to hibernate. That is the maintainer's to decide, and this waits on them. Nothing here waits on you.

stevederico and others added 9 commits November 2, 2026 08:41
UPower's Auto critical action is logind's Sleep(), which is
suspend-then-hibernate, and it fires at 2%. A laptop can drain in the
suspend before the hibernate runs and power off hard, even with
hibernation set up.

When hibernation setup runs on a machine with a battery, install a UPower
drop-in that hibernates directly at 5% (critical at 8%). Rerunning setup
adds it to machines set up before, and removing hibernation takes it out.

Fixes omacom#13256
Only omarchy-hibernation-setup installs the UPower drop-in, so a laptop
that set up hibernation before this change kept UPower's Auto action and
still suspends into a dying battery.

Rerun setup from a migration on laptops that already have the resume
hook. On those, setup only adds what is missing, and it leaves an
installed drop-in alone.
The drop-in's comment and the sleep manual now show how to set a
different PercentageAction in a later UPower drop-in, which setup
leaves alone, and that it has to stay below PercentageCritical.
Setup now installs the UPower drop-in only when the resume parameters
carry a swapfile offset, and on a fresh setup only after the UKI rebuild
succeeds, so a dying battery never powers off a session the kernel
cannot restore.

On a laptop that already has hibernation, a failed install now fails
setup, so the migration that runs it is retried instead of being marked
done.

The test no longer skips on hosts without /sys/power/image_size: the
scratch copy of setup reads a scratch file instead. New cases cover a
missing or empty resume offset and a failed install.
@stevederico
stevederico force-pushed the fix/critical-battery-hibernate branch from d269fea to 39a39b2 Compare November 2, 2026 17:13
Comment thread bin/omarchy-hibernation-setup Resolved
The setup test now redirects /etc/UPower and stubs battery detection, so
on a laptop host it no longer reaches the real UPower config. The separate
critical battery test and its grep of the config value are gone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Laptops don't hibernate on critical battery: UPower's default Auto resolves to suspend-then-hibernate at 2 %, and the battery can die first

5 participants