Skip to content

Walker fix - #11

Merged
infuse21 merged 56 commits into
Fix-The-Walker-Decompressionfrom
walker-fix
Aug 14, 2026
Merged

Walker fix#11
infuse21 merged 56 commits into
Fix-The-Walker-Decompressionfrom
walker-fix

Conversation

@infuse21

Copy link
Copy Markdown
Owner

No description provided.

infuse21 and others added 30 commits July 27, 2026 00:13
Adds util/death/Rs2Death, a static facade for handling a normal death:
locate the grave (an NPC, ids 9856-10367), loot free and paid items,
and optionally fall back to Death's Office once the grave expires.
Death's Office recovery is opt-in — its fee is unreadable before it is
charged, so it is never entered without an explicit flag.

Scripts poll hasDeathToHandle() and drive recoverItems(budget[, office]),
or compose the primitives directly. No automatic behaviour and no config
coupling: callers pass plain scalars, matching Rs2Bank/Rs2Walker.

Also reads the "Items Kept on Death" panel for the game's own numbers
(getPredictedGraveFee, getRiskValue, getItemsKeptOnDeath) and estimates
the office fee from wiki prices per the confirmed per-unit 100k rule.

Verified against a live client: grave/office interface groups and
components, the entrance object and reclaim dialogue, the GRAVESTONE_*
varbit encodings (VISIBLE is non-zero not boolean; DURATION is ticks),
and that both grave and office charge on per-unit value, not stack or
cumulative. Details and footguns in docs/entity-guides/death.md.

Wires onActorDeath/onVarbitChanged in MicrobotPlugin and regenerates the
client-thread guardrail baseline for the two event handlers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… low

Checked the recovery rules against the OSRS wiki's own tables. Both schedules are
confirmed as implemented: a grave charges flat coin amounts per item by tier
(1k / 10k / 100k for 100k-1m / 1m-10m / 10m+) capped at 500k, and Death's Office
charges a flat 5% on items worth 100k or more, uncapped; ironmen get 50% off both.
The wiki says "each reclaimed item", matching the per-unit behaviour observed in
game (862 coal at 146 each reclaimed free from both grave and office).

It also documents exceptions "to which the above rules do not neatly apply" —
notably that stacks of amulet of glory (6) worth over 100,000 are charged 10% at
Death's Office, double the rate and assessed on the stack's value rather than per
unit. estimateReclaimFee applies the per-unit rule, so it predicts free for such a
stack and reads LOW — the one direction a ceiling must not fail, since
reclaimAll(maxEstimatedFee) spends real gold against it.

Deliberately not special-cased: hardcoding glory would imply the exception list is
complete, and the wiki states it is not. Instead estimateReclaimFee and
reclaimAll(int) now state plainly that the estimate can read low, name glory as the
known case, and tell callers to leave real headroom rather than treat the ceiling as
a guarantee. Also removes a stale "biased high" claim and a duplicated javadoc block.

Still unobserved in game: an actual non-zero charge. Only the free-below-100k case
has been watched happen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes estimateReclaimFee() and reclaimAll(int) — about 80 lines, over half of it
caveats, with no callers outside the class.

The office never publishes its fee before charging, so any estimate is a guess, and
this one guessed LOW on documented exceptions: a stack of amulet of glory (6) over
100,000 is charged 10% on the stack rather than 5% per unit, so it is billed where
the per-unit rule predicts free. A ceiling that can be quietly exceeded is worse than
no ceiling, because reclaimAll(maxEstimatedFee) spent real gold against it. Special-
casing glory was rejected: the wiki states the exception list is not exhaustive, so
hardcoding one entry would imply a completeness that cannot be verified.

What remains covers the same ground honestly: getPredictedGraveFee() reads the figure
the game itself computed on the Items Kept on Death panel, reclaimAll() is unbounded
and says so, and walk/enter/open plus closeInterfaces() let a script inspect the
office and decline without paying — the trip is free, only the reclaim costs. A script
that insists on its own cap can price the contents itself and owns that assumption.

The fee schedules move from dead constants into the class javadoc as reference, since
nothing computes them any more.

Also corrects DeathsOfficeLocation's provenance note using the wiki's map data: every
x matches exactly and every y sits a constant two tiles south of the wiki figure (four
at Lumbridge, the one entry verified in-game against the real object). A uniform offset
on the entry with known ground truth indicates the wiki centres its map north of the
object, so these coordinates are the better estimate. Immaterial either way —
enterDeathsOffice() resolves the entrance by id, never by coordinate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every looting path was Take-All: lootGraveFreeItems/lootGravePaidItems click the
grave's section buttons and reclaimAll clicks the office's, so a script that only
wanted its gear back had to take everything or nothing. The interfaces support
picking individual slots; the API did not expose it.

Adds:
- getGraveFreeItems / getGravePaidItems / getDeathsOfficeItems — read what is
  waiting, with slot indices preserved (the slot is the param0 needed to click it).
- lootGraveItems(Predicate) — takes matching slots from both halves via the per-slot
  Take action.
- reclaimItems(Predicate) — takes matching slots from the office. The office selects
  first and only then reveals its quantity buttons, so each slot is a two-step click:
  Select, wait for ALL to become visible, then ALL.

Slots are clicked highest-index first, because taking one re-packs the container and
would otherwise invalidate the indices still to come. Both paths stop when the
inventory fills rather than clicking into a full backpack.

Also makes the Take-All paths report what they left behind: the office holds up to
120 stacks against 28 inventory slots, so a full reclaim can simply not fit. Nothing
is lost there — Death keeps the remainder indefinitely — but a grave expires, so the
grave warning includes the time left on the timer.

Note the asymmetry, documented on the methods: the office charges per item reclaimed
so taking less costs less, whereas a grave's fee covers its whole paid half at once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three of the four findings were valid:

1. getNearest used WorldPoint.distanceTo, which returns Integer.MAX_VALUE across
   planes. Every entrance is on plane 0, so a player on any upper floor scored
   MAX_VALUE for all eight and min() silently returned the first constant —
   Lumbridge — however far away it was. Switched to distanceTo2D.

2. Two Javadoc blocks were orphaned when the item-reader methods were inserted
   ahead of the methods they described: the grave "claims the half that costs
   nothing" block landed on getGraveFreeItems, and the detailed reclaimAll block
   (spending-limit rationale and @return) landed on getDeathsOfficeItems. Both
   moved to the methods they document; no implementation change.

3. The Death's Office example in the guide called reclaimAll() unconditionally
   under a comment about pricing the office first — stale since the fee estimator
   was removed. It now reads the contents, leaves the decision to the caller, and
   shows closeInterfaces() as the free way to decline.

The fourth — make reclaimItems resolve container and quantity buttons per retrieval
variant, mirroring reclaimAll — is not implementable as described. Confirmed against
the game cache (iftypes): death_office (669) has 1/5/x/all/takeall, while
gravestone_retrieval (602) has no quantity controls at all, only button /
button_bank / discard. There is nothing to resolve to. The real defect underneath it
was that reclaimItems read the DeathOffice container unconditionally even though
isDeathsOfficeOpen accepts either variant, so on 602 it would read an empty container
and report "took nothing". It now detects the variant and fails loudly, pointing the
caller at reclaimAll(). Both interfaces' component lists are documented in the guide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getDeathsOfficeItems always read InterfaceID.DeathOffice.ITEMS, but
isDeathsOfficeOpen accepts either retrieval variant. With the GravestoneRetrieval
variant up it returned an empty list, so an office still holding items looked
empty — both to callers inspecting it and to reclaimAll's inventory-full warning,
which would report "still holds 0 item(s)" while items remained.

It now resolves the container from whichever interface is visible, matching how
reclaimAll already picks between takeall and button. reclaimItems is unaffected:
its guard has already established that the DeathOffice variant is the open one
before it reads anything.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…"no requirement"

The Draynor underwall tunnel (42 Agility) was usable at any Agility level.

Two rows in agility_shortcuts.tsv carried their Duration value separated by
SPACES instead of a tab:

    3070 3257 0  3066 3257 0  Climb-into;Underwall tunnel;19036  [42 Agility      7]
    3066 3257 0  3070 3257 0  Climb-into;Underwall tunnel;19032  [42 Agility      7]

split("\s+", 2) then read the skill name as "Agility      7", which matches no
Skill, so skillLevels was never written. Zero is how "no requirement" is encoded,
so the requirement did not merely fail — it disappeared.

Worse than permissive. blocksWalkingEdgeWhenUnavailable blocks the walking edge a
shortcut spans when the shortcut is unusable, precisely so the planner routes
around it. With the gate erased the edge stays open and the shortcut looks free,
so the planner PREFERS it as the shortest route and sends the walker back to a
wall it cannot climb. The same tunnel's other approaches (lines 54-59) were well
formed, which is why this only bit from the y=3257 side.

Three parts:
- the two rows repaired and normalised to the header's 9 columns
- the parser now warns when a requirement names no known skill, instead of
  dropping it silently; an unreadable requirement and an absent one were
  indistinguishable in the logs
- a test over the shipped TSVs asserting every Skills entry resolves. Verified it
  fails on the pre-fix data naming both rows, so it pins the class and not just
  this instance

The row looks correct in an editor — the 7 sits where it belongs visually. That is
why this needed a test rather than review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ht-clicking

Veos opened the conversation, reached the Port Piscarilius / Land's End menu, and
the walker walked away leaving it on screen.

