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
- Only call left_mouse_up after a successful left_mouse_down in the same session.
- Track whether the button is held on the caller side and skip the mouse_up when it is not.
- 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
- Track hold/release symmetrically in caller-side state and skip redundant mouse_up calls.
- Never emit left_mouse_up in a generic cleanup path when no drag was started.
- Remember a session rebuild resets the lease — do not carry held-button state across sessions without re-establishing it.
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
- this session already holds the left pointer button; release…
- 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/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)