Skip to content

WIP: feat: blueprint namespaces#2725

Draft
paul-nechifor wants to merge 1 commit into
mainfrom
paul/feat/namespaces
Draft

WIP: feat: blueprint namespaces#2725
paul-nechifor wants to merge 1 commit into
mainfrom
paul/feat/namespaces

Conversation

@paul-nechifor

Copy link
Copy Markdown
Contributor

Problem

Closes DIM-XXX

Solution

How to Test

Contributor License Agreement

  • I have read and approved the CLA.

@mintlify

mintlify Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
dimensional 🟢 Ready View Preview Jul 4, 2026, 1:35 AM

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds namespaces for running multiple blueprint instances side by side. The main changes are:

  • Instance names for modules, config keys, RPC names, and coordinator state.
  • Namespace helpers for streams, refs, transports, TF prefixes, and exposed shared streams.
  • Updated local and remote module discovery for instance-specific RPC names.
  • A new multi-Go2 blueprint and usage documentation.

Confidence Score: 4/5

The namespace flow needs fixes for duplicate local lookup and empty multi-Go2 configuration before merging.

  • Local porcelain class-name lookup can return the wrong namespaced instance.
  • The new multi-Go2 blueprint can crash during import when ROBOT_IPS is empty.
  • The core namespace wiring appears covered for the standard fleet paths.

dimos/porcelain/local_module_source.py; dimos/robot/unitree/go2/blueprints/basic/unitree_go2_multi.py

Important Files Changed

Filename Overview
dimos/core/coordination/blueprints.py Adds instance naming, namespace composition, config-key escaping, and stream/transport remapping by instance name.
dimos/core/coordination/module_coordinator.py Moves coordinator module, ref, transport, restart, unload, and listing state from class keys to instance keys.
dimos/core/coordination/worker_manager_python.py Passes instance names into RPC clients and applies escaped instance config keys during deployment.
dimos/core/module.py Adds instance_name to module config and uses it as the optional RPC serving name.
dimos/core/rpc_client.py Adds remote_name support and preserves it across pickling for instance-specific RPC proxies.
dimos/porcelain/local_module_source.py Allows lookup by instance name, but class-name fallback is ambiguous with duplicate deployed classes.
dimos/porcelain/remote_module_source.py Keys remote descriptors by instance RPC name when available while keeping class-name fallback.
dimos/robot/cli/dimos.py Updates help and option parsing for escaped namespaced config keys.
dimos/robot/unitree/go2/blueprints/basic/unitree_go2_multi.py Adds a multi-robot Go2 blueprint driven by ROBOT_IPS, but empty input crashes during import.
docs/usage/blueprints.md Documents namespace behavior, exposed streams, config addressing, and limitations.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  BP[Blueprint atoms] --> NS[namespace prefix]
  NS --> IN[instance name]
  NS --> ST[stream remappings]
  NS --> TF[frame_id_prefix]
  IN --> CFG[escaped config key]
  IN --> RPC[RPC topic prefix]
  ST --> COORD[ModuleCoordinator wiring]
  RPC --> LOCAL[Local/remote module lookup]
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"}}}%%
flowchart TD
  BP[Blueprint atoms] --> NS[namespace prefix]
  NS --> IN[instance name]
  NS --> ST[stream remappings]
  NS --> TF[frame_id_prefix]
  IN --> CFG[escaped config key]
  IN --> RPC[RPC topic prefix]
  ST --> COORD[ModuleCoordinator wiring]
  RPC --> LOCAL[Local/remote module lookup]
Loading

Reviews (1): Last reviewed commit: "feat: blueprint namespaces" | Re-trigger Greptile

Comment thread dimos/porcelain/local_module_source.py Outdated
if cls.__name__ == name:
for instance_key, proxy in self._coordinator._deployed_modules.items():
cls = self._coordinator._instance_classes[instance_key]
if name in (instance_key, cls.__name__):

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 Class Lookup Picks Arbitrary Instance

When two namespaced instances share a class, get_module("NamedModule") matches both entries through cls.__name__ and returns the first match in coordinator dict order. Local porcelain callers can receive the wrong robot's proxy instead of the same ambiguity error the coordinator raises for class-based lookup.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +33 to +35