Terminal travel decides how to pick a destination from a static name whitelist:

    if ("Mountain Guide".equalsIgnoreCase(transport.getName())) -> DIALOGUE_DESTINATION
    return DIRECT;

DIRECT means "the right-click chose the destination, nothing more to do", and
selectTerminalTravelDialogueDestination returns immediately on it. Veos's
ships.tsv rows name a destination as the ACTION (Port Piscarilius, Land's End,
Port Sarim) exactly like Cabin Boy Herbert or Captain Barnaby, so it resolved to
DIRECT — but NPC 10724 (veos_visible_travel_amulet) does not offer those as
right-click options any more; it asks in conversation.

Right-clicking a destination is genuinely better than talking, so that stays the
preferred path. The walker just has to notice when it did not get it:
resolveTerminalNpcInteractionAction already reports which action the NPC actually
offered, and already LOGS the fallback to a generic "Travel" — it simply did not
act on it. When the configured destination-named action is unavailable, the
destination was not chosen by the click, so it must be chosen in the dialogue
regardless of the static mode.

Also: the destination is not always in the first menu. If it is absent,
try a menu-opening option ("Can you take me somewhere?") and look again, rather
than reporting the option missing while it sits one click away.

Runtime-detected rather than whitelisted, so the next ferryman Jagex moves into
dialogue does not need a code change. The dead Veos and Captain Magoro branches
gated on action=="Talk-to" — which their rows never carry — can now go, but that
is deliberately left for a follow-up rather than bundled here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ot just distance

Reported as "it says it succeeded before reaching the destination, if the
destination was near interactable objects". Confirmed — one branch:

    if (reachableTileCheck || (!walkableCheck && distToTarget <= distance))
        return WalkerState.ARRIVED;

An unwalkable target is normal: you cannot stand ON a door, chest or bank booth,
so the walk must finish beside it. But distanceTo is straight-line and knows
nothing about walls, so being within distance of an object counted as arrival
even with a wall in between. The caller then interacted from the wrong side and
failed, while the walker reported success — wrong success, which is worse than a
visible stall because the script blames itself.

Arrival at an unwalkable target now also requires a reachable tile ADJACENT to
it: somewhere we could actually stand to use it. That is the difference between
"close to the object" and "able to use the object", and it is exactly what
straight-line distance cannot express.

Deliberately falls back to the old distance-only answer when the reachability BFS
returns nothing, so a reachability hiccup cannot convert an arrival into a walk
that never terminates. Declining an arrival that would previously have been
granted logs arrival_declined_unreachable, so if this does cost a termination the
line says so rather than the walk just hanging.

The BFS was already being computed for the walkable case; this reuses it rather
than adding a second one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ally paying off

collision_conflict compares live against STATIC, so it answers "how wrong is the
shipped map here" — the disease, not the treatment. It reads identically whether
or not the persistent store works, which is why a whole evening of那 numbers said
nothing about whether persistence was earning its keep. Every conclusion drawn
from them was about the map, and none about the store.

Adds a coverage counter taken against the overlay as it stood BEFORE the capture
was merged in:

  overlayKnew=NN% (known=N new=N changed=N)

  known   static was wrong and we already had the right answer — a previous
          visit spared us the blind one, which is the entire point of the store
  new     static was wrong and we had nothing — the blind first visit
  changed the overlay disagreed with this capture: world changed, or stale
          learning worth knowing about separately

The prior view is pinned before overlay.set(); mergeScene replaces regions rather
than mutating them, so it stays a true "before" rather than seeing the capture it
is meant to be compared against.

Expected shape: high "new" on first exploration, climbing "known" on repeat
routes. If "known" stays near zero on ground walked before, persistence is not
working and no amount of static-map regeneration will help — which is exactly the
distinction the existing metric could not make.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
doorOther is the residual after the probe and both waits — ~790ms of a 3181ms
scan — and it is the only part of door handling that is neither the player
walking (2250ms, irreducible) nor scanning (60ms). It is therefore the only place
a fix could plausibly come from, and it is currently one undifferentiated number.

Two candidates, now timed separately rather than argued about:

  doorInteract  Rs2GameObject.interact: composition resolve, menu entry
                construction, mouse click
  doorVerify    doorStillHasAction: a radius-13 rescan that resolves a
                composition PER CANDIDATE outside the scan-scoped memo, plus nine
                transport-map lookups each. Only runs when traversal failed —
                but that is exactly the path a stuck door repeats

The reason for measuring rather than fixing: the sampled 3181ms scan released by
progress, so traversal SUCCEEDED and doorStillHasAction cannot have run in it.
Whatever consumed 790ms there was the click path. Reading the code makes the
rescan look like the expensive one, which is precisely the kind of plausible
inference that has been wrong repeatedly today — doorProbe turned out to be waits
rather than geometry, and the item fingerprint turned out to be loading
compositions on the client thread.

No behaviour change. Next door-heavy scan says which one to attack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nd replan

Refusing a walled route click was always correct but was never a recovery. The
planner keeps producing the same route, the net keeps refusing it, and the walker
oscillates with no escape — reported as "the walker just became unrecoverable, it
clicked back and forwards but was stuck in a loop".

Root cause at the reported spot is map data, not logic. Probed every tile the
walker kept clicking at Sinclair Mansion:

    2736,3460   n=true s=true e=true w=true
    2740,3463   n=true s=true e=true w=true
    2737,3461   n=true s=true e=true w=true

All four edges open on every one — the shipped map has no walls for that building
at all, while the live capture reported liveBlocksStatic=330 in the same scene
(every other reading this session has been single or low double digits). So the
pathfinder plans through the walls, the player-origin BFS proves the tile is
behind one, and route_click_walled fires forever. anchorIdx=-1 on every refusal is
the signature of that deadlock.

A refusal carries information nothing was using: the route crosses from reachable
to unreachable at a specific edge, and that edge is impassable whatever the map
says. Learning it makes the next plan route around. The first strike blocks the
edge for THIS session, so the replan takes effect immediately; persistence across
sessions still needs an independent second strike, which is what keeps a transient
refusal from poisoning the store. learnBlockedEdge returns false for an edge
already known, so the replan fires once per edge, not once per refusal.

Only edges with BOTH ends inside the BFS budget are learned — beyond it
"unreachable" means far away and the edge is innocent. That guard is the whole
correctness argument, so it is a decision table rather than a comment.

firstWalledRawEdge is deliberately pure: the obvious getClosestTileIndex start
hint reads the scene on the client thread and made the tests hang.

Does not fix the map data; makes the walker survive it. Fourth data gap tonight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit e93d3a5)
…re through it

Doors could not chain. Every release condition in the traversal wait observes the
PLAYER, not the door: isDoorEdgeResolved is "within 1 tile of the far side", "moved
closer to the far side", or "stationary, near, and reachable". Despite the name it
never looks at the door. So "resolved" means we already walked through, and nothing
could consider the next door until we were physically past this one.

That is the wrong budget by an order of magnitude. An unlocked door opens within one
game tick of the click landing — 0.6s at most. The traversal wait caps at 2200ms,
~3.7 ticks, and it was being spent walking, not opening. Measured cost was
doorWait=2250ms per raw scan against doorFind=60ms of actual scanning: the waits, not
the work.

Observing the door instead releases us the moment it is open, while the server keeps
walking us through it — which is the window in which the next door on the route can be
clicked. One click per door, at range, chained, which is what a player does.

doorStillHasAction already existed for after-the-fact verification and was never a
release condition; it is now, via doorObservedOpen.

TRANSPORT DOORS (the moves-you class) were the risk worth checking, since the door-scan
exclusion is `isCatalogTransportObject && !isDoorLikeSceneObject` — a transport door
that is also door-like stays on this path. They keep their action after relocating us,
so the new condition stays false and the positional conditions release the wait, which
they do at once because being moved is precisely what they detect.

The guard in doorObservedOpen is the correctness argument: doorStillHasAction cannot
distinguish "the action is gone" from "no object matched", and the second reading
happens whenever the probe leaves the scan radius — which would report a shut door as
open the moment we drifted. Requiring the door to be observable before trusting the
reading removes that.

Polling is rationed: nothing to see before the first tick, and the observation is a
scene scan rather than a field read, so it runs on an interval instead of every poll.

Guardrail baseline regenerated: 20 lines, verified as pure synthetic-lambda renumbering
(index-agnostic sets identical, zero non-lambda lines, no violation naming the new code).

Needs a live door-heavy run to confirm; door_await now reports releasedBy=door-opened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y is shut

The first live run produced zero releasedBy=door-opened, so the previous commit did
nothing. The build was loaded — the classes carry the symbols and the client started
after them — and the config defaults on, so the observation ran and always answered
"still closed".

It delegated to doorStillHasAction, whose predicate accepts any door-like object within
TWO tiles of the probe. That is right for its own job (verify, then retry) and wrong as
a release condition: in a door-heavy area a neighbouring shut door answers for the one
we clicked, and the answer never changes. Measured on the second door of the run —
released by progress at 1422ms after roughly five polls, on a door that was open by the
first of them.

Matching the probe tile itself, or the geometry of the edge being crossed, asks about
the door the click was aimed at. Threaded as a flag through the existing predicate
rather than a second near-identical one: the guardrail delta is then the same accepted
violation with one more parameter, not a new entry to grandfather. The walker has 75
door methods already; another copy of this one helps nobody.

