Hmbown/CodeWhale · error · ExecError
AT-SPI resolve failed
Error message
AT-SPI resolve failed: ${(r.stderr || r.stdout).slice(0, 250)} What it means
The internal atspiResolve helper runs an embedded python3 script to locate an element in the AT-SPI tree; this error is thrown when its output cannot be parsed as JSON. The first 250 bytes of stderr (or stdout) are included to show why the script failed.
Solutions
- Read the truncated stderr in the message to identify the python failure, then fix that root cause.
- Install python3-pyatspi and verify import pyatspi works.
- Re-fetch a fresh element path via get_app_state before resolving — stale paths from old snapshots fail.
- Ensure the target window/app is still open and the a11y bus is running; retry the action.
Example fix
// before
await backend.click({ target: { path: oldPath } }); // stale path -> AT-SPI resolve failed
// after
const state = await backend.get_app_state({ app_ref: { name: 'Firefox' } });
const fresh = findElement(state, 'Submit');
await backend.click({ target: { path: fresh.path } }); Defensive patterns
Strategy: retry
Validate before calling
const fresh = await backend.get_app_state({ app_ref }); // refresh paths before resolving/clicking Try / catch
try {
return await resolveElement(target);
} catch (e) {
if (String(e.message).startsWith('AT-SPI resolve failed')) {
await refreshAppState();
return resolveElement(freshTargetFor(target));
}
throw e;
} Prevention
- Always resolve element paths from a freshly taken app state snapshot
- Check stderr excerpt in the message for the real python failure
- Install python3-pyatspi before any AT-SPI-based operation
- Avoid operating on windows that may close between listing and resolution
When it happens
Trigger: The python script raised an exception (stderr traceback) — bad element path, pyatspi errors, node disappeared mid-walk; python3 or pyatspi missing so stdout is empty; the 30s timeout elapsing and killing the interpreter; the target element being stale (window closed between listing and resolve).
Common situations: Automating a window that was closed or navigated away between actions; passing an element path from a previous get_app_state snapshot that no longer resolves; machines missing python3-pyatspi so every resolve fails.
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
Related errors
- AT-SPI action failed
- application not found or name is ambiguous in the AT-SPI…
- AT-SPI walk failed — is python3-pyatspi installed and the…
- element_disabled
- element_read_only
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/199dd1610f0c43d0.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/linux.mjs:557
except Exception:
found = None
if found is None:
ok = False
break
node = found
if not ok:
print(json.dumps({"found": True, "element": None, "reason": "element_stale"}))
sys.exit(0)
ext = None
try: ext = node.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
except Exception: pass
print(json.dumps({"found": True, "reason": None, "element": {
"role": node.getRoleName() or None, "label": node.name or None,
"position": {"x": ext.x, "y": ext.y} if ext else None,
"size": {"w": ext.width, "h": ext.height} if ext else None}}))`;
const r = await run("python3", ["-c", script, name, JSON.stringify(pathArr ?? [])], { timeoutMs: 30_000 });
const out = tryJson(r.stdout.trim().split("\n").pop() ?? "", null);
if (!out) throw new ExecError(`AT-SPI resolve failed: ${(r.stderr || r.stdout).slice(0, 250)}`, r);
return out;
},
zoom: async ({ source, region, path: outPath }) => {
need("ffmpeg", "zoom/crop");
const src = source ?? lastRaster?.file;
if (!src) throw new ExecError("no screenshot taken yet on this computer — call screenshot first");
const out = outputPath(outPath ?? path.join(recordingsDir(), `zoom-${crypto.randomBytes(4).toString("hex")}.png`));
await runOk("ffmpeg", ["-y", "-loglevel", "error", "-i", src, "-vf", `crop=${Math.round(region[2])}:${Math.round(region[3])}:${Math.round(region[0])}:${Math.round(region[1])}`, out], { timeoutMs: 20_000 });
return { file: out, bytes: fs.statSync(out).size, region, source: src };
},
left_click: ({ target, strategy }) => { assertNum(target.x, "x"); assertNum(target.y, "y"); assertEventStrategy(strategy); return inputChain(target.x, target.y, () => clickButton(1, 1)); },
double_click: ({ target }) => inputChain(target.x, target.y, () => clickButton(1, 2)),
triple_click: ({ target }) => inputChain(target.x, target.y, () => clickButton(1, 3)),
right_click: ({ target }) => inputChain(target.x, target.y, () => clickButton(3, 1)),
middle_click: ({ target }) => inputChain(target.x, target.y, () => clickButton(2, 1)),
mouse_move: ({ target }) => inputMove(target.x, target.y),
left_click_drag: async ({ from_target: from, to }) => {
requireInputOwner();View on GitHub (pinned to 73e0f67d83)