Skip to content

fix: decode native MCAP recordings in memory tools - #4434

Open
TomCC7 wants to merge 6 commits into
mainfrom
cc/fix/native-mcap-tools
Open

TomCC7 wants to merge 6 commits into
mainfrom
cc/fix/native-mcap-tools

Conversation

@TomCC7

@TomCC7 TomCC7 commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Problem

  • The Rust recorder writes logical topics with message types in channel metadata but the reader ignored this metadata for LCM channels and returned raw bytes. Typed IMU and pose rendering and replay did not work.
  • The CLI selected the Go2 reader for every MCAP file. Native recordings do not use the Go2 topic contract.
  • The MCAP reader required an optional dependency. A core installation could record with Rust but fail to open the file.

Solution

  • Select built-in LCM and LZ4+LCM decoders from dimos.payload_type metadata. Keep unknown types as raw bytes.
  • Use the generic MCAP reader for native recordings. Keep the existing Go2 decoders and aliases for legacy files.
  • Add mcap to core dependencies. Reject conflicting channel definitions for the same topic.

@github-actions github-actions Bot left a comment •

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.

go2 replay realtime (arm64)

Details
Benchmark suite Current: beb61d3 Previous: f9c5a5c Ratio
peak memory 1711.832 MB 1795.23 MB 0.95
peak threads 356 threads 366 threads 0.97
network (transport) 2731.011 MB 2730.945 MB 1.00
disk write 1.754 MB 1.723 MB 1.02
instructions 133.594 G 137.801 G 0.97

This comment was automatically generated by workflow using github-action-benchmark.

@codecov

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
6580 1 6579 47
View the top 1 failed test(s) by shortest run time
dimos.experimental.isolated_python.test_end_to_end::test_isolated_python_rpc_refs_and_restart
Stack Traces | 10.9s run time
dimos.protocol.rpc.rpc_utils.RemoteError: [Remote builtins.RuntimeError] Isolated Python runtime failed to start: 

Remote traceback:
Traceback (most recent call last):
  File ".../protocol/rpc/zenohrpc.py", line 173, in _execute_rpc
    response = f(*args[0], **args[1])
               ^^^^^^^^^^^^^^^^^^^^^^
  File ".../protocol/rpc/spec.py", line 116, in override_f
    return getattr(module, fname)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../experimental/isolated_python/module.py", line 258, in build
    self._spawn_runtime()
  File ".../experimental/isolated_python/module.py", line 224, in _spawn_runtime
    raise RuntimeError(f"Isolated Python runtime failed to start: {message}")
RuntimeError: Isolated Python runtime failed to start: 


The above exception was the direct cause of the following exception:

    def test_isolated_python_rpc_refs_and_restart() -> None:
        coordinator = ModuleCoordinator.build(
            autoconnect(
                ExampleExternal.blueprint(initial_multiplier=3),
                Offset.blueprint(),
                Producer.blueprint(),
                Consumer.blueprint(),
            )
        )
        try:
            external = coordinator.get_instance(ExampleExternal)
            producer = coordinator.get_instance(Producer)
            consumer = coordinator.get_instance(Consumer)
            assert external.get_multiplier() == 3
            assert external.get_adjusted_multiplier() == 13
            assert external.set_multiplier(5) == "External multiplier set to 5"
            assert external.get_multiplier() == 5
            producer.publish(4)
            wait_until(lambda: consumer.latest() == 20, timeout=5.0)
            assert consumer.latest() == 20
    
>           coordinator.restart_module(ExampleExternal, reload_source=False)

consumer   = <dimos.core.rpc_client.RPCClient object at 0xffa702cea9f0>
coordinator = <dimos.core.coordination.module_coordinator.ModuleCoordinator object at 0xffa702d34620>
external   = <dimos.core.rpc_client.RPCClient object at 0xffa702ce9280>
producer   = <dimos.core.rpc_client.RPCClient object at 0xffa702cea2a0>

.../experimental/isolated_python/test_end_to_end.py:79: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../core/coordination/module_coordinator.py:680: in restart_module
    return self._restart_module(module, reload_source=reload_source)
        module     = <class 'dimos.experimental.isolated_python.example.contract.ExampleExternal'>
        reload_source = False
        self       = <dimos.core.coordination.module_coordinator.ModuleCoordinator object at 0xffa702d34620>