door_await now carries openPolls, because a release that is not door-opened was
ambiguous between "the observation never ran" and "it ran and the door was shut", and
that ambiguity cost the run.

Not yet fixed, from the same log: the FIRST door never opened at all (releasedBy=timeout,
traversalWaitMs=2683, no progress) — the click did not land, and the recovery/interim
clicks that followed are downstream of that stall, not an independent scheduler race.

Guardrail baseline: one line, the signature change, verified as the same method and same
target with no new violation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two live runs have ended without a single releasedBy=door-opened. openPolls proved the
check RUNS (2-3 polls per door) but not what it read, so the tighten to the probe tile
could not be told apart from the check being structurally unable to see an open door.
Inferring the difference has now failed three times — classifier, impostor resolution
and neighbour radius all read as correct on inspection.

door_await gains saw=, evaluated once and only when the release was not door-opened:

  strict=false   the check said OPEN, so a non-door-opened release is plumbing, not observation
  strict=true    the door on this very tile still offers the opening action
  strict!=loose  the tighten worked and a neighbouring door had been answering

Deliberately built from the two existing readings rather than by enumerating scene
objects: the enumeration version called TileObject#getWorldLocation() in a new method
and would have added guardrail entries for a temporary diagnostic. This adds none —
the baseline delta is 20 lines of pure synthetic-lambda renumbering, verified.

Costs one extra scan per slow door await, and only on the awaits that already log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…menu

The walker had no direct reading of "this door is open". Every release condition in the
traversal wait observes the PLAYER, and isDoorEdgeResolved — despite its name — is
position only: within a tile of the far side, closer to the far side than the near one,
or stationary-and-reachable. It learns a door opened by having already walked through
it, which is exactly why doors cannot chain.

The client's own collision data answers this directly. It is server-driven, so a door
that opens clears its movement-block flag on that tick, and Rs2Tile already reads those
flags for reachability. Rs2Tile.isEdgePassable asks about the one from->to step.

Deliberately NOT isTileReachable, which was the existing (and only) collision-flavoured
condition. That runs a BFS, so a still-shut door with a long way round reports the far
tile as reachable and would release the wait having gone nowhere near the door; it also
costs a scene search per call. One flag read has neither problem.

This supersedes the object-action check as the primary signal. Two live runs produced no
releasedBy=door-opened at all, and three rounds of inspecting the classifier, the
impostor resolution and the match radius explained none of it. Collision does not care
whether a door's menu text changes when it opens, which is the assumption that kept
failing. The action check stays as a fallback rather than being removed, so nothing
regresses where it did work.

Unknowns answer false — off-scene, wrong plane, or an instance, where raw coordinates
make the scene conversion unreliable. Callers release early on a true, so an unknown must
never read as open; those cases fall through to the positional conditions as before.

The collision rule is split into a pure isStepAllowed and covered by a decision table:
per-direction flags (a door blocking north must not read as blocking east), stepping into
a fully blocked tile, and diagonals not cutting corners.

Guardrail baseline: 6 entries for isEdgePassableInternal plus one lambda renumber. Same
scanner limitation as the already-baselined isTileReachableInternal beside it — the body
is only reached through runClientReadBoolean, which runs on the client thread or hops via
getClientThread().invoke(), verified before accepting the entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t decides

The collision edge check never released a door either — openPolls 1, 5 and 4 across three
doors, no door-edge-open. A bare false from isEdgePassable is ambiguous between "the door
is shut" and "this could not be decided" (instance, plane not loaded, off-scene), and
those want opposite responses: the first means wait, the second means the signal is
unavailable here and the whole approach needs rethinking.

isEdgePassable now records its decision — open / blocked / instance / plane-not-loaded /
off-scene / no-flags / not-adjacent — and door_await carries it as edge=.

Also fixes a flaw in the previous diagnostic. saw= was evaluated when the log printed,
which is AFTER the wait released, so it described the wrong instant: one run reported
strict=true loose=false, a pair the predicate cannot produce, because the two scans
happened either side of the door changing. The edge decision is captured at poll time.

No new guardrail entries: the recording sits inside the existing isEdgePassableInternal
rather than in a new method with its own client reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n a door outcome

edge= data closed the case: every "still shut" reading during a ranged click's wait was
CORRECT. A door's 0-1 tick opening is measured from the interaction, not the click — when
the click is issued from range the server first walks us over, and the door is genuinely
shut for that entire approach. There was never a broken observation; there was a wait
whose shape assumed the click happened from adjacent.

Two concrete failures follow from that shape, both measured live:

  - The flat 2200ms cap expired mid-approach (releasedBy=timeout at 11 tiles with the
    player still walking), handing the recovery machinery its window — the competing
    replan/interim clicks the user called "a competing race for recovery".
  - The positional conditions release BEFORE the door opens: "progress" at Chebyshev 2
    mid-approach, "edge-resolved" on reaching the near side. The release fails
    verification (door still shut), and the door is interacted a second time from
    adjacent — two full interactions per ranged door (1139ms + 2939ms on the same door
    in one run).

A ranged click's wait is now an APPROACH: the budget is sized by the click distance at
walking pace (capped 8s), and the positional conditions are disabled — it releases only
on a DOOR outcome (collision edge open, opening action gone, conversation) or on a stall
(idle-accept, unchanged) / walk cancellation (new, since budgets can now reach seconds
where the flat cap bounded a stale hold at 2.2s). Adjacent clicks keep today's conditions
and budget exactly.

The click distance comes from the await ticket's before-position, so no call site
changes. A "blocked" edge reading now also skips the scene-scan fallback — it is
definitive, and the fallback only earns its scan when the edge cannot be decided.

On release the door is open with the player beside it: verification passes on the first
attempt, the cross-nudge issues the follow-through click, and the next door on the route
is immediately clickable. That is the click-walk-click chain, with the double-interaction
serialization deleted.

Budget rule is a decision table (adjacent unchanged / approach-time scaling / hard cap).
Guardrail baseline: pure lambda renumbering, verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… tile

After a door opens the player stands on its near side with the click consumed — opening
does not walk you through. The follow-through was a canvas click on the single far-side
tile, which cost ~1.5s of nudge machinery per door and then ANOTHER click to resume the
route. A player clicks once, somewhere ahead.

tryDoorEdgeCrossNudge now selects the furthest REACHABLE route point past the opened
door (selectPostDoorRouteTarget) and clicks that: crossing the edge is still crossing
it when the destination is further along, so the nudge and the route-resume click fold
into one. The next door on the route then comes into ranged-click reach while we are
already moving toward it.

The reachability gate is the safety argument, learned the hard way: the reverted
3d03ed1 clicked a tile the walled-route net had just REFUSED, because it selected
without the gate. Every candidate here must be in the player-origin BFS — one BFS per
nudge, map lookups per candidate, not the per-candidate reachability probe that froze
MLM's loop (tryPostDoorFastMinimapClick still pays that; not touched here). The BFS runs
after the door opened, so it sees through the doorway. The success test
(isDoorEdgeNudgeResolved) is unchanged, and with no route or no qualifying candidate the
single-tile nudge behaves exactly as before.

The edge must be found ON the route — a route that merely folds past the door proves
nothing about what lies beyond it. Decision table covers furthest-reachable selection,
skipping tiles the BFS cannot vouch for, the null fallback, the off-route edge, the
Euclidean cap and the plane break.

Route threading: handleDoors passes its raw path through tryHandleDoorObject and into
the nudge; the tail-loop recent-attempt re-nudge passes rawPath. Recovery-path callers
without a route keep the old behaviour via the delegating overloads.

Guardrail baseline: one line, the tryHandleDoorObject signature gaining the route
parameter — same method, same accepted target, verified.

Note for the record: one full-suite run flagged RouteClickTargetRegressionTest, which is
pure path-generation and untouched by this change; it passed in isolation and in the
final full suite. The failing run coincided with the machine killing the next Gradle
daemon (exit 137) — the test's 10s pathfinder cutoff under memory pressure yields a
partial path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the post-door click now aimed past the door, success looks like "went through and
kept moving" — and the resolution test could not see it. It accepted only arrival on or
beside the far-side tile, which has a second, older blind spot the live log exposed: a
nudge starts on fromWp, so beforeTo=1 and "afterTo < beforeTo" can only fire on exactly
toWp — and a RUNNING player covers two tiles a tick and may never be observed there.
Observed as 3369 -> 3367 -> 3365 with every poll reading unresolved, then
door_edge_nudge_unresolved for a crossing that had plainly succeeded.

The fact being tested is "did we cross the door's edge", so test it directly: the
component of the player's displacement along the edge's own axis reaches the far side.
Door edges are cardinal; anything else keeps the strict near-toWp rule alone (the
wrong-neighbour diagonal case still answers false).

A false "failed" is not cosmetic: the caller skips markNearbyDoorFamilyOpened and
reports the door unhandled, inviting a re-probe of a door already behind us.

Decision table covers the exact live case, the skipped-tile running case, the east-door
variant, and walking parallel along the NEAR side (not a crossing, however far it gets).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oors chain

Two inter-door costs measured on the 3-door Lumbridge gauntlet (33s total, ~13s of it
between doors while standing still):

STALE NUDGE (~2s at walk start). The recent-door-attempt edge survives across walk
sessions and its 6s window comfortably spans a script's walk-to-walk gap, so a fresh
walk re-nudged the PREVIOUS walk's door — observed as first_door_edge_nudge pointing
BACKWARD at walk start (before=after: the click did nothing but burn the 1.2s wait).
Cleared at markWalkSessionStart, exactly like the interim target it sits next to, which
existed for the same reason.

