Skip to content

feat: in-house robot asset manager#2505

Draft
TomCC7 wants to merge 14 commits into
mainfrom
cc/chore/robot-description
Draft

feat: in-house robot asset manager#2505
TomCC7 wants to merge 14 commits into
mainfrom
cc/chore/robot-description

Conversation

@TomCC7

@TomCC7 TomCC7 commented Jun 16, 2026

Copy link
Copy Markdown
Member

Problem

DimOS currently keeps several manipulation robot model bundles in Git LFS. That copies upstream robot description assets into this repo, makes updates manual, and makes CI brittle on runners that do not have those LFS objects available.

robot_descriptions.py solves a related problem, but it is not a good fit for DimOS as the main asset layer:

  • it is a curated registry of pre-registered description modules, not a general loader for arbitrary upstream robot description repositories;
  • Piper and A-750 are not covered by the registry, and OpenArm has DimOS-local model changes;
  • DimOS needs robot-model-first catalog declarations, ROS package-root mappings, Xacro args, artifact roles, and Drake-specific rendering behavior that match the existing planning/control stack;
  • relying on an external registry would make adding or adjusting robot sources depend on upstream registry changes instead of a local DimOS declaration.

Because of those limits, this PR adds a small in-house Robot Asset Manager instead of replacing LFS with robot_descriptions.py.

Solution

Add a Git-backed robot asset layer for manipulation robot descriptions:

  • add a GitAssetCache that clones and updates upstream source repos under ~/.cache/dimos/robot_assets;
  • use a fresh-when-safe cache policy: clone missing sources, update clean checkouts, warn and keep cached content on update failure, and preserve dirty local checkouts;
  • add typed robot asset declarations for xArm6, xArm7, Piper, and A-750;
  • expose lazy RobotAssetPath / RobotAssetPackagePath objects so catalog imports do not trigger network or Git work;
  • migrate xArm, Piper, and A-750 runtime model paths and package roots to the new declarations where upstream sources are suitable;
  • keep assets on LFS when the upstream source does not publish the needed variant, such as generated FK-only/no-gripper models and OpenArm's locally modified models;
  • move generic URDF rendering/package URI handling into the robot asset module while keeping Drake-specific cleanup in Drake utilities.

The asset code is kept self-contained under dimos/robot/assets so it can be extracted into a standalone package later.

How to Test

Manual check for a human reviewer:

uv run --extra manipulation dimos run xarm7-planner-coordinator

Open the Drake Meshcat URL printed in the logs. The xArm7 model should load and visualize correctly. The first run may clone xarm_ros2 into ~/.cache/dimos/robot_assets; derived URDFs should be written under ~/.cache/dimos/robot_assets/derived.

For daemon mode:

uv run --extra manipulation dimos run xarm7-planner-coordinator --daemon
uv run --extra manipulation dimos log -f

Contributor License Agreement

  • I have read and approved the CLA.

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.69960% with 42 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
dimos/robot/assets/manager.py 88.31% 16 Missing and 2 partials ⚠️
dimos/robot/assets/git_cache.py 86.95% 10 Missing and 2 partials ⚠️
dimos/robot/assets/processing.py 86.88% 5 Missing and 3 partials ⚠️
dimos/control/examples/cartesian_ik_jogger.py 0.00% 2 Missing ⚠️
dimos/robot/model_parser.py 50.00% 1 Missing ⚠️
dimos/utils/ament_prefix.py 50.00% 1 Missing ⚠️
@@            Coverage Diff             @@
##             main    #2505      +/-   ##
==========================================
+ Coverage   70.09%   70.12%   +0.02%     
==========================================
  Files         838      846       +8     
  Lines       74337    74797     +460     
  Branches     6667     6742      +75     
