fix(web): classify agent-browser dispatch failures from typed backend phases

Merged
TH
13d ago
+345-28

Summary

A web interaction failure carried no dispatch verdict, so error.details.dispatched reached the wire only as the router's fallback unknown — even when the agent-browser CLI had provably refused the argv before the browser was touched, or the managed backend was never spawned. Callers could not tell a safe retry from one that might tap or type twice.

Stamp no from the two typed sources that prove the command never reached the browser: the CLI refusal envelope type (unknown_command, missing_arguments) and the provider's pre-spawn TOOL_MISSING refusal. Count the sends of compound operations — coordinate click is move/down/up, fill adds select-all and typing, a paced scroll sends one wheel step at a time — so a failure after a send returned reports unknown with details.dispatchedSteps through the shared kernel rule. A failure the backend answers keeps no verdict: the command reached the browser and its effect is not knowable from the reply. Keyed on typed fields only, never error text. Adds the web-agent-browser producer, six golden-table rows, and a driver that runs each through the real provider entry point. 4 files.

Closes #3503 (requirement 2; requirement 1 is #3541).

Validation

Tested 67e81177b. pnpm check:affected --run --base perf/web-provider-overhead passed (format, lint, typecheck, layering, fallow, build, related vitest; 337 files / 2,309 tests).

Live before/after on the same host, a web open against a state dir without the managed backend (base 46dae0f2e → the classification at 156bc97af, whose provider code is unchanged at the current head):

46dae0f2eclassified
"dispatched": "unknown""dispatched": "no"
agent-device open http://localhost:3000 --platform web --state-dir /tmp/missing-backend

fill is compound (click then type), so its post-click failure reports unknown with dispatchedSteps: 3 — asserted by the golden row and its driver — never no. Each row test fails if its stamp is removed.

The missing_arguments row now drives typeText(''), which reaches 0.27.1 as keyboard type with no text and really is answered with that envelope, so its fixture is the CLI's verbatim reply. No argv the current provider sends is unknown to 0.27.1, so the unknown_command row keeps a written envelope and is labelled as covering a newer provider against an older managed backend.

Follow-up, deliberately not here: the row drivers keep their own send counters beside discloseDispatchAfterSteps; a shared counter helper is worth a later PR.

Stacked on #3541; retarget at merge.