jevsnes.git / research / breaker-trip-2026-09-20.md

The circuit-breaker trip of 2026-09-20, 21:54:54

The live window tripped jev-http's 30-questions-a-minute breaker (~/.local/state/jev/paused.json, since 1789959294.51 = 21:54:54 CDT) about eighteen seconds after Link took the sword from uncle. This is what did it, from evidence rather than from the story that fit best.

The ledger

~/.local/state/jev/spend.jsonl has one line per paid request and, until the fix below, no reason on any of them. Times relative to the trip:

Seconds before the tripRequestsInput tokensOutput tokens
-152 to -2523, at 0.3-20 s spacing882-247772-192
-24.9 to -7.4none (the sword-get text and pose: no control)
-7.40 to -0.1817, at 0.29-0.94 s spacing (median ~0.33 s)1110 → 1262, rising143-150

The 30 requests in the minute before the trip are 11 from before the sword and the final 17 plus two. The spacing of the last 17 is one round trip plus the ten frames (6 + RELEASE 4) of the empty press that follows a choice (Brain::took) - so every decision in that stretch was a question. The input tokens rise by 0-31 per request, which is already_tried_from_this_spot growing (a new "tried, still going" entry, or a times count going up) and already_visited ticking: the same scene, re-asked, with the brain's memory of its own failed attempts getting longer.

The snapshots

The app keeps has-sword.state (written 21:54:36.7, the moment FindUncle was done) and resume.state (21:55:20, after the pause). Their work RAM is a plain 128 KiB run inside the bincode (offsets 8573 and 8575 respectively, found by matching the inventory block):

has-swordresume
module$07 dungeon$12 game over
room$55$55
Link(2806, 2680)(2683, 2899)
health24 of 240 of 24
sprite slot 1$4B green knife soldier, state 9, (2880, 2896)same
sprite slot 2$4B green knife soldier, state 9, (2768, 2912)(2683, 2893) - on top of Link

So there were two live soldiers in the passage, one of them walked to Link, and Link died where he stood.

The code path

Brain::navigate, before the fix:

if alarming.is_some() {
    self.doing = None;
}

alarming is the nearest live enemy within CLOSE (56 px). While one stayed that close this ran on every decision, so:

  1. decision N: no plan → where_to → a question (with a danger Noul attached);
  2. the answer arrives; took sets the plan and presses nothing for 10 frames;
  3. decision N+1: the enemy is still close → the plan is cleared before drive runs → drive returns None → where_to → a question.

Link never took a step. took also never read the danger answer, so every one of those requests paid for a question nothing looked at. Nothing else in the brain bounded the rate: the stall throttle keys on unchanged, and set_out resets unchanged on every choice, so a loop of choices can never look stalled.

The fix (jev 6aab4e52 and after)

  • packages/brain/src/pace.rs: no question leaves the brain unless Pace allows it - at most 20 per game minute, 30 frames apart, and never the same choices in the same place again within 600 frames. A refused question is decided by code (Jev's last choice if still on offer, else the goal's route, else the nearest) and Link keeps moving. Every question carries a Why, which jev_http::Jev::ask now REQUIRES and writes to the ledger line's reason.
  • A threat clears the plan once, when it arrives, and never a fight in progress.
  • With a sword, the nearest enemy within 112 px is a "fight" candidate: Then::Strike, re-aimed at the enemy every decision, B toward it on arrival, over when no enemy is near.
  • The unread danger Noul is gone; the state still says how close it is.
  • Tests: packages/brain/src/tests.rs, mod the_breaker_trip, which replays this scene at the live cadence.