DEV Community

Cover image for How do I sync learned operational lessons between desktop AI environments and remote workers?
Edward Izgorodin
Edward Izgorodin

Posted on Originally published at mnemoverse.com

How do I sync learned operational lessons between desktop AI environments and remote workers?

On Tuesday, in a Claude Desktop session, you found out that the integration suite hangs unless REDIS_URL points at the CI service instead of localhost. The agent fixed it with you and saved the lesson. On Thursday a CI worker you started from a script hits the same hang, because it starts from nothing and has no idea anything was learned.

Storing the lesson is the easy half. The half that goes wrong is the return. A worker starting fresh cannot search for what it missed, because it does not know what it missed. It can only ask what happened since it last looked.

The short answer: keep lessons in one store that both sides reach, write each one once as a sentence that makes sense on its own, and have every consumer start with a catch-up read before it searches. Newest entries first, no query, follow the cursor until none comes back, and only then move its own watermark forward. The watermark belongs to the consumer, not to the store.

Disclosure: I work on Mnemoverse. The code below uses its Python SDK and its MCP tools.

Two reads, not one

A search answers "what do I know about X". It needs the reader to name X, and it returns what matches, so an entry that exists but does not match is simply absent. A catch-up read answers "what happened lately". It takes no query and lists entries newest first, so nothing is ranked away.

A worker starting a task needs both, in that order. The catch-up read brings in the lessons written since it last ran, whatever they are about. The search then finds the older lessons that bear on this task. Skip the first and a worker gets only what its own query happens to match, so yesterday's lesson about something it did not think to ask about never reaches it.

Where each side keeps its place

A watermark is the newest creation time a consumer has already taken in. Three rules keep it safe:

  • One per consumer and per scope. A desktop agent and a CI worker read at different times, so they cannot share one.
  • Commit it only after the walk ends. A watermark moved forward halfway through a walk skips whatever the walk had not reached.
  • Keep the ids at the watermark instant. The since filter is inclusive, "Only entries created at or after this ISO-8601 instant, inclusive", so the entries at the watermark come back on the next pass. Holding their ids turns that into a repeat you skip, rather than a boundary you can fall through.

A desktop agent can keep its place loosely: a person starts the session and knows roughly when they last worked on the project. A remote worker is often a fresh container. Put its state where the worker's other state lives, a CI cache or a mounted volume. If it has none, every run starts without a state file and the function below reads the whole scope; that is fine for a small lessons scope, and past that point the state is worth a cache entry.

The worker side, in Python

This uses the mnemoverse SDK, 0.3.1, where recent() is the catch-up read.

import json
import os
from datetime import datetime


def catch_up(client, scope, state_path):
    """Everything written to `scope` since this worker's last call, oldest first."""
    state = {"since": None, "at_since": []}
    if os.path.exists(state_path):
        with open(state_path, encoding="utf-8") as f:
            state = json.load(f)
    since = datetime.fromisoformat(state["since"]) if state["since"] else None
    held = set(state["at_since"])  # since is inclusive, so entries at the watermark come back
    newest, at_newest = since, set(held)
    fresh, cursor = [], None
    while True:
        page = client.recent(domain=scope, since=since, limit=50, cursor=cursor)
        for item in page.items:
            aid = str(item.atom_id)
            if aid in held:
                continue
            fresh.append(item)
            if newest is None or item.created_at > newest:
                newest, at_newest = item.created_at, {aid}
            elif item.created_at == newest:
                at_newest.add(aid)
        if page.next_cursor is None:  # the end of the feed, not a short page
            break
        cursor = page.next_cursor
    os.makedirs(os.path.dirname(state_path) or ".", exist_ok=True)
    tmp = state_path + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:  # commit only after the walk has ended
        json.dump({"since": newest.isoformat() if newest else None, "at_since": sorted(at_newest)}, f)
    os.replace(tmp, state_path)  # a job killed mid-write keeps the old state, not half a file
    return fresh[::-1]
Enter fullscreen mode Exit fullscreen mode

And at the start of each job:

from mnemoverse import MnemoClient

client = MnemoClient()  # reads MNEMOVERSE_API_KEY

news = catch_up(client, "ops-lessons", ".mnemo/ops-lessons.json")
briefing = "\n".join(item.content for item in news)       # what was learned since the last run
related = client.read("run the integration test suite in CI", domain="ops-lessons", top_k=5)  # the job's own task
Enter fullscreen mode Exit fullscreen mode

Three things in that function carry the weight. It stops when next_cursor is None, not when a page comes back short. It never rebuilds a page from a timestamp or an offset: the reference says that passing the cursor back "continues the listing with no skips and no duplicates, which LIMIT/OFFSET cannot guarantee while writes are landing", so a lesson another worker writes during the walk does not shift the pages and turns up in the next pass. And it writes the state file once, at the end, creating its directory on a first run and replacing the file in one step, so a job killed mid-write keeps the old state rather than half a file.

I tested catch_up against a stub that follows that contract (newest first, inclusive since, a cursor that continues below the last position returned), not against a live account. Seven entries were walked with the stub serving pages of three, and two more were written after the first page. The first pass returned the seven, the second the two late ones, with no repeats. A pass with nothing new returned nothing and left the watermark where it was, and an entry written at the watermark instant came back exactly once. The first run started with no state directory at all. As a control, the same loop without the ids held at the watermark repeated an entry on its second pass.

The desktop side, over MCP

Connected to the same account, by sign-in or by an API key from that account, Claude Desktop and Cursor reach the same store over MCP. The tools are memory_write and memory_list_recent, and the dependable way to make an agent use them is a standing instruction rather than a reminder you type each time:

When a fix or a finding about our build, deploy or test setup has held up, save it with memory_write in domain ops-lessons: one sentence that makes sense without this conversation, plus concepts.

At the start of a session on this project, call memory_list_recent with domain ops-lessons and since set to when I last worked on it, and keep passing the cursor back until none comes back. Tell me what is new before we start.
Enter fullscreen mode Exit fullscreen mode

The instruction says to follow the cursor because the tool's page size is, in its own description, "A CEILING, not a promise": a page of long entries comes back shorter than the limit, with a cursor still attached.

What to write, so the other side can use it

  • A sentence that stands on its own. "Integration tests hang in CI unless REDIS_URL points at the redis service; nothing listens on localhost there." Not "fixed the redis thing", which means nothing to a worker that was not in the room.
  • Concepts the other side would search for, such as ci, redis, integration-tests, so the search half of the worker's start has more to match on next month.
  • Only after it held up. A lesson saved on the first guess teaches every worker the guess.
  • Check the write. A write can be refused when it is too similar to something already stored in the same domain. The SDK response carries stored, and reason says why when the service gives one; the MCP tool's result says whether the memory was stored or filtered.

If the workers run under a different account, the same loop runs on a shared room (Beta) that both accounts have joined: pass its address, xroom:<room_id>, as the domain on both sides. Rooms are separate stores, and a call without that domain never covers them, so give each room its own watermark.

Check it before you rely on it

  1. Walk to the end. With a small limit, follow the cursor until none comes back and count the ids. Nothing should repeat inside one walk.
  2. Write while you walk. After the first page, save one lesson from the desktop, then finish the walk. The new lesson should not be in this walk, and it should be in the next pass.
  3. Run a pass with nothing new. It should return nothing, and the watermark in the state file should not move.

The library page covers the same read in full, including what an empty answer says about the scope it covered. Where do your remote workers keep their place today?

The Python SDK is on PyPI as mnemoverse, and the MCP server package on npm is open source (MIT): github.com/mnemoverse/mcp-memory-server.

Top comments (0)