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
- Call left_mouse_up (with or without a target location) to release the currently held button before issuing a new left_mouse_down.
- Track drag state in your agent loop so a retried drag resumes at mouse_up rather than repeating mouse_down.
- 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
- Pair every left_mouse_down with exactly one left_mouse_up in a try/finally so the lease never leaks.
- Model drags as a single state machine (move → down → move → up) and never restart mid-drag from the down step.
- On any interaction error, attempt a cleanup left_mouse_up before starting new pointer work.
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
- no agent pointer button is held by this session
- no agent pointer position — mouse_move or left_mouse_down…
- no window at ( , ) — take a fresh screenshot and choose a…
- open_application first to choose which application receives…
- shared_pointer_required
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)