DEV Community

LubuSeb
LubuSeb

Posted on

The API Changed Shape: Repairing Memanto's Mem0 Migration Without Rewriting the Pipeline

This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.

Project Overview

Memanto is an open-source memory system for AI agents. One of its useful migration paths imports memories from Mem0: discover the source account's entities, page through the memories attached to each entity, map them into Memanto's format, and import them.

The pipeline was already built. The problem was that its transport layer still spoke an API dialect Mem0 no longer accepted.

Bug Fix or Performance Improvement

The exporter discovered entities through GET /v1/entities/, then requested each entity's memories like this:

GET /v1/memories/?user_id=alice&page=1&page_size=200
Enter fullscreen mode Exit fullscreen mode

Mem0's current Get Memories contract uses a different method, path, and filter location:

POST /v3/memories/?page=1&page_size=200

{"filters": {"user_id": "alice"}}
Enter fullscreen mode Exit fullscreen mode

That difference is small on paper and terminal in practice. The old request fails before Memanto reaches its response mapping, deduplication, progress reporting, or import logic.

The important debugging decision was to treat this as contract drift, not as an invitation to redesign the whole migration system.

Code

The complete fix and regression test are in Memanto PR #1707. The PR was merged on August 4.

The old paging loop mixed entity filters with pagination and sent everything through GET:

params = {**filters, "page": page, "page_size": page_size}
response = _get_json(client, "/v1/memories/", params=params)
Enter fullscreen mode Exit fullscreen mode

The repaired loop keeps pagination in the query string and moves the entity scope into the v3 JSON body:

params = {"page": page, "page_size": page_size}
response = _post_json(
    client,
    "/v3/memories/",
    params=params,
    json={"filters": filters},
)
Enter fullscreen mode Exit fullscreen mode

I added a deliberately small _post_json helper with the same error-reporting behavior as the existing GET helper. The rest of the exporter remains unchanged.

My Improvements

I tested the contract, not just the URL

Changing /v1/ to /v3/ would not have been enough. The regression test simulates a two-page response and records every outgoing call. It asserts all three parts of the current contract on every page:

  • POST /v3/memories/
  • page and page_size as query parameters
  • the entity identifier nested under filters in the JSON body

It also verifies that records from both pages come back in order:

assert [memory["id"] for memory in memories] == ["m1", "m2", "m3"]
Enter fullscreen mode Exit fullscreen mode

That catches the easy partial fixes: correcting only the first request, dropping the filter on page two, or returning only the final page.

I kept the blast radius narrow

The response shape, deduplication, progress output, migration mapping, and importer were not the cause. Preserving those paths reduced both regression risk and review burden. The production change is a small transport correction; most of the PR is focused regression coverage.

I validated the surrounding system

Before submission, the branch passed:

  • 473 tests, with 24 credential-dependent live tests skipped
  • Ruff lint and formatting checks
  • mypy on the changed exporter
  • git diff --check

The public PR also passed CodeRabbit's review checks and was accepted and merged by the project.

Result

memanto migrate mem0 now sends the request Mem0's current API actually expects while preserving the rest of Memanto's migration behavior.

The lesson I am keeping is simple: when an integration stops working, compare the request contract one dimension at a time—method, path, query, body, authentication, then response. A tiny mismatch in any one of them can make a healthy downstream pipeline look completely broken.

Top comments (0)