GLOBAL COOLDOWN SERIALISES CHAINS. The 1800ms window is anti-hammer for ONE door —
re-clicking the same edge before the world catches up. A DIFFERENT door immediately
after a successful open is chaining, not hammering; holding it for the full window cost
up to 1.8s per adjacent-door pair. The throttle is now edge-scoped: the same edge keeps
the full window, a different edge owes one game tick (600ms), and the dialogue defer
stays unconditional. Pure decision in Rs2DoorHandler with a table; the unused no-arg
wrapper is deleted rather than left as a trap.

For the record, twice tonight the full suite flagged RouteClickTargetRegressionTest —
pure path-generation, not loaded by this diff. It then failed once and passed twice in
isolation with identical code: its 10s pathfinder cutoff under machine load returns a
partial path. Flagged separately to be made hermetic (it also reads the developer's
real learned-blocked-edges file via defaultFile()).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The post-door verification decides whether the interaction opened the door — and it
asked the loose question, any door-like object within two tiles still offering "Open".
Beside a doubled door that reads the NEIGHBOUR: measured live as saw=strict=false
loose=true — this door open, the one beside it shut — reported as "did not traverse,
action still present", which suppressed markStationaryDoorOpened and the post-door
route click entirely. The walker stood still ~2s per door until the generic click
machinery caught up, in exactly the door-dense places chaining matters. The same
neighbour-answers-for-it bug as the open observation, one call site further down.

Both verify sites now use the strict per-tile/per-edge match. The loose reading
survives only in the saw= diagnostic, which deliberately reports both side by side,
and the loose delegate is deleted so nothing reaches for it again by accident.

Confirmed working in the wild within the hour: a genuinely-shut gate quick-failed
verification honestly (doorVerify=80ms) instead of burning a wait.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 7s timeout at the (3115,3449) door decomposed into three gaps, each now closed:

PASSED THE DOOR. Ranged holds had every positional release disabled, because near-side
proximity fired mid-approach. But "past" is not "near": the player ended standing ON the
far-side tile with the door still shut — the server had walked them there through
another opening — and the hold ran its full budget anyway. Standing on toWp, or beyond
the door along its own axis, is unambiguous and now releases (passed-door). The axis
reading is the same one the nudge-resolution fix introduced, moved to Rs2DoorGeometry
and shared instead of duplicated.

REPLANNED UNDERNEATH. Live collision saw the awaited edge blocked, recalculated, and
routed around via the trapdoor — one second before the timeout — while the hold kept
waiting on the old plan's door. The cancel supplier now also releases when the
Pathfinder instance changes (the replan signal), labelled cancelled-or-replanned. The
walk-target check stays for the cancel/retarget case.

WHY WAS IDLE-ACCEPT SILENT. Unanswerable from the log: silence is correct if the player
walked the whole budget and a bug if the pose-based isMoving trap held it. door_await
now tallies polls/movingPolls/animPolls so the next timeout answers it from one line.

Correction for the record: the earlier analysis suggested the route never crossed that
door's edge. Wrong — handleDoors derives the edge from consecutive raw route tiles by
construction, so a route-crossing precondition is an invariant that already holds, and
no such check was added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ded it

Reported: the walker locks the minimap at max zoom and re-zooms it the moment the user
changes it. Both walkMiniMap AND isMiniMapClickable forced setMinimapZoom(5) on every
call — including the probe, so merely ASKING whether a tile was clickable moved the
user's zoom.

There is no correctness reason for any of it. Perspective.localToMinimap reads the LIVE
zoom (s = 4d / client.getMinimapZoom()) and scales its range with it, so the conversion
is exact at every setting. Zoom only moves a trade-off: zoomed IN shrinks clickable
range (~16 tiles at zoom 5, ~40 zoomed out — the forcing was costing reach, not buying
it), zoomed out shrinks pixels-per-tile. A far point that does not convert already
degrades through the existing fallbacks to a nearer route point — which is what a human
at that zoom does — and the tile-exact clicks near walls use the canvas path, which is
pixel-precise at any zoom.

walkMiniMap now clicks at whatever zoom the user has; the probe is side-effect free.
The explicit-zoom overload keeps its contract for external callers that genuinely want
a particular zoom, but nothing in the walker calls it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…click

Scripts call walkTo for five-tile hops — spot changes, bank-to-spot shuffles — and every
call paid the pipeline's fixed head: transport refresh, pathfinder, session setup, the
startup handler pass. Measured at 1.3-1.5s before the FIRST click for moves a human does
in one click. The existing short-circuit (tryDirectShortWalk) sits INSIDE the pipeline,
takes the planned path as an argument, and can only ever save the tail. With hundreds of
Hub scripts funnelling through this one entry point, the entry point is the fix.

The gate is a reachability proof, not a distance guess: a target within 12 tiles that is
BFS-reachable on the client's live collision flags needs no door, no transport and no
plan — a shut door on the way reads as blocked and fails the gate, so anything that
needs the pipeline still gets it. Deliberately strict: the target TILE itself must be
reachable. Walk-beside-an-object calls (booths, trees) decline and take the full
pipeline, because "within distance" with a wall between is exactly the false arrival the
pipeline's richer checks exist to refuse — coverage traded for correctness on the
hottest path in the client.

Canvas click first, minimap at the user's own zoom second. The wait is bounded by
walking pace plus slack (decision table), stall detection is position-diffed rather than
isMoving() (the pose-based read stays true while turning on the spot), and walkUntil
completion conditions are polled in the wait. Every non-arrival outcome — no click
landed, stall, budget, interrupt — falls through to the full pipeline exactly as if the
fast path had never existed: the degraded case is yesterday's behaviour, never a new
failure mode.

Placed inside the walker lock ahead of the banked-transports branch, so both walk modes
benefit and concurrency semantics are unchanged. One short_walk log line per fast walk
(result=arrived/completion/handoff) so live runs show it engaging and winning.

Guardrail baseline: 23 lines, verified pure synthetic-lambda renumbering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Observed twice, identical fingerprint: the walker stands at a door working it, and
"[LiveCollision] route step A -> B now blocked; recalculating" yanks the route out from
under it — the Edgeville 3115,3449 hold-to-timeout, and tonight's 2585,3141 mid-handling
reroute the user called out ("it went out the door and then it rerouted for no reason").
Both doors are CATALOG TRANSPORT doors, which is the fact that explains it.

LiveRouteValidator recognises transports by shape — cross-plane or non-adjacent steps —
but a door transport joins two ADJACENT SAME-PLANE tiles, so its step is
indistinguishable from walking. While the door is shut the live collision edge honestly
reads blocked. That is the door's NORMAL state, not an obstruction: the pathfinder
planned through it as a transport edge and the runtime executor opens it on contact.
Recalculating on it is always wrong.

The validator now takes a transport-step predicate and skips planned catalog-transport
edges; the plugin supplies it from the transports-by-origin map (one map lookup per
validated step). Scanning continues past the skipped edge, so a genuine obstruction
further along still triggers the recalc this validator exists for.

The validator's class doc claimed openable doors read as passable in the overlay via the
door mask — true for scene doors the mask catches, demonstrably not for this class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…imed, not just catalog ones

The transport-catalog predicate from 5455512 was the right fix for the wrong
subclass. The door that kept triggering "route step now blocked; recalculating" in both
directions is object 81 — fightarena_door1, the Fight Arena quest door — identified
live off the agent server. It is in NO transport catalog (grep of every shipped TSV:
nothing at 2584/2585,3141), so a catalog lookup can never exempt it, and the user's
report held: cross it westbound, recalc; walk back eastbound, recalc again, straight
into a script-level retarget.

Quest doors are also exactly the class the live capture's door mask is least reliable
for (impostor-varbit compositions), so "the overlay reads door edges as passable" cannot
be assumed for them either.

The walker already stamps every door attempt with its edge before clicking. That stamp
is the authoritative "the executor owns this edge" signal, catalog or not:
Rs2Walker.isActiveDoorEdge(a, b) answers it for either direction within a 10s claim
window, and the validator's skip predicate now consults it ahead of the catalog lookup.
A door the walker is actively working can no longer have the route recalculated out
from under it, whatever kind of door it is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
infuse21 and others added 26 commits August 7, 2026 00:22
…shut itself behind them

Watched live at the Fight Arena doors (agent server, position sampled every 1.2s): the
character crosses the door east, and one tick later these quest doors close themselves.
"Shut door on my route" is then TRUE again for an edge the player is already past, and
the door machinery re-engaged it — the character stepped BACK through the door it had
just crossed, then oscillated: through, back, sidestep, through. The walker also issued
a route click one tile BACKWARD (to=2583 with the goal 9 tiles east) because with the
door shut again, the only reachable route tile was behind the player.

The invariant that ends it: a route door edge whose axis the player has already crossed,
in route direction, is RESOLVED for this walk — whatever the door reads right now.
handleDoors gates on it at entry (covering the segment loop, the raw scans and the
recovery probes, which all funnel through it), and tryDoorEdgeCrossNudge treats
at-or-past the far side as success instead of aiming its fallback click at toWp — one
tile backward — for a player standing beyond it.