.../core/coordination/module_coordinator.py:761: in _restart_module
    new_proxy.build()
        inbound_refs = []
        kwargs     = {'initial_multiplier': 3}
        module     = <class 'dimos.experimental.isolated_python.example.contract.ExampleExternal'>
        module_class = <class 'dimos.experimental.isolated_python.example.contract.ExampleExternal'>
        name       = 'exampleexternal'
        new_atom   = BlueprintAtom(kwargs={'initial_multiplier': 3}, module=<class 'dimos.experimental.isolated_python.example.contract.Exa..., spec=<class 'dimos.experimental.isolated_python.example.contract.OffsetSpec'>, optional=False),), instance_name=None)
        new_bp     = Blueprint(blueprints=(BlueprintAtom(kwargs={'initial_multiplier': 3}, module=<class 'dimos.experimental.isolated_pytho...=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=(), hosted_placements=())
        new_class  = <class 'dimos.experimental.isolated_python.example.contract.ExampleExternal'>
        new_proxy  = <dimos.core.rpc_client.RPCClient object at 0xffa767e8af60>
        old_atom   = BlueprintAtom(kwargs={'initial_multiplier': 3}, module=<class 'dimos.experimental.isolated_python.example.contract.Exa..., spec=<class 'dimos.experimental.isolated_python.example.contract.OffsetSpec'>, optional=False),), instance_name=None)
        outbound_refs = [('_offset', 'offset')]
        python_wm  = <dimos.core.coordination.worker_manager_python.WorkerManagerPython object at 0xffa767d1f050>
        ref_name   = '_offset'
        reload_source = False
        saved_transports = {'doubled': <dimos.core.transport.ZenohTransport object at 0xffa702ceae70>, 'value': <dimos.core.transport.ZenohTransport object at 0xffa702ce9fd0>}
        self       = <dimos.core.coordination.module_coordinator.ModuleCoordinator object at 0xffa702d34620>
        stream_ref = StreamRef(name='doubled', type=<class 'dimos.msgs.std_msgs.Int32.Int32'>, direction='out')
        t          = <dimos.core.transport.ZenohTransport object at 0xffa702ceae70>
        target_key = 'offset'
        target_proxy = <dimos.core.rpc_client.RPCClient object at 0xffa702ce87a0>
        transport  = <dimos.core.transport.ZenohTransport object at 0xffa702ceae70>
dimos/core/rpc_client.py:94: in __call__
    result, unsub_fn = self._rpc.call_sync(
        args       = ()
        kwargs     = {}
        self       = <dimos.core.rpc_client.RpcCall object at 0xffa702ceb3e0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.protocol.rpc.zenohrpc.ZenohRPC object at 0xffa767e89d90>
name = 'ExampleExternal/build', arguments = ((), {}), rpc_timeout = 86400.0

    def call_sync(
        self, name: str, arguments: Args, rpc_timeout: float | None = None
    ) -> tuple[Any, Callable[[], None]]:
        if rpc_timeout is None:
            method = name.rsplit("/", 1)[-1]
            rpc_timeout = self.rpc_timeouts.get(name) or self.rpc_timeouts.get(
                method, self.default_rpc_timeout
            )
        event = threading.Event()
    
        def receive_value(val) -> None:  # type: ignore[no-untyped-def]
            event.result = val  # type: ignore[attr-defined]  # attach to event
            event.set()
    
        unsub_fn = self.call(name, arguments, receive_value)
        if not event.wait(rpc_timeout):
            # Retries register new callbacks. Remove this expired callback so
            # repeated timeouts do not accumulate entries in the shared response map.
            unsub_fn()
            raise TimeoutError(f"RPC call to '{name}' timed out after {rpc_timeout} seconds")
    
        # Check if the result is an exception and raise it
        result = event.result  # type: ignore[attr-defined]
        if isinstance(result, BaseException):
>           raise result
E           RuntimeError: Isolated Python runtime failed to start:

arguments  = ((), {})
event      = <threading.Event at 0xffa702ceae10: set>
method     = 'build'
name       = 'ExampleExternal/build'
receive_value = <function RPCClient.call_sync.<locals>.receive_value at 0xffa7037f7a60>
result     = RuntimeError('Isolated Python runtime failed to start: ')
rpc_timeout = 86400.0
self       = <dimos.protocol.rpc.zenohrpc.ZenohRPC object at 0xffa767e89d90>
unsub_fn   = <function ZenohRPC.call_cb.<locals>.unsubscribe_callback at 0xffa7037f7920>

.../protocol/rpc/spec.py:88: RuntimeError

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

Comment thread examples/native-mcap/pyproject.toml Resolved
@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The changes affect the shared path used to read MCAP recordings.

Do not merge until mixed recordings retain typed legacy telemetry and control events; the current reader breaks consumers that access their event fields.

Findings

  1. P1 Mixed recordings lose typed events ▶

Summary

This PR adds automatic decoding for native MCAP recordings and routes the CLI through the generic reader.

  • Native MCAP streams decode into their recorded message types.
  • Dataset readers pick native or legacy Go2 codecs for each MCAP file.
  • Repeated MCAP topics share counts only when their channel definitions match.

Reviews (2) · Last reviewed commit: "fix: compare MCAP schemas by content and..." · Reviewed by Greptile

Comment thread dimos/memory/store/mcap.py Outdated
Comment on lines +235 to +237
signature = (ch.message_encoding, ch.schema_id, tuple(sorted(ch.metadata.items())))
if ch.topic in signatures and signatures[ch.topic] != signature:
raise ValueError(f"MCAP topic {ch.topic!r} has conflicting channel definitions")

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 Compatible channels block recordings

If two channels for the same topic reference identical schemas registered under different IDs, McapStore treats the IDs as a conflict and refuses to open the recording. The earlier reader opened such a recording and returned both messages. Compare the referenced schemas rather than their IDs; this regression must be fixed before merging.

Artifacts

Evidence from the check

  • The authored script builds the MCAP and exercises the pre-change and working-tree store constructors, making the comparison reproducible.

Command output from the check

  • Running the reproduction against `684ee07428^` opened the file and returned both payloads, showing the baseline behavior.

Command output from the check

  • Running the same reproduction against the working tree raised the conflicting-channel `ValueError`, showing the regression.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +35 to +38
with path.open("wb") as output:
writer = Writer(output)
writer.start(profile="dimos")
channel = writer.register_channel(

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 Rust recordings lack reader coverage

The new automatic-decoding test writes its recording with Python’s Writer, while the Rust-recording tests give McapStore an explicit codec. The tests can therefore miss a mismatch between a Rust-written file and automatic decoding. A Python-written recording with Rust-shaped metadata decoded successfully, but that does not test an actual Rust-written file. Add a test that opens one without supplying a codec.

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!

Artifacts

Evidence from the check

  • This script generated a Python-written fixture matching the Rust recorder’s declared channel shape and exercised both reader versions; it does not generate a recording with Rust.

Command output from the check

  • The command ran the pre-PR reader against the Rust-shaped fixture and observed a bytes stream, establishing the comparison baseline.

Command output from the check

  • The command ran the PR reader against the same Rust-shaped fixture without a codec and observed a typed Imu, showing the expected reader behavior.

Command output from the check

  • The command searched the relevant tests and located the Python Writer fixture and the explicit codec in the Rust-recorder test, confirming the uncovered combination.

Command output from the check

  • The attempted offline Cargo test exited 101 because `lcm-msgs` could not be checked out, preventing validation with an actual Rust-generated file.

View artifacts

T-Rex Ran code and verified through T-Rex

@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Oct 6, 2026
@TomCC7
TomCC7 force-pushed the cc/fix/native-mcap-tools branch from c9f7428 to c2f18c5 Compare October 6, 2026 07:04
@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Oct 6, 2026
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Oct 6, 2026
@github-actions github-actions Bot added ready-to-merge Required CI checks have passed on this PR and removed ready-to-merge Required CI checks have passed on this PR labels Oct 6, 2026
@TomCC7
TomCC7 removed this pull request from stack #4442 October 7, 2026 02:39
@TomCC7

TomCC7 commented Oct 10, 2026

Copy link
Copy Markdown
Member Author

close as we'll just do cdr migration...

@TomCC7 TomCC7 closed this Oct 10, 2026
@TomCC7 TomCC7 reopened this Oct 22, 2026
Comment on lines +58 to +61
reader.get_header().profile != "dimos"
and channel.message_encoding == "json"
and channel.topic in {"telemetry", "control_log"}
and "dimos.payload_type" not in channel.metadata

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 Mixed recordings lose typed events

In a mixed recording with profile dimos, this condition skips the Go2 decoders for legacy JSON channels named telemetry or control_log when they lack dimos.payload_type. The generic reader then returns bytes instead of Telemetry or ControlEvent. Opening the same recording with the previous reader preserves those types; with this change, consumers accessing fields such as .battery or .lx raise AttributeError. Select these decoders from each channel’s encoding and metadata rather than the file-wide profile.

Artifacts

Exact executed mixed-recording verification script

  • The authored harness creates both fixtures, executes base and head readers, and captures structured results with assertions, making the regression reproducible.

Reader output before the PR change

  • Executed the supplied base reader through both opening APIs against both profiles, showing that legacy telemetry and control events retain their types.

Reader output after the PR change

  • Executed head through both opening APIs against identical fixtures, showing raw bytes and failed typed field access only for dimos-profile legacy events.

Full-range relevant source diff and unchanged tracked-source check

  • Captured the supplied base-to-head diff, numbered source excerpts, and successful tracked-source integrity check, tying the observed regression to the profile guard.

Interpreter setup and verification execution record

  • Captured interpreter setup, Python version, successful before/after runs, tracked-source checks, and fixture hashes, documenting the actual execution.

Legacy-profile mixed-recording control fixture

  • Generated and read a ros2-profile MCAP with native and legacy channels, demonstrating that the same legacy JSON remains typed under the control profile.

DimOS-profile mixed recording that reproduces the regression

  • Generated and read a dimos-profile MCAP with native and legacy channels, demonstrating that head loses legacy decoded types while preserving their raw payloads.

View artifacts

TREX Ran code and verified through TREX

@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Oct 22, 2026
@TomCC7 TomCC7 assigned TomCC7 and leshy and unassigned TomCC7 Oct 23, 2026

This branch was previously deployed

1 inactive deployment
cachix — beb61d32 Deployed Oct 23, 2026 by TomCC7 via cachix-build-macos #14178
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.

2 participants