DEV Community

Mohammed Rafay
Mohammed Rafay

Posted on Originally published at memorysync.hashnode.dev

Mastra working memory with tool calls: fixing 'use: tool-call' and messages missing before tool calls

Disclosure: I'm Mohammed, founder of MemorySync. I drafted this article with AI assistance and reviewed it for accuracy against the official Mastra and MemorySync docs. The install, type checks and a full agent run with a mock model were run (no real LLM call); see the notes at the end of each step.

If you searched for "Mastra working memory tool-call messages not saved", you probably landed on mastra-ai/mastra#4221: with workingMemory: { enabled: true, use: 'tool-call' }, the text an agent wrote before calling a tool never reached the database. Turning working memory off made the messages come back.

That issue was filed against @mastra/memory 0.3.x and @mastra/core 0.9.x. A maintainer merged a fix in May 2025 and closed it. Today the bigger change is that the use option is gone altogether. Below is what that means for an old config, the current setup, and how to confirm nothing goes missing. Mastra behaviour comes from the Mastra working memory docs and the package type definitions. I'm building MemorySync, a hosted memory layer, and it gets one short section at the end (if you want it with Mastra, see the MemorySync Mastra guide).

Step 1: Upgrade, then drop the use option

Install current versions (Mastra's packages need Node.js 22.13 or later):

npm install @mastra/core @mastra/memory @mastra/libsql
Enter fullscreen mode Exit fullscreen mode

If your config still looks like the one in the issue, it won't load any more. In @mastra/core 1.79.0 the type of use is never, with the note "The use option has been removed. Working memory always uses tool-call mode." TypeScript flags it, and plain JavaScript fails when Memory is constructed:

Error: The workingMemory.use option has been removed. Working memory always uses tool-call mode.
Enter fullscreen mode Exit fullscreen mode

So the fix is to delete the line. Tool-call mode isn't something you opt into now; it's the only mode.

Step 2: The current working memory config

This is the docs' working memory setup with a custom template and libSQL storage:

import { Agent } from '@mastra/core/agent'
import { Memory } from '@mastra/memory'
import { LibSQLStore } from '@mastra/libsql'

const memory = new Memory({
  storage: new LibSQLStore({ id: 'mastra-storage', url: 'file:./mastra.db' }),
  options: {
    lastMessages: 20,
    workingMemory: {
      enabled: true, // no `use` option any more
      template: `# User
- Name:
- Preferred seat:`,
    },
  },
})

export const agent = new Agent({
  id: 'assistant',
  name: 'Assistant',
  instructions: 'You are a helpful assistant.',
  model: 'openai/gpt-6.1-sol', // the model string from Mastra's quickstart; use your own
  memory,
})
Enter fullscreen mode Exit fullscreen mode

The agent keeps working memory up to date by calling the updateWorkingMemory tool. Working memory is resource-scoped by default, so it follows the user across threads. libSQL is one of the adapters the docs list as supporting resource-scoped working memory, along with PostgreSQL, OracleDB, Upstash and MongoDB.

If your agent doesn't update it when you expect, the docs suggest saying how and when to fill the template in the agent's instructions.

Step 3: Call it with a thread and a resource

const res = await agent.generate("I'm Sam and I always pick the aisle seat.", {
  memory: { thread: 'thread-1', resource: 'user-42' },
})
Enter fullscreen mode Exit fullscreen mode

resource is the stable user ID and thread is the conversation. Resource-scoped working memory needs resource on every call.

Step 4: Check what was actually saved

This is the check the issue was really about. After a run, read the thread back and look at working memory:

const { messages } = await memory.recall({ threadId: 'thread-1', resourceId: 'user-42' })
console.log(messages.map((m) => m.role))

console.log(await memory.getWorkingMemory({ threadId: 'thread-1', resourceId: 'user-42' }))
Enter fullscreen mode Exit fullscreen mode

I ran Steps 2 to 4 on current packages with a mock model (no real LLM). The mock wrote "Noted, Sam. Saving that.", called updateWorkingMemory and then wrote "Done.". The stored assistant message kept both text parts, including the text written before the tool call. The updateWorkingMemory tool call itself wasn't stored as a message part, and working memory held the updated profile. A new process reading thread-2 for the same resource got the same profile back.

So if you're on current versions and something still looks missing, check two things before filing a bug:

  • Are you looking for the tool call itself? In my run the working memory tool call didn't appear among the stored message parts; the result went into working memory instead. The docs note one exception: with the experimental useStateSignals: true option, the tool is registered as setWorkingMemory and its tool-call parts are kept in the history.
  • Is the resource the same? A different resource, or scope: 'thread', means a separate working memory.

Step 5 (optional): Read-only working memory for sub-agents

Routing agents and sub-agents can read working memory without changing it:

await agent.generate('What do you know about me?', {
  memory: { thread: 'thread-1', resource: 'user-42', options: { readOnly: true } },
})
Enter fullscreen mode Exit fullscreen mode

Where MemorySync fits

Working memory is a single Markdown block (or a JSON object, if you use a schema) that the model rewrites. If you also want individual facts extracted and recalled per user from a hosted service, MemorySync's Mastra guide recommends running it next to Mastra's Memory: Memory for the transcript, MemorySync for what the agent should keep knowing. Its processors use processInput rather than a per-step hook, so a multi-step tool loop gets the recalled context once per generate/stream call. You don't need it to fix the issue above.

Troubleshooting

  • The workingMemory.use option has been removed. Delete use from workingMemory. See Step 1.
  • Working memory is empty in a new thread. Pass the same resource, and keep the default scope: 'resource'. Check that your storage adapter is on the supported list.
  • The agent never updates working memory. Add update rules to instructions, per the docs.
  • Working memory is updated, but the user's message is missing. Check that thread and resource are passed on that call.

Common questions

Does Mastra still support workingMemory.use: 'tool-call'?
No. The option has been removed, and working memory always uses tool-call mode. Setting it throws when Memory is constructed, so delete the line.

How does a Mastra agent update working memory?
Through the updateWorkingMemory tool. You can also set it yourself when you create or update a thread (metadata.workingMemory), or call memory.updateWorkingMemory(...) directly (Mastra working memory docs).

Is Mastra working memory shared across threads?
By default, yes. It's resource-scoped, so every thread with the same resource sees it. Set scope: 'thread' to isolate it per conversation (Mastra working memory docs).

Can I use MemorySync with Mastra's working memory?
Yes. MemorySync's guide recommends running both: Mastra's Memory for conversation history and MemorySync's processors for durable per-user facts (MemorySync Mastra guide).

Sources

  • GitHub issue mastra-ai/mastra#4221 and its comments (fix merged May 2025)
  • Mastra docs: mastra.ai/docs/memory/working-memory
  • @mastra/core 1.79.0 type definitions (dist/memory/types.d.ts: use?: never) and runtime error message
  • npm: @mastra/core 1.79.0, @mastra/memory 1.39.0, @mastra/libsql 1.27.0 (Node >= 22.13.0)
  • MemorySync Mastra guide: docs.memorysync.io/guides/mastra

Try it

If you want to see a memory survive a full page reload before wiring up an agent, the MemorySync playground runs the save, reload and recall loop in your browser: memorysync.io/try.

Top comments (0)