steam-backlog-enforcer/steam_backlog_enforcer/docs/DOCS-manual-pick-design.md
Krzysztof kuhy Rudnicki d0e8ed580c Auto-detect completed manual picks before enforcing the pick cap
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 14:53:04 +02:00

5.6 KiB
Raw Blame History

Add an option to pick a specific game manually (by providing its steam id) Picking a game should work like this:

  1. user invokes script with specific flag for picking a game manually
  2. user provides game steam id (in testing use 489830)
  3. Script shows what game it believes this id means (in this case it should show The Elder Scrolls V: Skyrim Special Edition)
  4. user confirms that this is the game they want to pick and confirm that they will not be able to use the script for up to 2 weeks or until the game is completed (100% achievements)

When user picks a game manually this should override the current pick if it exists After picking manually backlog enforcer should make a note of that and very aggressively disallow user to do anything else for a period of 2 weeks or until user completes a given game Logic should be as follows: 1. backlog checks if a user picked game manually a. if not -> continue as before 2. if yes check if the game is completed a. if yes -> continue as before 3. if NOT show info that user picked a specific game manually and they have to finish it before using ANY OTHER functionality of backlog enforcer

test the functionality with 489830 (The Elder Scrolls V: Skyrim Special Edition) as always first write full functionality confirm that it works alone and with the user and only AFTER that write tests and coverage and fix issues

Abandoning a pick (added 2026-07-19, grace window removed 2026-08-24)

A manual pick can be a mistake, and with no way out the user is stuck for the full 2 weeks. A pick is never irreversible:

  • abandon-pick <app_id> backs out of a pick at any time, however long ago it was made. There is no grace window.
  • The app_id must be passed explicitly and must match the active pick, so an abandon cannot be triggered by muscle memory.
  • abandon-pick is in _MANUAL_LOCK_EXEMPT_COMMANDS — otherwise the pre-dispatch lock check in main() would block the only way out.
  • Abandoning clears the lock and the assignment, uninstalls the game, and puts it on the existing skipped_until cooldown for ABANDON_COOLDOWN_DAYS = 30 so scan does not hand it straight back.
  • _actions.abandon_manual_pick is state-only (no uninstall), matching the apply_manual_pick rule, so the MCP abandon_pick tool can reuse it. Both MCP tools stay gated behind confirm=True.

The lock-active message advertises abandon-pick for every active pick, and the pick-manual warning mentions it up front.

Concurrent manual picks (added 2026-07-19)

Config.max_manual_picks (default 2) is how many games may be locked in at once. All active picks stay installed, visible and un-killed; the lock releases only when every pick is finished or past its own 14-day deadline.

  • State.manual_picks is the list of {app_id, game_name, started_at} entries. The old single-slot manual_pick_* fields are still read on load, migrated into the list, then cleared — a live lock survives the upgrade.
  • _actions.allowed_app_ids(state) (assignment ∪ active picks) is the single source of truth for "may exist". uninstall_other_games, hide_other_games, enforce_allowed_game and _guard_installed_games all take that set instead of one app id.
  • pick-manual is in _MANUAL_LOCK_EXEMPT_COMMANDS so another game can be added while earlier picks hold the lock; the cap is what limits it. Its post-pick cascade operates on the whole allowed set, so adding a pick never tears down an earlier one.
  • abandon-pick <app_id> drops only that pick; a survivor inherits the assignment and keeps the lock.
  • cmd_done still finishes current_app_id and auto-picks a replacement, so the remaining picks stay queued. A finished pick leaves the active set automatically via finished_app_ids — no pruning needed — but something has to put the id there in the first place; see the next point.
  • Completion is checked per pick, not just for the assignment (_pick_completion.py). finished_app_ids used to gain ids only from cmd_done/do_check, and both look at current_app_id alone — so a pick that was not the current assignment had no achievement-based release path and sat on its slot until the 14-day expiry. retire_completed_manual_picks now re-checks every active pick and records the ones at 100%, so the cap can never refuse on behalf of a game that is already finished. It runs from pick-manual, status, check, the MCP pick_manual tool, and the enforce loop (throttled to once every MANUAL_PICK_RECHECK_TTL_SECONDS, since that loop ticks every 3s).
    • A pick whose achievements cannot be read — the game has none, or Steam is unreachable — stays locked. Unknown must never look like complete.
    • Retiring frees the slot and nothing more: it never calls pick_next_game, so freeing a slot cannot hand you an unrequested extra game.
    • In the daemon it records without evicting. A retired pick leaves allowed_app_ids, which would otherwise make the loop kill and uninstall the game mid-session, seconds after the last achievement popped; the ids in daemon_sweep.retired stay allowed until a user-invoked command acts.
    • mark_finished is the single writer of finished_app_ids, so the sweep and done/check cannot both record the same completion.

Deployment note: the enforce daemon holds the allowed set in code, so sudo systemctl restart steam-backlog-enforcer is required after upgrading. A pre-upgrade daemon only knows current_app_id and will uninstall the other pick as "unauthorized" within seconds — this happened once during development.