Hmbown/CodeWhale · error · Error

Unknown control mode

Error message

Unknown control mode

What it means

`setControlMode` in the computer-use app handler only accepts the control modes "ready", "paused", or "stopped". It throws a plain Error("Unknown control mode") for anything else. This function is called only by the launcher's inherited control channel — never as an MCP tool — to pause or resume input injection.

Solutions

  1. Send one of exactly "ready", "paused", or "stopped" on the control channel.
  2. Fix the sender's mode constants to match the handler's accepted set.
  3. Check for protocol drift between the launcher and plugin versions and update both.
  4. Log the received mode string to identify what the sender is actually transmitting.

Example fix

// before\nsendControl({ mode: "resume" });\n// after\nsendControl({ mode: "ready" });
Defensive patterns

Strategy: validation

Validate before calling

const MODES = ["ready","paused","stopped"];\nif (!MODES.includes(mode)) throw new Error(`Unknown control mode: ${mode}`);

Type guard

const isControlMode = (m) => ["ready","paused","stopped"].includes(m);

Try / catch

try { await setControlMode(mode); } catch (e) { if (e.message === "Unknown control mode") { console.error("bad control mode from sender:", mode); } else throw e; }

Prevention

When it happens

Trigger: Sending a control-channel message with a mode string other than the three allowed values, e.g. "pause" (singular), "resume", "start", or an empty/garbage token, or a protocol version mismatch where the launcher uses different mode names than the handler expects.

Common situations: Hand-rolled launchers or supervision scripts issuing control commands, typos in a mode constant, or a renamed mode after a protocol change with an un-updated sender.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/app-handler.mjs:61

  return backends.get(key);
}

const sessions = new Map();
let controlMode = "ready";
let controlGeneration = 0;
let cleanupPending = false;

/** Human-facing state contains app identity and action names, never task text. */
export function controlStatus() {
  return { mode: controlMode, cleanupPending, sessions: [...sessions.values()]
    .filter((s) => !s.closed && s.target)
    .map((s) => ({ target: s.target, mode: s.mode, action: s.action ?? null })) };
}

// Called only by the launcher's inherited control channel, never an MCP tool.
// Abort before queuing cleanup so even a held gesture yields to the person.
export async function setControlMode(mode) {
  if (!["ready", "paused", "stopped"].includes(mode)) throw new Error("Unknown control mode");
  if (mode === "ready") {
    if (cleanupPending) throw new Error("Input is still being released; try again in a moment.");
    controlMode = mode;
    return controlStatus();
  }
  controlMode = mode;
  const generation = ++controlGeneration;
  cleanupPending = true;
  const results = await Promise.allSettled([...sessions.keys()].map((key) => {
    const colon = key.indexOf(":");
    return releaseSessionInput(key.slice(colon + 1), key.slice(0, colon), { close: mode === "stopped" });
  }));
  const failure = results.find((r) => r.status === "rejected");
  // A cleanup failure stays blocked. Resume cannot hide owned input.
  if (failure) throw failure.reason;
  if (generation === controlGeneration) cleanupPending = false;
  return controlStatus();
}

View on GitHub (pinned to 73e0f67d83)