Mastra Memory: The Address Decides What Survives
A Mastra agent reaches memory through two identifiers your code passes on every call. resource is a stable id for the user or entity. thread isolates one conversation. The framework derives neither. You mint both, and you hand the same pair back on the next turn or the agent reads an empty page.
That shape is on purpose. It is also where cross-session recall goes missing, in a way that looks like the model and is not.
What Mastra actually ships
Credit first, because the memory layer here covers more ground than the other TypeScript agent frameworks I have read the docs for. Five pieces, all documented.
Message history is on by default and adds the last N messages in chronological order, with your new message always last. Working memory is a markdown block, or a JSON schema if you pass Zod, injected as a system message so the agent always sees it. Semantic recall embeds messages and queries a vector store for the ones that match by meaning, and it is off until you enable it. Observational memory is the one the docs now recommend first: background agents fold older messages into a dense observation log and drop the raw history they replaced, so the context window stays small without losing the thread. Memory processors trim or prioritize when the combined result would not fit.
None of that is the problem. The address is.
The pair
A call looks like this:
await agent.generate('What did we decide about the retry policy?', {
memory: { resource: 'user-123', thread: 'conversation-123' },
})
Ask again with the same pair and the agent answers. Change either and it does not, and nothing throws. A new conversation gets a new thread id, which is correct behavior and also the exact moment a long-running project loses its record. Mastra is not losing anything. The messages are still in your database. They are filed under an address nobody is asking for.
There is a sharper edge here. Each thread has an owner, the resourceId, and that owner cannot be changed after the thread is created. Reusing a thread id across two different owners produces errors when you query, so the failure is loud only in the one case where you already noticed.
Two scopes that do not see each other
Working memory is written at one of two scopes. Resource scope, the default, persists across every thread for that user. Thread scope is isolated per conversation. The docs are explicit about the consequence: switching between scopes means the agent will not see memory from the other scope, and thread-scoped memory is completely separate from resource-scoped memory.
Read that as a migration warning. An agent that starts thread-scoped and later moves to resource scope does not carry its earlier working memory across. The block was written to a different address, and the new scope has no record of it.
The part that depends on your adapter
Here is the detail that decides most of these cases and gets almost no attention.
Resource-scoped working memory requires a storage adapter that supports the mastra_resources table. The supported list is libSQL, PostgreSQL, OracleDB, Upstash, and MongoDB. Use a different storage adapter and you are on thread scope, whatever the config says.
Semantic recall has the same shape. Searching across all threads for a resource, which is the setting that makes recall feel like memory instead of retrieval, is scope: 'resource', and that is supported by the LibSQL, OracleDB, PostgreSQL, MongoDB, and Upstash adapters. Any other storage adapter gives you same-thread recall only.
So the question "does my Mastra agent remember across sessions" is answered by which @mastra/* storage package you installed, months before anyone asked it. The agent is fine. The embeddings are fine. The address space is smaller than the one you assumed.
Writes have semantics too
Working memory in template mode uses replace semantics. The agent has to return the complete memory content on every update, not a diff. A partial write drops whatever it did not repeat. Schema mode uses merge semantics instead, so the agent sends only the fields it wants to change.
The merge path has one more condition. Atomic merges are supported by @mastra/pg. Other adapters merge in application code, which serializes writers inside a single process. Two processes updating the same user's working memory at the same time, which is normal for anything serverless, can drop fields.
Subagents get their own address
Delegation mints a fresh threadId per call, so a subagent starts with no message history, and a deterministic resourceId of {parentResourceId}-{agentName}, so resource-scoped memory does carry between delegations for the same user. That is a sensible split. It is also another place where the same user's working set is spread across addresses that only a few code paths know about.
Framework details above were checked against Mastra's memory overview, working memory, semantic recall, and MCP pages on October 23, 2026.
What a framework is for
Everything above is per-session state, handled with real care. A framework owns the run. It builds the context for one call, to one model, with one set of identifiers, and Mastra does that better than most.
What it cannot own is the record that outlives the run: the deployment, the framework version, the model you switched to last month, and the other tools your team reads. That record needs an address that does not come from application code, because application code is what changes.
Which is why the split that matters is not Mastra against something else. A parameter has to be reconstructed correctly by every caller. A vault is reached by a credential, and the store decides what is in it. In MemoryRouter the vault is fixed the moment you authorize the connection, so every call lands in that one vault. There is no vault or project parameter for a caller to get wrong or for a model to talk its way into. New connections start read-only, and write scope is something a client has to ask for.
Running the two side by side
Mastra's MCPClient speaks the 2026-07-28 MCP revision over stdio and Streamable HTTP, and calls authenticate() for OAuth-protected servers. MemoryRouter's remote server runs that same revision, stateless, at https://mcp.memoryrouter.ai/mcp, with ten tools covering semantic search with tier and importance filters, time-window recall, storing a memory, reflection passes, lineage, and deletion.
Keep Mastra's working memory for the scratchpad the agent maintains during a task. Point the durable layer at the vault, so the decision survives the next thread id, the next framework upgrade, and the editor your teammate opens instead. Setup for any MCP client is at memoryrouter.ai/mcp. The tool list, the scope model, and the deletion rules are in the docs. Mastra steps are covered at memoryrouter.ai/memory/mastra.
Checking it in five minutes
Write one fact into the vault through the connected agent, then open a run with a different thread id and ask for it back. If the fact returns, the durable layer is doing its job and Mastra's memory is doing the other one. If it does not return, check the connection's scope before you check anything else. A read-only connection cannot seed the vault, and read-only is where new connections start.
Cost
Two independent bills. Mastra's framework is open source with a commercial enterprise license. Your storage adapter and vector store are infrastructure you run and pay for, on top of whatever the embeddings cost. MemoryRouter runs $20 monthly from day fifteen, with 200M tokens inside the monthly allowance and $0.50 for every extra 1M. Your model provider keeps its own keys and its own bill.
If the memory your agents need has to be readable from every thread, every tool, and every machine your team works on, start a vault and give it the records that cannot afford to be refiled.