Hmbown/CodeWhale · error · ExecError
element_stale — re-run get_app_state; uitest indexes change…
Error message
element_stale — re-run get_app_state; uitest indexes change with the UI
What it means
centerOf resolves an element target by re-dumping the uitest layout and indexing into the flattened tree; if the index no longer exists or has no bounds, the element is stale. uitest element indexes change whenever the UI changes, so cached indexes from an earlier get_app_state can become invalid.
Solutions
- Re-run get_app_state and use a fresh element index, exactly as the message says.
- Re-locate the target element each time by role/label rather than caching indexes.
- Wrap element actions in try/catch for this error and retry once after re-observation.
- Minimize delay between get_app_state and the action to reduce staleness windows.
Example fix
// before
const idx = (await backend.get_app_state()).elements.findIndex(...); // cached long ago
await backend.select_text({ target: { type: 'element', index: idx } });
// after
try {
await backend.select_text({ target: { type: 'element', index: idx } });
} catch (e) {
if (!String(e.message).includes('element_stale')) throw e;
const state = await backend.get_app_state();
const fresh = state.elements.findIndex(...);
await backend.select_text({ target: { type: 'element', index: fresh } });
} Defensive patterns
Strategy: retry
Validate before calling
const state = await backend.get_app_state();
if (index < 0 || index >= state.elements.length) throw new Error('index out of range — re-observe'); Type guard
const isLiveElement = async (backend, index) => (await backend.get_app_state()).elements[index] != null;
Try / catch
try { await act(target); } catch (e) { if (String(e.message).includes('element_stale')) { const s = await backend.get_app_state(); await act({ type: 'element', index: relocate(s) }); } else throw e; } Prevention
- Never cache element indexes across UI-changing steps.
- Re-observe immediately before each element action.
- Relocate elements by role/label, not by remembered index.
- Reduce delays between observation and action.
When it happens
Trigger: Using an element index captured from an earlier get_app_state after the UI has changed (screen navigated, list reordered, dialog opened/closed), or an index out of range of the current flattened tree.
Common situations: Long-running automation holding element indexes across interactions; animations or async loading shifting the tree between observe and act; app updating its list between snapshot and select_text.
Related errors
- element_stale
- aa start failed
- {backoff.last_error}
- clipboard read is not exposed by hdc on current HarmonyOS…
- clipboard write is not exposed by hdc on current HarmonyOS…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/07160db0d8b27ada.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/harmonyos.mjs:112
try {
data = await exec.readFile(remote, { timeoutMs: 30_000 });
} finally {
await shell(["rm", "-f", remote]).catch(() => {});
}
return JSON.parse(data.toString("utf8"));
}
async function uiInput(args, opts = {}) {
await deviceOut(["uitest", "uiInput", ...args], opts);
return { action_sent: true, strategy: "event", backend: "uitest" };
}
async function centerOf(target) {
rejectAppSelectors(target);
const tree = await dumpLayout();
const els = flatten(tree);
const el = els[target.index];
if (!el || !el.bounds) throw new ExecError("element_stale — re-run get_app_state; uitest indexes change with the UI");
return el.bounds;
}
let frameSeq = 0;
let recording = null; // {id, dir, startedAt, intervalMs, timer, display}
let displayPixels = null;
async function stopFrames() {
const rec = recording;
if (!rec) return null;
rec.stopped = true;
clearInterval(rec.timer);
rec.controller.abort();
let timer;
try {
await Promise.race([rec.pending, new Promise((_, reject) => {
timer = setTimeout(() => reject(new ExecError("Harmony recording frame did not stop within 2 seconds")), 2_000);
})]);View on GitHub (pinned to 73e0f67d83)