Skip to content

Explain why navigation failed, and stop when the browser is gone - #118

Merged
DavertMik merged 2 commits into
mainfrom
navigator-failure-reporting
Aug 19, 2026
Merged

Explain why navigation failed, and stop when the browser is gone#118
DavertMik merged 2 commits into
mainfrom
navigator-failure-reporting

Conversation

@DavertMik

Copy link
Copy Markdown
Contributor

Why

Navigation failures reported only where the browser ended up — redirected to /users/sign_in and could not resolve — which names the symptom and never the cause.

The case that prompted this is a login page whose credentials were never provided as knowledge. That is one cause among several, so rather than special-case it, the reason is now composed the same way for every failure:

  • the blocker the AI reported through the stop tool, otherwise
  • the step that kept failing (3 attempts failed, last: …), otherwise
  • the AI proposed no working solution for this page

plus, when nothing is known about the page, the explorbot learn "<path>" command to run.

The reason reaches both callers: visit() puts it in the thrown error, and the interact tool appends it to its failure result instead of discarding it.

Refactor

resolveState held all of that inside a ~300 line method. It is now an orchestrator over value-returning helpers — buildResolutionPrompt, buildRetryFeedback, executeAttempt, verifyNavigation, ariaDiff, saveFlow, rescueDelayedRedirect, askUserToResolve — so the loop body reads as its three real phases: ask the AI for a batch, feed failures back, run one block.

Helpers derive what they can rather than taking it as arguments, so the signatures stay narrow: verifyNavigation(action, expectedUrl) returns the facts and lets the caller apply the resolution policy, and buildResolutionPrompt(message, actionResult) renders its own knowledge.

The refactor moved no behavior. The tests here were written against the unmodified method first and stayed green through it.

Tests

Written before the refactor, they pin the parts most likely to drift silently:

  • an AI call and the first attempt share one loop iteration
  • code blocks are alternatives without an expected URL, a sequence with one
  • progress is measured against two different baselines — the state resolveState started from for resolution, the state before the attempt for the ARIA diff
  • the retry prompt's shape: <previous_failures>, the ARIA-diff branch, full HTML injected exactly once
  • the stop tool wins over code blocks in the same response

One deliberate behavior change

The loop swallowed every error, including the BrowserRecoveryError raised when the browser cannot be restored, so a dead browser burned every remaining attempt and a model call with each — then reported it as an ordinary navigation failure. Fatal browser errors now propagate, which is what Action.attempt rethrows them for and what the interact tool's throwIfFatalBrowserError already expects. Ordinary failures still retry exactly as before, pinned by a test either way.

Recoverable crashes were already handled and are untouched: explorer.action() injects runWithRecovery, so action.attempt() and explorer.capture() self-heal without the navigator knowing.

Left alone, worth a follow-up

  • verifyState has the same swallow at navigator.ts:821. It is arguably worse there — a browser dying mid-verification returns verified: false, a false failed assertion. Both callers already tolerate a throw.
  • URL did not change (still X) is also emitted when the URL did match but the page state did not. Pinned as-is; it feeds slightly wrong evidence to the AI.

Verification

1008 unit + 80 integration passing, lint and format clean, no new tsc errors. No regression run started.

🤖 Generated with Claude Code

Navigation failures reported only where the browser ended up — "redirected to
/users/sign_in and could not resolve" — which named the symptom and never the
cause. The common case is a login page whose credentials were never provided as
knowledge, but that is one cause among several, so the reason is now composed
the same way for all of them: the blocker the AI reported through the stop tool,
otherwise the step that kept failing, plus the `learn` command to run when
nothing is known about the page.

The reason reaches both callers. `visit()` puts it in the thrown error, and the
`interact` tool appends it to its failure instead of discarding it.

resolveState carried that logic inside a ~300 line method. It is now an
orchestrator over value-returning helpers, so the loop body reads as its three
real phases: ask the AI for a batch, feed failures back, run one block. No
behavior moved in that step — the tests added here were written against the
unmodified method first, and stayed green through it.

Those tests pin the parts most likely to drift: an AI call and the first attempt
share one loop iteration, code blocks are alternatives without an expected URL
but a sequence with one, and progress is measured against two different
baselines — the state resolveState started from for resolution, the state before
the attempt for the ARIA diff.

One behavior did change deliberately. The loop swallowed every error, including
the BrowserRecoveryError raised when the browser cannot be restored, so a dead
browser burned every remaining attempt and a model call with each. Fatal browser
errors now propagate, which is what Action.attempt rethrows them for and what
the interact tool's throwIfFatalBrowserError already expects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DavertMik
DavertMik merged commit 754dffc into main Aug 19, 2026
2 checks passed
@DavertMik
DavertMik deleted the navigator-failure-reporting branch August 19, 2026 19:51
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.

2 participants