Skip to content

mem summary cleanup, go2 replay cleanup, progress bars#2730

Open
leshy wants to merge 11 commits into
mainfrom
feat/ivan/mem-summary-size
Open

mem summary cleanup, go2 replay cleanup, progress bars#2730
leshy wants to merge 11 commits into
mainfrom
feat/ivan/mem-summary-size

Conversation

@leshy

@leshy leshy commented Jul 6, 2026

Copy link
Copy Markdown
Member
  • dimos mem summary moved from dimos map summary

  • mem2 blobstores can tell the total size of a stream

  • rich shared cli progressbar compatible with mem2 streams

  • dimos mem rerun uses get_data like other mem tooling (previusly it required the full db path)

Go2 --replay - resolves go2_lidar/go2_odom (mid360 recordings, e.g. china_office) with fallback to lidar/odom (older datasets, e.g. go2_hongkong_office)

2026-07-06_18-20

leshy added 5 commits July 6, 2026 16:47
…to dimos mem

BlobStore grows an optional size_bytes(stream) capability (None = not
cheaply knowable): sqlite answers via SUM(LENGTH(data)) — record-header
only, no payload page reads (0.2s on a 9.5 GB db) — and the file store
via stat. Backend delegates to its blob store, and Stream.summary()
appends the size for unfiltered stream queries.

The summary verb moves from `dimos map` to `dimos mem` (its module now
lives in memory2/cli with imports deferred so `dimos --help` stays fast).
One row per stream plus a totals row; Items/Hz/Size are heat-colored
with the pubsub-benchmark red->green ANSI gradient, log-scaled per
column. Stream.size_bytes() becomes public API (None when the query
narrows the stream), and piped output gets a wide console so long
stream names don't truncate.
mid360 recordings (e.g. china_office) name the raw robot streams
go2_lidar/go2_odom; older datasets (go2_hongkong_office) use
lidar/odom. ReplayConnection now picks whichever the dataset has and
reports the available streams when neither exists.
…e other db verbs

