{"record":{"id":"eb3328a92215069a","repo":"Hmbown/CodeWhale","slug":"this-session-already-holds-the-left-pointer-button-release","errorCode":null,"errorMessage":"this session already holds the left pointer button; release it first","messagePattern":"this session already holds the left pointer button; release it first","errorType":"exception","errorClass":"ExecError","httpStatus":null,"severity":"error","filePath":"crates/tui/plugins/computer-use/src/backends/darwin.mjs","lineNumber":1045,"sourceCode":"    middle_click: ({ target } = {}) => pointerClick(\"middle\", target?.x, target?.y, 1),\n    mouse_move: async ({ target } = {}) => {\n      assertInScreen(target?.x, target?.y);\n      requireSharedPointer();\n      if (state.pointerLease) {\n        try {\n          const r = await state.pointerLease.send({ point: target });\n          state.pointer = { x: target.x, y: target.y };\n          return { action_sent: true, strategy: \"event\", at: state.pointer, ...pointerCost(r) };\n        } catch (error) { state.pointerLease = null; throw error; }\n      }\n      // A hover has to leave the pointer where it was asked to go.\n      const r = await gesture([{ type: MOUSE_MOVED, x: target.x, y: target.y, button: 0, clickState: 0 }], { restore: false, guard: target });\n      return { action_sent: true, strategy: \"event\", at: { x: target.x, y: target.y }, ...pointerCost(r) };\n    },\n    left_mouse_down: async ({ target } = {}) => {\n      assertInScreen(target?.x, target?.y);\n      requireSharedPointer();\n      if (state.pointerLease) throw new ExecError(\"this session already holds the left pointer button; release it first\");\n      await assertOwnsPoint(target.x, target.y);\n      state.pointerLease = await nativeLease(\"pointer_sequence\", { steps: [\n          { type: MOUSE_MOVED, x: target.x, y: target.y, button: 0, clickState: 0 },\n          { type: MOUSE.left.down, x: target.x, y: target.y, button: 0, clickState: 1 },\n        ], restore: false });\n      state.pointer = { x: target.x, y: target.y };\n      return { action_sent: true, strategy: \"event\", at: state.pointer, ...pointerCost(state.pointerLease.receipt) };\n    },\n    left_mouse_up: async ({ target } = {}) => {\n      if (!state.pointerLease) throw new ExecError(\"no agent pointer button is held by this session\");\n      const loc = target ?? state.pointer;\n      if (!loc) throw new ExecError(\"no agent pointer position — mouse_move or left_mouse_down first\");\n      assertInScreen(loc.x, loc.y);\n      // No ownership guard: the button is already held, and the drag may have\n      // legitimately left the originating window.\n      try { await withSignal(null, () => state.pointerLease.release({ point: loc })); }\n      finally { state.pointerLease = null; }\n      state.pointer = { x: loc.x, y: loc.y };","sourceCodeStart":1027,"sourceCodeEnd":1063,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/plugins/computer-use/src/backends/darwin.mjs#L1027-L1063","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: retrying a drag from the start while the button is still held\nawait left_mouse_down({ x: 100, y: 200 });\n// ... interrupted ...\nawait left_mouse_down({ x: 100, y: 200 }); // throws\n\n// after: release any held lease before pressing again\ntry {\n  await left_mouse_down({ x: 100, y: 200 });\n} catch {\n  await left_mouse_up(); // clear stale lease\n  await left_mouse_down({ x: 100, y: 200 });\n}","handlingStrategy":"validation","validationCode":"let pointerLeaseHeld = false;\nfunction assertCanMouseDown() {\n  if (pointerLeaseHeld) throw new Error('left button already held — release first');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await left_mouse_down({ x, y });\n  pointerLeaseHeld = true;\n} catch (e) {\n  if (String(e.message).includes('already holds the left pointer button')) {\n    await left_mouse_up(); // clear stale lease, then retry once\n    await left_mouse_down({ x, y });\n    pointerLeaseHeld = true;\n  } else throw e;\n}","preventionTips":["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."],"tags":["pointer","mouse","state-conflict","macos"],"backgroundTag":"invalid-state-transition","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T16:17:23.217Z"}