Hmbown/CodeWhale · error · ExecError

AT-SPI walk failed — is python3-pyatspi installed and the…

Error message

AT-SPI walk failed — is python3-pyatspi installed and the desktop running an accessibility bus (AT_SPI_BUS)?

What it means

get_app_state reads UI state by running a python3 pyatspi script over the AT-SPI accessibility tree. The library throws this when the python helper produced no parseable JSON — typically because python3-pyatspi is not installed or the session has no AT-SPI accessibility bus running.

Solutions

  1. Install the accessibility stack: sudo apt install python3-pyatspi (or distro equivalent).
  2. Enable accessibility: ensure the AT-SPI bus is running (e.g. org.a11y.Bus enabled; on GNOME set org.gnome.desktop.interface toolkit-accessibility true).
  3. Verify manually: python3 -c 'import pyatspi' should succeed, and check $AT_SPI_BUS or the session's a11y bus.
  4. Re-run get_app_state after confirming a desktop session is active (not a bare headless shell).

Example fix

// before
const state = await backend.get_app_state({ app_ref: { name: 'Firefox' } }); // throws without pyatspi
// after
const { code } = await run('python3', ['-c', 'import pyatspi']);
if (code !== 0) throw new Error('install python3-pyatspi and enable the a11y bus first');
const state = await backend.get_app_state({ app_ref: { name: 'Firefox' } });
Defensive patterns

Strategy: fallback

Validate before calling

const ok = spawnSync('python3', ['-c', 'import pyatspi']).status === 0;
if (!ok) throw new Error('python3-pyatspi missing / a11y bus unavailable');

Try / catch

try {
  return await backend.get_app_state({ app_ref });
} catch (e) {
  if (String(e.message).includes('AT-SPI walk failed')) return null; // degrade gracefully
  throw e;
}

Prevention

When it happens

Trigger: python3 missing or python3-pyatspi not installed; desktop without AT_SPI_BUS (no accessibility enabled in GNOME/KDE settings); the 45s timeout expiring so no output is emitted; the script crashing and its stderr being the last line, which fails JSON parsing.

Common situations: Minimal/tiling WM setups (sway, i3, hyprland) where accessibility support is not installed; headless CI machines; containers without the at-spi dbus; users who disabled assistive technologies.

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


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/e409be91e21649af. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/linux.mjs:492

          }
        } catch { /* no session/tools — the launch itself is still fine */ }
      }
      spawnDetached(target, urlArg ? [urlArg] : [], "", true);
      await new Promise((r) => setTimeout(r, 500));
      let focusRestored = false;
      if (prevWindow) {
        try {
          focusRestored = (await run("xdotool", ["windowactivate", prevWindow], { timeoutMs: 3_000 })).code === 0;
        } catch { /* best-effort */ }
      }
      return { launched: true, name: target, url: urlArg ?? null, activate: activate === true, ...(activate === true ? {} : { focus_restored: focusRestored }) };
    },
    get_app_state: async ({ app_ref, window_id } = {}) => {
      const name = appName(app_ref);
      if (window_id !== undefined) throw Object.assign(new ExecError("Linux app-state window_id selection is unavailable"), { code: "unsupported_selector" });
      const t = await run("python3", ["-c", PYATSPI_WALK, name, "10", "500"], { timeoutMs: 45_000 }).then((r) =>
        tryJson((r.stdout.trim().split("\n").pop() ?? ""), null));
      if (!t) throw new ExecError("AT-SPI walk failed — is python3-pyatspi installed and the desktop running an accessibility bus (AT_SPI_BUS)?");
      if (!t.found) throw new ExecError("application not found or name is ambiguous in the AT-SPI tree — use a unique exact app_ref.name");
      return t;
    },
    screenshot: async (args = {}) => {
      rejectWindowSelectors(args);
      const { display, region, path: outPath } = args;
      if (outPath != null) outputPath(outPath);
      await probeSession();
      const dir = recordingsDir();
      fs.mkdirSync(dir, { recursive: true });
      const file = outPath || path.join(dir, `shot-${new Date().toISOString().replace(/[:.]/g, "-")}-${crypto.randomBytes(3).toString("hex")}.png`);
      await takeShot(file, region);
      const dims = pngSize(file);
      lastRaster = {
        file,
        bytes: fs.statSync(file).size,
        // Region rasters describe the region; full shots get geometry from the
        // PNG itself (Linux shots are always scale 1: points == pixels).

View on GitHub (pinned to 73e0f67d83)