Hmbown/CodeWhale · error · ExecError
AT-SPI action failed
Error message
AT-SPI action failed: ${(r.stderr || r.stdout).slice(0, 250)} What it means
Thrown by atspiResolve when the embedded python3 AT-SPI script either fails to produce parseable JSON on its last stdout line or crashes. The message carries up to 250 chars of stderr (falling back to stdout) prefixed with 'AT-SPI action failed'. The wrapper runs `python3 -c <script> <name> <path-json>` with a 30s timeout, then tryJson()es the final line.
Solutions
- Read the 250-char payload: a Python traceback means missing modules or no AT-SPI bus; {"ok":false,"code":...} means the lookup/action itself failed.
- Install accessibility bindings: python3-pyatspi and at-spi2-related GIR packages for your distro.
- Confirm `python3 -c 'import pyatspi'` works in the same environment/user session.
- Ensure the desktop session exposes the accessibility bus (AT_SPI_BUS / accessibility enabled in the session settings).
- Re-list apps/windows and retry — the target's path may be stale after UI changes.
Example fix
// before (Debian/Ubuntu, missing bindings) # python3 exists but pyatspi doesn't -> AT-SPI action failed // after sudo apt install python3-pyatspi gir1.2-atspi-2.0
Defensive patterns
Strategy: try-catch
Validate before calling
import { execFileSync } from 'child_process';
function atspiReady() {
try {
execFileSync('python3', ['-c', 'import pyatspi'], { stdio: 'ignore', timeout: 10_000 });
return true;
} catch { return false; }
} Try / catch
try {
const out = await atspiResolve(name, target, extraArg);
} catch (e) {
if (e instanceof ExecError && e.message.startsWith('AT-SPI action failed')) {
if (/ModuleNotFoundError|ImportError/.test(e.message)) installAtspiBindings();
else if (/Traceback|No display|bus/i.test(e.message)) ensureDesktopSession();
else await refreshTargetsAndRetry(); // stale path — re-list and retry once
} else throw e;
} Prevention
- Install python3-pyatspi and the at-spi GIR bindings on the target machine.
- Enable accessibility in the desktop session so the AT-SPI bus is exposed.
- Re-resolve app/window paths right before acting; UIs change asynchronously.
- Run the plugin inside the logged-in desktop session's user/bus environment.
- Smoke-test `python3 -c 'import pyatspi'` in CI images.
When it happens
Trigger: Any AT-SPI-backed action (list_apps, tree resolution, invoke action) where: python3 is missing/broken, pyatspi/gi is not installed, the target application or path no longer exists (script prints its exception JSON or tracebacks to stderr), or stdout's last line is not the expected JSON object.
Common situations: Minimal Linux installs without python3-pyatspi / gir1.2-atspi-2.0; running outside a desktop session so no AT-SPI bus is available; app closed between listing and acting so the resolved path is stale; accessibility not enabled in the session.
Understand the failure class
Background: "not installed", "pip install", "required for": how missing-dependency errors surface across open-source libraries — this error's family across 34 libraries.
Related errors
- AT-SPI resolve 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/3b73acb31af8b870.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/linux.mjs:303
break
if len(p) > 12: continue
try:
for i in range(n.childCount):
c = n.getChildAtIndex(i)
if c: stack.append((c, p + [i]))
except Exception: pass
if found is None:
print(json.dumps({"ok": False, "code": "element_stale"}))
sys.exit(0)
try:
${pythonBody}
except Exception as e:
print(json.dumps({"ok": False, "code": str(e)}))`;
const argv = ["-c", script, name, JSON.stringify(target.path ?? [])];
if (extraArg != null) argv.push(String(extraArg));
const r = await run("python3", argv, { timeoutMs: 30_000 });
const out = tryJson((r.stdout.trim().split("\n").pop() ?? ""), null);
if (!out) throw new ExecError(`AT-SPI action failed: ${(r.stderr || r.stdout).slice(0, 250)}`, r);
return out;
}
// ---------- input helpers ----------
function clickButton(button, clicks) {
if (session === "x11") {
const args = ["click"];
if (clicks > 1) args.push("--repeat", String(clicks), "--delay", "80");
args.push(String(button));
return xdotool(args);
}
// ydotool click mask: down|up|count nibble (0xC0 = left click once, +1 per extra click;
// 0x04 bit selects right button, 0x02 middle).
const count = Math.max(1, Math.min(3, clicks));
const code = button === 3 ? 0xc0 + count + 0x04 : button === 2 ? 0xc0 + count + 0x02 : 0xc0 + count - 1;
return ydotool(["click", "0x" + code.toString(16)]);
}
View on GitHub (pinned to 73e0f67d83)