* 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>
The webhook scrub rewrote the 18 stable-only commits, so the hashes quoted in the
post-mortem no longer resolved. Updated, and the rewrite itself is noted inline so the
change of hashes is not a mystery later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main carried 19 commits of separate feature work (baal_xp, cold_plains, melee_hunt,
launch/tools) while stable carried the run_pindle repair. One real conflict, in
_run_wrapper, where both branches added a line at the same point:
stable self.tl("run", "approach", "start") <- run timeline
origin/main self._current_run = run_obj <- baal_xp run tracking
Independent, so both are kept. config/params.ini and src/config.py auto-merged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Narrative record of the incident: failure rate 100% -> ~8%, character 32 -> 69, cycle time
185-250s -> ~52s across 17 commits. CLAUDE.md already carries the per-bug detail as Bugs
23-30; this is the part that does not fit a bug entry — the order things happened in, why
the silent failures cost far more than the loud ones, and the four times my own obvious
answer was wrong.
Two files:
docs/postmortem_pindle_2026-08-27.md canonical, diffable
docs/postmortems/pindle-2026-08-27.html source of the published artifact, kept in-repo
so it survives the scratchpad and stays editable
The corrections section is the reason this is worth committing rather than leaving as a
list of fixes. Each wrong turn looked correct in isolation:
- the red-portal guard: the portal renders on BOTH sides, so "portal visible => still in
town" blocked every genuine entry
- XP as proof of a boss kill: it only proves something died (minions, merc kills)
- "the template is degenerate": the Qual-Kehk asset scored 1.000 against a frame where
the tag actually renders; the THRESHOLD was wrong
- selling the charms: 673 blocked-sell lines were the guard working, not a clog, and
disabling it vendored five resistance charms
Also records the durable lesson: every one of these cost hours because the log held events
rather than structure. That is what the TL>/FAIL>/digest instrumentation exists to fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
D2R UI hitboxes are tight; a 1px offset on the Join Game tab click was
enough to miss the button. Final SetCursorPos pass after the existing
verification check.
- Restore FIRST-RUN.md onboarding guide
- Pin environment.yml to requirements.txt (was loose 24 packages)
- Remove debug screenshots from git (debug_*.png -> .gitignore)
- Remove .hermes/ dev plans from git (already in .gitignore)
- Archive botty_next/ test harness to alexpolo1/my-botty-tools
- Archive docs/legacy-go/ to alexpolo1/my-botty-tools
- Move 14 dev tools from repo root to tools/ directory
- Extend .gitignore to prevent dev artifacts re-entering root
- Promote stable branch with all bug fixes from main
- Bump version 0.8.4 -> 0.9.1
Adds Messenger.send_error across the Discord (red embed with screenshot attached)
and generic (text-only) APIs. Bot._save_error_screenshot now also pushes the
failure to the configured messenger after saving the screenshot to disk, so each
approach/battle/exception failure is reviewable in Discord with the visual.
Gated by new config discord_log_errors ([general], default 1) and the
[discord_events] error toggle. Both optional/backward-compatible. Verified wiring
and the suppression path via a stubbed messenger.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When every run has been auto-disabled there are no routes left to run, so save a
session report and shut down via safe_exit() instead of calling restart_or_exit
(which would needlessly restart D2R into empty games when restart_d2r_when_stuck
is enabled).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recovery: each run now has its own consecutive-failure counter. After
disable_run_after_failures (default 5) consecutive failures, that single run is
disabled for the rest of the session and the bot keeps doing the other runs
instead of stopping. A success resets the counter. If every run is disabled the
bot stops for investigation. Disable is in-memory only (restart re-enables).
Diagnostics: on every run failure (approach, battle, or exception) the bot saves
a timestamped screenshot to log/screenshots/error/ named with the run, reason,
and game/run counters so logs and visuals can be cross-referenced. Gated by the
new error_screenshots config (falls back to info_screenshots). The error/ dir is
routed through log rotation.
Adds config keys error_screenshots and disable_run_after_failures (both optional,
backward compatible) and docs/recovery_and_error_logging.md. Verified bot startup,
config parsing, Bot construction, the disable/reset logic, and screenshot writing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- i_char.py: rewrite _pre_buff_cta() to verify each weapon switch via BC
skill-bar template; detects wrong slot on game start (leftover from
interrupted buff cycle), retries failed switches once, logs clearly
when stuck on CTA slot to prevent dying with wrong weapon in combat
- personal.py: protect needed consumables from sell/drop in inspect_items;
check get_needs() before marking a pot for discard — if the belt needs
that pot type, skip it so fill_up_belt_from_inventory can restock later
- bot.py: add fill_up_belt_from_inventory + update_pot_needs after
buy_consumables so inventory pots reach the belt even when out of gold;
add town-heal loop at start of on_maintenance to drink health/rejuv pots
until HP >= 95% before the next run (health manager is paused in town)
- game_stats.py: add rolling 10-game pickup health check; warns in log and
sends Discord alert when zero item pickups occur across 10 games while
chickens or merc deaths are present
- params.ini: fix show_belt=n -> show_belt=k (belt key was wrong, causing
1.5s wasted recovery attempt every first game in a session)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Updated bot behavior to end the game when no TP charges are left, instead of attempting to walk back to town.
- Refined item processing logic in `processing_helpers.py` to use identity checks for item comparisons and improved item removal methods.
- Enhanced game stats logging to include failure reasons and send Discord notifications for failed games.
- Improved health management logic to trigger chicken behavior based on health percentage after rapid rejuvination.
- Added error handling for item pickup failures in `pickit.py` to blacklist items that cannot be picked up.
- Introduced walking fallback nodes for Countess, Andariel, Mephisto, and Baal runs in `pather.py`.
- Implemented path data validation in run scripts for Countess, Andariel, Mephisto, and Baal to ensure paths are correctly defined.
- Enhanced item identification and stash processes in `diablo.py` with retry logic for failures.
- Updated town management to handle identification failures gracefully and log appropriate warnings.
- Improved character selection logic to provide clearer logging messages.
- Modified game restart logic to launch the game via PowerShell for better session handling.
- Added documentation for recording paths for the Countess run and created a PowerShell script for scheduled task management.