Directionality keeps the guard honest: from/to derive from consecutive route tiles, so a
walk genuinely routed back the other way carries the reversed edge and is unaffected.
crossedDoorAxis is the shared, decision-table-tested primitive from the nudge-resolution
fix.

Also for the record from the same live watch: quest automation was OFF — the larger
reversals in the trace were the user's own manual play between walker targets, and the
zero "now blocked; recalculating" lines confirm the two validator fixes hold on a build
that includes them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hat the BFS can vouch for

Un-pinning the minimap zoom made the question live: zoomed out, the minimap shows ~38
tiles of radius while every stride stayed capped at the flat 11 tuned for the pinned
zoom-5 window. A zoomed-out player clicks big strides — that is half the point of
zooming out — so reach now follows the visible radius (20*4/zoom, the exact scale
Perspective.localToMinimap uses), minus two tiles so a click never lands on the rim.

Floored at 11: a fully zoomed-IN minimap behaves exactly as before. Capped at 18, and
the cap is NOT the minimap's limit but the walled-click net's: every stride target must
sit inside the player-origin reachability BFS (20-step budget) or a wall between could
not be detected, which is the Clock Tower click-through-the-wall class. 18 leaves two
steps of path-vs-Euclidean slack inside that budget. Raising it further means growing
a client-thread BFS and gets measured first.

At the default zoom (4) strides go 11 -> 18; fully zoomed out likewise 18. Wired at all
five stride sites — the tail-loop stride, walkStep, the route-backed final click, the
interim continuation click, and the reachability sample radius that must cover whatever
the stride can reach. Pure decision (zoomAwareMinimapReach) with a decision table;
degenerate zoom readings fall back to the old flat reach.

Amended: the first cut of this commit accidentally swept an unrelated in-progress
QuestingScript.java change from the working tree (git add -A); this one carries only
the walker change, and the WIP is back in the tree untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit b949273a83dd24f1e6648d075ff4d3fd3a57d3ed)
…f flooding a million nodes

37 SEARCH_EXHAUSTED terminations in one evening's log, each expanding ~1.1M nodes over
1.2-3.8s of CPU — mostly for destinations TWO TILES away. An unreachable target made the
forward search flood the entire connected world component before admitting it, and
retries hammered the same flood.

A bounded reverse flood from the target answers first. It reuses the bidirectional
machinery's getReverseNeighbors with the incoming-transports index, so a room entered by
a staircase or door transport grows past its walls and reads reachable — an upstairs
destination is never falsely sealed — and anywhere-teleports (null origin, absent from
that index) are checked per component tile. Only a frontier that drains under the
1024-node budget without touching start proves anything; big components are
INCONCLUSIVE and the full search runs exactly as before.

A sealed verdict does not abandon the caller: the search retargets the component's
walkable rim, sorted nearest to START — the reachable rim is on the approach side, and
goal-side ordering burned the whole substitute budget on best-effort (measured 50k nodes
at Shantay Pass vs a direct walk to the near-side rim). The walk still ends beside the
sealed area, which is all the old flood's path ever bought, and the termination stays
SEARCH_EXHAUSTED because reaching a substitute is not reaching the caller's target. The
substitute pass carries both a 2s leash and a 50k node budget: its targets can
themselves prove unreachable (a moat tile whose rim is an unreachable pocket), and the
time leash alone WAS the flood. The probe is failure-proof — any exception degrades to
the full search, never a failed run.

Measured on the pinned corpus: Shantay no-ticket 1.1M nodes/multi-second -> path to the
gate's north side in 185ms goal-sorted, hundreds of nodes start-sorted; sealed courtyard
tile -> approach path beside it; void tile -> instant empty result.

The Shantay no-ticket corpus assertion needed its proxy tightened: "visits within 2 of
the gate" also condemned a route that walks UP TO the gate and stops — which is what the
fast path now correctly produces, and what a coinless player does. It now measures the
crossing itself (a tile strictly south of the gate line at the pass) plus a
must-not-arrive check, ending the proxy games its own comment history documents.

For the record: one full-suite run flagged RouteClickTargetRegressionTest, which then
passed 3/3 in isolation on identical code — the known tiebreaker-lottery flake, being
made hermetic separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit f9cfb3e1f078ad7a4d4d4735b947c3f60e58480c)
…es.tsv is the authority

Policy decision: the two-strike persistent learned-edge store is gone. What stays is the
part that was always right — the observing session blocks a failed edge immediately,
because it just watched the failure and anything less loops the walker into the same
obstacle (the Sinclair deadlock escape). What goes is everything that existed to manage
persistence: the file under ~/.runelite, the constructor load, the strike counting, the
independence window, the probation semantics, and the test seam for simulating restarts.

The store's history argued for this. Its probation machinery existed to self-heal its own
poisonings (the Wydin door needed a hand-edit before two-strike, and two-strike existed
to prevent the next one). Its default file leaked developer state into every test that
constructed a PathfinderConfig — the hermeticity exposure flagged on
RouteClickTargetRegressionTest. And in months of use it never accumulated a single
confirmed row: the user's live file holds four probation entries, nothing enforced. An
edge worth remembering across sessions is worth a reviewed row in blocked_edges.tsv,
which ships with the client, survives the live-collision override, and is already the
home of the Hemenster/Al Kharid/Varrock permanent blocks.

learnBlockedEdge keeps its signature and return semantics (true = newly blocked this
session), so Rs2Walker's walled-route learning and wrong-traversal callers are untouched.
LearnedBlockedEdges and its parser/strike tests are deleted; the session semantics get
their own decision table (block-once, direction-scoped, nothing survives a fresh config,
null-safe).

The suite run flagged only RouteClickTargetRegressionTest, the known tiebreaker-lottery
flake, green in isolation immediately after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 68fccb17de95c2fa527233c2ce211385950cae2a)
The 1158ms post-login client-thread freeze hides in code the stage timers never covered.
The outer wrapper read transports=1154ms while the inner stages (merge/cache/filter/
similar) summed to ~30ms on every instrumented run — and the slow-stage breakdown line
never fired for the slow case, because its threshold summed only the measured stages.
Three regions were dark:

  entry    the quest-state and item gates (four getQuestState calls — quest state can
           run a clientscript — plus fairy-ring inventory/equipment/bank checks and the
           gnome glider/spirit tree/quetzal gates)
  key      leagues context + the cache-key fingerprint
  verify/  condition encoding, verification hashing, and the snapshot capture's deep
  capture  copy of ~5k transport sets

Each now has a timer, carried on the stage log AND the slow log, and the slow threshold
sums all eight stages so the breakdown actually prints when the freeze happens. The next
slow login names its stage instead of hiding it; the fix targets whatever it names.

The only suite failure was RouteClickTargetRegressionTest, the known tiebreaker-lottery
flake, green in isolation immediately after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit a356c1b3795e8311b6f8d023323e14e1948b5386)
… instead of stalling forever

User-reported as widespread, and it is: scripts click NPCs and objects with a shut door
between, the server walks the player to the door and prints "I can't reach that!", and
nothing recovers — observed live as the Kudos museum script blocked on three separate
interactions in a building made of gates.

The interaction layer's ONLY reachability recovery was dead code. The chat listener has
set cantReachTarget on every can't-reach message all along, but the flag that gates the
recovery (isCantReachTargetDetectionEnabled) defaulted to false and NOTHING in the repo
ever set it true. Even enabled, the recovery was wrong twice over: the NPC path walked
to a nearest-line-of-sight tile — which for an NPC behind a door selects tiles on the
unreachable side — and its LOS "all clear" branch cleared the flag without walking, so a
through-window NPC re-clicked forever without ever escalating. The object layer had no
reactive handling at all, and its opt-in checkCanReach variant gated on line-of-sight —
which solid objects fail from everywhere (docs/entity-guides) — before falling back to a
raw canvas click that opens no door either.

Detection now defaults ON, and both layers recover the same way: on the game's own
can't-reach verdict, walk to the target with Rs2Walker.walkTo(loc, 2) — the walker opens
doors en route, and its arrival semantics require a standable tile BESIDE an unwalkable
target, which is precisely the reachability proof the follow-up click needs — then clear
the flag and click from beside it. The retry escalation (pause + operator message after
3-5 failed rounds) is preserved on both paths.

The trigger stays the game's own message, which is what keeps this safe: a ranged attack
through a fence or a shouted conversation across a chasm never prints can't-reach, so
legitimate at-range interactions are untouched. LOS is consulted nowhere.

Amended: the first cut of this commit accidentally swept the user's staged Kudos and
Varrock-cleaner plugin work in via the shared index; this one carries only the
interaction-layer change and their staging is preserved untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 4ec3ab817114cdd5838c0bec6cff708922feb7e3)
…its own doorways

The Kudos museum dead-end had a second cause under the interaction-layer one: every
route to a museum-interior target was unplannable, so even the new can't-reach recovery
had nothing to work with — walkTo cannot open a door the PLANNER refuses to route
through.

The proposed fix was a transports.tsv row for interior gate 24536 at
(3261,3446)->(3261,3447). Verification found the actual gap one level deeper:
restrictions.tsv has carried an unconditional "# Varrock museum" block since a Feb 2025
bulk upstream commit (no stated rationale) restricting the south double-door tiles
(3264/3265,3442) and the gate tile (3261,3446). Restricted points are skipped as graph
nodes entirely, so a transport row could never have helped — and none is needed: with
the restrictions lifted, the corpus proves the interior routable immediately, meaning
the static map already holds these as ordinary passable door edges. The runtime door
machinery opens them on contact — today's live log shows exactly that for the south
door at (3265,3442), which the walker opened in 1.2s on a walk that STARTED inside the
restricted tile.

