Run Review: logs/2026_07_11_1 (2026-07-11, 07:55–08:03)
Full findings from the first post-refactor VM run, with an actionable TODO list. Written so a future session can pick up any item without re-deriving the analysis.
Run metadata
| Command | uv run --package gameplay-agent aoe2-agent --overlay --iterations 30 (plain run — no just experiment, so no results.tsv row) |
| VM | Windows ARM64, 3024×1672 capture, remote detection → Mac 192.168.0.106:8420 |
| Models | Remote: v9 (Mac). Local fallback on VM: aoe2_yolo_v5.onnx (see F-5) |
| Outcome | Dark Age only, peak pop 14, survival 457.7 s, total food gathered 200, score 386 (last vs AI 437–482), cost ≈ $0.25 |
| Effective turns | 16 of 30 — iterations 19–30 burned on window-focus failure (F-1) |
| Evidence | logs/2026_07_11_1/logs.txt (line refs below), goals.log (UTF-16, byte-swapped — decode before reading), images/*.jpg (17 frames) |
Narrative
The opening was competent: recovered from housed-at-5/5 by turn 3, grew 4→14
villagers, pop cap 5→30. Then the economy starved: food crashed 200→10 by minute 3
and never recovered, wood ended at 4. The final frame
(images/20260711_080310_00018.jpg) shows 8 idle villagers, 7 food / 5 wood.
In the first phase (min 1–4) the executor’s only builds were 5 houses
(iterations 2, 3, 5, 6, 8); the first food building (mill) came at minute 7
(iteration 16), and no farm was ever built — both farm attempts (iterations
15, 17) were silent no-ops (F-2). At 08:03:34 the game window could no longer be
focused and the remaining 12 iterations did nothing (F-1).
Findings (ranked by impact)
F-1: Window focus loss consumed 40% of the run silently
- From 08:03:34, every iteration logged
focus_window_error×3 (Windows error text “Error code from Windows: 0 - The operation completed successfully” — the classic SetForegroundWindow-refused symptom) thencould_not_focus_game/ “Retrying in 1 second” — and still consumed an iteration from the budget (logs.txt:493–555). game_metrics_finalhasgame_end_reason=(empty), so downstream analysis cannot distinguish “played 30 turns” from “harness lost the window at 19”.
F-2: Farm builds are silent no-ops — effect-blind action success
- AoE2 tech gate: the Farm build button requires a completed Mill. Iteration
15 (08:02:12, logs.txt:398–401) pressed build-menu
q→awith no mill in existence → nothing selected → the placement click landed as a plain ground click. Iteration 17 (08:03:06) retried while the first mill was still under construction AND wood was 30 (< 60 farm cost). Both loggedcomposite_step success=Trueand counted as successful actions. - The executor’s iteration-15 reasoning claimed “Mill exists” — false; it confused the strategist’s goal of a mill with the presence of one. Nothing in the loop ever learned the farms didn’t happen.
- Success is measured at the input level (keys delivered), never at the effect level (foundation appears in detections).
F-3: House-only first phase; mill four minutes too late
- Build timeline: House ×5 (iters 2,3,5,6,8, min 1–4) → Farm no-op (15) → first Mill 08:02:44 (16) → Farm no-op (17) → second Mill 08:03:27 (18).
- Houses were built at 8/15, 9/25, and 10/30 pop — the last two with 15+ pop headroom. Cost: 125 wood total, vs the 60 the first farm needed.
- Drivers: (a) no headroom gate on house builds; (b) the strategist goal “Build houses to increase population cap” persisted in the goal list many turns after being satisfied (visible in goals.log); (c) house is the cheapest always-”succeeds” build, so the effect-blind success signal (F-2) reinforced the loop.
F-4: idle_count OCR misreads as 1 — and the misread throttled dispatch
idle_count=1on essentially every tick for 8 minutes; the final screenshot’s HUD badge clearly shows 8 (also earlier frames show growth).- Because
_distribute_idle_actions(apps/agent/src/reactive.py) sizes the batch asmin(idle_count, _IDLE_DISPATCH_MAX=6), a constant misread of 1 meant one dispatch per turn instead of the presence-only default of 3. The new P2.3 feature, failing silently, performed worse than the old presence heuristic. Idle villagers accumulated to 8. - Suspicion: badge geometry at live 3024×1672 differs from the harvested
fixtures — window constants live in apps/agent/src/resource_ocr.py
(
_IDLE_COUNT_X_LO=3.5, _IDLE_COUNT_X_HI=6.8, _IDLE_COUNT_Y_HI=1.8, anchored on pop.x0;read_idle_countat ~line 551). Possibly clipping the glyph so a partial “8”/“18” matches a “1” sample above the 0.45 NCC floor. Needs runtime crops to confirm (T-302).
F-5: VM local detection fallback is doubly broken (v5 + decode failure)
.gitignore:76has**/models/*.onnx; onlyaoe2_yolo_v5.onnx/.ptare git-tracked (committed before the ignore rule). Sogit pullon the VM yields only v5, andresolve_model_path’s highest-version fallback resolves v5 (logs.txt:5Loaded ONNX model: ...aoe2_yolo_v5.onnx).- When the remote server hiccupped once (
remote_detection_failed, logs.txt:406), the v5 fallback failed decode:Unrecognised ONNX output shape (1, 64, 33600); returning no detections(also(1, 64, 8400)) — iterations 15–16 ran blind (entity_count=0), exactly when the first mill was placed. - Remote (v9 on Mac) worked fine all run. Impact limited to fallback moments, but the failure is silent.
F-6: OCR latency is the APM ceiling (10–40 s per tick, 26 s first tick)
- First tick: screenshot 07:55:06.8 →
ocr_readings07:55:33.2 = 26.4 s; scout only moved at 07:55:42 (the “frozen period” after startup). - Causes, all in apps/agent/src/resource_ocr.py + providers/strategist.py:
- RapidOCR engine built lazily on first use (
_rapidocr_engine, ~line 394) — PaddleOCR det+rec ONNX model load on ARM64 CPU ≈ 10–15 s, paid mid-iteration-1. - Backend is
rapidocr(config.py:42 default) → 6 sequential neural OCR calls per tick (food, wood, gold, stone, population, age), ~2–4 s each on the VM CPU. Every tick, not just the first. calibration.3024x1672.yamlpoints attemplate_dir: templates/3024x1672— that directory does not exist (only3024x1964/andhud_digits/), so the millisecond-fast template/NCC backend can’t be selected at the VM’s live resolution.- On strategist turns the frame is OCR’d twice (game loop + strategist
prompt both call
read_hud_readings) — twoocr_readingslines ~9 s apart (e.g. logs.txt:25–26).
- RapidOCR engine built lazily on first use (
- Net: ~16 decisions in 8 minutes; one decision every ~30 s. The LLM was never the bottleneck.
F-7: Executor returned zero actions on 6 of 16 turns
no_actions_fallbackon iterations 7, 9, 11, 12, 13, 14 (logs.txt:190, 241, 294, 319, 345, 372): reasoning text describes actions (“Queue a villager and send the idle villager to sheep”) but no tool calls emitted. The h/q/. fallback papered over it. A third of executor spend was wasted.
F-8: Reactive tier is need-blind during a food crisis
- The idle-dispatch rotation is population-phase based (Dark-Age pattern food/food/food/wood/wood, phase = population + i), not need-based: with food at 10–30, iterations 6–10 sent idles to wood while queueing 50-food villagers every turn. The strategist diagnosed “food crisis” 5× but has no lever over the reactive rotation.
F-9: Metrics bugs
action_success_rate=2.38— numerator counts composite steps (31), denominator counts LLM actions (13). Meaningless as reported.game_end_reasonempty (see F-1).turn_rewardwas ~always −0.05…−0.1 (population term only) — no positive signal the whole game.
F-10 (minor)
right_click_off_map×3 (logs.txt:289, 368, 393) — idle-dispatch targets at screen edges rejected.- Intent-string bug: “Send idle villager to tree (food)” (logs.txt:393) — the label uses the requested kind, the target uses the resolved class.
- Turn-3 executor claimed
age='Feudal Age'(logs.txt:96) while OCR said Dark. - goals.log is written byte-swapped UTF-16 — unreadable with plain tools.
TODO — grouped, prioritized
Priorities: P0 = do before the next baseline run; P1 = next; P2 = nice.
A. Harness / run lifecycle
- T-101 (P0) Focus-loss handling: on repeated
could_not_focus_game, stop consuming iterations (pause the counter), attempt a stronger refocus (minimize/restore or Alt-Tab injection), and after ~30 s abort withgame_end_reason="lost_focus". Files: apps/agent/src/window.py, game_loop.py. Test: simulate focus failure, assert iteration counter frozen + end reason set. - T-102 (P0)
Always setDONE: normal exit →game_end_reasoniterations_exhausted, window gone →game_not_found; victory/defeat/ timeout/interrupted/error were already set. (lost_focuslands with T-101.) - T-103 (P0) Run baselines via
just experiment "<desc>"(orjust experiment-baseline 3 --time-budget 1200) so rows land inexperiments/results.tsv— this run left no ledger trace. Runbook: docs/runbooks/baseline-experiments.md. - T-104 (P2) Write goals.log as UTF-8 (it’s currently byte-swapped UTF-16 from PowerShell redirection — prefer opening the file from Python with encoding=“utf-8” instead of shell redirection).
B. Perception — OCR speed (fixes F-6, the APM ceiling)
- T-201 (P0) DONE:
resource_ocr.warm_up_ocr()(engine + one tiny inference, never raises; engine construction now lock-guarded) launched as a background thread ingame_loopstartup when the backend is rapidocr. - T-202 (P1) Harvest digit templates for 3024×1672 into
resource_ocr_assets/templates/3024x1672/(the calibration YAML already points there; use scripts/calibrate_resource_bar.py + frames from this run) and flip the VM toAOE2_OCR_BACKEND=template. Resource/pop reads drop to milliseconds. - Age: the template backend leaves age "" (keeps last-known). Keep RapidOCR for the age field only, sampled every ~5 ticks (age changes 3×/game). - Cleaner long-term: extend the resolution-independenthud_digitsmulti-sample-bank approach (whatread_idle_countuses via_normalize_glyph) to resource digits, so per-resolution harvesting is never needed again. - T-203 (P1) Share one HUD reading per iteration between game loop and
strategist (currently two full passes on strategist turns). File:
providers/strategist.py
read_hud_readingscallers. - T-204 (P0) DONE: the iteration-1 ground commands (zoom, select scout,
auto-scout) now run in
game_loopbefore the first OCR/detection pass;_run_routine_upkeepno longer owns them. (Villager queueing stays post-perception — running it twice would double-queue.)
C. Perception — idle-count trust (fixes F-4)
- T-301 (P0) DONE:
GameState.idle_streak(consecutive lit-badge turns, maintained once per iteration ingame_loop); after 4 turns (_IDLE_COUNT_SUSPECT_STREAK) the reactive batch is floored at the blind presence batch (max(count, 3), cap 6 intact; a pinned 0 is also overridden). Tests in tests/test_reactive.py. - T-302 (P1) Instrument: save the idle-badge crop + best-NCC score per
tick on the VM (debug flag) to fixture the real failure. Hypothesis:
badge window (
_IDLE_COUNT_X_LO/X_HI/Y_HI, pop.x0-anchored) clips the live-resolution glyph so a partial 8/18 matches a “1” sample ≥ 0.45 NCC. Then fix geometry or add the live crops to the fixture suite (tests/test_resource_ocr.py_EXPECTED_IDLE_COUNT).
D. Detection / model ops (fixes F-5)
- T-401 (P0, manual) DONE (validated in run 2: startup log shows
aoe2_yolo_v9.onnxloaded locally). Original instructions kept for the next model version — weights are gitignored; pull never updates them:scp martondobos@192.168.0.106:.../packages/detection/src/inference/models/aoe2_yolo_v9.onnx C:\Projects\aoe2-agent\packages\detection\src\inference\models\(orpython3 -m http.serverin the models dir + curl). Verify startup log saysaoe2_yolo_v9.onnx. Leave v5 files (git-tracked). - T-402 (P0) DONE:
get_detectornow substitutes the newest bundled weights (instead of degrading to mock) when the configured name is missing, with a warning naming both; the configured name is threaded throughget_remote_detector(model_name=...)so the VM’s local fallback warns too. - T-403 (P1) Fix or fence the v5 ONNX decode:
(1, 64, N)is a transposed head layout the parser doesn’t recognize → silent 0 detections. Either handle the layout or make the fallback refuse + alarm instead of running blind.
E. Decision-making / economy (fixes F-2, F-3, F-7, F-8)
- T-501 (P0) DONE:
executor.build_rejectionnow also gates on prerequisites (BUILD_PREREQ_CLASS: farm needs a mill that detection has EVER seen — evidence accumulates inset_detected_entitiesand from verified placements) and wood cost (BUILD_WOOD_COSTliterals). Applied in_handle_build,_execute_build, and_execute_reassign_villager; the reason string is the failure detail the LLM sees. - T-502 (P0) DONE:
_verify_build_placementreturns a verdict and_handle_clickfails the placement when no building of the expected class is detected near the click (unverifiable → benefit of the doubt); a verified placement records prerequisite evidence. Root-cause bonus: the verification had NEVER run in live games —ClickActionlacked abuilding_keyfield, so pydantic validation silently dropped it (this is why the run log had zero build_placement_verified/failed lines). Field added; the fallback house click now carries it too. NOTE: the “fail on unconfirmed” semantics were REVERSED by T-508 (run 2 proved unconfirmed ≠ failed — foundations aren’t detectable); T-502’s lasting contributions are the building_key field fix, the verdict plumbing, and evidence recording. - T-503 (P0) DONE:
executor.build_rejection(fed by a per-turnset_population_snapshotfrom the game loop) rejects house builds when headroom > 4 or the cap is 200 — in both_handle_build(single-shot) and the provider’s_execute_build(tool loop); the reason string is returned as the failure detail so the LLM plans around it. - [~] T-504 (P1) MERGED INTO T-510: its two halves went separate ways — “skip the villager queue when food < 60” CONFLICTS with continuous Dark Age villager production (villagers are the food engine) and is replaced by T-510’s banking-phase skip; “force the idle rotation to food during a crisis” survives as part of T-510’s economy schedule.
- T-505 (P1) Strategist goal hygiene: expire/down-rank a goal once its metric is satisfied (the pop-cap goal outlived its usefulness by many turns). File: apps/agent/src/goals.py.
- T-506 (P1) Zero-action executor turns: force tool choice (or prompt: “you must emit at least one action or explicitly pass”) — 6/16 turns returned reasoning with no tool calls. File: providers/claude.py.
F. Metrics (fixes F-9)
- T-601 (P1) Fix
action_success_rate: consistent numerator/ denominator (effect-verified successes / attempted actions, once T-502 exists). File: wherever game_metrics_final is computed (game_loop.py). - T-602 (P2) Intent-string fix: idle-dispatch intent should name the
resolved target kind, not the requested one
(apps/agent/src/reactive.py
_resolve_idle_targetcaller).
Suggested sequencing
- Quick wins, no VM: T-301 (idle trust gate), T-503 (house gate), T-204 + T-201 (startup), T-402 (substitution warning), T-102 (end reason).
- VM one-timers: T-401 (copy v9), then T-101 (focus handling — needs VM to verify).
- The economy fix that changes game outcomes: T-501 + T-502 (prereq + effect verification), then T-504.
- Throughput: T-202/T-203 (template backend + shared reading) — triples decision rate; re-run the baseline after this lands and compare tick times.
- Re-run
just experiment-baseline 3(T-103) only after 1–3, so the P0.1 baseline isn’t polluted by known harness bugs.
Follow-up: Run 2 review (logs/2026_07_11_2, 13:19–13:30 UTC)
First run with the P0 fix batch (commits 5cbab20 + 89e9521) live on the VM.
Peak pop 18 (was 14), survival 569 s (all 30 iterations — no focus loss this
time), cost $0.49, game_end_reason=iterations_exhausted. Still Dark Age.
User notes: lots of misclassification remains; farms finally got built; two
mills were built (one redundant); after the mill there were turns with idle
villagers but no farm building; villagers were sent to misclassified “farms”
(bare ground).
Fixes validated in the wild
- T-401 ✓
Loaded ONNX model: ...aoe2_yolo_v9.onnxlocally. - T-201 ✓
ocr_engine_warmed seconds=8.6in the background; T-204 ✓ scout commands fired at ~13 s, before the first perception pass. Iteration cadence improved to ~13 s (was ~30 s). - T-102 ✓ end reason recorded; metrics sane (
action_success_rate=0.88, was a bogus 2.38). - T-301 ✓ idle trust gate engaged:
idle_countwas pinned at 1 again on all 71 readings, but 11 turns dispatched the floored 3-pair batch (routine_executed count=8) instead of run 1’s single dispatch. - T-501/T-503 ✓ all gates fired correctly in-game: house blocked at 6 and 8 headroom, mill blocked at 8/100 wood, farm blocked at 40/60 wood.
- T-502 ✓ (behaviorally) the failure loop closed: iteration-3 executor reasoning literally says “The build composite failed last turn, so I’ll manually select an idle villager and build a house.”
New findings
F-11: Placement verification has heavy FALSE NEGATIVES — and caused the duplicate mill
- 16
build_placement_failedvs 1build_placement_verified— yet the HUD wood deltas prove many were real: mill #1 (13:23:12, x=870,y=801) dropped wood 160→70, mill #2 (13:23:54) dropped 100→10, and 4 farms existed by turn 30 (strategist: “4 farms visible”) against 12 farm attempts with 11 “failed”. - Root cause: the rescan runs ~1.5 s after the click, when the building is a construction foundation — YOLO’s farm/mill classes match the completed sprite, not scaffolding. So verification reports failure for almost every genuinely-successful placement.
- Direct consequence: the redundant second mill. Mill #1 was real (wood spent), the LLM was told it failed, so it placed mill #2 — 100 wasted wood. The false negative turned an honesty feature into an economic bug.
- The single “verified” farm is itself suspect: the check is “any entity of the class within 160 px”, so a pre-existing farm near the click point verifies a new one (false positive). Needs before/after count comparison, not presence.
F-12: Misclassification is now the dominant error source (user notes + screenshots)
- Screenshots show: a phantom
town_centerat 99% confidence on the black fog edge;stable 93%andknight_line 70%in Dark Age (tech-impossible classes); hugetreeboxes covering bare ground;farmdetections on plain terrain. - Gameplay impact: 6 idle dispatches targeted “farm (food)”, some of which were
misclassified ground — villagers walked to nothing and idled (matches the
user’s observation and the badge showing 7 idle at 13:26). Detection FPs also
poison
_buildings_confirmed(a phantom mill would unlock farms) and ownership/alarm logic. - This is the F1 0.67 ceiling showing up as concrete losses → prioritize P0.2 (eval set → 200 frames) and harvest THIS run’s frames as hard negatives (bare-ground farm FPs, fog-edge phantoms) for the next retrain.
F-13: idle_count still pinned at 1 (71/71 readings)
The trust gate contained the damage, but the underlying misread is unfixed — T-302 (save badge crop + NCC score per tick on the VM, fixture the failure) remains the path to actually reading 7/8-idle correctly.
F-14 (minor)
Send idle villager to tree (food)intent mislabel ×3 (T-602 still open).right_click_off_map×3;no_actions_fallbackon 6 of 27 LLM turns.total_food_gathered=200is broken as a metric — it records the max HUD food value ever seen (the 200 starting stock), not gathered food.
New TODOs (append to the categories above)
E. Decision-making / economy
- T-507 (P0, user-proposed) DONE:
GATHER_CLASSES_BY_KINDexcludesfarmfrom right-click targeting (job inference keeps it); a food turn with no huntables/forage on screen emits ONE build-farm action per turn — the executor’s mill/wood gates reject it at zero keystroke cost when it can’t work, and the builder auto-farms the field it finishes. - T-508 (P0) DONE: visual check now compares before/after COUNTS near
the point (a pre-existing neighbor can’t vouch for a new building);
unconfirmed placements return success (“foundations aren’t detectable”)
and queue in a pending ledger settled against the HUD wood spend on the
next
set_hud_snapshot(per-entry, ±20 wood income slack, identical readings treated as stale OCR and re-checked). A confirmed purchase records prerequisite evidence; a missing one logsbuild_purchase_missing. No more false “failed” → no more duplicate mills from our own feedback.
D. Detection / model ops
- T-509 (P1) Detection sanity filters: drop/downweight tech-impossible classes by age context (stable/knight_line in Dark Age), reject boxes centered on fog/black regions (phantom 99% town_center), and harvest run-2 frames as hard negatives for the v10 retrain (bare-ground farm FPs are the top gameplay-affecting class).
Follow-up: Run 3 review (logs/2026_07_11_3, 14:02–14:11 UTC)
First run with T-507/T-508 (commit d3e15d2) live. Peak pop 21 (14 → 18 →
21 across the three runs), all 30 iterations, iterations_exhausted, still
Dark Age. User notes: farming visibly better — but Feudal not reached in 30
rounds.
Fixes validated in the wild
- T-508 ✓ wood-delta settlement worked end-to-end: mills, houses, and a
farm
build_purchase_confirmed; genuinely-failed placements loggedbuild_purchase_missing; zero false “failed” reports; the confirmed mill unlocked the farm prerequisite via the ledger. - T-507 ✓ count-based visual verification confirmed 3 farms (was 1 suspect in run 2), and the reactive tier’s build-farm fallback fired (“Build farm for idle villager (no forage/huntables visible)” ×2). No idle villager was sent to a detected farm.
- T-501/T-503 ✓ gates blocked 4 house-spam attempts and 2 pre-mill farms.
Why Feudal wasn’t reached (F-16) — the new #1 issue
Feudal costs 500 food (plus two Dark Age buildings, which the agent has)
and one action: select TC (h) → Research Age Up (z — documented in
prompts/hotkeys.md line 25). Neither precondition path exists in the agent:
- All food goes into villagers, forever. The reactive tier pressed “Queue villager” on 28 of 28 turns (~1400 food attempted at 50 each). The food trajectory shows the consequence: 200 → 10 → 4 → …, never above 81 after the opening. Banking 500 is arithmetically impossible while the queue runs unconditionally.
- The only existing brake never engaged:
_POP_CAP_BY_AGE["Dark Age"]=22exists exactly to bank food, but pop only hit 21 at iteration ~28. - Nothing ever presses the button. The strategist keeps an “Advance to
Feudal Age” goal, but no tier — reactive, executor toolset, or fallback —
contains an age-up action. Grep confirms: zero
h→zsequences in any run.
Other findings
F-17 (corrected): Duplicate mill ATTEMPT — only one mill was actually built
Two mill placements were attempted 24 s apart (14:05:49, 14:06:13), but the
user confirms (and the wood math agrees) only one mill existed: attempt #2
no-oped in-game because the wood was already spent. No wood was wasted this
run — the game’s own affordability check did what our stale-snapshot cost gate
couldn’t (both attempts saw the same pre-purchase wood_before=160).
Two real issues remain:
- The LLM still tried to build a second mill — it has no persistent “buildings I own” memory, and mill #1’s settlement is inherently one snapshot late (T-512 stands).
- The ledger over-confirmed: one 160→8 wood drop settled BOTH pending
entries (
build_purchase_confirmed building=milltwice, identical readings). Per-entry settlement lets multiple pendings share one piece of evidence. Harmless today (the confirmed-set is idempotent), but it would corrupt any future count-based context line (“mill×2”) → see T-514.
F-18: Farm placement reliability ≈ 50%
4 farm placements build_purchase_missing with wood UNCHANGED or rising —
those placements genuinely no-oped (vs 4 confirmed). Likely cause: the
placement ring anchors on the TC and the near-TC ring is increasingly occupied
by houses/farms as the base grows; clicks land on blocked tiles/fog and all 6
retry offsets miss.
F-19 (persistent, from earlier runs)
idle_countpinned at 1 on 71/71 readings again (T-302 still the fix).no_actions_fallbackon 7 consecutive iterations (11–17) — T-506 worsening.action_success_rate=1.54— broken again (successful=40 > total=26); run 2’s sane 0.88 was luck, T-601 still needed.total_food_gathered=200still records starting stock, not gathering.
New TODOs
E. Decision-making / economy
- T-510 (P0) Dark Age food-banking schedule: stop the reactive villager
queue when banking for Feudal — e.g. in
_queue_villager_actions, skip the queue whencurrent_age == "Dark Age"andpopulation >= 16(or when a strategist “bank food” goal is active), so income accumulates toward 500. Without this, Feudal is unreachable at ANY iteration count. File: apps/agent/src/reactive.py. - T-511 (P0) Age-up action: reactive rule — when
current_age == "Dark Age",food >= 500(+ small buffer for OCR error): emitpress h→press z(Research Age Up). Optionally anadvance_agecomposite for the LLM too. Deterministic path to Feudal; pairs with T-510. Files: reactive.py (+ claude_tools if composite). - T-512 (P1) Surface owned buildings to the LLM: add a “Known
buildings: mill×1, farms×3 (pending: farm×1)” line to the executor/
strategist context from
_buildings_confirmed+ the pending ledger, and optionally gate a second mill (w) while one is confirmed/pending — kills the duplicate-mill loop at its root (the LLM forgets what it built). Files: executor.py (accessor), turn_phases.py/context_builder.py. - T-514 (P2) Evidence-consuming settlement: settle pending placements against a spend BUDGET (observed wood drop), deducting each confirmed entry’s cost before judging the next — one purchase can then confirm at most one pending of that cost. Prerequisite for any count-based “Known buildings” context (T-512). File: executor.py (_settle_pending_placements).
- T-513 (P1) Placement anchor spreading:
default_build_placementanchors only on the TC; as the ring fills with houses/farms, ~50% of farm placements no-op. Anchor farms on the MILL when one is confirmed, skip candidate points whose surroundings are undetected/fog, and consider larger ring radii as building count grows. File: executor.py.
Follow-up: Run 4 review (logs/2026_07_11_4, 16:15–16:25 UTC)
First run with the Feudal path (T-510/T-511) + honest metrics + clean-code
pass live. Peak pop 18, all 30 iterations, no focus loss, cost $0.50,
iterations_exhausted. Still Dark Age — but for a NEW reason (F-21).
User notes: (1) scout explored early ✓ but the first villager action still
came noticeably late; (2) 3 villagers died hunting boars; (3) food was
over-prioritized → wood hit 0 → no farms → berries dried → food income
collapsed.
Fixes validated in the wild
- T-601 ✓ metrics finally honest:
action_success_rate=0.86(≤ 1),executed_actions=56present,total_food_gathered=342(a real income number — was the fake 200 in every prior run). - T-510 banking ✓ (mechanism) the queue stopped exactly at pop 16 (last reactive queue press at pop 15, 16:23:54); food then climbed 114 → 154. The mechanism works — the economy underneath it didn’t (F-21).
- Gates ✓ all cost/headroom rejections correct: farm ×3 (wood 0–50), mill ×1, lumber_camp ×1, house ×5. Settlement confirmed 4 houses + 1 mill; no duplicate mill this run (T-508’s ledger + prereq evidence held).
- T-204/T-201 ✓ scout exploring at ~11 s; warm-up off the critical path (10.5 s in background).
- T-101 ✓ (vacuously) no focus loss occurred;
game_end_reasonlabeled. - NOTE: run launched plain again (
aoe2-agent --iterations 30) — T-103 still unexecuted, no results.tsv row for the 4th run in a row.
New findings
F-20: Boar hunts killed 3 villagers (user note 2)
9 idle dispatches targeted boar (food) — boar sits in the food gather
classes, and a lone villager right-clicked onto a boar ATTACKS it; the boar
wins. Population drops 8→7 and 12→10 confirm 3 deaths (~17% of peak
workforce). Real AoE2 boar-hunting needs 3+ villagers and TC luring — far
beyond the reactive tier. Compounding risk: at F1 0.67 a “deer” label isn’t
trustworthy either, so confidence-gating on species is not safe (F-12).
F-21: The food-crisis override starved wood — a REGRESSION from T-510
The famine rule (food < 60 → every idle slot to food) held for most of the
game (food was under 60 from ~turn 3 to ~turn 20), so idle routing sent
NOTHING to wood. Wood: 200 → 50 → 0 for the final third. Consequences
cascade: farms unbuildable (60 wood) → mill/lumber_camp also rejected →
berries dried with no farm replacement → food income collapsed → the crisis
persisted → the override kept starving wood. A positive feedback loop the
original rotation’s wood slots existed to prevent. Banking (working
correctly!) then had no income to bank: food peaked at 154, never 500, and
the age-up rule (0 h→z presses) correctly never fired.
F-22: Opening latency — scout fast, economy slow (user note 1)
Scout at ~11 s ✓. But: first villager queue at 52 s, and the first idle dispatch (16:16:01) was off-map (x=34) and wasted — the first effective villager action landed in iteration 2, ~85 s in. Two mechanical causes:
- The first OCR pass still took ~19 s even with the engine pre-warmed — per-field rapidocr cost (T-202’s territory).
- A 10-second stall between the
handqpresses (16:15:47 → 16:15:57), coinciding exactly with the strategist’s concurrent OCR pass — onnxruntime holds the GIL in its worker thread, starving the event loop between actions. T-203 (single shared HUD read) removes that second pass and the contention window with it.
New TODOs
E. Decision-making / economy
- T-515 (P0, user-directed) Safe-huntables rule: remove
boarfrom the food gather-targeting classes — anddeertoo, since at F1 0.67 a species label can’t be trusted enough to bet a villager’s life on (a boar misread as deer is fatal). Idle food routing targets sheep, berry_bush, and farm-builds only; job inference keeps all classes. Files: entity_utils.GATHER_CLASSES_BY_KIND; tests. - T-516 (P0, user-directed) Wood floor during the food crisis: the
famine override must not zero wood income. When
food < 60ANDwood < 60(a farm’s cost), reserve at least one dispatch slot per turn for wood — “prioritize food, but keep farming affordable.” Consider scaling: wood slots until wood ≥ 60 + one farm’s headroom. Files: reactive._idle_pattern; tests asserting the loop can’t lock. - T-517 (P1) Opening effectiveness: largely subsumed by T-202/T-203 (the 19 s first read and the GIL-contention stall) — this item tracks verifying, after those land, that the first EFFECTIVE villager dispatch happens < 30 s in. Includes the wasted off-map first dispatch (edge target from the first zoomed-out frame).
V-8 matrix additions
- T-515: sim rule “boar dispatch without escort kills the villager” → metric: villagers lost per game (3 today → 0 after).
- T-516: V-1’s closed loop reproduces the wood-starvation lock directly — run the current reactive tier from run 4’s turn-3 state and wood pins to 0 with farms unbuildable; after the fix, farms sustain food income and the banking → 500 → age-up chain completes. F-21 is the strongest argument yet for V-1: T-510 shipped with every unit test green and still broke the economy, because the failure only exists in 20-turn composition.
Follow-up: Run 5 review (logs/2026_07_11_5, ~19:2x UTC)
The first ledger-recorded run (T-103 finally executed, after clearing the
VM’s old-layout husk directories that were shadowing gameplay_agent and
detection as namespace packages): exp_0014, composite 0.3153
(survival 0.42, population 0.40, economy 0.063, age 0.0, action_success 0.80),
27 turns / 505 s, iterations_exhausted. First run with T-515/T-516 live.
UPDATE: the VM’s ledger copy (snapshotted at logs/2026_07_11_5/results.tsv)
carried a surprise: 13 historical rows (exp_0001–exp_0013, Mar–Apr)
preserved from the old mislocated ledger path, including exp_0013
(2026-04-25) which reached Feudal (age 0.33) under a time-budget run.
The ledger itself is deliberately NOT committed (machine-local; the VM is its
source of truth — gate runs there). Caveat for comparisons: those runs used
--time-budget (survival component = 1.0 on timeout), while exp_0014 used
--max-iterations 30 (survival 0.42 on iterations_exhausted) — composite
scores are only comparable within the same budget mode. exp_0014 is the first
row of the post-improvement-plan era and the reference for the current code
line.
Fixes validated in the wild
- T-515 ✓ perfectly: zero boar/deer dispatches (was 9 in run 4) and zero villager deaths — population 4→20 strictly monotonic, a first. Dispatch mix: 22 sheep, 19 berry_bush, 10 tree(wood), 2 tree(food-label, T-602).
- T-516 ✓: wood never collapsed (min ~48, was pinned 0 in run 4); the famine loop from F-21 did not form. Food recovered to 140–170 by run end.
- T-103 ✓: game_runner wrote the ledger row and printed the composite — the baseline pipeline works end to end.
- Peak pop 20; action_success_rate 0.80 (honest); no focus loss.
Why still no Feudal — the gap is now throughput, not correctness
Food ended at ~170, climbing ~15/turn late — 500 was ~20 turns away when the 30-iteration budget ran out. No rule misfired (0 age-up presses is CORRECT at food < 500). Two compounding limiters:
F-23: Wood plateaus JUST under a farm’s cost — farms locked out by a hair
Six farm attempts rejected at wood 48, 55×4, 59 — each failing by 1–12 wood; only ONE farm got built all game. Houses (4×25) + the mill (100) drained the stock, and the wood-dispatch share (crisis 2:1 + normal pattern) sustains ~50–55 wood but not 60+. The T-516 floor prevents collapse but its target equals the farm cost exactly, so the economy hovers at the boundary where every farm attempt loses the race against the next house purchase.
F-24: Ledger false-MISSING on the mill — the ±20 slack is too tight
build_purchase_missing building=mill wood_before=130 wood_now=60: 70 net
drop vs the required ≥80 (100 − 20 slack) — the mill WAS real (the strategist
later saw it and farm prereqs passed via detection evidence), but ~30 wood of
gathering income during the settle window masked the spend. Harmless this run
(visual evidence covered it) but the slack needs to scale with lumberjack
count / settle-window length, or the false-missing rate grows with the
economy.
F-25 (persistent)
idle_count pinned at 1 (71/71 — T-302 unmoved through five runs); 2×
“tree (food)” intent labels (T-602).
New TODOs
E. Decision-making / economy
- T-518 (P1) Farm-affordability wood bias: when a mill exists and
wood sits in the near-miss band (~40 ≤ wood < 60+margin), bias a
dispatch slot to wood even OUTSIDE the food famine, and/or raise the
T-516 floor target to
_FARM_WOOD_COST + margin— six farm attempts failed by ≤ 12 wood this run. File: reactive._idle_pattern. - T-519 (P2) Settlement slack scaling: ±20 wood is calibrated for a ~15 s window with few lumberjacks; run 5’s mill settled over a longer window with more gatherers and was falsely judged missing (F-24). Scale slack with elapsed snapshots (e.g. 20 per settle attempt) or estimate income from the wood-dispatch count. File: executor._settle_pending_placements.
Process note for the next Feudal attempt
30 iterations ≈ 8.5 min may be structurally short for a 500-food bank even
with a healthy farm economy. Until T-202/T-203 raise the decisions-per-minute,
run Feudal attempts with --max-iterations 45 (or --time-budget 900) so
strategy, not runway, is what’s being measured.
Follow-up: Run 6 review (logs/2026_07_11_6, exp_0015)
First 50-iteration run (per the run-5 process note). Composite 0.4579 —
best of the iterations-mode era (+45% over exp_0014) — 47 turns / 893 s, peak
pop 23, total_food_gathered=1053, zero villager deaths again. The economy
now WORKS: food banked past 500 by ~turn 33 and kept climbing to 767. Still
Dark Age, for one final, precise reason (F-26). User notes: (1) 3 towers were
built — towers must not be a Dark Age priority; (2) Feudal was blocked because
only ONE qualifying Dark Age building existed (the mill) — a lumber camp was
needed.
Fixes validated
- T-510/T-511 end-to-end ✓: banking held (food 451 → 532 → 627 → 767
while pop stayed 22–23) and the age-up rule fired — 14
h→zpresses (reactive + LLM). The trigger chain works; the game refused for its own reason (F-26). - T-515/T-516 ✓ again: zero boar/deer dispatches, zero deaths, wood never collapsed; 3 farms + 4 houses purchase-confirmed.
- Longer budget ✓: 47 effective turns; the run-5 “throughput not correctness” diagnosis held — more runway converted directly into banked food.
New findings
F-26: Feudal blocked by the TWO-Dark-Age-buildings requirement (user note 2)
AoE2 requires two qualifying Dark Age buildings to advance — and houses
don’t qualify. The agent’s only qualifying building all game was the mill, so
all 14 age-up presses were no-ops against a greyed button while 500–767 food
sat banked for ~15 turns. The painful irony: the simulator already encodes
this (world_sim.FEUDAL_PREREQ_BUILDINGS = {mill, lumber_camp}) — a V-1
closed-loop run would have caught it before the VM did, since the sim would
never have advanced either. The agent has no concept of the requirement in
either the reactive tier or the executor gates.
F-27: Three phantom towers — placed by leaked input state, not by decisions (user note 1)
The log contains zero tower/outpost intents — no LLM action, no reactive
action, no build-menu press for them. Yet three tower-like buildings exist.
Stone only moved 170→165 all game, so they’re cheap outposts (watch
towers would have cost ~75 stone). Hypothesis: some of the 14 age-up
sequences (h then z) landed with a villager + open econ build menu as the
active UI context instead of the TC — and in DE’s economic build menu, Z is
the Outpost slot; a later placement click then dropped the ghost. (Also worth
verifying: hotkeys.md documents “Z: Research Age Up” at the TC — confirm
against the live DE default profile.) Note: no anti-tower prompt rule
currently exists (hotkeys.md line 48 even documents the tower key for the
LLM); user note 1’s remembered rule predates this prompt set.
F-28 (minor)
- Age-up spam: 14 identical no-op presses across ~15 turns — harmless but noisy, and each press is an opportunity for F-27’s context leak.
no_actions_fallback×10 (T-506’s single retry reduces but doesn’t eliminate zero-action turns on this longer run).
New TODOs
E. Decision-making / economy
- T-520 (P0) Lumber camp in the Feudal plan + “already built” gate:
1. reactive: during Dark Age once the economy is established (e.g.
pop >= 12), emit a build-lumber-camp action (r, 100 wood) until one is confirmed — it’s BOTH the missing age-up prerequisite and a wood-income boost (synergy with T-518’s near-miss band). 2. executorbuild_rejection: rejectw/r(mill/lumber camp) when that class is already in the confirmed-buildings evidence — reactive can then emit the build every turn at zero keystroke cost once built, and this also closes T-512’s residual duplicate-mill gate half. Files: reactive.py, executor.py; tests. - T-521 (P0) Defensive age-up + no Dark Age towers:
1. Prepend an
escapepress to the age-up sequence (cancel any open build menu / placement ghost beforeh,z) — closes F-27’s leak window regardless of its exact mechanism. 2. Gate the age-up press on the prerequisites being met (mill + lumber camp confirmed) so it fires once instead of spamming 14 no-ops. 3. Prompt line: never build towers/outposts in the Dark Age. 4. VERIFY on the VM thatZis actually the DE age-up hotkey at the TC (hover the button); fix hotkeys.md if not.
Follow-up: Run 7 review (aborted by user at ~turn 10 — 3 towers again)
User stopped the run after seeing three towers. The log caught the full causal chain, and the user identified the game mechanic that explains the towers in BOTH runs 6 and 7.
F-29: A single-frame phantom mill poisoned the build gates BOTH ways
Timeline from the log: 22:44:29 farm correctly rejected (“requires a completed
mill and none has been seen yet”) → 22:44:43, fourteen seconds later, a farm
build PASSES the gate with no mill in the game — a one-frame misdetected
“mill” (F-12 class) had entered buildings_confirmed — → 22:45:49 + 22:46:08
the LLM’s REAL mill builds are rejected with “mill already built”. One phantom
frame simultaneously unlocked impossible farm builds and locked out the fix.
Fixed (T-522): detection evidence is now thresholded —
_BUILDING_CONFIRM_SIGHTINGS = 3 distinct frames before a class counts;
ledger (wood-delta) and verified-placement confirmations remain instant.
F-30: The tower mechanism, solved (user-identified) — corrects F-27
In the DE econ build menu WITHOUT a mill, the A slot is the OUTPOST.
Pressing A doesn’t no-op — it selects a tower, and the placement click
builds it. Every phantom-unlocked farm attempt (., q, a, click) built an
outpost: 3 farm attempts pre-mill = 3 towers, in both runs. (Run 6’s F-27
hypothesis blamed the age-up z presses; the real culprit was the same farm
sequences — run 7 reproduced the towers with zero z presses.) The farm
prerequisite gate is therefore a SAFETY gate, and the phantom evidence that
bypassed it was the direct cause. Fixed (T-522 above + T-523):
build_menu_steps now ends with an escape press (a leaked menu can never
re-map later keystrokes), and core.md/hotkeys.md document the
A-without-Mill=Outpost hazard for the LLM.
F-31 (minor): interrupted run logged game_end_reason= (empty)
The Ctrl-C abort produced a metrics line with an empty end reason — the KeyboardInterrupt path (which sets “interrupted”) apparently didn’t fire under the Windows asyncio runner. Low priority; recorded runs aren’t normally aborted.
Follow-up: Run 8 review (logs/2026_07_12_1, 17:37–17:44, “Feudal attempt 4”)
First run with the 2026-07-12 batch (T-202/203/512/514/518) live. 48 turns /
~420 s, composite 0.2765 (exp_0001 of the VM’s restarted ledger),
iterations_exhausted, action_success 0.94 — still Dark Age. User notes:
(1) the game menu opened multiple times; (2) villagers sent to random places,
coordinates seemed off; (3) the mill was built very far from the TC; (4) no
lumber camp was built.
Fixes validated in the wild
- T-202 + T-203 ✓: turn cadence ~8.7 s (48 turns in ~7 min, was ~13–20 s);
ocr_readingsevery tick with the age sampled exactly every 5th tick. - T-514/T-512 ✓: the mill purchase settled cleanly off its wood delta and a later second-mill attempt was rejected “mill already built” — the duplicate-mill loop is dead.
- Gates ✓: farm cost/prereq and house headroom rejections all correct.
- T-518 ✓ (mechanism only): wood climbed 25 → 65 under the bias — but the bias targets a FARM, and the binding constraint was the lumber camp (F-34).
New findings
F-32: the trailing escape OPENS the game menu (user note 1)
Frames 3/12/27/43 show the Main Menu dialog open — each captured ~1 s after an
escape press (17:38:05, 17:39:50 = the mill composite’s tail, 17:41:17,
17:43:0x). Mechanism: after a SUCCESSFUL placement click the build menu has
already closed itself, so T-523’s trailing escape lands on “nothing to cancel”
— and in DE that opens the game menu, which also PAUSES a single-player game.
T-521’s age-up escape prefix has the same hazard. The safety escape is correct
for a leaked menu and wrong on the common success path; ≥ 4 paused episodes
this run, each feeding dimmed garbage frames to detection while open. → T-526.
F-33: literal click coordinates go stale when . jumps the camera (notes 2 + 3)
The LLM’s send_villager/build composites press . (select idle villager —
the camera re-centers on that villager), THEN click at literal x/y computed
from the PRE-jump frame (right_click “to sheep” at (1987,586) with
target_id=None; the mill placement (1459,1325) “near berry bush”). After the
jump the literal lands on arbitrary terrain: villagers walk to random places
(note 2) and the mill rose wherever the idle villager happened to stand — far
from the TC and the berry bushes (note 3). The post-. rescan refreshes the
entity cache but cannot fix coordinates already baked into the action; the
reactive tier is mostly immune because it re-resolves target_class after the
rescan. The stale-coords failure class again, now in the composite seam. → T-525.
F-34: lumber camp cost-locked ALL game — the wood ceiling sits below 100 (note 4)
19 lumber-camp builds were emitted (LLM + Feudal prep, every turn from pop 12) and every one was rejected “costs 100 wood, you have 37–79”. Wood never reached 100: T-518 banks toward farm cost + margin (80), and houses (4×25) consume the rest, so the second Feudal prerequisite is arithmetically unreachable at ANY iteration count — F-16’s shape, one level up. The wood target must be goal-driven, not hardcoded to the farm. → T-527.
F-35 (minor)
- Template-OCR blips:
wood=0×4 amid steady 125-readings andgold=108×2 (true 100) — single-frame misreads that pass the 3-core-fields reliability gate. Harvest those crops (V-3); consider a per-field plausibility check. → T-528. right_click_off_map×7; the executor’s reasoning claimed “Feudal Age” at minute 1 again (F-10’s class).
New TODOs
E. Decision-making / economy
- T-527 (P0) Goal-driven wood bank target: replace T-518’s fixed farm-cost band with a target derived from the cheapest pending build goal — while Feudal prep wants a lumber camp, bank toward 100 + margin (and fall back to the farm band once the camp stands). Consider pausing house builds while the camp is the binding goal (houses ate 100 wood this run). Files: reactive.py; V-1-style test: “lumber camp affordable by turn N”.
G. Input / composite correctness (new category)
- T-525 (P0) Stale-coordinate clicks after camera jumps: a composite
step that follows a camera-moving key (
./h/,) must not use literal x/y computed from the pre-jump frame. Re-resolve the click from a named target (target_class/target_id) against the post-rescan cache, or re-anchor placement clicks (default_build_placement) on post-rescan detections — and reject LLM composites that pair a camera jump with raw coordinates at validation time. Files: executor.py (composite runner, _handle_click), models.py validation; tests. - T-526 (P0, user-directed: prevent, don’t detect) Never press the
menu-opening escape: in DE,
escapewith nothing to cancel OPENS the game menu (pausing single-player) — T-523’s trailing escape and T-521’s prefix do exactly this on the success path (F-32). Fix by replacing the blind escape withh(select TC):his not a build-grid slot (Q W E R T / A S D F G / Z X C V B), so it should close an open build menu / cancel a placement ghost by switching selection, and on the empty-state path it just selects the TC — no menu, no pause, and it re-centers the camera on the base as a bonus. Build composites end withh; the age-up sequence collapses fromescape, h, ztoh, z. VM VERIFY FIRST (30 s, joins the pending “verify Z” check): open the econ build menu with a villager, press H — confirm the TC is selected and the menu closes. If H is consumed by the menu instead, fall back to the detect-and-dismiss design (fixture frames 3/12/27/43 of this run). Files: executor.py (build_menu_steps tail), reactive.py (age-up sequence), prompts/hotkeys.md; tests.
B. Perception
- T-528 (P2) Template-OCR blip harvest: wood=0 / gold=108 single-frame misreads — save the offending crops as fixtures, then choose between a per-field plausibility gate and extra template samples. Files: tests/test_resource_ocr.py fixtures; resource_ocr.py only if a gate is warranted.
Follow-up: Run 9 review (logs/2026_07_12_2, exp_0002, “Feudal attempt 5”)
First run with the T-525/526/527 batch (commit a712247). Composite 0.3259
(was 0.2765), 47 turns / 546 s, peak pop 17, action_success 0.93,
iterations_exhausted — still Dark Age, and this time the entire failure
reduces to ONE root cause (F-36).
Fixes validated in the wild
- T-526 ✓ perfectly: zero escape presses, zero game-menu frames across all 50 screenshots — the pause episodes are gone.
- T-525 ✓: zero stale-coordinate incidents; the schema change removed raw x/y at the source (the LLM used target_class everywhere) and auto-placed clicks landed where aimed.
- T-527 ✓ — the lumber camp was BUILT (205→105, purchase-confirmed), the first game ever with the second Feudal prerequisite standing. Wood was never the constraint again (ended 218–247 banked).
The run-killer
F-36: a persistent phantom mill defeated the sighting threshold — 14 outposts, zero farms
No mill was ever purchased this game (the wood ledger has no 100-drop for one,
and the LLM’s single real mill attempt was REJECTED “mill already built”), yet
“mill” entered buildings_confirmed by ~minute 2.5 — a misdetection that
appeared in ≥ 3 frames, defeating T-522’s threshold, whose “phantoms flicker”
assumption persistent misdetections simply don’t honor (F-12’s F1 0.67). From
there the F-29/F-30 chain re-ran at scale: farm builds passed the prereq gate,
each a press in the MILL-LESS econ menu selected the OUTPOST, and the
auto-placement click faithfully built it. 32 farm placements settled
build_purchase_missing; ~14 became outposts (stone bled 200→130 in 5-stone
steps — 25w+5s each; the final frame shows the base ringed by towers and the
“—Outpost Built—” notification). With zero farms, food pinned ≤ 67 (ended at
2), villager production stalled at pop 17, banking never started, and the
age-up correctly never fired. Conclusion: no sighting count fixes this —
detection evidence must not gate builds at all. → T-529.
F-37: no circuit breaker on repeated failures
The farm build was re-emitted ~every turn for 30+ turns; each attempt burned
25 wood + 5 stone + a turn slot, and 32 consecutive build_purchase_missing
results never escalated beyond a log line. → T-530.
New TODOs
E. Decision-making / economy
- T-529 (P0) Purchase-grade evidence for build gates: the prereq
(
_BUILD_PREREQ_CLASS) and unique-building (_UNIQUE_BUILDING_CLASSES) gates must accept only LEDGER-confirmed or verified-placement evidence (record_confirmed_buildings) — drop the detection-sighting graduation intobuildings_confirmedentirely (detection may still feed the known-buildings context line, marked unverified). Kills the phantom vector permanently: a phantom mill can then neither unlock outposts (F-30/F-36) nor block the real mill (F-29), and a false-missing mill fails SAFE (farm rejected, mill re-attempt allowed). Files: executor.py (record_building_sightings, confirmed_buildings), game_loop._sync_turn_state, tests incl. a replay of this run’s sequence. - T-530 (P1) Repeated-missing circuit breaker: N consecutive
build_purchase_missingfor one building class → suppress that build for M turns and log an alarm (this run: 32 identical attempts, ~14 unintended outposts). File: executor.py settlement/gates.
Follow-up: Runs 10 + 11 review (logs/2026_07_12_3 exp_0003 50-iter, logs/2026_07_12_4 exp_0004 100-iter)
First runs with T-529/T-530 (commit 7c1743b). Run 10: composite 0.3210,
peak pop 21, 47 turns. Run 11: composite 0.5603 — best ever (+22% over
run 6’s record), peak pop 40, 96 turns / 950 s, 1148 food gathered — and
STILL Dark Age. User notes: the agent made far too many villagers (40+); it
doesn’t need 42 in the Dark Age — max ~30 — after which the priority should
be no idle villagers + banking the Feudal resources (once the two buildings
stand).
Fixes validated in the wild
- T-529 ✓ end to end: mill AND lumber camp genuinely PURCHASED in both runs (first time ever both prerequisites stood) — zero phantom unlocks, zero outposts, farms built through a real mill (3 confirmed in run 11).
- T-530 ✓: fired once in run 10 — a vanishing farm was suppressed after 3 missing settlements instead of being retried forever.
- The whole earlier chain held: no menus, no stale coordinates, camp + wood bias working. Run 11 ended with food CLIMBING ~+40/turn (247 → 437 over the last ~10 turns) — roughly two turns short of the 500 when iterations ran out.
Why still no Feudal
F-38: the villager brake fires on DELIVERED population — the TC queue is invisible
Every one of run 11’s 36 reactive queue presses happened at OCR population
≤ 15 (verified press-by-press): the pop-16 banking brake held PERFECTLY at
press time. But a villager takes ~25 s to produce and the loop presses q
every ~10 s turn, so by the last press the TC queue held ~20 undelivered
villagers — population kept climbing to 40 for minutes after the brake
“engaged” (36 presses + 4 starting villagers = the observed 40 exactly).
Those ~20 surplus villagers cost ~1000 food that WAS the Feudal bank. The
brake must count what the agent ORDERED, not what the HUD has delivered —
the same self-generated-evidence principle as the build ledger (T-529).
LLM queue_villager composites (4 in run 11) bypass the brake entirely —
same gate, executor-side. → T-531.
F-39 (minor): executor self-reports “Feudal Age” (5×) — already fenced
claude_response age='Feudal Age' while all 20 OCR age reads said Dark Age.
Harmless today: update_from_observations deliberately ignores executor age
(the exp_0011 fence) — logged here as evidence that fence is load-bearing.
New TODOs
E. Decision-making / economy
- T-531 (P0, user-directed) Villager-order ledger + Dark Age target 30:
1. Track villagers ORDERED (successful
qpresses at the TC while food ≥ 50, + the 4 starting villagers) — self-generated ground truth that leads the lagging OCR population by the TC queue depth. 2. The banking brake gates on orders, not delivered population, with the Dark Age target raised to the user’s 30 (the old pop-16 threshold over-delivered to 40 through the queue backlog). 3. Gate the LLM’squeue_villagercomposite with the same rule (executor-side, like build_rejection) so no path can over-order. 4. After the target + both Feudal prerequisites: idle-villager dispatch and food banking are the ONLY economy priorities (user directive) — no new spend beyond farms. Files: executor.py (order ledger + queue_rejection), reactive.py (_banking_for_feudal / _pop_below_cap on orders), providers/claude.py (_execute_queue_villager gate); tests incl. a run-11 replay (36 presses → brake at order 30, not press 36). - NOTE — T-302 rises in priority: at 30+ villagers the 3-6 dispatches per turn drain an idle backlog slowly; reading the true badge count is what makes “no idle villagers” (user priority) achievable at scale.
Follow-up: Run 12 review (logs/2026_07_13_1, exp_0005, “Feudal attempt 7”)
First run with T-531 (commit 58effc0). 100 iterations / 95 turns / 796 s,
composite 0.4646 (down from run 11’s 0.5603), peak pop 29, 874 food
gathered, action_success 0.944, iterations_exhausted — still Dark Age. But
the composite comparison is meaningless this time: the executor LLM was down
for 85 of 95 turns (F-40). The game was effectively played by the reactive
tier alone, which makes this an accidental — and very informative — ablation
study of what the reactive tier can and cannot do without the LLM.
Fixes validated in the wild
- T-531 ✓ (the run’s purpose): the order ledger worked on every path —
25 orders + 4 starting = peak pop 29 (run 11 hit 40),
villager_ordered total=Nmonotonic, and the executor-side gate rejected under-50-food attempts from BOTH the reactive tier and the LLM’squeue_villagercomposite (villager_queue_rejected ordered=N reason='villager costs 50 food, you have 10'). The Dark-Age-30 brake itself never fired — food was always the binding constraint (F-40’s famine) — but no path over-ordered, which is exactly what F-38 demanded. - T-530 ✓ again, now with recovery: 3 lumber-camp placements no-oped (wood 125→125, genuinely blocked ground — the strategist’s “Gather wood to 100 — construction blocked” goal agrees), the breaker suppressed the build for 5 snapshots with the teaching rejection, and the NEXT attempt after expiry genuinely built it (125→25, purchase-confirmed). Suppress → cool down → retry → succeed, end to end.
- T-520 chain ✓: lumber camp standing for the second run in a row, via reactive Feudal prep, then “already built” rejections stopped re-emits.
- The whole earlier safety chain held: zero menus, zero towers, zero stale coordinates, zero boar/deer dispatches, zero villager deaths.
The run-killer
F-40: executor down 85/95 turns — API 400 “compiled grammar too large”, caused by T-531’s schema addition
- Every single-shot executor call from iteration 1 to 100 failed with
claude_api_error400: “The compiled grammar is too large… Simplify your tool schemas or reduce the number of strict tools” — 90 errors, each burning the turn with a 1 s error-recovery wait. The only 10 real LLM responses were housed-emergency turns, which_INTERACTIVE_SIGNALSroutes to the TOOL LOOP — a different schema surface that stayed under the limit. The strategist (33/33 calls fine) was also unaffected. - Root cause:
58effc0addedQueueVillagerActionto theActiondiscriminated union that_parse_single_shotsends asoutput_format=LLMResponse— that one extra union member pushed Anthropic’s grammar-compile limit over the edge. Deterministic: the first call failed at 13:52:08, before the game had done anything. - Consequence chain (the ablation result): the reactive tier has no mill rule — every mill in runs 1–11 was LLM-built. No mill → the reactive farm fallback was rejected all game (“requires a completed mill”) → when sheep/berries ran out (~14:00), food income went to zero → food pinned at 34 for the final 4 minutes → villager orders stalled at 29 (food-gated, never the target) → the famine override routed every idle slot to “food”, which with no food entities visible resolved to TREES → wood piled to 1170 (vs 500-food Feudal cost sitting forever unreachable at food=34). The strategist correctly diagnosed it the whole time — “Build Mill near berry bush — food crisis” was its top local goal for dozens of turns — but the tier that executes strategist goals was deaf.
- Nothing alarmed. A 100%-executor-failure game ran to completion, logged
action_success_rate=0.944(reactive keystrokes dominate the denominator), and wrote a ledger row that looks like a mediocre-but-valid experiment.
F-41: the reactive tier cannot build the food engine — mill is an LLM single point of failure
F-40’s ablation exposed a structural gap independent of the API bug: Feudal
prep (T-520) emits the lumber camp but not the mill, even though BOTH are
Feudal prerequisites (world_sim.FEUDAL_PREREQ_BUILDINGS) and the mill
additionally unlocks farms — the entire late-Dark-Age food engine. With the
executor down, the agent had a deterministic path to a lumber camp and no
path whatsoever to a mill. (It also mirrors run 8’s F-34 shape: whichever
prerequisite the reactive tier doesn’t know about becomes unreachable.)
Other findings
F-42: 43 off-map right-clicks — food dispatch resolving to edge-of-screen trees
right_click_off_map fired 43 times (~7 in prior runs), mostly late-game:
with zero food entities on screen, the food-kind dispatch resolved to a tree
at (24, 800) — off-map, rejected, villager stays idle — and repeated the same
resolution every turn. Two compounding bugs: the kind→class fallback silently
substitutes wood targets for food requests (T-602’s mislabel is the visible
symptom), and target resolution doesn’t filter candidates by click-safe
screen bounds before selecting one (F-22’s off-map first dispatch was the
same class). In a famine with an executor outage, this wasted most idle
dispatches for the last third of the game.
F-43: strategist end-game fantasy goals
The final strategist turns (goals.log tail) invented an alternate reality: “currently in Feudal Age” (every OCR age read all game said Dark Age), Castle Age advancement goals (“need 800 food + 200 gold”), and COMPLETED markers for buildings that were never attempted (“Build Barracks (175 wood) to produce spearmen”, “Build Blacksmith and Market with surplus wood (1177 wood available)”) — no barracks/blacksmith/market/spearman appears anywhere in the action log or detections. Likely driver: dozens of turns of zero progress (reward +0.000, same readings) with an ever-growing goal history — the model eventually confabulated progress. T-505 (goal hygiene) gains a harder requirement: goals must be validated against ground truth (age from OCR, buildings from the confirmed ledger), not just expired.
F-44 (minor)
- Memory encoding broken both ways:
memory_load_failedat startup andmemory_extraction_errorat exit, both “utf-8 codec can’t decode byte 0x97” — a Windows-1252 em-dash in the memories file;memories_loaded=0. The memory feature has silently been a no-op on the VM. idle_countpinned at 1 on all 100 readings — six runs now (T-302).- Two late-game house settlements false-MISSING with wood RISING (70→95, 95→110): gather income masked the 25-wood spend — exactly F-24’s slack problem at the other end of the scale (T-519).
- goals.log remains mixed byte-swapped UTF-16 across appended sessions (T-104) — this review had to brute-force both byte orders to read it.
New TODOs
H. LLM transport / schema (new category)
- T-532 (P0) DONE (commit pending): the grammar blew up on bounded
integers, not member count — the
Actionunion carried 22Field(ge=, le=)bounds (Click/RightClick x,y; Drag’s 4 coords; Scroll x,y; Wait ms), and each numeric range compiles to a large digit-by-digit constrained-decoding automaton. AddingQueueVillagerAction(a bounds-free 2-field model) was merely the straw on an already-saturated grammar. Fix: the coordinate/duration ranges are now enforced byfield_validators (models.py_in_range) instead ofFieldbounds — identical validation (all reject-out-of-range tests still pass) but the JSON schema emits zero minimum/maximum, so the grammar collapses to plain ints. Full LLM vocabulary retained (queue_villager kept — the prompt relies on it). Regression guard:test_action_schema_has_no_numeric_boundsfails the moment anyField(ge=/le=)creeps back. VM confirms the live grammar (can’t measure Anthropic’s compiled grammar offline) — but T-533’s fallback (below) makes the next run useful even if headroom is still tight. - T-533 (P0) DONE (commit pending). Two parts, both landed:
1. Fallback: a 400 (
BadRequestError) on the single-shot path now retries the SAME turn via the tool loop (_single_shot_or_tool_loopin claude.py) — the tool-loop schema surface stayed under the limit all of run 12, so the turn produces real actions instead of a no-op wait; non-400 errors propagate unchanged. 2. Alarm + metric:LLMResult.errornow flags an executor no-op (both LLM paths failed);_process_responsefunnels it toAgentMemory.record_llm_outcome, which countsllm_calls/llm_errorsand a consecutive-failure streak. At_EXECUTOR_OUTAGE_STREAK=3the loop logs a loudexecutor_outageerror; the snapshot carriesllm_error_rate(→game_metrics_final) and a newllm_error_ratecolumn in results.tsv (header auto-upgrades on the VM), so a dead-executor run can no longer read as a valid experiment (run 12 would have logged 0.95). Tests:test_single_shot_falls_back_to_tool_loop_on_400,test_single_shot_non_400_error_propagates,test_llm_error_rate_counts_failed_executor_turns,test_record_llm_outcome_streak_resets_on_success,test_process_response_counts_executor_errors_into_metrics.
E. Decision-making / economy
- T-534 (P0) DONE (commit pending):
_feudal_prep_actionsnow emits the mill build (w, 100 wood) FIRST when neither prerequisite stands — it’s the other Feudal prereq AND the farm unlock — then the lumber camp once the mill is confirmed (one build per turn; the executor’s unique-building/pending gate and circuit breaker already coverw)._wood_bank_targetbanks toward the mill first, then camp, then the farm band. The reactive tier can now sustain the food engine with the executor down. A closed-loop test (test_reactive_tier_alone_builds_both_feudal_prereqs) drivesreactive.decideagainstworld_simwith NO LLM and asserts mill + camp both stand (the first V-1-style milestone test); plus unit tests for mill-first ordering, camp-after-mill, the mill wood target, and a drift pin of_MILL_WOOD_COST/_MILL_BUILD_KEYto the executor tables. - T-535 (P1) Click-safe target resolution (F-42): when resolving an idle-dispatch target, skip candidates whose click point falls outside the safe screen bounds (reuse the off-map rejection geometry) instead of selecting them and failing 43 times; and when a FOOD request resolves to a non-food class, prefer emitting the farm-build fallback over a silent wood substitution. Files: reactive.py (_resolve_idle_target); tests.
A. Harness / run lifecycle
- T-536 (P2) Memory file encoding (F-44): read/write agent memories with explicit utf-8 + errors=“replace” fallback so one smart-quote can’t zero out the memory feature; backfill-fix the VM’s current file. Files: memory.py.
Consolidated open TODOs (authoritative, updated after run 13 / logs/2026_07_17)
The per-run lists above are historical record; this section is the single list to work from. Duplicates merged, conflicts resolved, priorities re-graded on the run evidence. Done and retired: T-102, T-201, T-202, T-203, T-204, T-301, T-401, T-402, T-501, T-502 (failure semantics superseded by T-508), T-503, T-507, T-508, T-512, T-514, T-518 (band target generalized by T-527); T-504 merged into T-510; T-519 superseded by T-537.
P0 — after run 13 (first Feudal run; see the run-13 review at the end)
T-537 — Income-aware build settlementDONE (F-45, supersedes T-519): settlement now deducts an EMA income estimate (clean windows only, scaled by elapsed snapshots) from the observed wood delta before judging, andrecord_confirmed_buildingsclears the T-530 streak on ANY proof (wood-delta or visual) — details in the run-13 New TODOs entry.T-538 — Feudal-age reactive programDONE (F-46, economy-only): age-keyed executor villager gate +observe_agesync, reactive mining-camp prep, house un-stall rule, mining-camp wood bank, Feudal gold bias — details in the run-13 New TODOs entry. The Castle age-up press itself is split out as T-544 (P1): military/V-menu wiring + two-Feudal-building prep + the gatedh,zpress.
P0 — after run 12
T-532 — Single-shot schema under the grammar limitDONE (F-40): the culprit was 22 bounded-integerField(ge=,le=)constraints, notQueueVillagerActionper se — each numeric range is a heavyweight constrained-decoding automaton and the union was already saturated. Coordinate/duration ranges are now enforced byfield_validators (same validation, zero schema minimum/maximum → small grammar); full vocabulary kept; regression guard added. VM confirms the live compiled grammar (not measurable offline) — de-risked by T-533’s fallback.T-533 — LLM-outage resilience + alarmDONE (F-40): a 400 on single-shot retries the turn via the tool loop (smaller schema surface, worked all run 12); anexecutor_outagealarm fires after 3 consecutive failures andllm_error_ratenow lands in game_metrics_final AND results.tsv, so a dead-executor run can’t pass as valid (exp_0005would have shown 0.95).T-534 — Reactive mill ruleDONE (F-41):_feudal_prep_actionsemits the mill FIRST when neither prereq stands (it’s the farm unlock), then the camp;_wood_bank_targetbanks mill→camp→farm. A closed-loop world_sim test proves the reactive tier reaches mill + camp with NO LLM.- T-302 — idle-count fix (seven runs now; run 13 adds a SECOND failure
mode — pinned at 41 in Feudal with
idle_present=Falseand pop 32): with the executor able to go down (F-40) and 30 villagers to keep busy, the reactive tier’s dispatch sizing needs the true badge count more than ever.
P0 — after run 11 (DONE)
T-531 — Villager-order ledger + Dark Age target 30DONE (F-38, user-directed): villager queueing is now a first-classqueue_villageraction funneled through one executor handler — an order ledger (starting 4- each successful order) gates ALL paths (reactive, LLM composite,
fallback) on villagers ORDERED with the Dark Age target at 30 and a
50-food gate (a no-op press is never counted). The reactive tier’s
_VILLAGER_TARGET_BY_AGEreplaces both the pop-16 banking brake and the pop-22 cap; drift tests pin the target to the executor gate and the starting count tomemory.INITIAL_POPULATION. Raw h+q queueing no longer exists anywhere. Validated in run 12 (peak pop 29 vs run 11’s 40; all paths gated) — but the schema addition caused F-40, see T-532.
- each successful order) gates ALL paths (reactive, LLM composite,
fallback) on villagers ORDERED with the Dark Age target at 30 and a
50-food gate (a no-op press is never counted). The reactive tier’s
P0 — after run 9 (DONE)
T-529 — Purchase-grade evidence for build gatesDONE (F-36): detection sightings no longer graduate intobuildings_confirmed— only the wood-delta ledger and verified placements write it, so a phantom mill can neither unlock outposts nor block the real mill. Persistent sightings surface in the context line as “(unverified sightings, NOT owned: mill)”.T-530 — Repeated-missing circuit breakerDONE (F-37): 3 consecutivebuild_purchase_missingsettlements for one class suppress that build for 5 HUD snapshots with a teaching rejection (“placements vanished without the wood being spent”); a confirmed purchase resets the streak.
P0 — the batch after run 8 (DONE, pending VM hotkey checks)
T-526 — Never open the game menuDONE (F-32, user-directed prevention over detection): every blindescapeis gone — build composites end withh(select TC clears a leaked menu/ghost by switching selection), the age-up sequence is justh, z, and PressAction validation REJECTS escape/F10/F3 with a teaching message, so the LLM can’t press them either (hotkeys.md documents the rule). VM VERIFY before the next run: open the econ build menu with a villager, press H — the TC must get selected and the menu close (alongside the pending “verify Z” check).T-525 — Stale-coordinate clicks after camera jumpsDONE (F-33): build placements now resolve AT CLICK TIME (auto_placement— the.select rescans first, anddefault_build_placementruns against the fresh cache); send composites requiretarget_class(schema + runtime refusal with a teaching detail);execute_actionsrefuses any raw-x/y click that follows a camera-moving press in the same batch; the housed fallback now reusesbuild_stepsinstead of its own stale-coordinate copy.T-527 — Goal-driven wood bank targetDONE (F-34):_wood_bank_targetderives the target from the binding goal — lumber camp missing (Feudal prep) → 120, else mill standing → 80 (farm band), else no bias; the near-miss floor is gone (run 8 proved the plain rotation never reaches the target unaided). Drift test pins_LUMBER_CAMP_WOOD_COSTto the executor table.
P0 — the 2026-07-11 batch (DONE except baseline runs)
T-510 — Dark Age economy scheduleDONE: crisis override (food < 60→ every idle slot to food) + banking phase (Dark Age,pop >= 16→ villager queue stops so income banks toward the 500-food Feudal cost).T-511 — Age-up actionDONE: reactive rule — Dark Age +food >= 500→press h,press z, emitted BEFORE the queue so the bank buys the age. Harmless no-op if the button is unavailable; doesn’t spam (research spends the food).T-506 — Zero-action executor turnsDONE: single-shot responses with zero actions get ONE nudged retry (“EXECUTE your stated plan…”); still-empty falls to the game loop’s hardcoded fallback as before.T-601 — Honest metricsDONE:action_success_rate= successes / EXECUTED actions (new dedicated denominator; ≤ 1.0 by construction);total_food_gathered= sum of positive deltas between consecutive OCR readings (undercounts, never overcounts; 300-cap drops OCR glitches; LLM-echoed observations excluded). New snapshot keyexecuted_actions.T-101 — Focus-loss handlingDONE (verify on VM): focus failures no longer consume iterations (retried under the same number), attempt ≥ 2 uses a minimize/restore cycle (beats the Windows foreground lock), and 15 consecutive failures (~30 s) abort withgame_end_reason="lost_focus".T-103 — Record baselinesDONE in run 5:exp_0014composite 0.3153 is the first ledger row (ledger stays machine-local on the VM by decision; snapshot at logs/2026_07_11_5/results.tsv). Baseline runs (experiment-baseline 3) still pending.T-515 — Safe huntablesDONE: boar AND deer removed fromGATHER_CLASSES_BY_KIND["food"](job inference keeps them); a visible boar now triggers the farm-build path, never a dispatch.T-516 — Wood floor during food crisisDONE: famine + wood < 60 → the override routes 2:1 food:wood instead of all-food; drift test pins the duplicated farm cost to the executor table (V-4 seed).
P1
T-202 + T-203 — OCR throughput pairDONE (2026-07-12 batch):templates/3024x1672/harvested from run-3 frame 27 (all 10 digits + slash in one frame; validated over all 187 logged frames — field agreement ≥ 96% with template strictly MORE accurate than rapidocr on every eyeballed disagreement; rapidocr drops trailing digits and misreads lone digits). Age stays rapidocr via newread_age, sampled every 5 ticks (_AGE_OCR_INTERVAL). One HUD read per iteration:generate_goalsacceptsreadings=, threaded loop → strategist (the strategist’s own OCR pass and its GIL-contention window are gone;strategist_evalkeeps the self-OCR path viareadings=None). VM: setAOE2_OCR_BACKEND=template(runbook updated); the config default staysrapidocr— the template backend hard-fails without per-resolution assets. Root-cause bonus:load_templatesloaded MOST templates inverted. Otsu’s minority-foreground auto-invert flips a tight crop whose glyph fills most of its own image (8/11 digits at 1672 read as ‘3’/‘7’ garbage; 6/11 at 1964 — the lone-digit template fallback was silently degraded there all along). Fixed with fixed-polarity loading, plus a_FIELD_MIN_NCC=0.4floor so merged touching-digit blobs read as “unreadable” (keeps last-known) instead of a wrong number.- T-302 — Idle-count instrumentation (VM). Save badge crop + NCC score
per tick; fixture the pinned-at-1 failure (3 runs × ~70 readings, 100%
misread). The trust gate contains it; this fixes it.
PROGRESS (2026-07-12): the failure is now pinned in the test suite —
real_1672_dark_midgame(badge shows 2,read_idle_countreturns 1) is the first live-resolution vision fixture, xfail-marked intest_read_idle_count_real_fixtures; the xfail clears itself when T-302’s geometry fix lands. T-512 + T-514 — Known-buildings context, bundledDONE (2026-07-12 batch): settlement deducts confirmed spend per shared wood baseline (one drop confirms at most one pending of a cost — run 3’s double-confirmation is now a red test);known_buildings_line(“Known buildings: farm=3 mill=1 (pending: farm=1)”, confirmed-evidence-only so single-frame phantoms can’t appear as owned) feeds BOTH the executor context and the strategist prompt from one formatter, backed by the newpending_placement_counts()accessor.- T-513 — Placement anchor spreading. ~50% of run-3 farm placements no-oped (crowded TC ring). Anchor farms on the confirmed mill, skip fog candidates, widen radii with building count.
- T-505 — Strategist goal hygiene (hardened by run 12’s F-43): expire/ down-rank satisfied goals, AND validate goals against ground truth (OCR age, confirmed-buildings ledger) — run 12’s strategist ended the game planning Castle Age from the Dark Age with hallucinated COMPLETED buildings.
- T-517 — Opening effectiveness (run 4, F-22): verify first effective villager dispatch < 30 s once T-202/T-203 land; wasted off-map first dispatch. Mostly subsumed by the OCR throughput pair.
T-518 — Farm-affordability wood biasDONE (2026-07-12 batch): the famine wood floor now banks_FARM_WOOD_COST + _FARM_WOOD_MARGIN(80), and outside a famine a mill + wood in the 40–79 near-miss band prepends a wood slot to the idle rotation (_farm_wood_near_miss). Run 8: mechanism verified, but the fixed farm target starved the 100-wood lumber camp (F-34) — superseded by T-527’s goal-driven target.T-519 — Settlement slack scalingSUPERSEDED by T-537 (run 13 proved the failure at scale: 21 false-missing settlements, five building classes suppressed, Castle gold economy blocked). 14a. T-539 (P1) — Per-field OCR dropout handling (run 13, F-47): gold missing on 26/39 readings for 11 min → strategist prompt showed 0 → Dark Age gold panic. Surface last-known-good with staleness; fixture the gold crops. 14b. T-540 (P1) — Executor target grounding (run 13, F-49, pairs with T-535): list visible gather classes in the executor context; missing food class resolves to nearest food source or farm-build fallback, not a no-op (21target_class_not_foundthis run). 14c. T-541 (P1) — Single-shot grammar headroom (run 13, F-50): 37/37 single-shot calls 400’d post-T-532; verify the VM checkout, then shrink further or flag-gate the single-shot attempt and run tool-loop-first.T-520 — Lumber camp in the Feudal planDONE: reactive Feudal prep emits the lumber-camp build from pop 12 until evidence shows one; executor unique-building gate (confirmed OR pending) stops re-emits and closes the duplicate-mill window; GameState.buildings_seen synced per turn; drift test pins the prereq set to world_sim’s.T-521 — Defensive age-upDONE: escape-prefixed sequence, press gated on mill+lumber camp visibly standing (one press instead of 14 no-ops), no-towers prompt rule added. STILL TODO on VM: verifyZis the DE age-up hotkey (hover the button).- T-509 — Detection sanity filters + hard negatives. Age-impossible class filter (stable/knight_line in Dark Age), fog-region rejection (phantom 99% TC), harvest run-2/3 frames as hard negatives for the v10 retrain. Coordinates with IMPROVEMENT-PLAN P0.2 (eval set → 200 frames).
- T-535 — Click-safe target resolution (run 12, F-42): skip off-screen candidates during idle-dispatch resolution (43 wasted off-map clicks); food request resolving to a non-food class should prefer the farm-build fallback over silent wood substitution.
P2
- T-403 — v5 ONNX decode fence (demoted from P1: with v9 on the VM the v5 path is dormant; still a silent-blindness trap on fresh checkouts).
- T-104 — goals.log as UTF-8.
- T-602 — Intent label uses resolved class, not requested kind.
- T-524 — Resolution-independent resource-digit bank (deferred from the
T-202 batch): extend the
hud_digitsmulti-sample-bank approach (_classify_bank/_normalize_glyph) to resource digits so per-resolution template harvesting is never needed again. Needs a slash-glyph naming convention (bank keys are single chars viap.stem[0]). - T-528 — Template-OCR blip harvest (run 8, F-35): wood=0 / gold=108 single-frame misreads; fixture the crops, then plausibility gate or more template samples.
T-536 — Memory file encodingDONE (run 13 batch): tolerant_read_memory_file(utf-8, errors=“replace”) at all four read sites in memory_chain.py + the root cause fixed (_save_memorywrote with the platform-default encoding — cp1252 on the VM — which is where 0x97 came from). VM still owes a one-time cleanup of the corrupt file.- T-542 — action_success_rate > 1.0 again (run 13, F-51: 138/78 = 1.77 despite T-601): composite-step successes aren’t matched by executed counts; add the V-5 invariant test.
T-543 — game_end_reason on manual stopDONE (run 13 batch): the finally block defaults an unset reason to “interrupted” beforegame_metrics_final— covers CancelledError-style exits that bypass both except clauses.
Virtual testing environment: catching these issue classes before the VM
Reviewing the TODO list against the test infrastructure that already exists.
The striking observation: most of the pieces exist, but they don’t overlap
the failures. We have an economy simulator (packages/evaluation/src/world_sim.py
— villager queue with cooldown, building costs, Feudal timer + prereqs), a
synthetic game loop (apps/agent/src/synth_game_loop.py — drives the executor
LLM with no screenshots/pyautogui), a scenario runner
(apps/agent/src/scenario_runner.py — YAML fixtures through the REAL
ClaudeProvider, incl. age_up_gate_fires.yaml), 7 real vision fixtures, and a
log→scenario converter (log_to_scenario.py). Yet F-2, F-11, F-16, F-17 all
shipped — because each failure lived in a seam none of these exercise. The
proposals below close those seams, ordered by expected catch-rate.
V-1 (P0): Closed-loop reactive-tier simulation — “does 30 turns reach Feudal?”
Would have caught: F-16 (no Feudal), F-3 (mill too late), F-8 (need-blind
routing); guards T-510/T-511 forever.
The single biggest gap: synth_game_loop drives the LLM executor, and unit
tests validate each reactive rule in isolation — but nothing runs the
reactive tier + build gates + memory as one policy over many turns. Every
rule passed its test while the composition couldn’t bank 500 food, and only
a real VM run revealed it.
Build: a reactive_sim harness that loops reactive.decide(entities, state, alarm=False) → applies actions to a WorldState (world_sim.apply_actions +
tick) → renders state back into GameState/entities → repeats. Fully
deterministic, no LLM, milliseconds per game. Assert milestones as tests:
mill exists by turn N, farms ≥ K by turn M, Feudal reached by turn 30,
zero villagers idle > 3 consecutive turns. Any future policy change that
breaks the opening becomes a red test, not a wasted VM run.
V-2 (P0): Noisy-sensor layer over the simulator
Would have caught: F-11/F-17 (verification false negatives → duplicate-mill
attempt), F-12’s gameplay impact (villagers sent to phantom farms), F-4/F-13
(pinned idle_count); guards T-507/T-508/T-301.
The run-2/3 bugs lived in the perception→state seam, which today’s tests
bypass by feeding perfect entity dicts. Add a configurable corruption layer
between WorldState and what the policy sees:
- detection noise: phantom detections (bare-ground
farm, fog-edgetown_center), dropped detections, class confusion, center jitter — rates taken from the measured F1 0.67; - construction modeling: buildings placed in the sim are NOT rendered as detections until their build timer completes (exactly the foundation blindness that caused F-11);
- OCR noise: stale frames (identical repeats), pinned idle_count=1, dropped fields, garbled ints. Then property-style tests: under X% farm false positives, no idle villager is ever right-clicked onto a phantom (T-507’s rule); under foundation blindness, no building is purchased twice (T-508’s ledger); under pinned idle_count, the dispatch batch never drops below the blind default (T-301’s gate). This turns “robust to bad perception” from a hope into an invariant.
V-3 (P0): Frame corpus harvested from every VM run
Directly implements T-302 and T-509’s harvest half; guards T-202.
The 7 vision fixtures caught nothing about the live badge misread because
they don’t include the live failure. Make harvesting automatic: a debug flag
(or post-run script) that extracts from each run’s images/ + logs — resource
bar crops, idle-badge crops with the NCC score the agent computed, and frames
where detection disagreed with the wood ledger (placement “unconfirmed” but
purchase confirmed = a frame YOLO got wrong). Feed them into
tests/test_resource_ocr.py’s fixture tables and the hard-negative pool for
the v10 retrain. Every VM run then grows the regression corpus for free —
runs become test authors.
V-4 (P1): Sim↔gate constant drift guard
Prevents a whole class of silent divergence; the seeds are already visible.
world_sim.py has its own BUILDING_COSTS/FEUDAL_PREREQ_BUILDINGS, the
executor has _BUILD_WOOD_COST/_BUILD_PREREQ_CLASS, and aoe2.db has the
authoritative tables — three copies of game truth that nothing cross-checks
(the same shape as the v5/v9 model drift, F-5, in data form). One unit test
asserting executor tables == sim tables (== aoe2.db rows where present) makes
drift a test failure instead of a subtle sim-says-pass/game-says-fail bug.
V-5 (P1): Synthetic end-to-end metrics game
Would have caught: F-9/F-19 (action_success_rate 2.38/1.54,
total_food_gathered=200); guards T-601 and the results.tsv contract (T-103).
No test ever ran a whole game and looked at the final metrics — each metric
had a unit test, but the aggregation across paths (fallback actions,
composite steps, pipelined heads) is what broke. Run the V-1 closed loop (or
synth_game_loop with a stubbed provider) to completion and assert snapshot
invariants: action_success_rate <= 1.0, executed_actions >= successful_actions, total_food_gathered ≈ the sim’s known gathered total,
game_end_reason != "". Cheap, and it pins the MetricsSnapshot contract
end-to-end rather than field-by-field.
V-6 (P1): Record/replay for LLM scenarios
Makes the existing scenario suite continuously useful.
scenario_runner.py exercises the real ClaudeProvider — high fidelity, but it
needs an API key and money, so it isn’t in CI and quietly stales. Add a
cassette layer (record messages.parse/create responses per scenario to
JSON; replay in CI at zero cost; re-record with a just recipe when prompts
change). The zero-action executor turns (F-7, 6-7 per game) are exactly the
kind of drift a replayed scenario suite would have flagged when the prompt or
model changed, instead of a live run.
V-7 (P2): Harness fault-injection catalogue
Extends what the focus-loss tests started (T-101’s test style).
The loop-seam tests now cover focus loss and pending warm-up. Catalogue the
remaining faults as parametrized loop tests with the same _patch_loop_seams
machinery: remote detection outage mid-game (fallback path — F-5’s blind
iterations), OCR returning ({}, None) for N consecutive turns (stale-state
behavior), screenshot capture raising, strategist task exceptions. Each is a
few lines now that the seams exist, and each is a VM-only surprise today.
V-8: Calibrated failure reproduction — TODO acceptance metrics
The design principle that makes V-1/V-2 measurement instruments rather than happy-path validators: the sim’s failure models are parameterized from the measured run data, so TODAY’S code reproduces the live pathologies inside the sim — and each TODO’s completion flips a metric from red to green. Fidelity becomes checkable in both directions: if current code does NOT fail in the sim the way it failed live, the sim is wrong, not the code.
Reproduction matrix (open TODOs that reproduce deterministically):
| TODO | Sim feature required | Metric | Before fix (expected) | After fix |
|---|---|---|---|---|
| T-302 idle count | OCR noise: idle_count pinned at 1 while true idle grows (calibrated: 71/71 readings, all 3 runs) | avg/max idle villagers; total idle-time | trust gate floors dispatch at 3/turn, backlog still grows past 3 | dispatch matches true count; backlog ≈ 0 |
| T-514 settlement | foundation blindness + wood ledger (V-2) | ledger confirmed-count vs sim ground truth | fails today: replaying run 3’s sequence (2 placements, 1 purchase, shared baseline) confirms 2 mills for 1 | confirms exactly 1 |
| T-513 placement | 2D occupancy — buildings claim tiles as the base grows (world_sim has grid positions/sizes); clicks succeed only on free tiles incl. retry offsets | placement success rate per game | ~50% farm no-ops once the TC ring fills (run 3) | success ↑, farms/game ↑ |
| T-509 filters | noise layer emitting the MEASURED FP classes (bare-ground farm, fog-edge town_center, Dark Age stable/knight_line) | phantom-poisoned prereq unlocks; false alarms; villager trips-to-nothing | phantom mill FP unlocks farms early; possible false combat alarms | counts → ~0 |
LLM-behavioral items (NOT deterministically reproducible — use
scenario_runner + V-6 cassettes with N-run repetition instead):
- T-512 known-buildings context: scenario “mill built but off-screen, food crisis” → measure the fraction of runs proposing a redundant mill (run 3 says > 0 today); the context line should drive it toward 0.
- T-505 goal hygiene: the goal-manager half (satisfied goals never expiring) is deterministic and unit-testable today; only the LLM-obeys-stale-goals consequence needs scenarios.
Deliberately measured elsewhere: T-202/T-203 (OCR latency) — a sim time model (each OCR call costs sim-seconds → fewer decisions by minute 30) would show latency’s strategic cost, but the fix itself is better measured by a plain perf benchmark over the vision fixtures.
Build order within V-8: T-514’s and T-302’s reproductions first — both are near-pure replays of already-observed sequences (run 3’s double-confirmation is deterministic; the pinned count is a one-line noise rule), so they validate the sim’s fidelity immediately. T-513/T-509 need V-1’s renderer + V-2’s noise layer first.
Sequencing note
V-1 and V-2 share the state→entities renderer, so build V-1 first and add V-2’s corruption layer on top of it. V-3 requires only a small VM-side script plus fixture-table entries. V-4/V-5 are pure test files. The payoff logic: the VM run cadence is the bottleneck (~10 min + human attention per experiment); every issue class moved into the virtual loop converts a VM run from “discover bugs” into “confirm improvements.”
Cross-references
- IMPROVEMENT-PLAN.md — this review feeds P0.1 (baseline hygiene), P2.3 (idle-count hardening), detection ops.
- docs/runbooks/baseline-experiments.md — how baselines should be launched.
- Prior findings: inference-res-must-match-training-res; v9 = current served
model (config.py
detection_modelis the single source of truth).
Follow-up: Run 13 review (logs/2026_07_17, “Feudal attempt 8”) — FEUDAL REACHED
Run metadata
- Log dir:
logs/2026_07_17(logs.txt, goals.log, results.tsv snapshot, 39 frames) - Started 21:40:20, user-stopped 22:05:30 (~25 min, survival_time 1423 s)
- 39 iterations, 35 LLM turns,
llm_error_rate=0.0, cost $1.82 - Final: Feudal Age (
age_score=0.33— first run ever to leave Dark Age), pop 32/45 (peak 32), food 985, wood 1833, gold 90, stone 200 - No results.tsv row for this run (manual stop before the ledger append;
game_end_reason=""in game_metrics_final — see T-543) - Model claude-sonnet-4-6 both tiers; detection remote v9 @1280; template OCR
Why Feudal was reached this time (the user’s question)
Every prior run (attempts 4–7, exp_0001–0005, and all of the 07-11 series) died in the Dark Age. This run reached the age-up press at 21:56:46 — 16 min 17 s into the run — and OCR confirmed Feudal at 22:00:14 (~20 min). The cause is not one fix but the Dark Age pipeline composing end-to-end for the first time; each stage was individually broken in a previous run:
- T-531 villager ledger + Dark Age target 30 banked the food. 42
villager_queue_rejectedevents show the gate holding on every path (reactive, composite, fallback) as the ledger climbed 6→9→12→22→30. Target hit ~21:54:55; food then banked 262→520 in under two minutes and the reactive age-up fired on the next tick it could. Previous runs spent that food on villager 31, 32, 33… forever. - T-534/T-520 reactive Feudal prep built both prerequisites. Mill confirmed by 21:48:36 (“mill already built — one is enough”), lumber camp by 21:54:25 — goal-driven, mill-first, despite circuit-breaker interference (F-45).
- T-511/T-521 reactive age-up needed zero LLM cooperation. The
h,zsequence fired once (Research Feudal Age (reactive)), the food bank paid (520→20), no button-mash, no menu escape. - T-533 outage fallback kept the executor alive. The single-shot path
400’d on EVERY call (37×, F-50) — the exact error class that made run 12 a
dead-executor run (would-be error rate 0.95) — but each turn fell back to
the tool loop and produced real actions:
llm_error_rate=0.0. - No game-menu incidents, no focus spiral (T-526/T-101 holding; only 2
benign
focus_window_errorwarnings).
So: “relatively early” is really “at all, and promptly” — the banking gate plus the reactive prereq/age-up chain removed the LLM from the critical path of the age-up, and the transport fallback kept the rest of the loop useful.
Fixes validated in the wild
- T-531 — ledger gated all queue paths all game; peak pop 32 (vs run 11’s uncapped 40); rejection messages actively taught banking.
- T-534 — mill-first ordering observed; both prereqs stood by 21:54.
- T-511/T-521 — single age-up press, gated on prereqs + 500 food.
- T-533 — 37/37 single-shot 400s recovered via tool loop; executor never went dark. (But see F-50: the 400s themselves should not be happening.)
- T-530 circuit breaker — fired exactly as designed… on false positives (F-45). Mechanism works; its evidence is wrong.
- T-526 — zero blind escapes, zero game-menu incidents.
New findings
F-45: Build circuit breaker false-positives are now the #1 issue
21 build_purchase_missing warnings; house, mill, lumber camp, farm AND
mining camp all got suppressed at some point — yet the “already built”
rejections at 21:48:36 (mill) and 21:54:25 (lumber camp) prove placements
WERE succeeding. The settlement check compares raw wood before/after, and
~30 villagers’ gather income swamps the spend: e.g. 21:56:41 house settle
wood_before=95 wood_now=235 — +140 income masks a −25 purchase (T-519’s
slack problem, now at ruinous scale). Consequences this run:
- Housed stalls at 5/5, 10/10 and 28/30 while house builds sat suppressed.
- Farm suppression → food economy ran on berries/sheep only, all game.
- Post-Feudal mining-camp suppression → gold stuck at ~90/200 → Castle advance hard-blocked (22:00:27: “mining_camp builds suppressed for 3 more turns” the very turn the strategist pivoted to Castle).
F-46: No Feudal-age program — the economy idled at the top of the run
After Feudal (~22:00) the reactive tier kept running its Dark Age script: villager target still 30 (rejection message still says “bank food for the Feudal Age” while IN Feudal), no mining camp (suppressed, F-45), no farms, no Feudal buildings, no wood sink. Wood ballooned 231→1833 in five minutes; food 985; gold flat. The LLM burned its turns shift-dispatching “41 idle villagers” (a misread, F-48) to gold with no camp to drop at. The Dark Age pipeline now works; there is nothing equivalent for Feudal→Castle.
F-47: Gold OCR dropped out for 11 minutes; strategist assumed 0
Gold appeared in only 13 of 39 ocr_readings — present (100) until
21:50:22, then missing until 22:02:57 (70). The strategist prompt evidently
rendered the gap as Gold=0 (“need 200 gold… currently at 0” at 21:57:11,
while gold was actually ~100), driving a premature panic gold-rush in the
Dark Age. Also confused: that goal said 200 gold “for Feudal Age” (Feudal
costs 500 food, zero gold).
F-48: idle_count now misreads pinned-41 (was pinned-1)
All Dark Age readings: idle_count=1 (the known T-302 failure, 7th run).
From the first Feudal reading onward: idle_count=41 with
idle_present=False and pop only 32 — internally contradictory twice over.
The LLM trusted it (“41 idle villagers!”) for its last ~8 turns.
F-49: Target-class grounding gaps wasted ~35 actions
21 target_class_not_found (LLM requested berry_bush/deer right-clicks
when no such detection existed this frame) → right_click_no_coords no-ops;
plus 20 right_click_off_map rejections (T-535’s geometry, still open).
F-50: Single-shot grammar STILL over the limit — 37/37 calls 400’d
Every single-shot attempt failed with “compiled grammar is too large” and fell back to the tool loop. T-532 (bounds → validators) was supposed to fix this; either the VM checkout predates it or the fix is insufficient. Cost is latency + tokens per turn (double round-trip), not correctness — T-533 absorbed it — but the fallback is now the de-facto primary path.
F-51 (minor)
memory_load_failed0x97 at startup again — T-536 not yet applied/deployed;memories_loaded=0for the 13th straight run.action_success_rate=1.769(138 successes / 78 executed) — the T-601 invariant is violated again; composite steps are likely counted as successes without their parent being counted as executed.game_end_reason=""on manual stop — an interrupted run is indistinguishable from a crashed one in the metrics line.
New TODOs
E. Decision-making / economy
- T-537 (P0) DONE (commit pending): income-aware build settlement
(fixes F-45, supersedes T-519).
_BuildGates.wood_income_per_snapshotholds an EMA of the wood delta over CLEAN windows (no pending spend — a polluted window would drag the estimate down and re-open the hole);_settle_pending_placementsdeducts_expected_income(EMA × elapsed snapshots since the baseline reading, so stale-OCR retries are credited fully) from the observed delta before judging; both settlement log events carryincome_estimate. EMANone(no clean window yet) degrades to the exact pre-T-537 behavior, which is why all existing settlement tests pass unmodified. Amnesty:_clear_missing_streakmoved INTOrecord_confirmed_buildings, so the visually-verified placement path now lifts T-530 suppression exactly like a wood-delta confirmation (it previously didn’t — the asymmetry behind F-45’s “suppressed after proven standing”). Tests: run 13’s house settle (95→235 across a 25-wood house) is the red-first fixture (test_income_masked_house_purchase_still_confirms), plus vanished-placement-still-missing, EMA-frozen-while-pending, stale-snapshot elapsed scaling, and verified-placement amnesty. - T-538 (P0) DONE (commit pending, economy-only scope): the executor
villager gate is now age-keyed (
_VILLAGER_ORDER_TARGET_BY_AGE— Dark 30 / Feudal 35, uncapped past the map; whole-map drift-pinned to the reactive_VILLAGER_TARGET_BY_AGE) with an age-correct message (“bank resources for the Castle Age”) fed by a newobserve_agesync in_sync_turn_state. Reactive tier gains_castle_prep_actions(mining campewhile in Feudal and none confirmed — same one-build-per-turn contract as the Feudal prep), a_house_actionsun-stall rule (any age, headroom ≤ 2, kept inside the executor’s headroom-reject band by a drift pin), a mining-camp branch in_wood_bank_target, and a Feudal gold bias in_idle_pattern(extra gold slot until 200 is banked; wood bias outranks it). Closed-loop test proves the reactive tier alone stands up exactly one mining camp from a fresh Feudal state. NOT included (see T-544): the Castle age-up press itself — it needs two Feudal-age buildings and only the blacksmith is reachable through the wired econ menu. - T-544 (P1) Castle age-up path (follow-on from T-538’s scope cut):
wire the military (
W: barracks/archery range/stable) and/or more-buildings (V: market) menus into the executor’s build composite (today only the econ menuqexists), add a reactive prep for two Feudal-age buildings (blacksmithsis already on the econ menu), and a Feudal-ageh,zpress gated on food ≥ 800, gold ≥ 200, both prereqs confirmed. VM must verify the new menu hotkeys before trusting them (same protocol as the pendingZcheck).
B/C. Perception
- T-539 (P1) Per-field OCR dropout handling (fixes F-47): when a resource field is unreadable, surface last-known-good WITH staleness to both tiers instead of letting the prompt render 0; log dropped fields; fixture the gold-region crops from this run’s frames.
- T-540 (P1) Executor target grounding (fixes F-49, with T-535): include the currently-visible gather classes in the executor context, and resolve a missing food class to the nearest visible food source or the farm-build fallback instead of a no-op.
- T-302 (still open, evidence grown): idle badge now misreads 41 in Feudal — both failure modes (pinned-1, pinned-41) belong in the fixture set.
H. LLM transport / schema
- T-541 (P1) Grammar headroom (fixes F-50): first verify the VM checkout actually contains T-532’s validator change; if it does, measure and shrink the single-shot schema further (or accept the tool loop as primary and stop paying the doomed single-shot round-trip — flag-gate the attempt).
F. Metrics
- T-542 (P2)
action_success_rate> 1.0 again (F-51): align the composite-step success counting withexecuted_actions; add the V-5 invariant test (successful_actions <= executed_actions). - T-543 (P2) DONE (commit pending): the empty reason came from an
exit that bypasses BOTH except clauses (CancelledError is a
BaseException) — the
finallynow defaults an unset reason to “interrupted” right beforegame_metrics_final, the one choke point every exit passes through;log_game_end’s dead “unknown” fallback removed. Test:test_cancelled_loop_labels_end_reason_interrupted.
A. Harness / run lifecycle
- T-536 (P2) DONE (commit pending): all four memory-file reads in
memory_chain.py route through
_read_memory_file(utf-8 + errors=“replace” — one mangled character beats losing every memory), and the ROOT CAUSE is fixed too:_save_memorywrote withpath.write_text(file_content)and no encoding, so Windows’ cp1252 default is what put the 0x97 em-dash there in the first place. Still owed on the VM: re-save or delete the existing corrupt memories file. Test:test_non_utf8_byte_does_not_break_loading.