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

  1. Read the truncated stderr in the message to identify the python failure, then fix that root cause.
  2. Install python3-pyatspi and verify import pyatspi works.
  3. Re-fetch a fresh element path via get_app_state before resolving — stale paths from old snapshots fail.
  4. 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

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


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)