==========================================
+ Hits        52109    52449     +340     
- Misses      20553    20643      +90     
- Partials     1675     1705      +30     
Flag Coverage Δ
OS-ubuntu-24.04-arm 64.03% <89.70%> (+0.19%) ⬆️
OS-ubuntu-latest 64.86% <89.70%> (+0.19%) ⬆️
Py-3.10 64.85% <89.70%> (+0.19%) ⬆️
Py-3.11 64.85% <89.70%> (+0.19%) ⬆️
Py-3.12 64.86% <89.70%> (+0.19%) ⬆️
Py-3.13 64.85% <89.70%> (+0.19%) ⬆️
Py-3.14 64.86% <89.70%> (+0.19%) ⬆️
Py-3.14t 64.85% <89.70%> (+0.18%) ⬆️
SelfHosted-Large 30.40% <43.25%> (+0.05%) ⬆️
SelfHosted-Linux 38.40% <60.00%> (+0.19%) ⬆️
SelfHosted-macOS 37.12% <54.05%> (+0.18%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
dimos/manipulation/planning/utils/mesh_utils.py 58.09% <100.00%> (-0.50%) ⬇️
...mos/manipulation/planning/utils/test_mesh_utils.py 100.00% <100.00%> (ø)
dimos/manipulation/planning/world/drake_world.py 50.63% <100.00%> (ø)
dimos/manipulation/test_manipulation_module.py 97.20% <100.00%> (-1.42%) ⬇️
dimos/robot/assets/declarations.py 100.00% <100.00%> (ø)
dimos/robot/assets/test_manager.py 100.00% <100.00%> (ø)
dimos/robot/assets/test_processing.py 100.00% <100.00%> (ø)
dimos/robot/catalog/a750.py 100.00% <100.00%> (ø)
dimos/robot/catalog/piper.py 100.00% <100.00%> (ø)
dimos/robot/catalog/ufactory.py 100.00% <100.00%> (ø)
... and 9 more

... and 7 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@TomCC7 TomCC7 changed the title WIP: in-house robot description loader feat: in-house robot description loader Jun 16, 2026
@TomCC7 TomCC7 marked this pull request as ready for review June 16, 2026 20:08
@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a Git-backed robot asset manager that clones upstream robot description repositories into ~/.cache/dimos/robot_assets on demand, replacing the previous Git LFS bundles for xArm, Piper, and A-750. A lazy RobotAssetPath/RobotAssetPackagePath adapter layer ensures catalog imports do not trigger network work at import time.

  • Adds GitAssetCache with a fresh-when-safe clone/update policy (clone on miss, update clean checkouts, warn-and-keep on failure), plus RobotAssetManager / lazy path adapters, URDF rendering pipeline, and Drake-specific cleanup.
  • Migrates xArm6/7, Piper, and A-750 runtime model paths to the new declarations; preserves LFS for FK-only/no-gripper models and OpenArm's locally-modified assets.
  • Splits the old monolithic mesh_utils.py xacro/package-URI logic into a generic render_urdf step (in processing.py) and a Drake-specific post-processing step (in mesh_utils.py), reducing duplication.

Confidence Score: 4/5

Safe to merge for its primary use case; one gap in the cache-update guard can silently discard local commits made to cached checkouts.

The core asset management pipeline is well-structured and the lazy path adapters, xacro rendering, and Drake cleanup layers are clean. The one concrete defect is in _is_dirty: it checks only the working tree for uncommitted changes and does not detect local commits ahead of origin. Because _checkout_ref uses git checkout -B + git reset --hard, any committed-but-not-pushed work in a cached checkout is silently discarded on the next update run. For the standard automated workflow this is harmless, but the mismatch with the documented preserve-dirty guarantee warrants a fix.

dimos/robot/assets/git_cache.py — the _is_dirty / _checkout_ref interaction.

Important Files Changed

Filename Overview
dimos/robot/assets/git_cache.py Core cache implementation; contains the incomplete dirty-check that can silently discard local commits on update.
dimos/robot/assets/manager.py RobotAssetManager and lazy path adapters; clean implementation with correct use of object.setattr to avoid triggering Path's new machinery.
dimos/robot/assets/declarations.py Asset declarations for xarm6/7, piper, a750; piper's package_roots relies on checkout directory name matching a URDF path component, documented but fragile to URL changes.
dimos/robot/assets/processing.py Universal URDF render/cache pipeline; mtime-based cache key is fast but misses changes to xacro includes.
dimos/manipulation/planning/utils/mesh_utils.py Refactored to delegate xacro/package-URI work to render_urdf; Drake-specific cache key simplified correctly.
dimos/robot/catalog/ufactory.py Migrated xArm model paths to RobotAssetPath; xacro args merged correctly with catalog-level defaults.
dimos/robot/catalog/piper.py Migrated piper model paths; FK model changed from MJCF to upstream URDF artifact.
dimos/utils/ament_prefix.py Correctly updated to use os.fspath() so lazy RobotAssetPackagePath instances trigger resolution at the right time.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Catalog as catalog/ufactory.py
    participant RAP as RobotAssetPath (lazy)
    participant RAM as RobotAssetManager
    participant GAC as GitAssetCache
    participant FS as ~/.cache/dimos/robot_assets

    Catalog->>RAP: RobotAssetPath("xarm7", "urdf") -- no I/O
    Note over RAP: Stores model+role, manager ref<br/>resolved_cache = None

    Catalog->>RAM: robot_asset_package_paths("xarm7") -- no I/O
    RAM-->>Catalog: "{xarm_description: RobotAssetPackagePath(...)}"

    Note over Catalog: First use of model_path or package_paths triggers resolution

    RAP->>RAM: resolve_artifact("xarm7", "urdf")
    RAM->>GAC: resolve(repo_url, "humble")
    GAC->>FS: FileLock acquire
    alt Cache miss
        GAC->>FS: "clone xarm_ros2 to sources/{hash}/xarm_ros2/"
    else Cache hit, clean
        GAC->>FS: fetch + checkout -B humble origin/humble
    else Cache hit, dirty
        GAC-->>RAM: warn, return cached path
    end
    GAC-->>RAM: GitAssetCheckout(path)
    RAM-->>RAP: checkout/xarm_description/urdf/xarm_device.urdf.xacro
    RAP->>RAP: cache resolved Path
    RAM->>FS: "render_urdf to derived/rendered_urdfs/{hash}/*.urdf"
    RAM->>FS: "Drake cleanup to derived/drake_urdfs/{hash}/*.urdf"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Catalog as catalog/ufactory.py
    participant RAP as RobotAssetPath (lazy)
    participant RAM as RobotAssetManager
    participant GAC as GitAssetCache
    participant FS as ~/.cache/dimos/robot_assets

    Catalog->>RAP: RobotAssetPath("xarm7", "urdf") -- no I/O
    Note over RAP: Stores model+role, manager ref<br/>resolved_cache = None

    Catalog->>RAM: robot_asset_package_paths("xarm7") -- no I/O
    RAM-->>Catalog: "{xarm_description: RobotAssetPackagePath(...)}"

    Note over Catalog: First use of model_path or package_paths triggers resolution

    RAP->>RAM: resolve_artifact("xarm7", "urdf")
    RAM->>GAC: resolve(repo_url, "humble")
    GAC->>FS: FileLock acquire
    alt Cache miss
        GAC->>FS: "clone xarm_ros2 to sources/{hash}/xarm_ros2/"
    else Cache hit, clean
        GAC->>FS: fetch + checkout -B humble origin/humble
    else Cache hit, dirty
        GAC-->>RAM: warn, return cached path
    end
    GAC-->>RAM: GitAssetCheckout(path)
    RAM-->>RAP: checkout/xarm_description/urdf/xarm_device.urdf.xacro
    RAP->>RAP: cache resolved Path
    RAM->>FS: "render_urdf to derived/rendered_urdfs/{hash}/*.urdf"
    RAM->>FS: "Drake cleanup to derived/drake_urdfs/{hash}/*.urdf"
Loading

Reviews (2): Last reviewed commit: "spec: remove" | Re-trigger Greptile

Comment on lines +118 to +136
def _generate_render_key(
urdf_path: Path,
package_paths: Mapping[str, Path],
xacro_args: Mapping[str, str],
package_uri_mode: PackageUriMode,
) -> str:
processing_version = "urdf-render-v1"
mtime = urdf_path.stat().st_mtime if urdf_path.exists() else 0
key_data = repr(
(
processing_version,
str(urdf_path),
mtime,
sorted((name, str(path)) for name, path in package_paths.items()),
sorted(xacro_args.items()),
package_uri_mode,
)
)
return hashlib.sha256(key_data.encode()).hexdigest()[:16]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale render cache when only included xacro files change

The cache key is built from the top-level xacro file's mtime only. When _refresh_cached runs git reset --hard origin/{ref}, git only updates the mtime of changed files. If an upstream commit modifies an included xacro (e.g., a joint-limit macro or mesh include) without touching the entry-point file, the cache key stays identical and the old rendered URDF is silently reused — potentially loading the robot with outdated joint parameters or mesh paths. To fix this robustly the key would need to fold in the mtime (or content hash) of each file reachable from xacro_path, or at least the mtimes of every file under the package-root directories supplied via package_paths.

Comment thread dimos/robot/assets/processing.py Outdated
Comment on lines +64 to +66
cache_path = _RENDERED_URDF_CACHE_ROOT / cache_key / source_path.stem
cache_path.mkdir(parents=True, exist_ok=True)
rendered_urdf = cache_path / f"{source_path.stem}.urdf"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Double .urdf extension for .urdf.xacro sources

Path("xarm_device.urdf.xacro").stem is "xarm_device.urdf", so the rendered file ends up as xarm_device.urdf.urdf inside the cache. This also propagates to the Drake-specific cache in mesh_utils.py (rendered_urdf.stem = "xarm_device.urdf", final file = "xarm_device.urdf.urdf"). The file is still read correctly, but the double extension makes cache directories harder to inspect and could confuse tools that infer format from the extension. Consider stripping all non-final suffixes when building the stem, e.g. source_path.stem.removesuffix(".urdf").

TomCC7 and others added 2 commits June 16, 2026 13:18
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@TomCC7 TomCC7 changed the title feat: in-house robot description loader feat: in-house robot asset manager Jun 16, 2026
Comment on lines +153 to +165
@staticmethod
def _is_dirty(repo: Repo) -> bool:
return repo.is_dirty(untracked_files=True)

@staticmethod
def _checkout_ref(repo: Repo, ref: str) -> None:
"""Check out a branch, tag, or commit and update clean worktrees safely."""
remote_ref = f"origin/{ref}"
remote_refs = {str(r) for r in repo.remotes.origin.refs}

if remote_ref in remote_refs:
repo.git.checkout("-B", ref, remote_ref)
repo.git.reset("--hard", remote_ref)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 _is_dirty misses local commits, causing silent data loss on update

_is_dirty returns True only when the working tree has uncommitted modifications or untracked files. It does not detect local commits that have been made on the branch but not yet pushed. As a result, when a cached checkout has clean working-tree state but one or more local commits on the tracked branch, the guard passes and _checkout_ref immediately runs git checkout -B ref origin/ref + git reset --hard origin/ref. These two commands forcibly move the local branch pointer back to origin/ref, making those local commits unreachable without going through the reflog — and also removing any files introduced by those commits from the working tree.

This contradicts the class docstring's policy: "if the cached repository has local changes, warn and skip updates." Local commits are local changes in the meaningful sense. A developer who tests a model tweak by committing it inside ~/.cache/dimos/robot_assets/ would lose that work silently on the next DimOS run.

To also protect local commits, _is_dirty should additionally check whether the local branch is ahead of its remote tracking branch.

@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Jun 16, 2026
@TomCC7 TomCC7 marked this pull request as draft June 16, 2026 22:10
@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant