Hmbown/CodeWhale · error · ExecError

no agent pointer button is held by this session

Error message

no agent pointer button is held by this session

What it means

left_mouse_up releases the pointer lease held by left_mouse_down. If state.pointerLease is null — no button was ever pressed, or it was already released — the call throws this error instead of sending a stray AX/CG mouse-up event that could pop up a context menu or complete an unintended click.

Solutions

  1. Only call left_mouse_up after a successful left_mouse_down in the same session.
  2. Track whether the button is held on the caller side and skip the mouse_up when it is not.
  3. If the button was physically stuck from a prior session, use mouse_move first to re-establish pointer position, then rebuild the press/release pair rather than sending an orphan mouse_up.

Example fix

// before: unconditional release
await left_mouse_down({ x: 50, y: 60 });
await left_mouse_up();
await left_mouse_up(); // second call throws

// after: guard on caller side
let held = false;
if (!held) return;
await left_mouse_up();
held = false;
Defensive patterns

Strategy: type-guard

Validate before calling

let held = false; // set true after a successful left_mouse_down
function canMouseUp() { return held; }

Type guard

function hasActivePointerLease(sessionState) {
  return sessionState != null && sessionState.pointerLease != null;
}

Try / catch

try {
  if (!held) return; // nothing to release
  await left_mouse_up();
  held = false;
} catch (e) {
  if (!String(e.message).includes('no agent pointer button is held')) throw e;
  held = false; // already released; reconcile local state
}

Prevention

When it happens

Trigger: Calling left_mouse_up without a prior left_mouse_down in this session, or calling it twice (the first call sets state.pointerLease = null in its finally block).

Common situations: Agent loop unconditionally emits mouse_up after every action; a previous mouse_up already fired on an error path but the retry logic repeats it; the session was recreated (new state object) and the lease from the old session is gone.

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

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/darwin.mjs:1055

      }
      // A hover has to leave the pointer where it was asked to go.
      const r = await gesture([{ type: MOUSE_MOVED, x: target.x, y: target.y, button: 0, clickState: 0 }], { restore: false, guard: target });
      return { action_sent: true, strategy: "event", at: { x: target.x, y: target.y }, ...pointerCost(r) };
    },
    left_mouse_down: async ({ target } = {}) => {
      assertInScreen(target?.x, target?.y);
      requireSharedPointer();
      if (state.pointerLease) throw new ExecError("this session already holds the left pointer button; release it first");
      await assertOwnsPoint(target.x, target.y);
      state.pointerLease = await nativeLease("pointer_sequence", { steps: [
          { type: MOUSE_MOVED, x: target.x, y: target.y, button: 0, clickState: 0 },
          { type: MOUSE.left.down, x: target.x, y: target.y, button: 0, clickState: 1 },
        ], restore: false });
      state.pointer = { x: target.x, y: target.y };
      return { action_sent: true, strategy: "event", at: state.pointer, ...pointerCost(state.pointerLease.receipt) };
    },
    left_mouse_up: async ({ target } = {}) => {
      if (!state.pointerLease) throw new ExecError("no agent pointer button is held by this session");
      const loc = target ?? state.pointer;
      if (!loc) throw new ExecError("no agent pointer position — mouse_move or left_mouse_down first");
      assertInScreen(loc.x, loc.y);
      // No ownership guard: the button is already held, and the drag may have
      // legitimately left the originating window.
      try { await withSignal(null, () => state.pointerLease.release({ point: loc })); }
      finally { state.pointerLease = null; }
      state.pointer = { x: loc.x, y: loc.y };
      return { action_sent: true, strategy: "event", at: state.pointer, pointer_moved: true, pointer_restored: false };
    },
    left_click_drag: async ({ from_target: from, to } = {}) => {
      assertInScreen(from?.x, from?.y); assertInScreen(to?.x, to?.y);
      const steps = [
        { type: MOUSE_MOVED, x: from.x, y: from.y, button: 0, clickState: 0 },
        { type: MOUSE.left.down, x: from.x, y: from.y, button: 0, clickState: 1, delayMs: 60 },
      ];
      const n = 12;
      for (let i = 1; i <= n; i++) {

View on GitHub (pinned to 73e0f67d83)