On Monday the test-fixer bot spends a whole run finding out why test_refund_webhook fails about once a day: it shares a Redis database with test_retry_queue and fails whenever it runs second. The bot opens a fix and the run ends. On Wednesday the reviewer bot meets the same failure on a branch cut before the fix and asks the author to look into a flaky test. On Friday the release bot holds the release over it. Each bot did its job. What the fleet learned on Monday lived in one run's context and ended with it.
The short answer: treat every bot run as a session that starts empty, and keep what the fleet learns in one store outside the runs. Each bot reads from it before it acts and writes to it the moment it concludes something that will still hold on its next run. What only its role needs goes to a scope of its own, what any bot could trip over goes to one project scope that every bot reads, and a bot catching up on what the others found lists everything new since its last run instead of searching for it. When the bots run under different accounts, the shared scope moves to a store both accounts are members of.
Session state and session knowledge
Session state is what a run holds while it works: the diff, the logs, the plan for its next step. Session knowledge is what the run concluded that will still be true on the next run: a diagnosis, a fact about the repository, a warning for the bot that comes after. State should end with the run. Knowledge is what the fleet loses when it does.
A fleet loses it faster than a single assistant. Every run is a fresh process started by a webhook or a CI job, no person in the loop remembers, and the bot that finds a fact is rarely the one that needs it next. The SAMEP paper (arXiv:2507.10562) opens its abstract with both gaps: "Current AI agent architectures suffer from ephemeral memory limitations, preventing effective collaboration and knowledge sharing across sessions and agent boundaries." Between runs is the session boundary; between bots, the agent boundary.
Three kinds of knowledge, three readers
| Kind | Example | Written by | Read by | Where it goes | How it is read |
|---|---|---|---|---|---|
| Role lesson | "Maintainers here reject a new dependency without a lockfile change" | the reviewer | the reviewer | the reviewer's own scope | search by the task at hand |
| Repository fact | "test_refund_webhook fails after test_retry_queue: both use Redis database 0" |
the bot that confirmed it | every bot | the project scope | search by the task at hand |
| Handoff | "PR 812 adds migration 0042; deploy it before the worker change in PR 815" | the bot that found it | the next role | the project scope | list everything since the last run |
The third row is the one fleets miss. A handoff is addressed to a role, not to a question, and the release bot does not know to ask about migration 0042. A search returns what resembles the query, so a handoff unlike anything the release bot asks stays out of the results, and nothing in them shows it was missed. Hence the rule: search for lessons, list for handoffs.
One run, start to finish
- Read its own scope, then the project scope, with the task as the query.
- List what is new in the project scope since this bot's last run, and keep the time this run started, not ended, for the next one.
- Do the work.
- Write each conclusion when it is reached, not in a summary at the end: a CI job that hits its timeout is killed mid-run. Write the sentence for a bot that never saw this run, with its evidence ("seen in CI runs 4411 and 4417").
If every bot in the fleet is GitHub's own
Then sharing what one bot learns about the repository is built in. GitHub's page About GitHub Copilot Memory says Copilot Memory "is currently used by Copilot cloud agent, Copilot code review, Copilot CLI, and agentic autofix", and that "Facts and preferences captured by one Copilot feature can be used by another." Repository facts there "are stored with citations pointing to the code that supports them", which Copilot checks against the current branch before it uses a fact. The feature is in public preview. The rest of this article is for bots you build yourself, which share only what they write to a store they all call.
A minimal version in code
The code below uses Mnemoverse (the author's company) through mnemoverse 0.4.0 from PyPI. Each scope in the table is a domain, the domain argument of a read, a listing or a write. The multi-tenant guide gives the naming patterns agent:{agent_name} and project:{project_id}, so the reviewer's scope is agent:reviewer and the fleet's is project:orders-api. A read that names a domain returns only memories from that domain, so the priority order the guide sketches, own scope first and shared ones after, is the bot making one read after another, as the code below does. Every key on one account reaches the same memory (MCP server page), so on one account a domain sorts what each bot reads, and separating bots takes separate accounts. Domains sort, accounts separate.
Bots run unattended, so they connect with an API key, through the Python SDK or the npm MCP server; the hosted connector signs in through a browser, which suits an agent a person starts. A write that is too similar to the nearest memory in the same domain is not stored, and the result says why, so a bot that writes down a fact the project scope already holds, in words too close to tell apart, gets a refusal instead of a second copy.
Every bot runs the same code under its own name:
import os
from mnemoverse import MnemoClient
BOT = os.environ["BOT_NAME"] # "reviewer", "test-fixer" or "release"
SHARED = os.environ.get("FLEET_DOMAIN", "project:orders-api")
LAST_RUN = os.environ.get("LAST_RUN_AT") # when the previous run started (ISO time), kept by CI; unset on the first run
def start_of_run(memory, task):
own = memory.read(task, domain=f"agent:{BOT}", top_k=5).items
shared = memory.read(task, domain=SHARED, top_k=5).items
new, cursor = [], None
while True: # everything since the last run, newest first, nothing ranked away
page = memory.recent(domain=SHARED, since=LAST_RUN, limit=100, cursor=cursor)
new += page.items
if not page.next_cursor:
return own, shared, new
cursor = page.next_cursor
with MnemoClient() as memory: # reads MNEMOVERSE_API_KEY
own, shared, new = start_of_run(memory, "test_refund_webhook fails intermittently")
# ... the test-fixer's work ...
result = memory.write(
"test_refund_webhook fails when it runs after test_retry_queue: both use Redis "
"database 0. Fixed in PR 807. Seen in CI runs 4411 and 4417.",
concepts=["flaky-test", "redis", "test_refund_webhook"],
domain=SHARED, # a role lesson would go to f"agent:{BOT}"
)
if not result.stored:
print("Not stored:", result.reason)
When the bots run under different accounts
Say the release bot belongs to another team's account. Its key reaches only that account's memory, where project:orders-api holds none of what the other bots wrote. What both accounts can reach is a room (Beta), a memory space shared across accounts and addressed as xroom:<room_id> in the same domain field. Creating a room, minting an invite, joining and listing your rooms are self-service tools in the local MCP server and the hosted connector; Join a shared room has the steps. Once its account has joined, a bot runs the code above with FLEET_DOMAIN set to the room address.
-
A room is its own bucket. Nothing in
project:orders-apimoves into it, and joining shares nothing from a member's own memory. The bots write the shared part to the room address. -
An invite carries its scope. A
readinvite fits a bot that should use what the fleet knows and never change it, such as that release bot. A contractor's reviewer that leaves handoffs needsread_write, the default. Invites minted with the tools default to one redemption and a 7-day expiry, so mint one per bot account when it is ready to join. - Membership is checked on every request. A bot account revoked in the console is refused on its next call; what it already read, it keeps.
- The room address belongs in the bot's configuration. An unscoped read does not search shared rooms, so a bot that leaves the address out reads only its own account's memory.
Check it with a control
- Have the reviewer bot write a handoff to the shared scope that no release query would match well, such as the migration note in the table.
- Start the release bot in a fresh process. Its listing should show the handoff. Then run its usual search alone: if the handoff does not come back, you have seen why the listing is there.
- Control, with the fleet split across accounts: call the room from an account that was never invited. It should be refused with "Not an active member of this room" (in the SDK, a
MnemoAuthErrorwith that message). A bot holding areadinvite that tries to write should get "Read-only membership cannot write to this room". If either call succeeds, the boundary is not where you think it is.
The library page goes further: private and shared domains, how memory differs from a coordination protocol such as A2A, and the boundaries between accounts. Which of your bots found something last week that another one is still rediscovering?
Top comments (0)