The enforce loop now waits on an inotify watch of log.json's directory
(_file_wake, ctypes, no new dependency) instead of a plain sleep, so a
credited workout moves the gaming budget within the tick. Earner answers
(workout, LeetCode, reading) are memoised with the source file's
(inode, mtime_ns, size) stamp, so a rewritten file re-asks at once instead
of after the 60 s TTL. Any inotify setup or read failure warns and falls
back to the plain sleep; the stamp still catches the change next tick.
Tests: conftest stubs the wake factory and points workout_log_path at a
tmp path, so no test ever watches the real screen-locker tree.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RHwVPyiiQMPh4M3JQwQku
earned_time 0.4.0 only changes shutdown resolution and adds the workout
ledger reader; every gaming_minutes this daemon reads is identical for every
input, so no code change here. Also brings aiohttp 3.14.4 and types-requests
2.33.0.20261006 to newest stable, which the dependency-freshness gate needs
before any commit to these manifests can land.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrjUTo1yhgAsUUasdw4bpG
The slowest workflow took ~75 s: pip installs (~20 s each), a serial
1764-test pytest run (43 s), and one pre-commit job running mypy and pylint
back to back (49 s).
- Tests: setup-uv with a cached wheel store, pytest -n auto.
- pre-commit: mypy / pylint / everything else as three parallel matrix
jobs, each with its own hook-env cache; installs only pre-commit plus the
two imports the system hooks need instead of all of requirements.txt.
No check was removed from CI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhzaTAWtRfJk8gre6NLSka
Requiring 100% locked the user onto one game for weeks, which proved
ineffective. The assignment and every manual pick now release as soon as one
achievement is unlocked after the game was assigned (Steam unlock_time vs the
pick's started_at / State.current_assigned_at).
- finished_app_ids still means 100% complete; a game released below 100% gets
released_at plus a 7-day cooldown and returns to the pool.
- Next pick: least recently assigned first, then shortest HLTB time.
- Releasing never evicts: released/expired picks stay installed and playable
until the user explicitly chooses another game (new pick-manual, or
accepting the next assignment); with no replacement the current game stays.
- Tampering detection judges a released game by unlocks after its release.
- The daemon reloads every State field each tick so its sweep save cannot
overwrite cooldowns or assignment times written by the CLI.
- State.load backfills assignment times once and persists them; load-time
migrations moved to _state_migrations.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhzaTAWtRfJk8gre6NLSka
require_snapshot_games / snapshot_game_name (_snapshot), apply_configured_pick (_actions) and hide_others_and_report (library_hider) replace copies spread across the CLI, MCP and stats paths. Tests retargeted to the new owners; test_main_pick_manual split to stay under 250 lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D55DvFEHiwcPN59kwPqwiU
SDL3's HIDAPI lg4ff driver advertises an effect-status query it cannot
answer; BeamNG.drive 0.39 native treats the failed query as a dead effect
and the wheel goes limp (12k "Failed to get joystick3 FFB effect status"
lines per session). Steam's environment on this box is exactly this
allowlist, so the hint has to live here, not in /etc/environment.
The narrow hint only demotes the Logitech wheel to the kernel driver;
gamepads keep HIDAPI (rumble, LEDs over Bluetooth).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SS8M5wk88nJTXxDWz61KKk
Steam's srt-logger runs inside steam-backlog-enforcer.service's cgroup
and mirrors Steam's console into journald on its own — the DEVNULL
stderr the spawn already uses does not reach it, and the env carried
SRT_LOG_TO_JOURNAL=0, which is not the variable it reads. One boot on
2026-09-12 wrote 481k "joystick FFB effect status" lines (99% of the
journal) and evicted two weeks of history from a 300M journal, along
with the evidence for that night's curfew failure.
- desktop_env_args(): SRT_LOGGER_USE_JOURNAL=0, the variable srt-logger
actually honours. Steam keeps its own logs/console-linux.txt.
- Unit: LogFilterPatterns for the two known flood patterns plus a
per-unit rate limit, so a future Steam that ignores the variable still
cannot take the journal with it.
The unit file is deployed and daemon-reloaded; the running service is
not restarted, because Steam is its child and a restart would kill a
live game. The env fix takes effect on the next enforcer start.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mLnUeoJ3mUwGRTowTUH6d
The 250-line split left function-sized copies behind: the confidence-only
batch fetch was the search half of _fetch_batch verbatim (now
_search_batch, and _SearchCtx carries one _HLTBExtras instead of two of
its dicts); the workout and LeetCode bonuses each had the same loopback
GET and answer cache (now _status_api); the poll-count refetch lived in
both _cmd_done and _polls_reporting (now refetch_poll_counts); the
candidate-selection prelude in both scanning and _scanning_assign; the
/etc/hosts rewrite loop in both block and unblock (now
rewrite_hosts_lines with a per-line rule); the timed HLTB fetch and its
rate line; and the whitelist constants each module only half used.
_hltb_cached's dead facade constants go too. Tests patch where the code
now lives.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCbkJWXHjtTUaqvnHRAqA
The pinned howlongtobeatpy (1.0.23) takes a user agent in
send_website_request_getcode and dropped parse_all_scripts; the call
still matched 1.0.20, which only worked because the deployed env was
behind its own pin. Now on the pinned API, and the env upgraded.
Three modules carried the same executable/path constant block since the
250-line split; it is _store_tools. Four carried the same "restore the
hours a refetch lost" loop; it is restore_prior_hours. Both detached
launches go through one helper that returns the process. Thirteen
try/except/else blocks are flat, dataclass fields come from
dataclasses.fields, and the rest are one-line fixes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCbkJWXHjtTUaqvnHRAqA
hltb, _cmd_done, _whitelist and _actions each kept a module __getattr__
that served names moved out in the 250-line split, via importlib to
dodge the resulting import cycle. pylint cannot see through that, and
graded every such import as no-name-in-module (26 of them). The 32
import statements now name the real module; with nothing left going
through the facades, they are gone.
Also the pylint hook on 4.0.8 with the source-only scope config, the
three implicit-booleaness disables the reference repos carry, the
http.server do_GET admission, and the third-party pins the hook was
missing (aiohttp, websockets, howlongtobeatpy). Gate still 8.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCbkJWXHjtTUaqvnHRAqA
Also bumps mcp 2.2.0, ruff 0.16.7, types-requests 2.33.0.20260906 and, in
web/, react 19.3.0, vitest 5.0.0 (+ coverage-v8), eslint 10.10.0,
typescript-eslint 8.70.0, vite 8.3.0 and the small ones, so the freshness
gate lets this through. Web lint/build/tests/coverage (100 %) unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
Keeps the dependency-freshness gate green; unrelated to the engagement
gating removal in an earlier commit. Not installed in this dev
environment (pip is externally-managed here), so this is a manifest-only
bump — CI installs it fresh and is the strict authority per the
dependency-freshness rule.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keeps the dependency-freshness gate green; unrelated to the engagement
gating removal in the previous commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A qualifying game process running is now sufficient on its own to bill
the budget. Previously a tick only counted if the screen wasn't locked,
the game's window was X11-focused, and input hadn't gone idle for 5
minutes — with an idle-grace refund on top. That leniency is gone
entirely: no focus check, no idle check, no screen-lock check, no
refund. Deletes the engagement-probing modules (_playtime_engagement,
_engagement_probes, _engagement_types, _x_focus, _x_probe, _screen_hold,
_controller_input) and their config fields, simplifies the audit log
and budget-view/web session schema accordingly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>