Hmbown/CodeWhale · error · ExecError

this session already holds the left pointer button; release…

Error message

this session already holds the left pointer button; release it first

What it means

left_mouse_down enforces a single-pointer-lease invariant: the session may hold the left button only once at a time (state.pointerLease). Calling left_mouse_down while a previous down is still active (not released via left_mouse_up) throws this error, preventing nested drags and stuck buttons.

Solutions

  1. Call left_mouse_up (with or without a target location) to release the currently held button before issuing a new left_mouse_down.
  2. Track drag state in your agent loop so a retried drag resumes at mouse_up rather than repeating mouse_down.
  3. Ensure error handling for pointer sequences always attempts a cleanup left_mouse_up before starting a new interaction.

Example fix

// before: retrying a drag from the start while the button is still held
await left_mouse_down({ x: 100, y: 200 });
// ... interrupted ...
await left_mouse_down({ x: 100, y: 200 }); // throws

// after: release any held lease before pressing again
try {
  await left_mouse_down({ x: 100, y: 200 });
} catch {
  await left_mouse_up(); // clear stale lease
  await left_mouse_down({ x: 100, y: 200 });
}
Defensive patterns

Strategy: validation

Validate before calling

let pointerLeaseHeld = false;
function assertCanMouseDown() {
  if (pointerLeaseHeld) throw new Error('left button already held — release first');
}

Try / catch

try {
  await left_mouse_down({ x, y });
  pointerLeaseHeld = true;
} catch (e) {
  if (String(e.message).includes('already holds the left pointer button')) {
    await left_mouse_up(); // clear stale lease, then retry once
    await left_mouse_down({ x, y });
    pointerLeaseHeld = true;
  } else throw e;
}

Prevention

When it happens

Trigger: Calling left_mouse_down twice without an intervening left_mouse_up — e.g. a previous drag was interrupted before its mouse_up ran, or an earlier press threw after acquiring the lease but the caller retried without releasing.

Common situations: An aborted automation step left the button logically held; an agent loop retries a drag from its beginning instead of resuming at mouse_up; concurrent calls to pointer actions within one session race past the lease check.

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

Appendix: source

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

    middle_click: ({ target } = {}) => pointerClick("middle", target?.x, target?.y, 1),
    mouse_move: async ({ target } = {}) => {
      assertInScreen(target?.x, target?.y);
      requireSharedPointer();
      if (state.pointerLease) {
        try {
          const r = await state.pointerLease.send({ point: target });
          state.pointer = { x: target.x, y: target.y };
          return { action_sent: true, strategy: "event", at: state.pointer, ...pointerCost(r) };
        } catch (error) { state.pointerLease = null; throw error; }
      }
      // 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 };

View on GitHub (pinned to 73e0f67d83)