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
- Call screenshot or get_app_state on the registered target once, then retry the original tool.
- Re-read the current computer id from the registry and target calls at the new id.
- 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
- After any route change or re-registration, call screenshot/get_app_state before acting.
- Avoid caching computer ids across route switches; read the current registration.
- Interleave observation calls in long-running automation so the server's view stays fresh.
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
- app_denied
- consent_required
- control_stopped
- foreground_denied
- MCP configuration changed; reload it before connecting this…
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)