You build a support workflow in two steps. At 06:00 a scheduled job connects to your MCP server, reads the night's tickets and works out that 4127 and 4133 are the same bug. At 10:00 you open a chat client and ask the agent which tickets can be closed. That is a new client connection, perhaps to a different server instance, and the agent starts from nothing. What the first step worked out lived with the first connection, and that connection closed four hours ago.
The short answer: do not key the context by the connection. Store it outside the server process, under the stable identity it belongs to, such as the user, the project or the agent, and have every connection authenticate as that identity and read the context before it acts. Inside one multi-step task, carry an explicit handle as an ordinary tool argument. Since the 2026-07-28 revision of MCP there is no protocol session left to key context by, and the specification tells servers not to require the same connection for related operations.
Disclosure: I work on Mnemoverse, the memory layer in the example near the end.
The connection was a weak key, and now it is not a key at all
Earlier revisions of MCP gave you a key to keep context under. The 2025-06-18 transport says a server "MAY assign a session ID at initialization time" and that the server may "terminate the session at any time" (2025-06-18 transports). The current transport page sums up that model as "a connection-scoped session with an initialize handshake" (2026-07-28 transports). Context kept under that ID could be found only by requests that carried the same ID, which means only inside the same session.
The 2026-07-28 revision drops the session: the first major change in its changelog is "Remove protocol-level sessions and the Mcp-Session-Id header from the Streamable HTTP transport". The Statelessness section of the specification says what follows for everyone who builds on the protocol:
A server processes each request independently; no state should be inferred from previous requests, even those on the same connection or stream.
Two of the rules under that sentence describe the opening example. Servers "SHOULD NOT require that a client reuse the same connection or process to perform related operations", and state that spans requests "MUST be referenced by an explicit identifier the client passes on each request". Behind a load balancer the next request can land on any instance, so the store has to be one that every instance reaches.
A server that also serves older clients still serves them by the legacy rules, "scoped to the stdio process (stdio) or the session (HTTP)" (versioning and compatibility), and context kept there ends with that process or session.
Three keys, and which connections reach each
What a piece of context is keyed by decides which future connection can find it.
| Context keyed by | Example | Ends when | A new connection reaches it if |
|---|---|---|---|
| The connection or session | state in server memory under an Mcp-Session-Id, or per stdio process |
the connection closes, the session is terminated, or the process restarts | only with the same session ID, which an independent client lacks; 2026-07-28 requests carry none |
| A workflow handle | a job_id returned by the tool that started a long job |
the handle expires or the job is done | the model passes the handle back as a tool argument |
| A stable identity | the user, the team, the project, the agent | it is deliberately removed | it authenticates as that identity and reads before acting |
The first row is what broke in the opening example. The second row is the protocol's answer for work that spans calls: the changelog says "Servers that need cross-call state use explicit, server-minted handles passed as ordinary tool arguments". A handle reaches a new connection only if the model still holds it, which suits one job and not a conversation that starts four hours later. The third row is the only one that answers the question as asked.
Put as a definition: connection-keyed context ends with the connection that produced it; identity-keyed context outlives every connection, and any connection that authenticates as its owner can read it.
Four rules that make identity-keyed context work
The identity comes from the credential, not from the model. The connection presents an API key or an OAuth token, and the server derives whose context it is from that. If the model chooses the key, any user can read another user's context by naming it.
Read at the start of every connection, before acting. A new connection brings nothing with it, so the workflow has to ask. Put the read in the agent's standing instructions or in the first step of the workflow.
Write when the context is established, not when the connection ends. Connections end without warning, and on the 2026-07-28 revision not even the request in flight survives a break: "A broken response stream loses the in-flight request; clients MUST re-issue it as a new request with a new request ID" (changelog). A summary planned for the end of a session is the first thing lost.
Scope inside the identity. One user runs several projects. Keep each project's context under its own namespace, so a connection working on one project does not read the conclusions of another.
With Mnemoverse
In Mnemoverse, memory belongs to the Mnemoverse account, not to the connection. A connection shows which account it is through an API key or a one-click OAuth sign-in. Our MCP server page says "Memory is scoped to the account behind the key" and that "two keys from the same console account share one memory", and the hosted connector, which signs in with OAuth instead of a key, reaches the same memory. So a 10:00 client on the same account can read what the 06:00 job wrote.
One catch, stated on the connector page and true of OAuth connectors in general: the host app holds the connection, and signing out or switching accounts there usually drops it. The memory is untouched: "Sign in again with the same Mnemoverse account and everything is back, unchanged". For unattended workflows the docs recommend the local package with an API key, which lives in your config, not in the host app's session. Connecting gives the agent the tools, not the habit of reading first; Make Your Agent Use Memory has the instructions for that.
The same rule holds outside MCP. With the Python SDK (mnemoverse 0.4.0 from PyPI), which calls the REST API, the two steps of the opening example are two processes with two connections, sharing only the account behind the key and the domain:
# step_one.py: the 06:00 job
from mnemoverse import MnemoClient
with MnemoClient() as memory: # reads MNEMOVERSE_API_KEY
result = memory.write(
"Tickets 4127 and 4133 are the same bug: both fail when an OAuth refresh "
"token has expired. Keep 4127, close 4133 as its duplicate.",
concepts=["tickets", "duplicate", "oauth"],
domain="project:support",
)
if not result.stored:
print("Not stored:", result.reason)
# step_two.py: 10:00, another process, the same account
from mnemoverse import MnemoClient
with MnemoClient() as memory:
found = memory.read("which tickets are duplicates", domain="project:support", top_k=5)
for item in found.items:
print(item.created_at, item.content)
domain is the namespace inside the account (the API reference gives forms such as user:X and project:Z), so the second script reads the support project and nothing else. A write that is too similar to the nearest memory in the same domain is not stored, which here would mean the conclusion is already there, so the first script prints the reason instead of failing silently. For everything written since a given time, recent() lists the newest entries first with nothing ranked away. Setup for scripts is on the Python SDK page.
Check it with a control
- In one client, have the agent save a conclusion it could not guess, such as a made-up pairing of two ticket numbers, then close that client.
- Open a different client, on a new connection signed in to the same account, and ask about it. The answer should come from the record. A different client on purpose: inside one client, the client's own memory could pass the test.
- Control: ask the same question from a connection authenticated as a different account. It should find nothing. If it answers anyway, the answer came from somewhere other than the store, and you need to know where before you trust step 2.
The library page has the longer account: what the 2026-07-28 revision removes, how a handle differs from durable memory, and why A2A also leaves memory outside the protocol. Which step of your workflow still assumes the next call arrives on the same connection?
The Python SDK is open source (MIT) and on PyPI as mnemoverse: github.com/mnemoverse/mnemoverse-sdk-python. The MCP server package on npm is open source too: github.com/mnemoverse/mcp-memory-server.
Top comments (6)
This one hit close to home, the 06:00 job / 10:00 chat split is exactly the failure we kept tripping over, except ours was "Claude Code session on Tuesday, Codex session on Wednesday, same repo, zero shared memory"…
We built cairn (a work-harness plugin for Claude Code plus seven other agent CLIs) around basically your four rules, just with a different identity. Instead of keying context on an account, we key it on the project. The session handoff is a small JSON file written outside the server process under ~/.cairn/handoff/, named by a hash of the repo path, and the durable stuff (plans, memory cards, the ledger) lives in the repo itself and gets mirrored into an external tracker like GitHub Issues or Jira. So the "stable identity" is the codebase plus whoever can authenticate to the tracker, and any harness that opens that repo reads the same context.
The two rules that mattered most in practice were yours #2 and #3. Read-before-act is a SessionStart hook that injects the handoff and a recall banner as context, so the model never has to remember to go ask. And write-when-established is a PostToolUse breadcrumb that refreshes the handoff every ~60 seconds plus an unthrottled refresh right before compaction, because the one time you really need the handoff is the session that crashed before it got to write a summary…
Where we differ from your rule #1: the cairn server itself doesn't derive identity from a credential. The repo path and the tracker's auth do that job, which works great on one machine or a shared repo and gets fuzzier for the "scheduled job on a different box" case you opened with.
Code and docs if you want to kick the tires:
Curious where you land on project-keyed vs account-keyed memory. For a team working one repo across several agent CLIs, does git-committed context mirrored into the tracker get you most of what a hosted account store does, or do you see the account as the only identity that survives once the work leaves a single machine?
John, for one team on one repository the repo is a sound identity for what the code knows: a private repository already gates reading on a credential, it is versioned, and every harness that opens it reads the same cards. Where it differs from an account store is the moment something counts as written. A card reaches another machine once it is committed, pushed and on the branch that machine checks out, so the 06:00 job on another box sees only what was pushed before it ran, and rule 3 becomes commit and push when the context is established. That also needs the cards to be tracked at all. The comment on the dirty flag in server/src/planning/resync.ts says .cairn/ itself is usually untracked, and the cairn repository lists .cairn/ in its own .gitignore, so in a checkout set up that way a card never leaves the machine that wrote it.
The account covers what is not about one repository. Your post on the six articles shows where that goes otherwise: the cross-project rules went into global Claude rules on one machine. Codex reads its global guidance from its own home, ~/.codex/AGENTS.md by default, so a Codex session on that machine starts without them unless they are copied there, and another box does not have them at all. The two keys combine rather than compete. The example in the article authenticates as the account and scopes inside it with a domain such as project:support: the credential says whose context it is, and the project says what it is about.
Two for two, and the first one is embarrassing in the useful way…
I checked every repo on this machine that has a card store. Five of them, four commit every card, and the one that doesn't is cairn itself, the public one, with fifty-seven cards that have never been anywhere but this laptop, exactly as you read it off the gitignore. Every doc we ship says cards are git-committed and the remember verb even tells the user they ship with the repo, and nothing checks it, the only gitignore check we have is for the local tracker store. So the fix going in is that the server says it: card stats carry tracked, ignored and unpushed counts from git, the write result says "this card is gitignored here and will not leave this machine" when that's true, and the memory audit raises it. Whether I un-ignore cairn's own cards is a separate call, that's publishing fifty-seven files, but at least the tool will stop letting me not know.
Your reading of rule 3 as commit-and-push is already how the trust order works, I just hadn't said it that way. The handoff is a disposable cache in the home directory, and the resume path ranks tracker plus git log above the ledger above the handoff, so the thing that counts as written on another box is the tracker comment and the pushed commit, never the local file. What was missing is the server telling you when a card hasn't made that crossing yet, which is the same fix as above.
On the account key, I think you've got the split right, and it moves a decision in a phase I'm about to build. The axioms I pulled out of the six-article read went into my global Claude rules, which is to say one machine and one harness, and the Codex seat on this same box reviews code without them. The phase that writes cairn's axioms into the architecture doc now also renders them into the generated harness spine, the same AGENTS.md fragment every CLI already reads from the checkout, so they travel with the repo and a drift check keeps the copy honest. That's the project key doing the project's job. What stays in a home directory, my own preferences, voice, cross-project habits, is mine and the harness vendor's, and cairn has a decision record saying it is not a persona store, so I'd rather it never grew a second account-keyed layer, it would just be a worse copy of yours…
One question back, since your account is a team account in the example: when the 06:00 job writes a conclusion under project:support and a person at 10:00 disagrees with it, whose write wins, and does the store know the difference between what one connection established and what the team has accepted? That's the line I keep tripping over with a shared tracker, a comment from the agent and a comment from a human look identical until you check the author…
Written as two plain entries, the 06:00 conclusion and the 10:00 disagreement both stay live, and a read about those tickets can return both with nothing in either saying which one stands. Being later does not settle it. A correction that names the earlier entry in supersedes does: the earlier entry is kept and marked replaced, and a second correction aimed at the same entry gets a 409 naming the first.
Established against accepted is a status, not an author, and in the example both steps authenticate as the same account, so it belongs in the namespace, rule 4 again. The job writes under a domain of its own, and acceptance is a person writing the conclusion into project:support with supersedes pointing at the job's entry, which the API allows across domains. A read of project:support then holds only what someone accepted, and the job's unanswered entries are the ones without superseded_by. In one shared domain, an agreement close enough to the conclusion is the write the novelty gate refuses as already there, so acceptance can leave no trace.
The axioms phase has one gap. By default Claude Code reads AGENTS.md only when there is no CLAUDE.md in the working directory or above it, so in a project with both, a Claude Code session never sees the fragment. The setup script writes it into GEMINI.md and the Copilot instructions too, but for Claude Code it only points to the plugin, which does not carry it. Your machine hides it, since your global Claude rules load in every project, and the drift check compares the fragment with its sources, not with what each harness loads. An @AGENTS.md import in the project CLAUDE.md, the fix the Claude Code memory docs give, closes it.
Three for three, and this one I had the rule for and still missed…
You read it exactly right, and it's a little worse than you said. The setup script does write the fragment into AGENTS.md for every harness including the Claude target, line 293, it just never asked whether Claude Code would open it, and next to a CLAUDE.md it won't, the memory docs say so in one sentence. The plugin can't carry it either, Claude Code doesn't load a CLAUDE.md at a plugin root, plugin instructions are skills, and the trailhead skill has the shared rules and not the axioms. Then I looked at cairn's own checkout: CLAUDE.md present, no AGENTS.md at the root at all, and my Codex global file has no axioms in it. So on this machine Claude gets them from my home directory and Codex gets them from nowhere, in the repo whose docs say they're repo-keyed. The drift check did its job, the copy matched its sources every time, nobody asked who reads the copy. That's check 3 on the five-item list I wrote two days ago, enumerate the readers, failing on the first mechanism it was applied to, which is about what I deserve for writing it as a sentence… #398 is the @AGENTS.md import written under the same markers, a readers leg in the check that is red when any harness has no path to the fragment, and running the setup on cairn itself so the dogfood checkout stops depending on my laptop.
Acceptance as an edge rather than an author is the cleanest answer to the question I've seen, and the honest part is the last line, in one domain the novelty gate eats the agreement and acceptance leaves no trace. That's the exact shape of what I trip over in a shared tracker: a person says yes on an agent's comment and nothing records that the status changed. Cairn's comments carry an author field and nothing reads it, and the close writes a receipt that says what was measured, not who accepted it. So I'm taking your shape, the job's writes in one place, the acceptance in another, and the acceptance pointing at the thing it accepts, because a pointer survives the dedup and a near-duplicate doesn't.
One question back on that. In your example acceptance is a person writing into project:support, but my rule 4 says done is the declared check having run, not a human nodding, so the thing I'd want superseding the job's entry is often another job, a verifier, in a third domain. Does project:support distinguish a supersede written by a person from one written by a verifier job on the same account, or does that bring the author question back one level up, which domain is allowed to accept into which…
Your rule 4 already decides it, and not by author. A verifier's identity says which job wrote the supersede, not that the declared check ran against this commit, so the difference belongs in the entry. The evidence your ledger asks of every close, the proving command, its result and the commit range, is what the rule accepts, and the verifier's supersede can go into project:support like a person's, carrying it in metadata that a query there filters on. A person's acceptance is then the one without that evidence, and a reader can rerun the check from the entry instead of trusting whoever wrote it.
That does move the question up a level, to which domain may accept into which, but there it becomes a check. The supersedes field links atoms, not domains, and read items and chain revisions each carry their own domain, so a check over project:support can be red when a live entry replaced one from a domain the rule does not list, or lacks the evidence where the rule asks for it: a writers leg next to the readers leg in #398.
The readers table in #398 also needs rows inside Claude Code. The import reaches the main session and agents like cairn-designer and cairn-uat, which load CLAUDE.md like any custom subagent without omitClaudeMd, but the built-in Explore and Plan start without CLAUDE.md files, and Claude hands codebase searches to Explore on its own. So a session where /memory lists AGENTS.md still has two readers the fragment never reaches. The docs' answer is that the main conversation still has CLAUDE.md when it reads their results, so each row could say whether an axiom has to bind the reader or only the session that acts on its report.