The likely original motive — the pre-rewrite executor could not survive these doors, so
the area was banned — is obsolete: doors chain now, and a museum through-route is at
worst a valid shortcut. Pinned by a corpus route from outside the south door across the
gate line to a display-pen tile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 040c6e9963287240e584340158d6ae36f343f7e3)
…t, not a door

User was right and my previous commit was half a fix. Lifting the restrictions let the
PLANNER route through the museum doorways, but gate 24536 (vm_barrier_guard_gate) is the
moves-you class, so the EXECUTOR still had nothing it could do with it.

Measured through the agent server rather than inferred, standing beside it:

    player (3261,3447) -> click Open -> (3261,3446)
    player (3261,3446) -> click Open -> (3261,3447)
    player (3261,3447) -> click Open -> (3261,3446)

One click relocates the player across the barrier every time, and the gate's composition
never changes (transformedId stays 24536, action stays "Open" — it never enters an open
state). That is the Al Kharid toll gate / Wydin back room signature: the door pipeline
waits forever for an "open" that cannot happen, so the edge needs catalog rows. Added as
a bidirectional pair beside the Wydin rows that document the same class.

Note the trap this hid behind: the static map already reports every edge at both tiles
passable (probed n/s/e/w all true), so the planner was happy to route through and only
the executor failed — exactly the "collision says yes, executor can't" shape recorded in
the transport-doors notes. Collision evidence alone would have said there was nothing
wrong here.

The corpus test from the previous commit was too weak to have caught any of this: it
asserted the path passed within one tile of the gate, which any route merely reaching the
doorway satisfies. It now asserts the route SELECTS transport 24536.

Not claimed: an exhaustive audit of every museum interior door. The two south doors
(24565/24567) are ordinary — today's live log shows the walker opening one and stepping
through in 1.2s — but the agent server's /objects endpoint refused radii past ~6 tiles,
so the interior beyond the barrier is unsurveyed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 24a013b9c0ef50760243eb374e9066dc07d1e433)
…rings

processWalk carried its control flow in a String. Forty-seven assignment
sites produced forty-three distinct values, and eight places downstream
matched them by equality or startsWith to decide three things: whether the
iteration counted as route progress (partial-retry budget), whether it was
exempt from the tail-iteration cap, and whether a post-door canvas nudge
was owed. A value no branch had classified simply fell through to the
default in each, silently.

Two of the reasons are produced inside a ternary and never appear in a
search for `exitReason = "..."`, so the set could not even be recovered by
reading the code.

Replaces it with WalkExit: the reasons are now enumerable, the three
behaviours are flags on the constant, and a new value cannot be added
without deciding what it means. Wire names are preserved exactly, because
live walker debugging here is log-driven and renaming a reason would blind
the one diagnostic that works.

This commit is deliberately inert. WalkExitTest asserts, for every
constant, that each flag agrees with the legacy string predicate evaluated
on that constant's wire name, so the classification is provably unchanged.
The legacy predicates stay (deprecated, unused by production) purely to
hold that characterization up; the follow-up that corrects the
classification moves the expectations in that test, making the test diff
the record of exactly what behaviour changed.

The architecture guard caught the three lines this added and is ratcheted
DOWN 1647 -> 1646 rather than raised. It was raised once already to absorb
a merge; doing that again would make it a rubber stamp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cing

On a partial path — a route the pathfinder could not run all the way to the
goal, which is every long or awkward walk — each iteration that is not
classified as route progress spends one of three retries and forces a full
replan. Three of them and processWalk returns UNREACHABLE and the walk is
abandoned.

Fourteen reasons were misclassified. Every one of them means the walker
either just advanced the route or is waiting on movement it issued itself:

  - recovery took a transport or mined the obstacle on the blocked edge
    (transport-handled-local-reachability, frontier-obstacle-handled)
  - a click was issued and the player is walking (local-recovery-click,
    door-suppressed-approach-click, recent-door-edge-nudge,
    route-move-in-flight)
  - the door actually opened (the three door-edge-resolved-* reasons)
  - we are waiting on an action we issued (door/transport settle yields,
    door-traversal-pending, recovery-click-preempted-by-action)
  - the pass was abandoned because the player MOVED
    (recovery-position-stale)

So three settle windows at one ordinary door, on a partial route, could
exhaust the budget and abort a walk that was working exactly as designed.

The predicate this replaces documented this very failure mode in its own
javadoc and then covered about half the cases. It could not cover the rest,
because its list was hand-maintained against a set of forty-three strings
that no one could enumerate: seven were matched by a startsWith("door-handled")
prefix, which silently excluded door-edge-resolved-* and
door-suppressed-approach-click precisely because they read like door-handled
reasons without matching the prefix.

The divergence from the old classification is pinned as an explicit list in
WalkExitTest, asserted in both directions: an unlisted reason that changes
meaning fails, and a listed one that reverts fails too. The reasons that
genuinely mean "not advancing" are pinned separately, so the budget still
drains and a truly unreachable goal still terminates.

Follow-up: the deprecated string predicates in Rs2Walker are now referenced
only by that divergence test. They can be deleted once the historical
baseline moves into the test as data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
For fifteen seconds after taking a transport the walker deliberately runs
degraded: it skips the raw scene scan, skips the per-segment door, rockfall
and transport handlers, disables ranged door dispatch for the whole pass
(one skipped segment sets segmentSkippedThisPass, which withdraws the
nearest-obstacle guarantee that ranged dispatch depends on), and bypasses
the off-path recalc entirely. That is right for the seconds after a landing
and badly wrong for a walk that has only just started: a fresh walk would
ignore the door in front of it and never replan when it drifted off route.

setTarget(null) already cleared the handoff on the normal completion path,
with a comment saying exactly why. Walk-session start did not agree: it
nulled the three location fields and left lastTransportHandledAtMs, which
is the field every window check actually reads. So the window stayed armed
for its full duration while the destination it describes was already null.

The gap showed up on any walk that ended WITHOUT clearing its target — an
exception, the tail cap tripping, or an external cancellation, the last of
which is routine when a quest script interrupts a walk mid-route.

Clearing at session start is sufficient on its own: walkWithStateInternal
is the only caller of markWalkSessionStart and the only route into
processWalk, banked walks included. So the walk-ending paths do not each
need their own clear, and nothing is added to processWalk.

Also drops lastTransportHandledAtLocation, which was written on every
transport handoff and never read by anything. Removing it takes a
Rs2Player.getWorldLocation() client-thread hop off the transport path.

The state reset is split out of markWalkSessionStart as resetWalkSessionState
so it performs no game reads and these staleness invariants can be
unit-tested rather than re-discovered live.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… regression

calculationCutoffMillis is a NO-PROGRESS guard, not a runtime budget. Under
CPU contention — a full-suite run, or the client running alongside the
build, which is the normal state of a dev machine here — the search gets
starved and returns a best-effort PARTIAL path. A partial path wanders
through tiles this test requires to be absent, so it fails in exactly the
shape of a real routing change.

That is not theoretical. This test going red was read as a route-data
regression specific to one branch, and sent an investigation off bisecting
for a change that did not exist. The apparent "green on one branch, red on
the other" split was an artifact of isolated versus contended runs: a clean
worktree at the same commit passes.

The route is now verified to actually reach the goal before any of its
content is asserted, with one retry and then an explicit "pathfinder
starved — INCONCLUSIVE, not a route regression" failure naming where the
search stopped. The no-progress cutoff goes 10s to 30s, which costs nothing
on a healthy run because the guard resets on every heuristic improvement.

Deliberately not addressed: the pathfinder's per-node random tiebreaker can
vary equal-cost routes, which is a separate source of flakiness here. A
hermetic rework of this test exists on another branch and should not be
duplicated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…and see the livelock

Two things end a processWalk iteration: the partial-retry accounting and
the tail-iteration exemption. Both were inline, both are load-bearing, and
neither could be tested — which is how a partial route came to report
UNREACHABLE while the player was still advancing.

TailDecision now owns the decision and is pure: given "did we arrive", "is
this a partial route", the exit reason and the budget, it returns the
action. The caller still does the work — replanning, telemetry, clearing
the target. Thirteen decision-table cases pin the interactions, including
the budget-refill rule, which is subtle enough to have needed a paragraph
of comment to survive: the route-progress timestamp is also bumped by a
mere replan, and every retry replans, so refilling on the timestamp alone
would let a retry refill the budget it just spent.

Also makes the loop's real termination behaviour visible. The iteration cap
is not a bound: several exit reasons decrement the counter, so a walk that
keeps producing one of them goes round forever, and nothing else in the
call chain imposes a time limit. Two observations now say so out loud —
a wall-clock budget, and a cap on uninterrupted tail-exempt iterations,
which is the state the iteration cap structurally cannot see because those
iterations refund their own charge.

Both are OBSERVE-ONLY: they log and do not abort. A budget that kills a
working long walk would be a worse bug than the livelock it guards against,
and a banked walk across several transports is legitimately minutes. Decide
enforcement from live logs, once we know they fire on real livelocks and
never on healthy walks.

processWalk 1646 -> 1641 lines; the guard ratchets down again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both of these come from one Ardougne-to-Catherby-to-Ardougne run that
succeeded end to end. A walk arriving is not evidence that it was right.

