Troubleshooting and FAQ#
Answers to the problems users actually hit, grouped by topic. Each entry gives the short fix and links to the page that covers the subject in depth. If your problem is not here, check Environment variables and the CLI reference — many surprises turn out to be a default you can override.
Install and environment#
uv sync failed with “does not appear to be a Python project”.
You cloned the engine without its submodules, so the vendored packages
that pyproject.toml references are empty directories. Re-clone with
git clone --recurse-submodules, or fix an existing checkout with
git submodule update --init --recursive. See
Installation.
I ran a bare uv sync and my simulator/perception install disappeared.
uv sync is exact: it makes the environment match exactly what you ask
for, so a bare uv sync after uv sync --extra quickstart removes the
quickstart extras again. Always repeat the --extra flags you want, or
use uv sync --inexact … for additive installs. See
Installation.
pip downgraded numpy to 1.26 when I installed SAM3.
SAM3’s published metadata over-pins numpy==1.26. The engine’s
pyproject.toml carries a [tool.uv] override-dependencies entry that
relaxes it to numpy>=1.26, but plain pip does not read uv overrides and
will happily downgrade your numpy. Use uv sync for any extras set that
includes SAM3, or pin numpy back by hand after a pip install.
Rendering crashes on a headless machine.
Set MUJOCO_GL=egl on every sim command — MuJoCo’s default GL backend
needs a display. Spawned benchmark workers get this set for you, but
plain gap run --sim … does not. To spread workers across GPUs, set
GAP_MUJOCO_EGL_DEVICES=0,1,2 as well. See
Environments.
When do model weights download? gap skills check --download said
“declares no weights (no prefetch())”.
--download runs each bundle’s optional prefetch() function. The
perception bundles (sam3, grounding-dino) define one, so their
weights (~3.5 GB) fetch eagerly during the check. A bundle without a
prefetch() prints that message, and its weights download lazily — the
first call into the model runs HuggingFace from_pretrained, so the
first trial pays the fetch. Set HF_TOKEN before the first download:
the gated SAM3 weights sit in the default perception path. See
Installation.
CuRobo fails to build.
CuRobo JIT-compiles CUDA code at install time: CUDA_HOME must point at
a toolkit matching your torch build’s CUDA version, and pip installs need
--no-build-isolation. The supported recipe is
CUDA_HOME=/usr/local/cuda uv sync --extra grocery (or --extra curobo
inside the open-robot-skills checkout). The first planner call per
process also pays a JIT warmup of several seconds — that is normal. See
Skill catalog.
Can I bump the mujoco version?
Not without re-validating. mujoco is pinned ==3.6.0 because the
acceptance numbers were measured on it; even minor bumps move contact
physics enough to shift grasp success rates. If you change it, re-run the
acceptance benchmark before trusting results. See
Benchmarking.
Running graphs and sim#
What goes in gap run --sim?
A suite/task_index pair such as libero_object/0. Only the libero
sim family is wired into the CLI; other environments are reached through
the Python API or custom connectors. See
Execution and
Environments.
gap run can’t find a workflow in my gap generate output directory.
Generation writes each task to a task_NN subdirectory —
<out>/task_00/ for a single task. Point gap run at the task subdir,
not at the --out directory itself. See
Generation.
My script calls time.sleep() but the sim state never changes.
Wall-clock time does not advance the simulator — only stepping does. Use
the settle_steps input on gripper and motion tools instead of sleeping
(e.g. settle_steps=40 after opening the gripper).
except Exception doesn’t catch GuardLimitExceeded.
That is intentional: GuardLimitExceeded inherits from BaseException
so skill code cannot accidentally swallow a safety guard violation — the
workflow must terminate. Guard counters are process-global but reset
automatically at the start of every execute(); call reset_counters()
yourself only if you drive tools outside the executor.
Limits are configurable via GAP_MAX_PERCEPTION_CALLS,
GAP_MAX_PLANNING_CALLS, and GAP_MAX_SIM_STEPS. See
Executor.
My loop only runs once — the second visit to a node is skipped.
Within a single subgraph activation, a re-entered node is silently
skipped (it is already in completed_nodes). Express loops as subgraph
revisits — route back to the subgraph so it re-activates — not as cycles
inside one subgraph scope. Runaway loops are stopped by
GAP_ITERATION_CAP (default 10000). See
Patterns.
My run reached a failure end node and then raised PipelineError. Bug?
No — WorkflowExecutor.execute() raises PipelineError whenever the
workflow terminates at a failure end node. Only the high-level
gap.execute() entry point converts that into a result object you can
inspect. Use gap.execute() unless you specifically want the raw
executor. See API.
Validation passed but my tool node fails at runtime with an unknown tool.
Validation treats tool nodes that reference unregistered tools as
warnings, not errors, because connector tools are only known at run
time. A typo in a connector tool name therefore surfaces only when the
node executes. Read validation warnings, not just the error count. See
Workflow schema.
What do LIBERO seeds actually control?
Seeds index baked initial-state variations, not an RNG: the mapping is
(seed - 1) % n_inits, and seed=0 means unseeded. Seeds 1..50 are
deterministic scene variations you can replay exactly. See
Environments.
Generation and LLM#
gap generate returns a 404 / unknown-model error on OpenRouter.
OpenRouter model slugs are usually namespaced (e.g.
google/gemini-3.1-flash-lite-preview, anthropic/claude-sonnet-4.5). If
the bare default 404s for your account, pass --model or set llm.model
in your config YAML with the namespaced slug. Vertex serves Gemini models
only and additionally needs the [vertex] extra and ADC credentials. The
GAP_VLM_* environment variables only configure the runtime VLM
perception path, not codegen. See
LLM providers.
I regenerate with temperature > 0 but get the identical graph every time.
The codegen disk cache (active when GAP_LLM_CACHE_DIR or cache_dir:
is set) memoizes responses by prompt, so at nonzero temperature it
replays the single cached sample instead of drawing a new one. Set GAP_LLM_NO_CACHE=1 when you want diverse regenerations. See
Generation.
gap generate “succeeded” but the graph fails validation.
Generation runs an LLM fix loop over validation errors; when the loop
exhausts its retries, the residual errors are logged as a warning and
the pipeline still returns success. Always gate generated output with
gap run <task_dir> --validate-only before executing it. See
Generation.
Perception returns stale results when I re-run the same scene.
The perceiving-objects skill keeps a result cache at
<open-robot-skills>/.llm_cache/perceiving-objects, keyed on the image,
GAP_VLM_PROVIDER/GAP_VLM_MODEL, and an internal cache version. If a
repeated scene serves outdated detections, set GAP_PERCEPTION_CACHE=0
or wipe that directory. Note this is a different cache from the codegen
LLM cache (GAP_LLM_CACHE_DIR). See
Environment variables.
Molmo tools fail with a ToolError about a base URL.
Molmo has no hosted API — you must self-host it (e.g. vLLM serving
allenai/Molmo2-8B) and set GAP_MOLMO_BASE_URL (optionally
GAP_MOLMO_MODEL). The zero-GPU alternative is the gemini-er bundle.
See Tool catalog.
I set model: in an agent’s frontmatter but it had no effect.
AgentSpec frontmatter model: is parsed but never consumed. Per-role
models are configured only via composition.coordinator_model and
composition.subgraph_model in the generation config. See
Generation.
Checkpoints and traces#
A checkpoint reports passed=False but the robot clearly did the task.
LLM-generated perception-audit checkpoints (e.g. comparing a perceived
OBB to ground truth) can false-fail on a robot-frame vs world-frame
mismatch — the comparison is in the wrong frame, not the robot. Trust
physical checkpoints (target_held, target_in_basket-style checks on
world state) for the task verdict, and treat perception-audit failures as
triage signal. See Checkpoints.
Checkpoints silently did nothing on my run.
Checkpoint enforcement needs ground truth: when the connector exposes no
world_snapshot (all real robots, some custom connectors), enforcement
degrades to a single logged warning and every checkpoint is skipped.
Also mind the defaults: gap.execute() defaults to "warn" (failures
log but the run continues) while a hand-constructed WorkflowExecutor
defaults to "off". Pass checkpoints="raise" to make failures fatal.
See Checkpoints.
My run crashed and there is no dag_trace.json in the trace directory.
The trace file is flushed only when the workflow reaches an end node — a
hard crash (or kill) before that leaves per-node node_data and assets
on disk but no flushed trace. The partial artifacts are still useful for
forensics; the missing file just means the run never terminated cleanly.
See Traces.
What do gap trace-diff exit codes mean?
0: traces aligned and every matched node agrees. 1: traces aligned
but verdicts disagree somewhere. 2: zero nodes matched — the traces did
not align at all, usually because the two runs used different
workflow.json node names. trace-diff matches nodes by name, so it only
compares runs of the same workflow. See Traces.
Trace JSON shows placeholder summaries instead of my image arrays.
Arrays with ≥ 100 elements are summarized in dag_trace.json to keep it
readable; real pixel data lives in the trace’s assets/ directory. This
also means viz replay is best-effort — replayed tools may receive
placeholder strings where large arrays were summarized. See
Traces.
Where is my run video? gap viz doesn’t show it.
gap run --record-video (which requires both --sim and tracing)
writes run_video.mp4 into the trace directory, but the viz UI renders
only PNG/JPEG assets — open the mp4 from disk with your video player. See
Traces.
Skills and registries#
I passed --skills and my other registries vanished.
--skills and $GAP_SKILLS_PATH are full overrides, not additive:
configured, project, and auto-discovered registries are all suppressed
(gap registry list prints a note when this happens). An invalid entry
in $GAP_SKILLS_PATH is a hard error, never a silent fallthrough. To
add a registry instead, use gap registry add. See
Registries.
My registry isn’t auto-discovered.
Auto-discovery only picks up a sibling directory named exactly
open-robot-skills with populated bundle roots (or running inside such a
checkout). A freshly gap registry init-ed registry is empty, fails that
test, and must be added explicitly with gap registry add or pointed at
via --skills. Note that gap registry add prepends — the new
registry gets highest precedence and shadows same-named bundles
everywhere else. See Registries.
I changed registry precedence but the old bundle code still runs. Bundle modules are imported once per process into a process-global namespace, so precedence changes require a fresh process. Cross-registry name shadowing is first-wins with a loud warning; a duplicate bundle name within one registry is a hard error. See Registries.
gap check exits 0 even though half my bundles are not ready.
By design: missing a GPU on a laptop is not an error. Pass --strict to
make any not-ready bundle (or zero resolved registries) fail the command
— use that in CI. See Registries.
gap skills test skips my GPU tests / my pytest flags are ignored.
The default registry pytest config deselects gpu and llm markers, so
GPU smokes need an explicit gap skills test <bundle> -- -m gpu. Flag
ordering matters: GaP’s own flags go before the --, pytest arguments
after it. The command also runs the current interpreter’s pytest with
cwd at the registry — registries with their own venv should run
uv run pytest there instead. See
Testing bundles.
pip install open-robot-skills gave me no skills.
The wheel intentionally contains no code — only dependency metadata.
Bundles are discovered from a checkout on disk, so you need the git
clone next to the engine (or registered via gap registry add). See
Registries.
CuRobo’s solve_ik raises PlanningFailed saying it’s not supported
on v0.8.
The curobo bundle straddles an API split: solve_ik and
batch_grasp_feasibility are built on the v0.7 IKSolver API that
curobo v0.8 removed, while plan_grasp_motion requires v0.8. On a
v0.8-only install, plan via plan_to_grasp_poses or plan_grasp_motion
instead. The SHA pinned in the bundle’s extra is the supported install.
See Tool catalog.
Does gap.testing have a fake connector?
No. gap.testing exports exactly four names: FakeContext,
ToolCallRecord, make_test_observation, and assert_graph_valid.
FakeContext is fail-loud: unscripted tools and exhausted response
sequences both raise ToolError, so script every tool your test touches.
See Testing bundles.
Benchmarks and policies#
Trials that were visibly succeeding got marked as failures.
Check trials.task_timeout_secs — when it is too low, the watchdog
hard-kills trials that were still completing. The grocery acceptance
configs use 900 s because passing flat-box trials measured 391–537 s;
an earlier 600 s setting caused false failures. Budget for your slowest
passing trial, not the average. See
Benchmark config.
The gate failed even though my success rate is above threshold.
--gate fails on any errored cell and on a run where zero trials ran,
not just on low success rate — an infrastructure error is treated as a
red result. Fix or exclude the erroring cell; on resume, error cells always
re-run while completed cells are reused from the latest stamped
directory. See Benchmarking.
The benchmark deleted files from my output directory.
launch() cleans the artifact directory at start, so every cell must
write to a unique directory — never point two cells (or a cell and your
own files) at the same path. Generated workflows are written to a __wf
sibling for exactly this reason. See
Benchmarking.
My policy benchmark can’t reach the policy server.
The harness never owns the server lifecycle for url: policies — start
it yourself first, e.g. gap policy serve pi05-libero --port 9100. Note
gap policy serve picks a random port unless you pass --port, and
its 900 s startup timeout exists because the first run downloads
checkpoints (GAP_OPENPI_DIR must point at your openpi/MolmoAct
checkout). Throttle num_workers when sharing one inference endpoint —
a single websocket serializes the workers anyway. See
Policies.
My benchmark YAML uses base: inheritance and is rejected.
base: inheritance has been removed everywhere (benchmark YAMLs and
generation configs alike) — it is a hard error, not a deprecation.
Benchmark configs must be standalone files. See
Benchmark config.
Real robots#
robot.go_home does nothing on my real robot.
That is the safety gate, not a bug: go_home is a no-op (with a warning
log) whenever the connector config says the robot is real, because a
blind joint-space home through an unknown scene is dangerous. Real
connectors also never receive sim.* tools. Plan explicit, validated
motions instead. Read Safety before running
anything.
Why don’t checkpoints run on my real robot?
Checkpoints need privileged ground truth (world_snapshot), which only
simulators provide. On real hardware, enforcement silently degrades to a
single warning. Use perception-based verification in the graph itself
(e.g. a VLM yes/no check) instead. See
Checkpoints.
My UR camera poses look identical to the wrist pose.
The hand-eye calibration file is missing or not found: without
GAP_UR_ZED_CALIB, the connector degrades silently to an identity
extrinsic (warning log only), so every camera pose equals the wrist pose
and your perception is offset with no error raised. Set the env var to a
valid calibration before trusting any 3D output. See
Connectors.
import pyzed fails after installing the [real] extra.
pyzed is not on PyPI — it ships with the ZED SDK and must be installed
manually from Stereolabs. The [real] extra covers the rest (e.g.
rtde_receive for UR). See Connectors.
How do I tell whether the Franka stack is hung, stuck, or dead?
Read the heartbeat fields: obs_age growing means the realtime side is
hung; joint_max_diff ≈ 0 with fresh observations means the robot is
physically stuck; republish_hz → 0 means the GaP-side republisher died;
obs_stale flips after 2 s of silence. See
Connectors.