From c8e63bc9fa0299628a510615a6d9bd3b47cbef05 Mon Sep 17 00:00:00 2001 From: alexpolo1 Date: Fri, 28 Aug 2026 12:46:41 +0200 Subject: [PATCH] fix(stealth): roll the AFK break on the path a single-route rotation actually takes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit afk_break fired 0 times in 9 games at a 50% test rate — odds of roughly 1 in 500, so not variance. The four call sites added earlier all sit in on_end_run's town-return branches, and with one route configured the bot never reaches them. After the run it goes straight to end_game: Loot from run_pindle: ... TL> g8 r8 | game | end | ok Clicking SAVE_AND_EXIT ... End game. Elapsed time: 69.80s Starting game #9 No return_to_town or tp_town line appears anywhere in the log. Maintenance runs at game START, not after the run, so those branches are dead code for this configuration. The roll now sits in on_end_game beside the scheduled-break check — the same path that demonstrably works, since scheduled_break has fired and resumed. Between games is also the right moment semantically: the game is closed, so idling there is safe. Worth recording why the guard missed it. The STEALTH> manifest reported "afk_break 5% wired - 4 call sites" throughout, which was true and useless: a call site EXISTING is not the same as a call site being REACHED. Static reachability is not something the manifest can decide. The digest's "NEVER FIRED" line is the check that actually catches this class, and it is why that line exists. Co-Authored-By: Claude Opus 5 --- src/bot.py | 14 ++++++++++++++ test/test_stealth_config.py | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/bot.py b/src/bot.py index 8308c7a..0127b7e 100644 --- a/src/bot.py +++ b/src/bot.py @@ -1201,6 +1201,20 @@ class Bot: # time: a break at exactly 120 minutes every time is still a pattern, # just a slower one than taking no break at all. from utils.stealth import break_schedule, idle_drift + # Unscheduled AFK break, rolled BETWEEN GAMES. + # + # It used to live only in on_end_run's town-return branches. Those are + # never reached on a single-route rotation: after the run the bot goes + # straight to end_game (loot -> game|end -> save+exit -> next game), so + # all four call sites sat on dead code and afk_break fired 0 times in 9 + # games at a 50% test rate — odds of about 1 in 500. + # + # The manifest reported "wired - 4 call sites" throughout, which was + # true and useless: a call site existing is not the same as a call site + # being REACHED. The digest's "NEVER FIRED" line is the check that + # actually catches this. + maybe_afk_break() + if self._next_break_after is None: self._next_break_after, self._next_break_len = break_schedule() if self._next_break_after: diff --git a/test/test_stealth_config.py b/test/test_stealth_config.py index 778241c..5e2ef67 100644 --- a/test/test_stealth_config.py +++ b/test/test_stealth_config.py @@ -349,3 +349,24 @@ def test_pickup_skip_is_decided_once_per_item(): src = inspect.getsource(PickIt._should_walk_past) assert "_stealth_skipped" in src, "skip decision is not memoised per item" + + +def test_afk_break_is_rolled_on_the_end_of_game_path(): + """The roll must sit where a single-route rotation actually goes. + + It originally lived only in on_end_run's town-return branches. With one + route configured the bot never reaches them — after the run it goes straight + to end_game (loot -> game|end -> save+exit -> next game) — so afk_break + fired 0 times in 9 games at a 50% test rate, odds of roughly 1 in 500. + + The manifest said "wired - 4 call sites" the whole time. That was true and + useless: a call site existing is not a call site being reached. + """ + import inspect + from bot import Bot + + src = inspect.getsource(Bot.on_end_game) + assert "maybe_afk_break()" in src, ( + "afk_break is not rolled on the end-of-game path, which is the only path " + "a single-route rotation takes" + )