1. Stop learning a blocked edge from the BFS frontier.

The walled-route net refuses a click when the selected tile is Chebyshev-near
yet absent from the player-origin BFS, and then LEARNS the first route edge
that leaves that BFS as blocked. But the proximity guard is Chebyshev while
the BFS budget counts steps, and those are not the same thing: a tile
thirteen tiles away as the crow flies can be thirty steps away around a
building, and it is then missing from the BFS for want of budget rather than
because anything blocks it.

The log caught it outright at the Port Sarim / Land's End docks. A click to
(2760,3238) was refused as walled and the edge (2759,3230)->(2759,3231)
learned as blocked. Nine seconds later the walker was standing on
(2760,3238), having simply walked there.

Refusing the click on that evidence is merely conservative and has fallbacks.
Writing it into the learned-blocked-edge store is not: routing believes it
for the rest of the session. So the edge is now only convicted when its near
end is strictly INSIDE the frontier — the BFS expands every tile below its
budget, so an interior tile whose neighbour is still missing proves the
neighbour unreachable, whereas a tile sitting AT the budget never had its
neighbours enumerated and proves nothing.

This also removes the replan that followed each false conviction, which was
on the critical path of the first click: two of the three walks in the log
took ~5.4s to their first click against ~1.1s for the one that did not
trigger it.

2. The exempt-run bound counted yields, and walking is mostly yields.

Shipped this morning at 24 consecutive tail-exempt iterations. The log shows
a completely healthy Catherby-to-Ardougne leg yielding interim-in-flight 28
times in a row while steadily covering ground, because that is simply what
travelling between minimap clicks looks like. It would have fired a false
livelock warning on a working walk.

A bound on yields is a bound on walking. The state worth reporting is
yielding while STATIONARY, so the run now resets whenever the player tile
changes. Being observe-only is what made this cost a log line instead of an
aborted walk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…wning it

The door cascade's completion contract is "the blocked edge became
passable": it clicks, then waits for the edge to open. A stile never opens.
You climb over it and arrive on the far side, so that wait can only ever
time out.

A catalog transport was classified door-like on its NAME, and "stile" is in
the door-name fragments, so a Stile at (2637,3350) with action Climb-over
was handed to the door cascade. Measured near Ardougne, from first contact
to actually crossing: twenty seconds. The handler clicked it, logged
door_edge_post_unresolved because the edge never opened, and the walk then
spent six refused route clicks, a recovery click onto the far side of the
fence, a stall, a replan and an idle nudge before the transport handler got
the same object and crossed it in one action.

So the action now wins over the name: a catalog row whose action moves the
player ACROSS the obstacle — Climb-over, Climb-through, Squeeze-through,
Cross — is not door-like, whatever it is called, and
shouldDeferDoorHandlingToTransport hands it to the transport handler that
can actually complete it. Opening actions are untouched: a named gate you
Open is still the door cascade's job, because the door cascade is what knows
how to open things.

This is the third obstacle in this class to need the same correction (the
Varrock museum guard barrier and the Port Sarim back-room door were both
fixed as individual data rows). Deciding on the action generalises it:
moves-you obstacles are their own class, and the class now has a rule
instead of a growing list of coordinates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two live logs have now shown a stretch of walk startup with NO output at
all, heartbeat included. By the heartbeat's own contract that means the
thread is blocked inside a wait rather than spinning, but nothing says
inside what, and the last two attempts to answer that from the log alone
were both guesses. Diagnostics only; no behaviour changes.

Startup marks are deduped per phase per walk, so a startup that REPLANS
goes silent for the whole of its second pass: pf_wait_retry, pf_ready and
path_snapshot have all been logged already and never fire again. That is
precisely the window a walled-click replan lands in, which is why the
slowest starts are the least visible ones — a four-second gap containing
nothing but the replan that caused it. They now re-arm on a replan taken
before the first movement click, so each startup attempt narrates its own.

The heartbeat also carries the player-origin BFS cost now. Every
getClosestTileIndex runs one, and the loop asks for a route index many
times per iteration — route progress, interim tracking, near-path checks,
click selection, each recovery probe. There are thirty call sites. Each is
a fresh breadth-first search executed on the CLIENT thread, so the cost is
a round trip rather than arithmetic, and it appears in no existing timing
line. That makes it the leading candidate for the missing seconds, and it
is a candidate precisely because nothing has ever measured it.

Deliberately measuring before optimising. The obvious fix — memoise the BFS
per tick — trades correctness for speed in the one place the walker can
least afford it: the BFS reflects live collision, so a memo held across a
door opening answers with the world as it was. That trade is only worth
making against a number, and there is no number yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The blocked-frontier recovery tries, in order: door handlers, then a
suppression branch for "an unresolved door is near the route", then the
transport on the blocked edge. The suppression branch breaks out of
recovery, so whenever it fired the transport never got its turn — even when
the transport WAS the thing blocking us and the only handler able to cross
it.

Measured near Draynor on a catalog transport at (3064,3282): the click to
it was refused as walled, the door handlers declined it
(non-standard-door-action), the path-adjacent scan found no candidates, and
recovery then suppressed itself for a "nearby route door" which was this
very transport. Four seconds later the raw scene scan dispatched it from
range and crossed in a single action. Ten seconds at a gate, and every
step of it was the walker asking the wrong handler.

The transport attempt now runs before suppression. Suppression is unchanged
and still guards the generic recovery click below it — the Clock Tower
failure it exists for is untouched. It simply no longer outranks the handler
that can resolve the edge.

Mechanically this is a move, not new logic, and the block it moves above
already ran on every path where suppression declined. processWalk stays at
1641 lines; the comment was trimmed to fit rather than raising the guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Whether a route segment's obstacle handlers run was inline boolean soup:
two independent skip reasons — the window after a transport, and startup
before the first movement click — with the logged reason derived from a
ternary over which one fired.

The pair matters more than it looks. A skipped segment was never EXAMINED,
so an obstacle on it is neither resolved nor ruled out, which is why a skip
silently withdraws the right to click a door at range. That coupling is what
produced the Falador U-turn: segments 11 and 12 skipped with
no_nearby_planned_transport, the door at (2985,3341) then clicked from range
while the door at (2981,3340) was still shut between us and it, the server
routed around the building, and a traversal wait that could never be
satisfied timed out. Ten seconds and a U-turn, out of two booleans that
never appeared in the same expression.

SegmentGate now owns the decision as one enum-returning function, with the
log reason carried by the constant rather than reconstructed at the call
site, and mayDispatchDoorAtRange named for the invariant it protects.
Twelve decision-table cases pin it, including the ones that must NOT skip:
a planned transport nearby, an unreachable segment tile, a door attempt or
settle or recovery in flight, an immediate transport step at startup, and
the precedence when both skips apply.

Behaviour-preserving: same conditions, same precedence, same wire strings.
The two startup-preclick cases move from Rs2WalkerUnitTest into the new
table, where the rest of their family now lives.

processWalk 1641 -> 1630; the guard ratchets down a third time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rs2Player.isMoving() compares the pose animation against the idle pose, so
it reads TRUE while the player merely TURNS ON THE SPOT. Stall accounting
credited that as progress and refreshed the clock, so a player wedged
against a wall or a door who kept re-facing it could never be declared
stuck — the one state the stall detector exists to catch. The walker would
sit there indefinitely, because its own sensor kept insisting it was
walking.

Requiring an actual tile change instead would be worse. A walking step
takes ~600ms and this check samples faster than that, so "same tile as the
last sample" is the normal state of a healthy walk; demanding a delta every
sample would declare every walk stalled. That is presumably why the pose
flag was used in the first place.

The right question is not whether the tile changed since the last sample
but whether it has changed at all RECENTLY. Walking refreshes that
continuously; spinning never does. So the pose flag is now credited only
when a real tile change happened within 2.5s — several walking steps of
slack, and no help at all to a player who is only rotating.

Tracked on its own field rather than reusing lastMovedTimeMs, which several
places deliberately refresh to buy grace and therefore cannot answer "is the
player really covering ground". Seeded at walk start, because an unknown
tile-change time credits the pose and would otherwise hand a spinning player
the benefit of the doubt for the whole first stall window.

Scoped to stall accounting on purpose. Rs2Player.isMoving() has 65 call
sites in the walker alone and more across every other plugin; changing its
meaning globally is an unrelated blast radius. This changes who is allowed
to believe it, not what it says.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…re the legacy predicates

Three separate places in the walk loop yield to a live interim waypoint,
and all three logged the same word. A line reading interim-in-flight could
mean the route-level yield, the blocked-frontier recovery deferring to a
click it already made, or click selection finding the player still
travelling. That ambiguity has now blocked two stall diagnoses: the log
says the walker is waiting, and nothing says which wait.

Each site gets its own constant, wire names suffixed :route, :recovery and
:click. The shared interim-in-flight prefix keeps one grep matching all
three, and the flags are identical, so nothing about the behaviour changes
— only what the log can tell you afterwards.

This also discharges the follow-up left by the classification fix. The
string predicates the enum replaced survived only to hold up the
characterization test that proved that refactor inert; they have done that
job, and keeping dead production code alive to serve a test is the kind of
accretion this whole effort exists to reverse. They are deleted, and their
classification moves into WalkExitTest as three explicit sets, checked
exhaustively against every constant.

