Hmbown/CodeWhale · error · ServerError

control_stopped

control_stopped

Error message

stop_computer_control is active; no further actions are permitted this session

What it means

stop_computer_control is a session kill switch. Once active, the dispatch loop re-checks it before every tool call and refuses any non-read-only tool with control_stopped, including actions already mid-preparation when stop arrived. Read-only tools (e.g. screenshot, get_app_state) remain allowed by READ_ONLY_TOOLS.

Solutions

  1. Re-initialize or restart the computer-control session to clear the kill switch if further actions are genuinely needed
  2. Remove subsequent mutating steps after stop_computer_control in any queued run_actions batch
  3. Restrict post-stop work to read-only tools (screenshot, get_app_state) that READ_ONLY_TOOLS permits

Example fix

// before
await stopComputerControl();
await click({ target: el }); // throws control_stopped
// after
await stopComputerControl();
// only read-only calls afterwards, or start a new session before mutating calls
Defensive patterns

Strategy: try-catch

Validate before calling

if (controlStopped && !READ_ONLY_TOOLS.has(toolName)) throw new Error("session control stopped; refusing " + toolName);

Type guard

const isReadOnlyTool = (name) => READ_ONLY_TOOLS.has(name);

Try / catch

try { await callTool(name, args) } catch (e) { if (e.code === "control_stopped") { /* stop the queue; do not retry mutating calls; restart session if actions are still required */ } else throw e; }

Prevention

When it happens

Trigger: Any mutating computer-use call (click, type, key, run_actions, app_script, ...) issued after stop_computer_control was called in the session; a stop racing with an in-flight prepareArgs resolves first and still blocks dispatch.

Common situations: Automations that finished their stop but retried; a user pressing a stop/emergency control then a queued step firing; long-lived sessions where an earlier stop is forgotten.

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

Appendix: source

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

              ex.closeChannel?.();
              return ex.remote(request, opts);
            }
            throw err;
          }
        }
        return ex.remote(request, opts);
      };
      const resolve = async (req) => {
        const rep = await remoteCall({ tool: "resolve_element", args: req }, { timeoutMs: 30_000 });
        if (!rep?.ok) return { found: false, element: null, reason: rep?.error?.code ?? "remote_error" };
        return rep.data;
      };
      const wireArgs = await prepareArgs(computer, name, args, resolve, sink);
      throwIfAborted();
      await assertCurrentRoute(computer, binding);
      // Re-check the kill switch: a stop that arrived while the executor was
      // being resolved still blocks this dispatch.
      if (controlStopped && !READ_ONLY_TOOLS.has(name)) throw new ServerError("control_stopped", "stop_computer_control is active; no further actions are permitted this session");
      inFlight++;
      try {
        dispatched = true;
        const timeoutMs = backendMethod.startsWith("recording") || backendMethod === "get_app_state" ? 60_000 : 30_000;
        const invoke = async (tool, a) => {
          const r = await remoteCall({ tool, args: a }, { timeoutMs });
          if (!r.ok) throw new ServerError(r.error?.code ?? "remote_error", r.error?.message ?? "remote agent failed");
          return r.data;
        };
        data = name === "type" ? await invokeType(invoke, wireArgs) : await invoke(backendMethod, wireArgs);
      } finally {
        inFlight--;
      }
      await assertCurrentRoute(computer, binding, true);
      if (Array.isArray(data)) data = { items: data };
      if ((backendMethod === "screenshot" || backendMethod === "zoom") && data?.file) {
        if (ex.filesLocal) bindRaster(computer, data);
        else {

View on GitHub (pinned to 73e0f67d83)