Hmbown/CodeWhale · error · ServerError

computer_observation_required

computer_observation_required

Error message

Computer route changed — call screenshot or get_app_state on the registered target before acting

What it means

Thrown after bindComputer() reports that the computer's route changed (needsObservation) when the requested tool is not in ROUTE_INSPECTION_TOOLS. After a route switch the server's cached view of the target is stale, so it demands a fresh observation — a screenshot or get_app_state on the newly registered target — before allowing action tools to run.

Solutions

  1. Call screenshot or get_app_state on the registered target once, then retry the original tool.
  2. Re-read the current computer id from the registry and target calls at the new id.
  3. If the route change was unintended, re-register/rebind the expected computer before resuming.

Example fix

// before: acting on a stale route
await callTool({ name: "run_actions", arguments: { computer: "box1", steps: [...] } }); // throws computer_observation_required

// after: observe first
await callTool({ name: "screenshot", arguments: { computer: "box1" } });
await callTool({ name: "run_actions", arguments: { computer: "box1", steps: [...] } });
Defensive patterns

Strategy: try-catch

Try / catch

try {
  return await callTool({ name, arguments });
} catch (e) {
  if (e.code === "computer_observation_required") {
    await callTool({ name: "screenshot", arguments: { computer: arguments.computer } });
    return await callTool({ name, arguments }); // single observation-then-retry
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling an action tool (e.g. run_actions, open_application) right after the computer route was switched/re-registered, without first calling screenshot or get_app_state on the new target.

Common situations: Automation scripts that assume one registration per session; a container restart or rebind changing the route mid-workflow; retry logic replaying actions against a re-registered computer.

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/7a6786abda94a379. Report an issue: GitHub.

Appendix: source

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

      const resolved = await resolveAppIdentity(computer, ref);
      const allKeys = resolved ? [...new Set([...keys, ...consent.appKeys(resolved)])] : keys;
      if (verb) {
        const r = consent.record(computer.id, allKeys, verb, { remember: args.remember === true, name: resolved?.name ?? ref.name ?? null });
        return { content: [{ type: "text", text: JSON.stringify(receipt(computer, { ok: true, tool: name, switched, scope, decision: verb, app: resolved ?? ref, keys: allKeys, persisted: r.persisted, note: `${resolved?.name ?? ref.name ?? ref.bundle_id ?? `pid ${ref.pid}`} ${verb === "allow" ? "allowed" : "denied"} ${r.persisted ? "until revoked" : "for this session"}.` })) }] };
      }
      const r = consent.revoke(computer.id, allKeys);
      return { content: [{ type: "text", text: JSON.stringify(receipt(computer, { ok: true, tool: name, switched, scope, app: resolved ?? ref, keys: allKeys, ...r, note: "Decisions removed — the next call targeting this app asks again." })) }] };
    } catch (err) {
      return { content: [{ type: "text", text: JSON.stringify(fail(computer, err.code ?? "consent_error", err.message ?? String(err), { tool: name, switched })) }], isError: true };
    }
  }

  let binding;
  let dispatched = false;
  try {
    binding = await bindComputer(computer);
    if (binding.needsObservation && !ROUTE_INSPECTION_TOOLS.has(name)) {
      throw new ServerError("computer_observation_required", "Computer route changed — call screenshot or get_app_state on the registered target before acting");
    }
    // Per-app consent: the first call that targets an application on the local
    // computer must carry a recorded user decision. open_application returns
    // the grant so its resolved identity can be aliased below.
    const gateResult = await consentCheck(computer, name, args);
    if (name === "run_actions") {
      const steps = args.steps;
      if (!Array.isArray(steps) || steps.length < 1 || steps.length > 8) throw new ServerError("bad_args", "run_actions needs 1..8 steps");
      const results = [];
      for (const [i, step] of steps.entries()) {
        if (!step || typeof step.tool !== "string") throw new ServerError("bad_args", `step ${i} needs a tool name`);
        if (step.tool === "run_actions") throw new ServerError("bad_args", "run_actions cannot nest");
        if (!TOOL_NAMES.has(step.tool)) throw new ServerError("unknown_tool", `unknown tool "${step.tool}"`);
        const result = await callTool({ name: step.tool, arguments: { ...(step.arguments ?? {}), computer: computer.id } });
        const body = JSON.parse(result.content[0].text);
        results.push({ tool: step.tool, ok: body.ok !== false, receipt: body });
        if (body.ok === false || result.isError) {
          return { content: [{ type: "text", text: JSON.stringify(fail(computer, body.error?.code ?? "step_failed", body.error?.message ?? "step failed", { tool: "run_actions", switched, stopped_at: i, steps: results })) }], isError: true };

View on GitHub (pinned to 73e0f67d83)