Hmbown/CodeWhale · warning · ServerError

computer_route_changed

computer_route_changed

Error message

Computer route changed during this request — observe the registered target again before acting

What it means

assertCurrentRoute() re-checks, per request, that the registered computer's route fingerprint still matches the fingerprint recorded in the binding when it was first observed. If the binding was retired or the registry entry now routes elsewhere, it rebinds to the current target and throws a ServerError with code computer_route_changed, forcing the caller to re-observe and act on the new target rather than the stale one.

Solutions

  1. Re-observe the registered target: list/get the computer by id from the registry and re-read its current route, then retry the action against the new target.
  2. Discard any cached computer handle or binding from before the change; rebind (the server already called bindComputer) and rebuild state from the fresh observation.
  3. If your workflow pinned a specific route, verify the re-registered computer is the same machine you expect before re-issuing input actions.
  4. Retry the request after re-observation; if it recurs constantly, reduce the time between registration and use or investigate what is re-registering the target.

Example fix

// before
const computer = getStaleCachedComputer(id);
await act(computer, action);
// after
let computer;
try { computer = await act(cached, action); }
catch (err) {
  if (err.code === "computer_route_changed") {
    computer = await registry.get(id); // re-observe current target
    await act(computer, action);
  } else throw err;
}
Defensive patterns

Strategy: try-catch

Try / catch

try { await callComputerTool(computer, action); }
catch (err) {
  if (err.code === 'computer_route_changed') {
    const current = await registry.get(computer.id); // re-observe
    await callComputerTool(current, action);
  } else throw err;
}

Prevention

When it happens

Trigger: The computer identified by computer.id was unregistered and re-registered with a different route between when the binding was captured and when the tool call dispatched; registry.get() returns a current entry whose routeFingerprint differs from binding.route; the binding was retired concurrently; the underlying target moved (reconnected session, changed host/port/display identity).

Common situations: A long-running agent workflow holding a stale computer reference while the target session was restarted or re-registered; VM/remote machine reconnected with a new route; two clients racing where one re-registered the computer; load balancer or session manager reassigning the backing target mid-conversation.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/a9efa828685eed3e. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/mcp/server.mjs:139

  if (binding && (binding.route !== route || binding.retired)) {
    await retireBinding(computer.id);
    binding = { route, needsObservation: true };
    backendCache.set(computer.id, binding);
  } else if (!binding) {
    binding = { route, needsObservation: false };
    backendCache.set(computer.id, binding);
  }
  return binding;
}

async function assertCurrentRoute(computer, binding, dispatched = false) {
  try {
    let current;
    try { current = registry.get(computer.id); }
    catch (err) { await retireBinding(computer.id); throw err; }
    if (binding.retired || routeFingerprint(current) !== binding.route) {
      await bindComputer(current);
      throw new ServerError("computer_route_changed", "Computer route changed during this request — observe the registered target again before acting");
    }
  } catch (err) {
    if (dispatched) err.requestDispatched = true;
    throw err;
  }
}

async function getBackend(computer, binding) {
  if (!binding.backend) binding.backend = (await backendFor(computer)).backend;
  return binding.backend;
}

/**
 * Element target -> enriched target with cached app identity and AX path.
 * An explicit state_id pins a specific observation; a bare index addresses
 * the latest observation on this computer — the flat addressing a caller
 * uses when it acts on what it just saw.
 */

View on GitHub (pinned to 73e0f67d83)