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
- Send one of exactly "ready", "paused", or "stopped" on the control channel.
- Fix the sender's mode constants to match the handler's accepted set.
- Check for protocol drift between the launcher and plugin versions and update both.
- 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
- Share one modes constant between launcher and handler.
- Validate control messages at the channel boundary before dispatch.
- Test control-channel senders against all three accepted modes.
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
- click supports left with 1-3 clicks, right x1 or middle x1
- open_application needs a plain executable/desktop name
- pointer action must be "move", "down" or "up
- unknown_tool
- 1
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)