The sets are strictly better than the predicates were. A reason cannot
change meaning without changing the list, a new constant cannot be added
without being classified, and the lists read as documentation of what the
walker considers progress rather than as a startsWith("door-handled")
prefix rule that silently excluded half the door reasons.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… 15s

Two things padded the stall clock, and the first was papering over the
sensor that has since been fixed.

Every pass refreshed the clock outright for 12 seconds after any successful
minimap click, on the reasoning that a click can outrun the tile delta. It
cannot matter when it is right: while the player is walking, checkIfStuck
already refreshes the clock on every real tile change. The blanket only
ever had an effect while the player was NOT moving — precisely the state
the clock exists to measure. It is deleted, not shortened, because there is
no residue left for it to cover.

The interim multiplier was the same mistake in smaller print. A sticky
waypoint bought a 1.75x threshold in case a long segment outlasted the base
stall, but a player walking toward an interim refreshes the clock the whole
way, so the multiplier only ever bound the stationary-with-interim case —
which the idle nudge already rescues within a second or two, long before
any stall threshold comes into view. Now 1.25, for the tick or two between
issuing a click and the first step.

The base stays at 12s and should stay there. The longest LEGITIMATE
motionless stretch measured across four live farm runs is ~7.1s, waiting
out a transport handoff with nothing wrong. Cutting the base is the obvious
way to make recovery snappier and the wrong one: it buys a walker that
interrupts its own ships.

Resulting budget, now pinned as wall-clock seconds rather than as
multipliers, because seconds are the thing anyone actually cares about:
12s plain, 15s with an interim live (the common case for most of a walk),
24s worst case with everything applying at once. Was 12s + up to 24s = 36s.

processWalk 1630 -> 1623; the guard ratchets down a fourth time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t (B2 slice 1)

The reachable-tile set is captured once at the top of each pass, and then
the obstacle handlers run: opening a door, waiting out a transport, each
blocking for seconds inside interaction awaits. By the time the recovery
cascade reads the verdict the player can be standing somewhere else
entirely, and reachability computed from where we USED to be is not
evidence about where we are.

reachableTilesCacheOrigin has been declared, assigned twice and read never
since it was introduced. Reading it is the fix: recapture whenever the
player is no longer standing where the set was built.

Two bugs fall out of the recapture that was already there.

It ran at a SMALLER radius than the original capture — 18 steps against 39
— so it could answer "unreachable" for a tile the wider map had already
reached. A double-check that manufactures the verdict it exists to
question is worse than no double-check.

And it was gated on the tile being within ~15 tiles of the player rather
than on anything having changed, so it rebuilt the map when nothing had
moved and left it stale when everything had. Precisely inverted.

Scope note: the audit proposed also moving the recovery_position_stale
guard to the top of this branch. On reading it that is wrong, and the guard
should stay where it is. It does not duplicate this fix — it covers a
different window, the seconds the door cascade itself spends between the
verdict and the recovery click. Moving it earlier would defeat its purpose,
the same way B1's suggested exit-path enumeration turned out unnecessary
once the single entry point was found.

The remaining WalkTick slices (one snapshot threaded through the segment,
frontier and click-selection reads) are untouched; this is the behaviour
half of B2, not the structural half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e read from

Two things walker-fix was owed, hand-applied against its own copy rather than
cherry-picked — the earlier scripted pick silently dropped a slice's tests.

Drops the two changes that made things worse in live runs: the short-walk fast
path (which broke `distance = 0` — the walker stopped wanting to end on the goal
tile) and the zoom-aware minimap stride. Every call site is back on
NORMAL_MINIMAP_REACH_EUCLIDEAN and both helpers are gone.

Then lifts the frontier cascade's judgement out of processWalk and into
FrontierDecision, where it can be stated as a table instead of inferred from
1600 lines of control flow: which route index is the earliest blocked one, which
edge the frontier sits on, what a door wait means once it returns, when to yield
before touching a door at all, how far back a recovery index may be clamped,
which of three candidate targets wins and what happens when the winner is
dangerous, and whether a scene click is worth trying at the tail.

41 rows pin it, seeded from incidents rather than invented: the Clock Tower
rewind, the stepping-stone origin precedence, the fall-through door wait, and the
hazard asymmetry between the raw-gated target and the shortcut origin. Four
latent issues surfaced writing them that reading the cascade had not.

processWalk 1623 -> 1599. Interactions stay in the shell; nothing here touches
the game. Guardrail baseline regenerated for this branch — the delta is pure
lambda renumbering, 0 non-lambda lines. Full suite green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…orward-only recovery, minecart 947, zoom strides, B2 slice 2

Batched sync of six PluginTesting commits (8b49bdba02, 8aabe7cceb,
160a5fbe4f, ed7d5333e8, 2ea5c902e6 and the raw watermark), applied as a
three-way content merge per the branch policy — plus three OLDER fixes the
merge exposed as never having reached this branch at all: the Dwarf Cannon
catalog-transport guard in learnWalledRouteEdge, and the adventure-log
letter-prefix / null-safety / keyboard-shortcut fixes in
interactWithAdventureLog.

The batch, live-verified on PluginTesting before travelling:

- Door strike-out: three concluded-but-uncrossed attempts session-block the
  edge WALK-scoped (withdrawn before the next walk's first plan) and replan.
  Ends the Tithe Farm seed-gate class in ~25s instead of 4+ minutes.
- Route stagnation bound, ENFORCED: 60s without raw-index advance replans
  (twice), then honest UNREACHABLE. Fed by the raw watermark, which advances
  tile-by-tile on healthy walks and refuses oscillation.
- Recovery may only attack the obstacle AHEAD: the scan anchor is
  forward-corrected past raw-passed route tiles, and a wall door whose face
  the player is already beyond is never clicked (door_skip_crossed) — the
  Stronghold of Security paired-gate bounce, both organs.
- Tail dither: no re-clicks while moving inside the final band; canvas
  precision for the finish.
- Minecart destination selection: the Lovakengj menu is interface 947, not
  the adventure log; select by verbatim tsv displayInfo text.
- Zoom-aware minimap strides, both directions: reach follows the visible
  radius, 8 tiles fully zoomed in to the 18-tile BFS-vouched cap.
- B2 slice 2: WalkLoopSnapshot carries moving/animating/interacting, states
  its re-capture contract, and the stuck-sidestep stale-position read is
  fixed by re-capturing after its sleep.

Residual 55-line divergence from PluginTesting's copy is this branch's own
(door-wait local naming, WALK_TO_ORIGIN comment placement, no
isWalkSuperseded) and is deliberate. Guardrail baseline regenerated here —
delta is one pure lambda renumber. Full suite green on this branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…es 1-3, Stronghold latency, goal-object and wing guards, fold stall

Batched sync of PluginTesting 2ea5c902e6..a46b2a5768 (walker/API scope), all
live-verified on the Stronghold of Security corridor:

- B2 slices 3a-3c: the walk pass reads one world snapshot, re-captured at
  every blocking branch and per segment iteration
- Door-attempt ledger slices 1-3: DoorAttemptLedger owns ATTEMPTED (cooldowns
  + latest claim), REFUSED (strikes + walk-scoped blocks) and the tile facets
  (opened-suppression + session blacklist); Rs2DoorHandler reduced to the key
  builder and global-cooldown pair
- Stronghold gate latency: door-leg stage instrumentation, crossed-face
  release in the await, dialogue-or-crossing release with distance-scaled
  budget in handleStrongholdOfSecurityAnswer (5.4s/gate constant -> 1.5-3s)
- A crossed door satisfies nothing (conquered-door fall-through) and an
  unreachable fallback click arms nothing
- Goal-object rule: an object on the goal tile is the destination, not an
  obstacle (door_skip_goal_object)
- Walled-net learning defers to doors ADJACENT to the edge (double-gate wing)
- Fold stall ended: scan past behind/branch tiles, conquered doors resolve in
  both route-door classifiers

Method: three-way merge-file against base 2ea5c902e6 (Rs2Walker merged clean,
walker-fix's three recovered fixes preserved); guardrail baseline regenerated
on this branch, delta confined to walker entries. Full suite green here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… classification, fold stall, E1 extraction

Batched sync of PluginTesting a46b2a5768..da951b48d5 (walker/API scope;
tithefarming and Rs2Farming stay home per policy):

- D3 slice 4: ALL TEN door-state stores folded — the walk-runtime quartet
  (pass budget, settle window, global cooldown, raw-scan focus) joins
  DoorAttemptLedger; WalkerRouteState loses seven fields, door signatures
  lose their threaded Map parameter
- Requirement #3: one route-door classification for all ten sites
  (Rs2DoorClassifier.isRouteDoorObject — a chest is never a door; GameObject
  gates by name, tollgates by traversal verb; wall semantics unchanged)
- Fold stall ended live-verified; goal-object and double-gate wing guards
  live-verified
- keyDetail sub-timers for the cold-login transport refresh (PathfinderConfig)
- E1: the transport component (dispatcher + ~90 handlers, 3,090 lines) moves
  to Rs2WalkerTransports; Rs2Walker 14.1k -> 11.2k lines; baseline verified
  an exact multiset re-home on PluginTesting

Method: three-way merge-file against base a46b2a5768 (clean; walker-fix's
deliberate local lines preserved), baseline regenerated on this branch.
Full suite green here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f51b9e44-eecc-4d28-b84e-592a82074a5f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@infuse21
infuse21 merged commit 103ea79 into Fix-The-Walker-Decompression Aug 14, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant