{"record":{"id":"07160db0d8b27ada","repo":"Hmbown/CodeWhale","slug":"element-stale-re-run-get-app-state-uitest-indexes-change","errorCode":null,"errorMessage":"element_stale — re-run get_app_state; uitest indexes change with the UI","messagePattern":"element_stale — re-run get_app_state; uitest indexes change with the UI","errorType":"exception","errorClass":"ExecError","httpStatus":null,"severity":"error","filePath":"crates/tui/plugins/computer-use/src/backends/harmonyos.mjs","lineNumber":112,"sourceCode":"    try {\n      data = await exec.readFile(remote, { timeoutMs: 30_000 });\n    } finally {\n      await shell([\"rm\", \"-f\", remote]).catch(() => {});\n    }\n    return JSON.parse(data.toString(\"utf8\"));\n  }\n\n  async function uiInput(args, opts = {}) {\n    await deviceOut([\"uitest\", \"uiInput\", ...args], opts);\n    return { action_sent: true, strategy: \"event\", backend: \"uitest\" };\n  }\n\n  async function centerOf(target) {\n    rejectAppSelectors(target);\n    const tree = await dumpLayout();\n    const els = flatten(tree);\n    const el = els[target.index];\n    if (!el || !el.bounds) throw new ExecError(\"element_stale — re-run get_app_state; uitest indexes change with the UI\");\n    return el.bounds;\n  }\n\n  let frameSeq = 0;\n  let recording = null; // {id, dir, startedAt, intervalMs, timer, display}\n  let displayPixels = null;\n\n  async function stopFrames() {\n    const rec = recording;\n    if (!rec) return null;\n    rec.stopped = true;\n    clearInterval(rec.timer);\n    rec.controller.abort();\n    let timer;\n    try {\n      await Promise.race([rec.pending, new Promise((_, reject) => {\n        timer = setTimeout(() => reject(new ExecError(\"Harmony recording frame did not stop within 2 seconds\")), 2_000);\n      })]);","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/plugins/computer-use/src/backends/harmonyos.mjs#L94-L130","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst idx = (await backend.get_app_state()).elements.findIndex(...); // cached long ago\nawait backend.select_text({ target: { type: 'element', index: idx } });\n// after\ntry {\n  await backend.select_text({ target: { type: 'element', index: idx } });\n} catch (e) {\n  if (!String(e.message).includes('element_stale')) throw e;\n  const state = await backend.get_app_state();\n  const fresh = state.elements.findIndex(...);\n  await backend.select_text({ target: { type: 'element', index: fresh } });\n}","handlingStrategy":"retry","validationCode":"const state = await backend.get_app_state();\nif (index < 0 || index >= state.elements.length) throw new Error('index out of range — re-observe');","typeGuard":"const isLiveElement = async (backend, index) => (await backend.get_app_state()).elements[index] != null;","tryCatchPattern":"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; }","preventionTips":["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."],"tags":["harmonyos","stale-element","ui-automation","retry"],"backgroundTag":"element-stale","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"}