Scanning big datasets no longer looks frozen — a transient rich
progress bar names the stream being scanned. open_store() goes through
resolve_named_path (cwd -> data/ -> LFS pull) instead of requiring an
on-disk path, defaulting to .db unless the name says .mcap.
progress() keeps its Stream.tap callback shape but now returns a
ProgressBar backed by ONE process-wide rich Progress, so sequential or
interleaved streams compose instead of fighting over the terminal.
Completed bars persist as a plain line; close() finalizes a bar
abandoned early (mem rerun's --seconds window). map replay drops its
private copy of the old print-based progress.
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR moves memory dataset summary and replay tooling into the memory CLI path. The main changes are:

  • dimos mem summary replaces the mapping summary command.
  • Stream and blob stores now expose payload byte totals.
  • Memory summary output now includes rich tables and payload sizes.
  • Shared rich progress bars replace local replay progress printing.
  • mem rerun resolves named data paths consistently.
  • Go2 replay falls back between old and new lidar/odom stream names.

Confidence Score: 4/5

This is close, but the file-backed size accounting should be fixed before merging.

  • The subset stream size handling now avoids returning full-stream sizes for narrowed queries.
  • File-backed summaries can still include non-owned numeric .bin files in reported payload totals.
  • That can make Stream.summary() and dimos mem summary show incorrect sizes.

dimos/memory2/blobstore/file.py

Important Files Changed

Filename Overview
dimos/memory2/blobstore/file.py Adds file-backed payload size totals, but the directory scan can still include numeric files that are not owned blobs.
dimos/memory2/stream.py Adds stream payload size reporting and now treats narrowed queries as unknown-size streams.
dimos/memory2/cli/summary.py Adds the new memory summary command with per-stream counts, timing, rates, and payload sizes.

Reviews (6): Last reviewed commit: "Merge branch 'main' into feat/ivan/mem-s..." | Re-trigger Greptile

Comment thread dimos/memory2/stream.py Outdated
Comment on lines +503 to +504
if self._query.filters or self._query.limit_val is not None:
return None

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 Query Subsets Report Full Size

size_bytes() only returns None for filters and limits, so reachable narrowed streams such as stream.offset(100).size_bytes() or search queries without an explicit limit still return the full backend blob size. Callers can receive an incorrect payload size for a subset stream instead of the documented unknown size.

Suggested change
if self._query.filters or self._query.limit_val is not None:
return None
if (
self._query.filters
or self._query.limit_val is not None
or self._query.offset_val
or self._query.order_field is not None
or self._query.search_vec is not None
or self._query.search_text is not None
or self._query.live_buffer
):
return None

Comment thread dimos/memory2/blobstore/file.py Outdated
@leshy leshy changed the title mem summary: per-stream sizes + rich table; shared rich progress; go2 replay stream fallback mem summary cleanup, go2 replay cleanup, progress bars Jul 6, 2026
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
2651 3 2648 72
View the top 1 failed test(s) by shortest run time
dimos.protocol.pubsub.test_spec::test_high_volume_messages[ros_context-topic1-values1]
Stack Traces | 4.8s run time
pubsub_context = <function ros_context at 0x79749a675da0>
topic = RawROSTopic(topic='/test_ros_topic', ros_type=<class 'geometry_msgs.msg._vector3.Vector3'>, qos=<rclpy.qos.QoSProfile object at 0x79749a645460>)
values = [geometry_msgs.msg.Vector3(x=1.0, y=2.0, z=3.0), geometry_msgs.msg.Vector3(x=4.0, y=5.0, z=6.0), geometry_msgs.msg.Vector3(x=7.0, y=8.0, z=9.0)]

    @pytest.mark.self_hosted
    @pytest.mark.skipif_macos_bug
    @pytest.mark.parametrize("pubsub_context, topic, values", testdata)
    def test_high_volume_messages(
        pubsub_context: Callable[[], Any], topic: Any, values: list[Any]
    ) -> None:
        """Test that all 5k messages are received correctly.
        Limited to 5k because ros transport cannot handle more.
        Might want to have separate expectations per transport later
        """
        with pubsub_context() as x:
            # Create a list to capture received messages
            received_messages: list[Any] = []
            last_message_time = [time.time()]  # Use list to allow modification in callback
    
            # Define callback function
            def callback(message: Any, topic: Any) -> None:
                received_messages.append(message)
                last_message_time[0] = time.time()
    
            # Subscribe to the topic
            x.subscribe(topic, callback)
    
            # Publish 5000 messages
            num_messages = 5000
            for _ in range(num_messages):
                x.publish(topic, values[0])
    
            # Wait until no messages received for 0.5 seconds
            timeout = 2.0  # Maximum time to wait
            stable_duration = 0.1  # Time without new messages to consider done
            start_time = time.time()
    
            while time.time() - start_time < timeout:
                if time.time() - last_message_time[0] >= stable_duration:
                    break
                time.sleep(0.1)
    
            # Capture count and clear list to avoid printing huge list on failure
            received_len = len(received_messages)
            received_messages.clear()
>           assert received_len == num_messages, f"Expected {num_messages} messages, got {received_len}"
E           AssertionError: Expected 5000 messages, got 2602
E           assert 2602 == 5000

_          = 4999
callback   = <function test_high_volume_messages.<locals>.callback at 0x7974656e51c0>
last_message_time = [1783360451.9705079]
num_messages = 5000
pubsub_context = <function ros_context at 0x79749a675da0>
received_len = 2602
received_messages = [geometry_msgs.msg.Vector3(x=1.0, y=2.0, z=3.0), geometry_msgs.msg.Vector3(x=1.0, y=2.0, z=3.0), geometry_msgs.msg.Vec....0, y=2.0, z=3.0), geometry_msgs.msg.Vector3(x=1.0, y=2.0, z=3.0), geometry_msgs.msg.Vector3(x=1.0, y=2.0, z=3.0), ...]
stable_duration = 0.1
start_time = 1783360449.7937179
timeout    = 2.0
topic      = RawROSTopic(topic='/test_ros_topic', ros_type=<class 'geometry_msgs.msg._vector3.Vector3'>, qos=<rclpy.qos.QoSProfile object at 0x79749a645460>)
values     = [geometry_msgs.msg.Vector3(x=1.0, y=2.0, z=3.0), geometry_msgs.msg.Vector3(x=4.0, y=5.0, z=6.0), geometry_msgs.msg.Vector3(x=7.0, y=8.0, z=9.0)]
x          = <dimos.protocol.pubsub.impl.rospubsub.RawROS object at 0x797401bdce00>

.../protocol/pubsub/test_spec.py:365: AssertionError
View the full list of 2 ❄️ flaky test(s)
dimos.e2e_tests.test_dimsim_spatial_memory::test_go_to_the_bed

Flake rate in main: 20.39% (Passed 82 times, Failed 21 times)

Stack Traces | 561s run time
lcm_spy = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x76545b623f50>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x765459ffd760>
human_input = <function human_input.<locals>.send_human_input at 0x765459ffd800>
dim_sim = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x76545ad1c890>
explore_house = <function explore_house.<locals>.explore at 0x765459ffdd00>

    @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 0x76545ad1c890>
