Skip to content

feat: stay-awake inhibitor — OS sleep no longer kills long turns (salvage #19484) - #106401

Open
teknium1 wants to merge 2 commits into
mainfrom
kilocode-port/stay-awake-salvage
Open

teknium1 wants to merge 2 commits into
mainfrom
kilocode-port/stay-awake-salvage

Conversation

@teknium1

@teknium1 teknium1 commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Stay-awake inhibitor: agent.stay_awake keeps the OS from sleeping mid-turn

A laptop that sleeps mid-turn drops the in-flight API call and halts tool execution; with agent.stay_awake: true (default false) Hermes holds an OS sleep inhibitor for exactly the duration of each agent turn, on every surface (CLI, gateway, TUI, Desktop, cron, subagents).

Salvage with credit: the inhibitor module is PR #19484 by @aommi (May 2026), cherry-picked so authorship survives, then rebased onto the Sep-2026 decomposed layout and rewired. Port motivated by the same feature landing in Kilo Code this week (Kilo-Org/kilocode#12974 keep-awake toggle, #13927 /caffeinate CLI).

Changes

  • agent/stay_awake.pyStayAwake context manager (macOS caffeinate -i, Linux systemd-inhibit --what=sleep:idle, Windows SetThreadExecutionState; display sleep untouched; graceful no-op where unavailable) + new turn_scope(): one process-wide refcounted inhibitor shared by concurrent turns. Refcounting matters because Windows SetThreadExecutionState is not nestable — a second scope exiting would clear the first one's flags.
  • agent/turn_facade.pyturn_scope() joins the existing with at the single choke point every surface passes through. This replaces the original PR's three per-surface threads (cli.py kwarg, gateway raw-YAML loader, AIAgent.__init__ param): zero new constructor parameters, and surfaces added later inherit it for free.
  • hermes_cli/config_defaults.pyagent.stay_awake: False.
  • tests/agent/test_stay_awake.py — 3 invariant tests (disabled ⇒ zero side effects; refcount contract: nested scopes share one inhibitor, released exactly once; config default off). The original PR's 14 tests faked the host OS by patching platform.system() — repo policy forbids that, so they were replaced, not trimmed.
  • website/docs/user-guide/configuration.md — user-facing docs.

Validation

Check Result
Live E2E (this Linux host, temp HERMES_HOME, real imports) enabled config ⇒ systemd-inhibit --list shows the hermes inhibitor during a turn_scope(); nested scope keeps exactly one; released on exit; disabled config spawns nothing
scripts/run_tests.sh tests/agent/test_stay_awake.py 3/3 green
scripts/run_tests.sh tests/agent/ green (2 unrelated parallel-run flakes pass in isolation and on base)
ruff / windows-footguns / compat-pointers / subprocess-stdin clean

Live repro: before — no inhibitor exists, systemd-inhibit --list shows nothing hermes-owned during a turn; after — inhibitor registers for the turn's duration and releases on exit (probe transcript in commit message).

Architectural difference vs Kilo Code

Kilo ties keep-awake to a client-side service (VS Code extension / /caffeinate TUI command) with session-busy tracking. Hermes runs turns synchronously in-process, so the turn lease choke point already IS the busy signal — the inhibitor scope maps 1:1 to real work with no polling or client wiring.

Infographic

stay-awake

aommi and others added 2 commits September 9, 2026 02:04
Adds agent.stay_awake config option (default: false). When enabled,
prevents system/idle sleep during run_conversation() so long-running
LLM calls and tool execution aren't interrupted mid-session.

OS support:
- macOS: caffeinate -i (idle sleep only, display free to dim)
- Linux: systemd-inhibit --what=sleep:idle (graceful no-op if absent)
- Windows: SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)

Design decisions:
- Prevents system/idle sleep only, NOT display sleep (user doesn't
  need the screen lit — they need API calls to complete)
- Applies to BOTH CLI and gateway runs (a message at 3am that causes
  a broken session because the machine slept is worse than battery
  drain — and the setting is opt-in)
- Context-manager pattern with try/finally ensures cleanup even on
  errors and interrupts
- Degrades gracefully to no-op when the inhibitor isn't available
  (containers, headless servers, WSL1)
Harden the salvaged inhibitor (#19484) onto the Sep-2026 layout:

- move hermes_stay_awake.py -> agent/stay_awake.py (topical sibling, not a
  new root module)
- add turn_scope(): one process-wide refcounted inhibitor shared by
  concurrent turns; first turn in starts it, last turn out stops it.
  Refcounting matters because Windows SetThreadExecutionState is not
  nestable - a second scope exiting would clear the first one's flags.
- wire at the single choke point every surface passes through
  (TurnFacadeMixin.run_conversation), replacing the original PR's
  three per-surface constructor threads (cli.py kwarg, gateway raw-YAML
  loader, AIAgent __init__ param) - CLI, gateway, TUI, Desktop, cron and
  subagents all inherit it with zero per-surface wiring
- config default agent.stay_awake: False in config_defaults.py (the
  original patched the pre-split hermes_cli/config.py)
- replace platform.system()-mocked OS tests with 3 invariant tests
  (disabled = zero side effects; refcount contract; config default off) -
  repo policy forbids faking the host OS
- document under user-guide/configuration.md

Live E2E on this host: real systemd-inhibit registers during a scoped
turn, stays single when nested, releases on scope exit; disabled config
spawns nothing.

Port inspired by Kilo-Org/kilocode#12974 + #13927 (keep-awake /
/caffeinate); implementation salvaged from hermes-agent PR #19484
(@aommi) with authorship preserved on the base commit.
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on dd61acd — refactor: wire stay-awake into the turn facade as a refcount

❌ Job failures

Check contributors / check-attribution · View job

Job Check contributors / check-attribution failed.


⚠️ Action required

Unmapped contributor email(s) · View job

New contributor email(s) are not in AUTHOR_MAP.

amirali.ommi@gmail.com (aommi)

How to fix:

Run from the PR branch:

python3 scripts/audit_pr_attribution.py --fix
git add contributors && git commit -m "chore: map contributor emails" && git push

Or map one email manually (do NOT edit AUTHOR_MAP in release.py):

python3 scripts/add_contributor.py <email> <github-username>

To find the GitHub username for an email:

gh api 'search/users?q=EMAIL+in:email' --jq '.items[0].login'

⚠️ Warnings

OSV vulnerability scan · View job

71 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 5m11s vs 4m55s (+5.4%). 5 job(s) slower, 8 faster, 2 unchanged.

  • OS-specific tests / macOS-only tests: -51.0s
  • Docs Site / docs-site-checks: -44.0s
  • OSV scan / Emit review status: -38.0s
  • OS-specific tests / Windows-only tests: +16.0s
  • Check no committed infographics / check-no-committed-infographics: -15.0s

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/config Config system, migrations, profiles labels Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants