Two additions on top of the TL> timeline.
1. FAIL> records. A bare "Approach failed [step: click_red_portal]" names the step that
blew up, but that is frequently not the step that cost the time or caused the problem.
Each failed game now emits a self-contained record:
FAIL> g7 r5 | Approach failed for run_pindle [step: click_red_portal]
FAIL> g7 r5 | at=a5_larzuk | step=resurrect_merc | shot=./log/screenshots/error/...png
FAIL> g7 r5 | slowest: town.resurrect_merc=114s, run.approach=72s, town.repair=19s
FAIL> g7 r5 | trail: game.spawn > town.repair(19s) > town.resurrect_merc!(114s) > run.approach!(72s)
FAIL> g7 r5 | note: 2 failing steps this game: town.resurrect_merc, run.approach
grep "FAIL>" log/log.txt. The trail is the last 12 timed steps of that game with "!"
marking failures, so a failure is diagnosable from the log without replaying it.
2. Periodic Discord digest, default every 2h (general.discord_timing_report_h, 0 disables).
Aggregated from the same timeline the log uses, so the report and the log cannot drift:
**Timing report** - last 2.0h
Games: 92 (81 ok, 11 failed - 12.0%)
Avg town 31s | approach 46s | battle 21s | cycle ~98s
__Slowest steps (avg)__
`town.resurrect_merc ` 103s x7 (5 fail)
`run.approach ` 46s x92 (9 fail)
__Failures by step__
`run.approach ` 9
__Stealth__
`afk_break ` x2 10m total
Sent at game end (a natural boundary; games are ~60s so granularity is fine) and the
window resets on each send, so every report covers exactly the period since the last.
Ranking excludes umbrella entries ("maintenance", the run_name step) since they contain
the others and would always top the list, and excludes the stlth phase since an AFK break
is deliberate idling — it gets its own section instead. Report failures are caught and
logged non-fatally; nothing here can end a run.
Verified by rendering both formats against synthetic data rather than waiting for a live
failure to be the first test.
Co-Authored-By: Claude Opus 5 <[email protected]>