Hmbown/CodeWhale · error · ServerError
element_stale
element_stale
Error message
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 What it means
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.
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.
Example fix
// before
await act({ type: "element", state_id: st.state_id, index: 5 }); // long delay, dialog gone
// after
try {
await act({ type: "element", state_id: st.state_id, index: 5 });
} catch (e) {
if (e.code === "element_stale") {
const fresh = await getAppState(computerId);
const i = fresh.elements.findIndex(el => el.role === st.elements[5].role && el.label === st.elements[5].label);
if (i >= 0) await act({ type: "element", state_id: fresh.state_id, index: i });
} else throw e;
} Defensive patterns
Strategy: try-catch
Try / catch
try {
return await act({ type: "element", state_id, index });
} catch (e) {
if (e.code === "element_stale") {
const fresh = await getAppState(computerId);
const el = fresh.elements.find(x => x.label === observed.label && x.role === observed.role);
if (!el) throw new Error("element gone after re-observe; aborting");
return await act({ type: "element", state_id: fresh.state_id, index: fresh.elements.indexOf(el) });
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
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
- element_disabled
- element_read_only
- element_stale — re-run get_app_state; uitest indexes change…
- invalid_value
- value_rejected
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/f1cfd8ee2a5999a8.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/mcp/server.mjs:216
}
if (target.x < 0 || target.y < 0) throw new ServerError("bad_target", "raster coordinates must be non-negative");
const pt = rasterToPoints(computer.id, target.x, target.y);
return { x: Math.round(pt.x), y: Math.round(pt.y), strategy: "event", coordinate_space: "raster" };
}
if (target?.type === "element") {
const { state, element, stateId } = resolveElement(target, computer);
if (state.computerId && state.computerId !== computer.id) {
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`);
}
// The receipt must name the observation actually resolved — a bare index
// binds the computer's latest state, so reporting `target.state_id` would
// say "undefined" for the common case.
const where = `state ${stateId} (${state.app_ref?.name ?? state.app_ref?.bundle_id ?? `pid ${state.app_ref?.pid}`})`;
let fresh = null;
if (resolve) {
const res = await resolve({ app_ref: state.app_ref, windowIndex: element.windowIndex ?? 0, path: element.path });
if (!res?.found || !res.element) {
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`);
}
fresh = res.element;
if (fresh.role !== element.role) {
throw new ServerError("element_stale", `element ${target.index} of ${where} changed role (${element.role} → ${fresh.role}) — call get_app_state again`);
}
// In-place replacement: same role and geometry but a different label is
// still a different element (e.g. "Load" → "Confirm").
if (fresh.label !== element.label) {
throw new ServerError("element_stale", `element ${target.index} of ${where} changed label (${element.label} → ${fresh.label}) — call get_app_state again`);
}
}
if (kind === "semantic") {
return {
app_ref: state.app_ref, windowIndex: element.windowIndex ?? 0, path: element.path,
strategy: "a11y", role: element.role, label: element.label, reacquired: false,
...(element.runtime_id ? { runtime_id: element.runtime_id, window_runtime_id: element.window_runtime_id } : {}),
};
}View on GitHub (pinned to 73e0f67d83)