{"record":{"id":"f1cfd8ee2a5999a8","repo":"Hmbown/CodeWhale","slug":"element-stale","errorCode":"element_stale","errorMessage":"element ${target.index} of ${where} no longer resolves (${res?.reason ?? \"not_found\"}) — the user or the app may have changed it; call get_app_state again","messagePattern":"element (.+?) of (.+?) no longer resolves \\((.+?)\\) — the user or the app may have changed it; call get_app_state again","errorType":"error_code","errorClass":"ServerError","httpStatus":null,"severity":"error","filePath":"crates/tui/plugins/computer-use/mcp/server.mjs","lineNumber":216,"sourceCode":"    }\n    if (target.x < 0 || target.y < 0) throw new ServerError(\"bad_target\", \"raster coordinates must be non-negative\");\n    const pt = rasterToPoints(computer.id, target.x, target.y);\n    return { x: Math.round(pt.x), y: Math.round(pt.y), strategy: \"event\", coordinate_space: \"raster\" };\n  }\n  if (target?.type === \"element\") {\n    const { state, element, stateId } = resolveElement(target, computer);\n    if (state.computerId && state.computerId !== computer.id) {\n      throw new ServerError(\"state_wrong_computer\", `state_id \"${stateId}\" belongs to computer \"${state.computerId}\", not \"${computer.id}\" — call get_app_state on that computer again`);\n    }\n    // The receipt must name the observation actually resolved — a bare index\n    // binds the computer's latest state, so reporting `target.state_id` would\n    // say \"undefined\" for the common case.\n    const where = `state ${stateId} (${state.app_ref?.name ?? state.app_ref?.bundle_id ?? `pid ${state.app_ref?.pid}`})`;\n    let fresh = null;\n    if (resolve) {\n      const res = await resolve({ app_ref: state.app_ref, windowIndex: element.windowIndex ?? 0, path: element.path });\n      if (!res?.found || !res.element) {\n        throw new ServerError(\"element_stale\", `element ${target.index} of ${where} no longer resolves (${res?.reason ?? \"not_found\"}) — the user or the app may have changed it; call get_app_state again`);\n      }\n      fresh = res.element;\n      if (fresh.role !== element.role) {\n        throw new ServerError(\"element_stale\", `element ${target.index} of ${where} changed role (${element.role} → ${fresh.role}) — call get_app_state again`);\n      }\n      // In-place replacement: same role and geometry but a different label is\n      // still a different element (e.g. \"Load\" → \"Confirm\").\n      if (fresh.label !== element.label) {\n        throw new ServerError(\"element_stale\", `element ${target.index} of ${where} changed label (${element.label} → ${fresh.label}) — call get_app_state again`);\n      }\n    }\n    if (kind === \"semantic\") {\n      return {\n        app_ref: state.app_ref, windowIndex: element.windowIndex ?? 0, path: element.path,\n        strategy: \"a11y\", role: element.role, label: element.label, reacquired: false,\n        ...(element.runtime_id ? { runtime_id: element.runtime_id, window_runtime_id: element.window_runtime_id } : {}),\n      };\n    }","sourceCodeStart":198,"sourceCodeEnd":234,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/plugins/computer-use/mcp/server.mjs#L198-L234","documentation":"Before acting, normalizeTarget re-resolves the element by its accessibility path (via the injected resolve callback) and throws code \"element_stale\" when the element no longer exists (res.found false or no element). The UI changed between get_app_state and the action, so the flat index is no longer trustworthy; the message includes the resolve failure reason and tells the caller to re-observe.","triggerScenarios":"Element target whose recorded path cannot be resolved in the live app: the element was closed/removed, the window changed (element.windowIndex no longer valid), the app navigated to another screen, or the app restarted between observation and action.","commonSituations":"An app shows a transient dialog that disappears before the click; a page finishes loading and re-renders the tree; the user interacts with the machine concurrently; automation retries an action long after the observation expired.","solutions":["Call get_app_state again and re-locate the element (by role/label) in the fresh observation, then retry the action once.","Minimize the delay between observation and action; don't cache element targets across long waits.","If the element legitimately disappeared, treat it as success-or-no-op and update the automation plan rather than retrying blindly.","Handle the res.reason when available (e.g. window closed vs not_found) to choose between re-observing and aborting."],"exampleFix":"// before\nawait act({ type: \"element\", state_id: st.state_id, index: 5 }); // long delay, dialog gone\n\n// after\ntry {\n  await act({ type: \"element\", state_id: st.state_id, index: 5 });\n} catch (e) {\n  if (e.code === \"element_stale\") {\n    const fresh = await getAppState(computerId);\n    const i = fresh.elements.findIndex(el => el.role === st.elements[5].role && el.label === st.elements[5].label);\n    if (i >= 0) await act({ type: \"element\", state_id: fresh.state_id, index: i });\n  } else throw e;\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  return await act({ type: \"element\", state_id, index });\n} catch (e) {\n  if (e.code === \"element_stale\") {\n    const fresh = await getAppState(computerId);\n    const el = fresh.elements.find(x => x.label === observed.label && x.role === observed.role);\n    if (!el) throw new Error(\"element gone after re-observe; aborting\");\n    return await act({ type: \"element\", state_id: fresh.state_id, index: fresh.elements.indexOf(el) });\n  }\n  throw e;\n}","preventionTips":["Re-observe and re-resolve by role/label rather than retrying a stale index.","Keep observation-to-action latency short; don't hold element targets across waits.","Expect transient UI (dialogs, spinners) and re-plan when elements vanish.","Retry at most once after re-observation to avoid acting on the wrong control."],"tags":["staleness","ui-automation","accessibility","retry"],"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-22T21:17:16.096Z"}