* fix: raise maintenance budget for cross-act routes; document Trav findings Travincal leaves the character in Act 3, but maintenance relocates it to Act 4 (stash at a4_tyrael_stash, repair at a4_halbu) and the next run then needs the A3 waypoint again -- two cross-act waypoint trips per game. That travel is not attributed to any timed step, so a FAIL> trail can show ~38s of work inside a 250s maintenance window. The 240s budget (tuned when Pindle kept all town business in one act) was tripped 5 times in one session, each costing a whole game. Raised to 420s. Also documents, in HANDOVER.md: - The open_wp failures are NOT a stale template. Scored a5_wp.png against all three real failure frames: 0.96-0.98 full-frame, but that match is the belt/mana-orb HUD false positive the a5.py comment already documents. Inside the real cut_skill_bar ROI it scores 0.436-0.480 against a 0.55 threshold at scattered positions -- noise. The waypoint is genuinely not on screen; the character never reaches it. Recapturing would fix nothing. Scoring a template without the ROI the code actually uses produces a confident wrong answer. - PR #39's pather abort is merged, live, and finally firing (1 abort in 21 games, after being 0-for-152 while it looked correct). - Keeping Trav town business in Act 3 is the real fix for both the timeouts and the waypoint failures. a3.py already reports can_buy_pots/can_heal/ can_stash as True, so the Act 4 trip is not a missing capability. - FoH vs hammerdin on the council, and the [fohdin]/[paladin] section trap: concentration and redemption lived in [fohdin], which hammerdin does not read, so both would have gone silently unbound after a respec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: post-mortem for the hell Travincal / hammerdin session Narrative write-up of 29 Aug 2026: run_trav on hell, FoHdin -> hammerdin. Outcome: health chickens 3-in-8 -> 0-in-11, battle 64-68s -> 41-60s, failure rate 12% -> 9%, zero deaths throughout. The document covers the four times the obvious answer was wrong: 1. Battle time did not move after the respec, which looked like failure. hammerdin.kill_council casts for a FIXED duration with no kill detection, so the clock cannot report damage -- loot proved the respec worked. 2. a5_wp.png scored 0.96-0.98 full-frame on every failure frame, which reads as "template fine, search broken". That match is the belt/mana-orb HUD false positive the a5.py comment already documents. Inside the real cut_skill_bar ROI it scores 0.436-0.480 against a 0.55 threshold at wandering positions -- noise. The waypoint was never on screen. 3. Ormus: not masked (4-channel but fully opaque, so no mask is passed), and not a threshold problem (in-ROI best 0.342, background level -- lowering the bar would recreate the Bug 4 false-positive clicks). A3 town has frames with ZERO pather landmarks over threshold, so the character never arrives. One cause, three symptoms. 4. Reverses this session's own earlier recommendation to move Trav town business into Act 3. That was reasoned from a3.py reporting can_buy_pots/ can_heal/can_stash as True -- a capability check, not evidence about pathing. A3 is the worst-supported town in the project. Also documents three settings that were live but inert: binds under [fohdin] which hammerdin never reads, casting_frames=8 (unreachable -- a paladin's fastest cast is 10 frames, so every cast was cut short), and repair_npc= a5_larzuk, which Bug 12 set to keep repair in-act for Pindle but which forces a cross-act trip on Trav (35.8s vs 9.0s). And a caution on the key auto-detector: it reported the wrong stand_still bind both before and after a live remap, consistent with a stale .keyo read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: correct stale hammerdin migration notes in HANDOVER (Codex review) Addresses Codex feedback on PR #40: the handover still described the character as needing conversion to hammerdin with both skills blank, while the same commit verified type=hammerdin and concentration='f6' loading live. The conflict was worse than reported. The stale "Two things only you can do" section instructed the operator to rebind F7 -> Concentration, but F7 is Battle Orders (CTA). Following it would have overwritten the CTA bind and cost a large share of the life pool -- the exact problem this session was fixing. - Replaces the migration paragraph with the verified end state, and records that Blessed Hammer lives on LEFT-CLICK, not an F-key (_cast_hammers puts the aura on the right slot, so a hammer hotkey would replace it). - Notes that concentration/redemption had to move into [paladin]; under [fohdin] a hammerdin never reads them and both would be silently unbound. - Adds the full verified bind map so F7 cannot be reassigned by mistake. - Rewrites the operator TODOs: fire resist is superseded (Zaka + Mara took chickens 3-in-8 -> 0-in-11); the live items are lightning/poison resist and the 75% FCR breakpoint, with the casting_frames mapping (75%=11, 60%=12, <48%=13) so cast timing tracks any gear change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: baal_xp farm — game browser join, hide-and-wait state, config - src/ui/game_browser.py: retry-wrapped join flow (Play -> Join Game tab -> OCR game list -> click row -> loading/InGame confirm), wait_for_in_game - src/bot.py: 'baal_xp' state + run_baal_xp/end_run transitions; on_run_baal_xp: fast_save_and_exit -> hero select -> join public game -> corpse/nopickup/pre_buff -> walk to hide spot -> wait loop (XP every 30s, HP check, death recovery, InGame-miss re-anchor) -> leave -> recover to own game; on_end_run short-circuit straight to maintenance - src/config.py: [baal_xp] section (enabled, game_name_filter, max_wait_s, xp_threshold, min_hp_pct, hide_x/y, join_timeout_s) with profile overrides - config/params.ini: [baal_xp] section, order=run_baal_xp, run_trav - profile paladalla: order=run_baal_xp, run_trav (profile routes override base config — without this the route never activates) - stealth: daily budget cap (persisted, restart-proof), buy at A4 Jamella from A3 too (Ormus measured unreliable), session budget 20h * fix: import mouse in bot.py (baal_xp hide-spot click) * feat(stealth): hard per-day runtime cap that survives restarts, and quit D2R session_budget_h is per PROCESS. Every restart re-rolls it, so a bot that gets restarted -- by the user, or by the restart-on-crash path -- can run all day and never trip it. That is exactly the signal a runtime cap is meant to remove. It also silently failed in practice: params.ini still carried the temporary levelling value session_budget_h = 20, which rolls 13-27h, so the live config could not stop the bot within a day at all. Restored to 8. Adds daily_budget_h, keyed on the CALENDAR DATE and persisted to log/.daily_runtime.json, so restarts cannot hand out a fresh allowance. Design points that matter: - The rolled target is stored WITH the date. Re-rolling per process would make restarting a way to draw a bigger budget; once a day's target is chosen it is fixed until the date changes. - Jitter subtracts ONLY. daily_budget_h is a ceiling, not an average: a two-sided roll on 8h could hand out 9h, which is not what "cap it at 8 hours" means. 8 with 0.12 jitter now runs 7.0-8.0h and never more. Verified over 300 rolls: min 7.04h, max exactly 8.00h. - Time before the first tick in a process is not counted, so a crash under-counts rather than over-counts -- the safe direction for a cap. Also closes D2R when the cap trips (daily_budget_close_game, default on). The check runs in on_end_game AFTER save-and-exit, so the character is at the menu with nothing in progress and nothing is lost -- this is not the mid-game force kill that leaves D2R unenterable. A bot parked at character select for 16h is itself the pattern the cap exists to remove; a real player quits the game. The stealth manifest reports daily_budget by checking the CALL SITE in bot.py, not just the config value -- the lesson from chicken_variance and the AFK break that was 0-for-225 while the manifest said "wired". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: alexpolo1 <alex@alexpolo1.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
14 KiB
Handover — 2026-08-29
Everything you need to run this yourself. Written after a long debugging session;
CLAUDE.md has the deep detail, this is the operating manual.
Running the bot
run_botty.bat # starts the process (idle)
python scripts/hermes_bot_control.py start # begins playing
python scripts/hermes_bot_control.py status # running=X paused=Y
python scripts/hermes_bot_control.py stop # exits the process
start is idempotent (fixed 2026-08-28) — pressing it twice is safe. pause
and toggle are the toggle. Before the fix, a repeated start paused the bot
and status still said running=True; that cost ~5 hours once.
Always verify with status, not the "OK: command sent" reply. And check the
log actually moves — === BOT START === is the proof it began a game.
Restarting after a code or config change
A running bot does not pick up edits. Python loads modules at process start.
python scripts/hermes_bot_control.py stop
# wait until nothing is listening:
netstat -ano | grep 18899
run_botty.bat
python scripts/hermes_bot_control.py start
If stop times out, retry it — the socket occasionally needs two attempts. Only
force-kill as a last resort: killing mid-game leaves D2R in a state the bot
cannot re-enter, and you then have to save+exit to the main menu by hand.
Only ever run one instance. Two both bind the control socket and fight over
start/pause, and the logs become nonsense. Check with tasklist | grep -i python.
Is it stuck, or just idling?
The bot sits at the D2R character-select menu during a normal break. The stuck case looks identical. Do not judge by the screen.
LAST=$(grep -n "control socket listening" log/log.txt | tail -1 | cut -d: -f1)
tail -n +$LAST log/log.txt | grep -cE "select_char|Restarting bot|Uncaught exception"
| Normal break | Stuck | |
|---|---|---|
status |
running=True paused=True |
the same |
select_char errors |
none | present |
Restarting bot |
none | every ~20s |
| Log | quiet | new process repeatedly |
The tell is the log filling with restart lines, not the menu.
Break lengths are longer than they look
maybe_afk_break calls wait(m, m*1.5) and wait() applies its own jitter (up
to 1.44x). They compound:
| planned | actual |
|---|---|
| 3.9m | 7.1m |
| 11.9m | 19.5m |
| 20:56 | 25.5m |
So multiply any break setting by 1.5 x 1.44 before deciding it is safe. A
~25 minute idle is what left D2R unable to re-enter once. afk_break_max_m is
capped at 7 for this reason (=> ~15m worst case).
Health check
LAST=$(grep -n "control socket listening" log/log.txt | tail -1 | cut -d: -f1)
tail -n +$LAST log/log.txt > /tmp/c.log
echo "games $(grep -c 'game | start' /tmp/c.log) | failed $(grep -c 'game | end .*fail' /tmp/c.log) | deaths $(grep -c 'You have died' /tmp/c.log) | crashes $(grep -c 'Uncaught exception' /tmp/c.log)"
Useful greps:
| what | grep |
|---|---|
| Step-by-step timeline | grep "TL>" log/log.txt |
| Failure records | grep "FAIL>" log/log.txt |
| Stealth manifest at startup | grep "STEALTH>" log/log.txt |
| Mana threshold crossings | grep "MANA>" log/log.txt |
| Level / exp | ls -t log/stats/mini_stats_*.json | head -1 |
FAIL> gives the whole story of a failed game — reason, location, the three
slowest steps, and a breadcrumb trail with ! marking failures. Read the trail,
not just the reason: the step that blew up is often not the one that caused it.
Temporary settings to revert
| file | setting | now | should be |
|---|---|---|---|
config/params.ini |
session_budget_h |
20 | 8 |
config/params.ini |
difficulty |
hell | your call |
session_budget_h = 20 was raised for a levelling push. It rolls to 13-27h,
which largely disables the stop-for-the-day behaviour that the session-rhythm
work exists to provide. Put it back to 8 once you have the levels. It is
uncommitted, so git checkout config/params.ini reverts it.
Outstanding
PR #39 is merged and live (ec6599f). Confirmed working 2026-08-29: the
pather abort fired for the first time (aborting traverse 1, taking a random guess 8, over 21 games) after being 0-for-152 while it looked correct.
Travincal (hell) status — 2026-08-29: 21 games, 2 failed (9.5%), 0 deaths.
Council dies; 10 of 13 runs produced loot. Both failures were open_wp, but
with different waypoints — A1_WP once, A5_WP once — because the character's
act varies between games. The A5 case follows a Larzuk repair (stale believed
location, Bug 16 family). Waypoint template matching is now on the critical path
for every Trav game, where Pindle never used it at all.
FoH is the wrong build for Travincal. Blessed Hammer does magic damage and
the council are not magic immune; FoH's holy-bolt half only damages undead, so
against the living council half its output does nothing. That is the 2-3k vs
~10k gap — not a tuning issue. atk_len_trav was raised 6.0 -> 10.0 in the
profile as a stopgap (40s min / 120s max across the four attack sequences).
Done 2026-08-29 — respecced and verified live: type=hammerdin,
concentration=f6, redemption=f3. Note Blessed Hammer is NOT on an F-key:
_cast_hammers puts the aura on the right slot and spams left-click, so Hammer
lives on left-click permanently — pressing a hammer hotkey would replace the aura.
concentration and redemption had to move into [paladin]; they were under
[fohdin], which a hammerdin never reads, so both would have been silently
unbound. atk_len_trav is now 3.0 (the fight is a fixed clock, so lower = less
exposure, not less damage).
Trav costs two cross-act waypoint trips per game. The run leaves the
character in Act 3; maintenance relocates it to Act 4 (stash at a4_tyrael_stash,
repair at a4_halbu) and the next run then needs the A3 waypoint again. That
travel is not attributed to any timed step, so a FAIL> trail can show ~38s of
work inside a 250s maintenance window — do not read the trail as the whole cost
here. This blew the 240s budget 5 times; max_maintenance_time_s raised to 420.
It also explains the open_wp failures: Trav exercises waypoint templates on
every game, where Pindle walked to an in-act portal and never touched them.
Not yet investigated: a3.py reports can_buy_pots/can_heal/can_stash all
True, so the trip to Act 4 is not a missing A3 capability — something in the
maintenance chain relocates the character. Keeping town business in Act 3 would
remove both waypoint trips and is the real fix if Trav becomes the main route.
(Cosmetic: a3.py defines can_identify twice, identically — harmless.)
The open_wp failures are NOT a template problem — proven 2026-08-29.
Scored a5_wp.png against all three real failure frames:
- full-frame, no ROI: 0.96-0.98 at (924, 594) — but that is the belt/mana-orb
HUD false positive the
a5.pycomment already documents, not the stone. Thecut_skill_barROI exists precisely to exclude it. Do not "confirm" this template by scoring without the ROI; it produces a confident wrong answer. - inside the real
cut_skill_barROI (0,0,1284,653): 0.436-0.480 against a 0.55 threshold, at scattered positions (1,401) / (712,519) / (32,520). Wandering match positions = noise.a5_wp_2(masked, 4-channel) scores 0.26-0.29.
The waypoint is genuinely not on screen: the character never reaches it.
Recapturing the template would fix nothing. The cause is stale believed location
after town business happens in a different act (Bug 16 family), consistent with
the taking a random guess lines in the same failures.
Therefore: keeping Trav town business in Act 3 is the real fix, not a
performance tweak. It removes the A5 waypoint dependency, the two cross-act
trips, and the maintenance-timeout pressure in one change. 7 of 8 waypoint
misses were A5_WP; A1_WP was a single one-off.
A3 town landmark coverage is the root cause of the Trav failures (2026-08-29).
Measured against the two info_npc_menu_timeout frames where the bot was stuck:
| frame | a3_town landmarks >=0.68 | >=0.62 | best |
|---|---|---|---|
| 191640 | 2 | 3 | 0.728 |
| 191758 | 0 | 0 | 0.607 |
With zero landmarks over threshold the pather cannot localise at all — hence
6 taking a random guess across 2 traverses to a3_ormus. The character then
ends up somewhere arbitrary, which produces all three symptoms from one cause:
Ormus not in his ROI, the waypoint not on screen, and maintenance burning its
budget.
Ormus himself is fine: best match INSIDE his ROI (444,13,372,318) is 0.342/0.334 — noise — while the global best (0.479/0.508) sits outside it. Do NOT lower the body threshold; 0.34 is background level and dropping the bar there invites the Bug 4 false-positive clicks. The NPC is not there to be found.
CORRECTION to the earlier entry in this file: "keeping Trav town business in
Act 3 is the real fix" was wrong. That was based on a3.py reporting
can_buy_pots/can_heal/can_stash as True — a capability check, not evidence
about pathing. A3 is in fact the worst-supported town in the project; moving more
work into it makes things worse. The fix is A3 landmark coverage.
New pather node templates need the character's absolute position at capture time, so they cannot be made from saved frames — this needs a live capture pass in A3.
BELT WAS HALF EMPTY — found 2026-08-30 after the session's only death.
belt_hp_columns=1 # 4 healing potions
belt_mp_columns=1 # 4 mana potions
belt_rejuv_columns=2 # 8 slots for potions VENDORS DO NOT SELL
Rejuvenation potions cannot be bought in D2 — they only drop. So the bot asked
for rejuv=8 on every restock, never got them, and ran Travincal on 4 healing
potions with half the belt permanently empty. 50 Failed to drink rejuv events
in 200 games. The death: drank at 47%, chickened at 33.8%, died in the gap with
nothing left to drink. Many of the 37 chickens were likely "belt empty", not burst
damage.
Now belt_hp_columns=2 / mp=1 / rejuv=1 (8 healing, 4 mana, 4 slots for dropped
rejuvs — convert_rejuv=1 cubes them to Full). Verify after restart that the
HP columns actually fill.
Repair NPC — settled by measurement (2026-08-30):
| NPC | result |
|---|---|
a5_larzuk |
6 fail / ~250 — 2.4% ← use this |
a4_halbu |
6 fail / ~50 — 12% |
a1_charsi |
0 for 3 — pathing never reaches her |
Bug 12's original reasoning holds: Halbu detection is unreliable even after Bug
30's threshold fix. Charsi failed for a different reason — Traverse from a1_wp_north to a1_charsi fails, so open_npc_menu never runs and her "she does
not move" advantage is never tested. A1's wp->charsi route is broken like A3.
A repair_npc code fix went in: the cross-act destination used to be
hardcoded to A5, so the setting was silently ignored on every route not already
standing in A5. It is now honoured (town_manager.repair()).
Damage profile at hell Travincal (confirmed from screenshots): council cast Hydra (fire), Lightning/Charged Bolt, and Frost Nova (cold). Lightning is both the most common and the weakest resist (~48% with Mara) — Thundergod's Vigor (+10 max lightning res, 20% absorb, +20 vit) is the targeted upgrade, ahead of Verdungo's. Fire is well covered by Dwarf Star's absorb.
Do NOT raise atk_len_trav to reduce the "running around". Hydras are
stationary fire turrets, so kill_council's repositioning is actively dodging
sustained damage. 3.0 is correct.
Merc blocks gold pickups. Mizan says: I can't use that = a pickit click
landing on the merc. ~4 gold piles lost per 18 games. The bot retries twice then
moves on; not worth fixing.
Lightning charm rules added to config/bnip/Den gode.bnip (gitignored,
backup at .bak), 558 -> 561 expressions. The existing resist rules sum all four
resists (>= 12 for smalls), and a max single-res lightning small charm is 11 —
so pure lightning charms were picked up and then vendored. Two older rules
existed but each required a second stat ([Maxhp] >= 10, [Fhr] >= 3).
Current bind map (verified live 2026-08-29) — do NOT rebind F7:
| key | skill | key | skill | |
|---|---|---|---|---|
| F1 | Battle Command (CTA) | F5 | Teleport | |
| F2 | Holy Shield | F6 | Concentration | |
| F3 | Redemption | F7 | Battle Orders (CTA) | |
| F4 | Town Portal (not a skill) | F8 | free (Vigor if wanted) |
Blessed Hammer is on left-click, not an F-key. An earlier version of this file said to bind Concentration to F7 — that would overwrite Battle Orders and cost a large part of the life pool.
Two things only you can do:
- Resistances — lightning 23% / poison 12% are the survivability ceiling. Herald of Zakarum + Mara's took health chickens from 3-in-8 to 0-in-11, so this is largely addressed; the remaining holes are gear, not config.
- 75% FCR — currently 60% (HotO 40 + Trang's 20). The 48→75 breakpoint means
everything between is wasted; Arachnid Mesh in place of Goldwrap closes it.
If FCR changes, update
casting_frames: 75% = 11 frames, 60% = 12, and below 48% = 13. The bot derives its cast wait from that number, so a wrong value cuts every buff short.
Known-stale: the CONVICTION preflight template scores 45.9% while the bind
is provably correct, so every startup logs a false alarm. Cosmetic.
Checking your binds after any gear change
python tools/testbed.py spellbook --assets
Hovers the whole bind grid, reads each skill from its tooltip and each hotkey
from the icon corner, and prints which config keys disagree with the game. Exits
1 on a mismatch. This exists because an Enigma put Teleport on F5 and displaced
Conviction, and conviction=f5 would have teleported the character mid-fight.
The one habit worth keeping
Most of what went wrong here was configured behaviour that never executed,
and nothing reported it. AFK breaks were 0-for-225 at a configured 5%. The
manifest said wired throughout.
When something should be happening and you are not sure it is, count it:
grep -c "<the thing>" log/log.txt
A zero where you expected a number is the most informative result in this project. It found the AFK break, the loot-filter clicks, the Chronicle panel, and the pather abort — twice.