unitree_go2_multi = autoconnect(
*[namespace(f"robot{i}", GO2Connection.blueprint(ip=ip)) for i, ip in enumerate(_ips)],

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 Empty Fleet Crashes Import

When ROBOT_IPS is unset or empty, _ips is [] and the module still evaluates autoconnect(*[]) during blueprint import. That reaches reduce(...) with no blueprints and crashes before the CLI can report a clear configuration error for the new unitree-go2-multi blueprint.

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
2669 2 2667 72
View the top 1 failed test(s) by shortest run time
dimos.perception.test_image_embedding.TestImageEmbedding::test_clip_embedding_initialization
Stack Traces | 25.5s run time
request = <SubRequest 'monitor_threads' for <Function test_clip_embedding_initialization>>

    @pytest.fixture(autouse=True)
    def monitor_threads(request):
        # Capture threads before test runs
        test_name = request.node.nodeid
        with _seen_threads_lock:
            _before_test_threads[test_name] = {
                t.ident for t in threading.enumerate() if t.ident is not None
            }
    
        yield
    
        with _seen_threads_lock:
            before = _before_test_threads.get(test_name, set())
    
        # Threads intentionally left running for the whole process and cleaned up on
        # exit, so they don't count as per-test leaks.
        expected_persistent_thread_prefixes = [
            "Dask-Offload",
            # HuggingFace safetensors conversion thread - no user cleanup API
            # https://github..../transformers/issues/29513
            "Thread-auto_conversion",
        ]
    
        def live_new_threads():
            # Threads created during this test that are still running. A thread that
            # has already stopped is not a leak -- it's done, just not yet reaped
            # from threading's registry -- so we key on is_alive(), not presence.
            result = []
            for t in threading.enumerate():
                if t.ident is None or t.ident in before or t.name == "MainThread":
                    continue
                if any(t.name.startswith(prefix) for prefix in expected_persistent_thread_prefixes):
                    continue
                if t.is_alive():
                    result.append(t)
            return result
    
        # Some C extensions tear their callback threads down asynchronously, so a
        # thread can stay alive briefly after the test cleaned up its owner (notably
        # zenoh's pyo3-closure threads, freed shortly after the session is closed).
        # A single snapshot races that teardown and flags a thread that is about to
        # exit. Give new threads a grace period to drain; only ones that stay alive
        # are real leaks (a genuinely leaked thread never exits).
        deadline = time.monotonic() + 5.0
        leaked = live_new_threads()
        while leaked and time.monotonic() < deadline:
            time.sleep(0.02)
            leaked = live_new_threads()
    
        if not leaked:
            return
    
        with _seen_threads_lock:
            # Report each leaked thread only once across the session.
            truly_new = [t for t in leaked if t.ident not in _seen_threads]
            for t in leaked:
                _seen_threads.add(t.ident)
    
        if not truly_new:
            return
    
        thread_names = [t.name for t in truly_new]
    
>       pytest.fail(
            f"Non-closed threads created during this test. Thread names: {thread_names}. "
            "Please look at the first test that fails and fix that."
        )
E       Failed: Non-closed threads created during this test. Thread names: ['Thread-248']. Please look at the first test that fails and fix that.

before     = {123815862372032, 123815870764736, 123815887550144, 123815895942848, 123815912728256, 123815921120960, ...}
deadline   = 5598474.048626508
expected_persistent_thread_prefixes = ['Dask-Offload', 'Thread-auto_conversion']
leaked     = [<TMonitor(Thread-248, started daemon 123815845586624)>]
live_new_threads = <function monitor_threads.<locals>.live_new_threads at 0x709c83c4c7c0>
request    = <SubRequest 'monitor_threads' for <Function test_clip_embedding_initialization>>
t          = <TMonitor(Thread-248, started daemon 123815845586624)>
test_name  = 'dimos/perception/test_image_embedding.py::TestImageEmbedding::test_clip_embedding_initialization'
thread_names = ['Thread-248']
truly_new  = [<TMonitor(Thread-248, started daemon 123815845586624)>]

dimos/conftest.py:273: Failed
View the full list of 1 ❄️ flaky test(s)
dimos.e2e_tests.test_dimsim_spatial_memory::test_go_to_the_bed

Flake rate in main: 20.79% (Passed 80 times, Failed 21 times)

Stack Traces | 567s run time
lcm_spy = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x73d72b61c620>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x73d729b09440>
human_input = <function human_input.<locals>.send_human_input at 0x73d729b0b100>
dim_sim = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x73d72b5fc200>
explore_house = <function explore_house.<locals>.explore at 0x73d729b0b600>

    @pytest.mark.self_hosted_large
    def test_go_to_the_bed(lcm_spy, start_blueprint, human_input, dim_sim, explore_house) -> None:
        start_blueprint(
            "run",
            "unitree-go2-agentic",
            simulator="dimsim",
        )
        lcm_spy.save_topic(".../McpClient/on_system_modules/res")
        lcm_spy.wait_for_saved_topic(".../McpClient/on_system_modules/res", timeout=1200.0)
    
        explore_house()
    
        human_input("go to the bed")
    
>       lcm_spy.wait_until_odom_position(-3.567, -1.332, threshold=2, timeout=180)

dim_sim    = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x73d72b5fc200>
explore_house = <function explore_house.<locals>.explore at 0x73d729b0b600>
human_input = <function human_input.<locals>.send_human_input at 0x73d729b0b100>
lcm_spy    = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x73d72b61c620>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x73d729b09440>

dimos/e2e_tests/test_dimsim_spatial_memory.py:32: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/e2e_tests/lcm_spy.py:167: in wait_until_odom_position
    self.wait_for_message_result(
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x73d729b0b7e0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x73d72b61c620>
        threshold  = 2
        timeout    = 180
        x          = -3.567
        y          = -1.332
dimos/e2e_tests/lcm_spy.py:153: in wait_for_message_result
    wait_until(
        event      = <threading.Event at 0x73d72abb24e0: unset>
        fail_message = 'Failed to get to position x=-3.567, y=-1.332'
        listener   = <function LcmSpy.wait_for_message_result.<locals>.listener at 0x73d729b0b6a0>
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x73d729b0b7e0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x73d72b61c620>
        timeout    = 180
        topic      = '/odom#geometry_msgs.PoseStamped'
        type       = <class 'dimos.msgs.geometry_msgs.PoseStamped.PoseStamped'>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

predicate = <bound method Event.is_set of <threading.Event at 0x73d72abb24e0: unset>>

    def wait_until(
        predicate: Callable[[], bool],
        *,
        timeout: float,
        interval: float = 0.1,
        message: str | None = None,
    ) -> None:
        """Poll ``predicate`` until it returns truthy or ``timeout`` elapses."""
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            if predicate():
                return
            time.sleep(interval)
>       raise TimeoutError(message or f"Timed out after {timeout}s waiting for condition")
E       TimeoutError: Failed to get to position x=-3.567, y=-1.332

deadline   = 3236508.836507859
interval   = 0.1
message    = 'Failed to get to position x=-3.567, y=-1.332'
predicate  = <bound method Event.is_set of <threading.Event at 0x73d72abb24e0: unset>>
timeout    = 180

.../utils/testing/waiting.py:35: TimeoutError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

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