explore_house = <function explore_house.<locals>.explore at 0x765459ffdd00>
human_input = <function human_input.<locals>.send_human_input at 0x765459ffd800>
lcm_spy    = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x76545b623f50>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x765459ffd760>

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 0x765459ffdee0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x76545b623f50>
        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 0x76545ad1de20: unset>
        fail_message = 'Failed to get to position x=-3.567, y=-1.332'
        listener   = <function LcmSpy.wait_for_message_result.<locals>.listener at 0x765459ffdda0>
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x765459ffdee0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x76545b623f50>
        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 0x76545ad1de20: 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   = 3459647.622978734
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 0x76545ad1de20: unset>>
timeout    = 180

.../utils/testing/waiting.py:35: TimeoutError
dimos.e2e_tests.test_dimsim_walk_forward::test_walk_forward

Flake rate in main: 23.23% (Passed 76 times, Failed 23 times)

Stack Traces | 205s run time
lcm_spy = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x76545ad1d130>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x765459ffda80>
human_input = <function human_input.<locals>.send_human_input at 0x765459ffe480>
dim_sim = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x76545ad1f020>

    @pytest.mark.self_hosted_large
    def test_walk_forward(lcm_spy, start_blueprint, human_input, dim_sim) -> None:
        start_blueprint(
            "run",
            "--disable",
            "spatial-memory",
            "--disable",
            "security-module",
            "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)
    
        origin_x, origin_y = 1, 2
        dim_sim.set_agent_position(origin_x, origin_y)
    
        human_input("move forward 3 meter")
    
>       lcm_spy.wait_until_odom_position(origin_x + 3, origin_y, threshold=0.4, timeout=120)

dim_sim    = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x76545ad1f020>
human_input = <function human_input.<locals>.send_human_input at 0x765459ffe480>
lcm_spy    = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x76545ad1d130>
origin_x   = 1
origin_y   = 2
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x765459ffda80>

dimos/e2e_tests/test_dimsim_walk_forward.py:37: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
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 0x765459ffd9e0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x76545ad1d130>
        threshold  = 0.4
        timeout    = 120
        x          = 4
        y          = 2
dimos/e2e_tests/lcm_spy.py:153: in wait_for_message_result
    wait_until(
        event      = <threading.Event at 0x76545ad1f740: unset>
        fail_message = 'Failed to get to position x=4, y=2'
        listener   = <function LcmSpy.wait_for_message_result.<locals>.listener at 0x765459ffe8e0>
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x765459ffd9e0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x76545ad1d130>
        timeout    = 120
        topic      = '/odom#geometry_msgs.PoseStamped'
        type       = <class 'dimos.msgs.geometry_msgs.PoseStamped.PoseStamped'>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

predicate = <bound method Event.is_set of <threading.Event at 0x76545ad1f740: 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=4, y=2

deadline   = 3459852.894566363
interval   = 0.1
message    = 'Failed to get to position x=4, y=2'
predicate  = <bound method Event.is_set of <threading.Event at 0x76545ad1f740: unset>>
timeout    = 120

.../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.

The deferral wrapper bought nothing — memory2/utils/progress.py is a
light module (rich is deferred inside it), so import it at the top.
Comment thread dimos/memory2/stream.py Outdated
Comment thread dimos/memory2/blobstore/file.py Outdated
…nly blobs

Review (PR #2730): offset/search queries narrowed the stream but still
reported full-stream size — treat them like filters/limit (ordering
keeps membership, so it stays allowed). FileBlobStore now sums only
{key}.bin files instead of everything in the stream dir.
leshy added 2 commits July 6, 2026 18:24
…ed singleton

Simpler model: one bar live at a time, per-bar transient Progress, and
close() is just stop-the-live-bar + a plain persisted print. Starting a
new bar finalizes a dangling one (rich allows a single live display).
Comment thread dimos/memory2/blobstore/file.py Outdated
Review (PR #2730): match put()'s {key}.bin layout exactly so a stray
backup.bin or temp file beside real blobs can't inflate the size.
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Jul 6, 2026
@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Jul 6, 2026
@leshy leshy added PlzReview ready-to-merge Required CI checks have passed on this PR labels Jul 6, 2026
Comment on lines +77 to +78
return sum(
p.stat().st_size for p in stream_dir.glob("*.bin") if p.is_file() and p.stem.isdigit()

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 Numeric Strays Still Count

This still counts any digit-named .bin in the stream directory, even when that file was not written as a blob for this stream. A copied 999999.bin, a padded 001.bin, or a symlink named like an observation id passes p.stem.isdigit(), and p.stat() follows symlinks. In those cases Stream.summary() and dimos mem summary can still report bytes that are not payload data for any stored observation. The count needs to be tied to the blob store's owned keys rather than every numeric-looking file present on disk.

@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant