Highlights
Durable Agent Crash Recovery (server + client + boot-time)
Durable agents can now discover and recover orphaned RUNNING runs after a restart via DurableAgent.listActiveRuns(), recover(), and recoverActiveRuns(), plus Mastra.recoverAllDurableAgents() and an opt-in MastraConfig.recovery: { durableAgents: 'auto' } for boot-time recovery. There’s also a standard HTTP/client surface (POST /agents/:agentId/recover + client-js agent.recover({ runId })) to reattach and stream the remaining output from dashboards or admin tools.
Scoped AgentController Sessions (parallel sessions per resource)
AgentController sessions now support a scope/sessionScope, allowing multiple independent sessions to run in parallel over the same resourceId (e.g., one per git worktree) without sharing run loops, threads, or state. The scope is also exposed in request context for integrations, and controller APIs now report run activity (running + per-thread state) for better UIs.
New Workspace + Sandbox Providers for Mastra Platform
A new package, @mastra/platform-workspace@0.1.0, adds PlatformFilesystem and PlatformSandbox providers that connect agents to Mastra Platform sandboxes and bucket-backed filesystems via the workspace-proxy API. This enables running workspace-dependent tooling against Platform-managed infrastructure with env-var or config-based setup.
Mastra Code SDK Now Public + More Extensible Integrations
@mastra/code-sdk@0.1.0 is now published as a first-class package (previously internal), enabling third parties to build custom UIs/surfaces on top of the Mastra Code coding agent. It also adds async per-request extraTools resolution and a postToolObserver hook for reacting to tool calls without replacing built-ins.
MCP Server: Better Serverless Streaming + Dynamic Tooling + Notifications
@mastra/mcp adds serverlessStreaming to MCPServer.startHTTP() to deliver request-scoped SSE progress notifications even in serverless mode, and expands notification support (tools list changed, server logs, per-session resource subscriptions, and reliable broadcast across streamable HTTP). MCP tool execution context also gains optional log/progress functions so tools can report status back to clients.
Breaking Changes
- None noted in this changelog.
Changelog
@mastra/core@1.51.0
Minor Changes
-
Added session scoping to
AgentControllerso independent sessions can run in parallel over the same resource (for example one session per git worktree). (#19357)Previously
createSession()was get-or-create byresourceIdalone, so two callers sharing a resource always resolved to the same session — with one run loop, one thread binding, and shared mode/model/state. Passing the newscopeoption creates an independent session per scope:// Two independent sessions over the same resource: const a = await controller.createSession({ resourceId: 'repo-123', scope: '/worktrees/feature-a', tags: { projectPath: '/worktrees/feature-a' }, }); const b = await controller.createSession({ resourceId: 'repo-123', scope: '/worktrees/feature-b', tags: { projectPath: '/worktrees/feature-b' }, }); // Look up a scoped session later: const session = await controller.getSessionByResource('repo-123', '/worktrees/feature-a');
Calls with the same
resourceIdandscopestill resume the same session (get-or-create), and unscoped sessions behave exactly as before. -
Added support for custom processors on
createScorerjudges. You can now passinputProcessors,outputProcessors,errorProcessors, andmaxProcessorRetriesin a scorer'sjudgeconfig (or per-step judge config) to apply processors to the internal judge agent — for example wiring upStreamErrorRetryProcessorto retry transient LLM errors while scoring. (#19195)import { createScorer } from '@mastra/core/evals'; import { StreamErrorRetryProcessor } from '@mastra/core/processors'; const scorer = createScorer({ id: 'my-scorer', description: 'Scores responses', judge: { model: myModel, instructions: 'You are an expert evaluator...', errorProcessors: [new StreamErrorRetryProcessor()], maxProcessorRetries: 3, }, });
-
Added support for resolving
foreachconcurrency at execution time.concurrencycan now be a function that receives the foreach input and the workflow's init data and returns a number, in addition to a static number: (#19329)workflow.foreach(step, { concurrency: ({ inputData, getInitData }) => (getInitData().fast ? 10 : 1), });
Durable agents use this to honor
toolCallConcurrency: parallel tool calls now run concurrently (default 10) instead of always sequentially, while runs that require tool approval or use tools that can suspend still execute tool calls one at a time. -
Added an option to retry unknown stream errors while allowing known authorization failures to surface immediately, and enabled resilient retries for coding agents. (#19290)
import { StreamErrorRetryProcessor } from '@mastra/core/processors'; const processor = new StreamErrorRetryProcessor({ retryUnknownErrors: true, maxRetries: 2, delayMs: 3000, });
-
Added:
runEvals()now supports gate-only runs.scorersis optional when at least one gate is provided. (#19348) -
Added
maxRetryAfterMstoStreamErrorRetryProcessor, with a default of 30 seconds, so providerRetry-Afterwaits can't exceed a configured limit. Aborting during the wait now stops the processor retry before another model request. (#19383)Improved structured-output recovery so transport and provider failures don't trigger JSON prompt injection. Scorer judges that use Mastra's current generation API can use existing error processors for a coordinated retry budget. Legacy model adapters keep their separate
generateLegacy()retry settings.Before
import { StreamErrorRetryProcessor } from '@mastra/core/processors'; new StreamErrorRetryProcessor({ maxRetries: 2, });
After
import { StreamErrorRetryProcessor } from '@mastra/core/processors'; new StreamErrorRetryProcessor({ maxRetries: 2, maxRetryAfterMs: 30_000, });
-
Workflow run event topics are now cleaned up automatically. The evented workflow engine deletes each run's
workflow.events.v2.<runId>pub/sub topic shortly after the run reaches a terminal state (success, failure, or cancellation), so persistent transports like Redis Streams no longer accumulate streams from finished workflow runs (#19123). (#19418)clearTopicis now part of thePubSubbase class with a default no-op implementation. Custom transports that retain messages per topic should override it to delete that state:import { PubSub } from '@mastra/core/events'; class CustomPubSub extends PubSub { async clearTopic(topic: string): Promise<void> { // delete retained state for the topic } }
Callers no longer need to probe for the method before calling it —
CachingPubSuband the durable-agent runtime now forwardclearTopicunconditionally. -
Added anonymous feature usage telemetry for server startup surface counts and a
trackFeatureUsage()API. (#19159) -
Added atomic caller-defined dataset IDs for idempotent dataset creation across built-in storage adapters. Supplying
idtomastra.datasets.create()now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throwDATASET_ID_CONFLICT. (#19370) -
Added PROVIDER_TOOL_CALL observability spans for provider-executed tools (e.g. Anthropic code execution, server-side web search). Provider tool input and output are now visible in traces and Studio, with spans anchored to the AGENT_RUN parent. (#19261)
-
Added the ability to explicitly disable a storage domain on
MastraCompositeStoreby setting it tofalsein thedomainsconfig. A disabled domain no longer falls back to theeditorordefaultstore, so writes for that domain are dropped instead of silently landing in the fallback database. (#19059)const storage = new MastraCompositeStore({ id: 'my-storage', default: libsqlStore, domains: { // don't persist traces/metrics when observability is turned off observability: false, }, });
prune()also accepts a per-callretentionoption that replaces the configured retention policies for that call only — for example to skip a domain (keep chat history) or prune more aggressively without reconstructing the store:// prune everything except the memory domain, one time await storage.prune({ retention: { observability: { spans: { maxAge: '14d' } }, }, });
-
Added the authoritative session scope to agent controller request context for scoped session integrations. (#19446)
const controllerContext = requestContext.get('controller'); console.log(controllerContext?.scope);
-
Added caller-defined dataset item identities for safe retries across all dataset storage adapters. (#19384)
Dataset items can now include an
externalIdwhen callingaddItemoraddItems:await dataset.addItem({ externalId: 'source-item-123', input: { prompt: 'Hello' }, });
Retrying with the same identity and payload returns the existing item. Reusing an identity with different content returns a typed conflict, including during concurrent writes. Updates and deletes preserve the identity, Spanner retries transactions without changing the outcome, and MySQL batch writes now preserve every supported dataset item field.
-
Added optional
logandprogressfunctions to the MCP tool execution context type so tools running in an MCP server can send log and progress notifications to the calling client. (#19193)Added
mastra.removeTool(key)to remove a dynamically registered tool from the Mastra instance, the counterpart tomastra.addTool():mastra.addTool(calculatorTool); mastra.removeTool('calculator-tool'); // returns true if a tool was removed
-
Added file-system routing for a Mastra logger and per-agent scorers. (#19262)
Define a logger in
src/mastra/logger.ts(default export) and it is auto-registered as the Mastra logger, just likestorage.tsandobservability.ts. A code-registered logger still wins.Register scorers per agent by adding an
agents/<name>/scorers/folder. Each module's default export (aMastraScorer, or a{ scorer, sampling }entry) is wired into that agent, keyed by filename.config.scorerswins on collision.src/mastra/ logger.ts # export default new PinoLogger({ name: 'App' }) agents/weather/ config.ts scorers/ relevance.ts # export default myRelevanceScorer
Patch Changes
-
Update provider registry and model documentation with latest models and providers (
fe1bda0) -
Fixed background-task cancellation so cancelled tasks no longer look completed to the agent, terminal workflow events still surface a valid result when cancellation happens before one is produced, and completed, failed, cancelled, and suspended background tasks each get clearer continuation instructions. (#19255)
-
Added an optional
scopefield toResolveToolsOptsso tool providers can see a connection's identity bucketing (per-author,shared, orcaller-supplied) when resolving tools. Providers can use this to let the backend auto-resolve an account within a caller's bucket instead of pinning a specific one. The field is optional and defaults to previous behavior when absent. (#19144)Also added an optional
defaultScopetoBaseToolProviderOptions(surfaced on theToolProviderinterface asdefaultScope). This lets an app author set a tool provider's connection scope at config time — for exampledefaultScope: 'caller-supplied'for multi-tenant OAuth — so every connection authorized against the provider is bucketed correctly without any per-connection UI control. Defaults to'per-author'when absent.import { BaseToolProvider, type ResolveToolsOpts } from '@mastra/core/tool-provider'; class MyToolProvider extends BaseToolProvider { // ...required members like `info` and `capabilities` elided constructor() { // Config-level tenancy decision: bucket connections per caller. super({ defaultScope: 'caller-supplied' }); } async resolveToolsVNext(opts: ResolveToolsOpts) { if (opts.scope === 'caller-supplied') { // Let the backend auto-resolve an account within the caller's // bucket instead of pinning a specific connected account. } // ... } }
-
Fixed —
CachingPubSub.clearTopicnow forwards to the wrapped transport. Because the durable-agent runtime wraps every pubsub inCachingPubSub, clearing a topic previously dropped only the in-memory cache and never told a persistent backend (e.g. Redis Streams) to delete its stream — so finished runs' streams leaked. It now also callsclearTopicon the inner transport when the inner implements it. (#19138) -
Fixed
MemorygenerateTitlenever firing for durable agents created withcreateEventedAgent(including Inngest). Thread titles now generate and persist on the durable path whengenerateTitleis configured, including when Observational Memory is enabled. Existing thread titles are preserved. (#19315) -
Fix durable agents orphaning per-step output processor spans. In
createDurableLLMExecutionStep, therunProcessOutputStep(...)call omittedtracingContext(unlike the siblingrunProcessInputStep/runProcessLLMRequestcalls), sooutput step processorprocessor_runspans were created without a parent and appeared as their own root traces — one per LLM step — instead of nesting underMODEL_STEP→AGENT_RUN. PassingtracingContext: modelSpanTracker?.getTracingContext() ?? tracingContextrestores parity with the non-durable path. Fixes #19312. (#19313) -
Fix
ToolNotFoundErrorfor workspace/skill tools (skill,skill_read,skill_search,mastra_workspace_*) when a durable agent's steps execute on a cross-process engine (e.g. the@mastra/inngestconnect()worker). (#19331)The durable tool-call step resolved tools only from the per-process
globalRunRegistryplus Mastra-instance-level tools, while the sibling LLM-execution step already rebuilds the full toolset from the agent viaresolveRuntimeDependencies/getToolsForExecution. On a worker process the registry is empty, so the model could callskill(the LLM step saw it) but the tool-call step rejected it withToolNotFoundError. The tool-call step now falls back to rebuilding the toolset from the agent (rebuildRunToolsFromMastra) when the registry misses, resolving workspace/skill tools symmetrically cross-process.resolveRuntimeDependenciesalso now rebuildsinputProcessors/outputProcessors(and writes the rebuilt tools + processors back intoglobalRunRegistry) when the registry entry is a cross-process placeholder, so theSkillsProcessorandWorkspaceInstructionsProcessorrun cross-process too — restoring the available-skills list and workspace instructions in the system prompt on the worker.Placeholder registry entries are detected via a new explicit
RunRegistryEntry.isPlaceholderflag (set by@mastra/inngestwhen seeding resume-segment entries) or the absence of a live model instance — never by an emptytoolsmap, which is a legitimate state for agents configured without tools.Fixes #19330.
-
Pass tracingContext through to runProcessAPIError so error-processor runs show up as processor_run spans in observability exports (#19188)
-
Fixed
iterationCountalways being 1 fordountilanddowhileloops on the evented workflow engine. The count was never carried forward between iterations, so any loop whose condition depended oniterationCountnever advanced and ran forever. (#19233)Before
// On the evented engine this loop never terminated: iterationCount stayed 1. workflow.dountil(step, async ({ iterationCount }) => iterationCount >= 3);
After
The condition now receives an incrementing count (1, 2, 3, ...) exactly as it does on the default engine, so the loop stops as expected. Loops whose conditions read step output (
inputData) were unaffected. -
Fix the
createHandleroption type onregisterApiRoute. It's called with{ mastra }at runtime but was typed as(c: Context); it's now(opts: { mastra: Mastra }) => Promise<ApiRouteHandler>, matching the runtime and the internalApiRoutetype. (#19320) -
Fixed
DurableAgentlosing reasoning items on turns that include tool calls, which caused OpenAI reasoning models likegpt-5-minito fail on the next turn. Reasoning is now preserved and replayed correctly on subsequent turns. (#19408)Fixes #19365.
-
Fix heap OOM when a workflow uses
.map({ key: mapVariable({ initData: <workflow> }) }). The map reducer kept the liveWorkflowinstance by reference andJSON.stringify'd it into the map step'smapConfig, deep-walking the whole workflow (its logger, nested step graph, …) into a multi-hundred-MB string — and the length-guard truncation only ran after the full string was built, so.commit()could OOM at module load. TheinitDatamapping is now serialized as a slim{ initData: <id>, path }reference. Runtime behavior is unchanged (the execute path only readsinitDatafor truthiness). (#19033) -
Fixed file attachments with AI SDK v7 models. (#19316)
-
fix: handle PathSegment objects in validation error messages (#19125)
-
Durable agents now stop cleanly when cancelled via an abort signal, reporting the correct
abortfinish reason instead of crashing. ThesendMessageandsendSignalwake flows also work reliably for durable agents, so thread subscribers receive streamed chunks as expected. (#19046) -
Fixed typo in agent-network e2e test description (#19162)
-
Fixed
stream.textso it resolves only to the final step's answer and excludes interim commentary between tool calls when no memory or output processors are configured. All text still streams in full throughfullStream, and per-step text remains available onsteps. Fixes #17986. (#18644) -
Fixed an infinite retry loop in evented workflows when workflow storage is unavailable (for example when the workflows table does not exist). A failing workflow.fail event no longer republishes another workflow.fail after exhausting its retry budget, which previously flooded logs with endless "error processing event" entries. (#19194)
-
Fixed the tool payload transform policy being dropped on non-durable agent streams. When an agent was configured with a
transformpolicy,loop()rebuilt its internal state bag but did not carry the policy forward, so it never reached the run scope and silently no-opped for the whole run. The policy is now forwarded, so tool call, tool result, and tool input delta payloads are transformed as configured. Closes #19102. (#19103) -
Fixed a crash that could happen when a language server process exited or stopped responding while a request was being sent to it. The request now fails with a clean timeout error instead of crashing the host process. (#19322)
-
Renamed the built-in gateway's display name from "Memory Gateway" to "Gateway" so it reads as a plain model gateway alongside the other providers. No behavior change. (#18691)
-
Add MastraNonRetryableError for workflow steps to signal permanent failures and skip retries (#19321)
import { MastraNonRetryableError } from '@mastra/core/error'; throw new MastraNonRetryableError('Invalid template ID');
-
Fixed parallel sub-agent approvals so they can be handled in any order. listSuspendedRuns() now returns each pending sub-agent call, and approving one resumes that specific call instead of using another call’s suspended state. (#19450)
-
Fixed background task start chunks reaching agent stream onChunk callbacks. (#18628)
-
Fixed a memory leak where every discarded standalone agent (an
Agentused directly without being registered on aMastrainstance) stayed reachable for the lifetime of the process. The internal Mastra instance created for standalone execution no longer registers a module-level scorer hook it can never use, so standalone agents are garbage-collected once discarded. Applications that create one agent per request no longer see linear heap growth. (#19413)Fixes #19404.
-
- Fixed DurableAgent returning stale/empty values for Studio metadata, processors, skills, workflows, voice, and other inherited Agent accessors by adding delegation overrides to the wrapped agent (#19076)
- Fixed missing traces in Studio: span
entityIdnow uses the durable agent's ID instead of the wrapped agent's ID - Fixed dropped
background-task-completedevents instreamUntilIdlecaused by the same agent ID mismatch \_\_setMemory,\_\_setPubSub, and\_\_setWorkspacenow propagate to both the DurableAgent base class and the wrapped agent
-
Fixed file attachments carrying AI SDK v5 metadata failing to persist and keep their media type. File parts using the v5 shape are now read wherever stored messages are converted, so they save correctly and retain their content type instead of erroring out. Also fixed distinct file attachments being wrongly deduplicated when they carried this metadata. (#19021)
-
Extract text from DB-shaped user messages in goal judge prompts instead of stringifying them as
[object Object]. Goal judge prompts now also skip malformed, empty, or synthetic reminder messages when selecting the latest user context. (#19070) -
Fixed durable agents resuming persisted runs after a process restart. (#19363)
-
Added durable-agent recovery for orphaned RUNNING runs after process restart. (#19191)
What changed
DurableAgent.listActiveRuns()— discover in-flight durable runs for an agent from persistent storage, filtered by agentId, threadId, and resourceId (mirrorslistSuspendedRunsbut forrunningstatus).DurableAgent.recover(runId, options?)— rehydrate a single orphaned run's non-serializable state (MessageList, model, tools, memory,SaveQueueManager, processors, request context, agent span,BackgroundTaskManager+ agent background-tasks config) from its persisted workflow snapshot, re-subscribe to the pubsub topic, and re-drive the workflow in the background. Returns the same{ output, fullStream, runId, threadId, resourceId, cleanup, abort }shape asstream()/resume(), so callers can stream the recovered response or attach later viaobserve(runId). Memory writes flow through the rebuiltSaveQueueManagerexactly like a fresh run.DurableAgent.recoverActiveRuns(options?)— bulk recovery hook that delegates torecover(runId)for each in-flight run discovered vialistActiveRuns()(or a caller-suppliedrunId), awaits each workflow settlement, and returns{ recovered, succeeded, failed }counts. Use this on boot to drain the backlog; userecover()when you want to stream a specific run.- The default workflow engine now persists
runningsnapshots for the durable agentic loop, with a guard that prevents arunningwrite from overwriting an already-suspendedsnapshot for the same run. Without this,listActiveRuns()would never see a live durable run in storage. - Background-task state is re-wired on recovery so the
bg-task-checkstep waits for pre-crash in-flight tasks (still tracked inBackgroundTaskManagerstorage), thetool-callstep can still dispatch new tools as background tasks, andllm-executionstill injects the background-task system prompt. Without this, the recovered segment would silently run with background-tasks disabled and could end before storage-backed tasks delivered their tool-result chunks. - Snapshot rows are now deleted after a durable run reaches any non-suspended terminal status — this applies to
stream/generate(the initialrun.start()),resume(),recover(), andrecoverActiveRuns(). Suspended terminals still keep their snapshots so a later resume/recover can find them. Mirrors the existing loop-stream cleanup so snapshot storage doesn't grow one stale row per completed durable run. Mastra.recoverAllDurableAgents()— new server-level fan-out that walks every registered agent, filters those exposingrecoverActiveRuns()(default-engineDurableAgentonly — Inngest and other externally-executed durable wrappers run their own recovery), and aggregates{ agents, recovered, succeeded, failed }counts. A per-agent failure is logged and skipped so one bad agent doesn't stop the rest.- New
MastraConfig.recoveryoption:recovery?: { durableAgents?: 'auto' | 'off' }(default'off'). When set to'auto', the deployer's/__restart-active-workflow-runsboot handler will invokemastra.recoverAllDurableAgents()right afterrestartAllActiveWorkflowRuns(), so both user workflows and durable-agent runs are re-driven from the same boot hook the CLI/deployer already call. Also exposed asmastra.recoveryConfigfor callers who want to gate their own recovery pipelines on it. - Recovery is opt-in on purpose. Auto-recovery re-runs the agentic loop from the last persisted snapshot, so it re-issues LLM calls (real cost) and re-executes tool calls (must be idempotent); in multi-instance deploys every replica will race to recover the same runs until a lease/lock is added. Leave
'off'and callmastra.recoverAllDurableAgents()(or the per-agent APIs) from a cron, a leader-elected worker, or an admin endpoint if you need finer control.
Why
Previously, the durable agent's agentic loop was an awaited in-process Promise and
globalRunRegistrywas an in-memory TTLCache, so any RUNNING run silently died on process restart with no boot-time recovery or re-drive API (see issue #19056). Suspended runs already hadprepare/resume/listSuspendedRuns; RUNNING runs now have the equivalent discover-and-recover pair.Usage
// Opt into boot-time recovery for every durable agent on this Mastra instance. // The deployer will call `mastra.recoverAllDurableAgents()` automatically // after restarting active workflow runs. const mastra = new Mastra({ agents: { support: supportDurableAgent }, storage, recovery: { durableAgents: 'auto' }, }); // Or drive it yourself (cron, leader election, admin endpoint, etc.): const { agents, recovered, succeeded, failed } = await mastra.recoverAllDurableAgents(); // Per-agent: drain all orphaned RUNNING runs on one agent. const agent = mastra.getAgent('support') as DurableAgent; await agent.recoverActiveRuns(); // Per-run: recover a specific run and stream its output to the caller. const { fullStream, runId, cleanup } = await agent.recover('run-abc123'); for await (const chunk of fullStream) { // forward chunks to the client, log them, etc. } cleanup();
-
Fixed targeted agent resumes so an approval for a tool call that is not suspended fails without executing a different tool call. (#19377)
@mastra/ai-sdk@1.6.2
Patch Changes
-
Fixed AI SDK v6 native tool approvals resuming the wrong tool call. When multiple tool calls were awaiting approval, approving one could execute a different one. The approval response now carries the answered tool call's ID through to the resume, so the approved tool call is the one that runs. (#19266)
-
Fixed a crash in the AI SDK stream transformer when a supervisor agent delegates to a remote A2A agent (A2AAgent) through chatRoute() or handleChatStream. A2A sub-agent streams do not emit a start chunk and use a flat finish payload, which caused the UI stream to end with "Cannot read properties of undefined" errors. The remote agent's answer now streams to the client correctly. (#19443)
The same guard now also covers resumed sub-agent streams, which skip the start chunk as well and could crash or corrupt the delegation state on tool calls and structured output.
-
Fixed background task chunks being dropped from AI SDK UI streams. (#18628)
@mastra/auth-workos@1.6.3
Patch Changes
- Added
mapUserToResourceIdtoMastraAuthWorkosOptionsso it can be set directly in theMastraAuthWorkosconstructor. This maps an authenticated user to a resource id that multi-tenant tool providers use to bucket connected accounts (for example, Composio'scaller-suppliedscope). Previously the option was consumed at runtime but missing from the typed constructor surface, forcing a post-construction property assignment. (#19144)
@mastra/braintrust@1.2.4
Patch Changes
-
Fixed tool calls showing as
unknown_toolin the Braintrust Messages tab. Tool spans (TOOL_CALL,MCP_TOOL_CALL, andPROVIDER_TOOL_CALL) are now converted to OpenAI chat messages with the real tool name and paired result, so Braintrust renders them correctly. (#19364) -
Added PROVIDER_TOOL_CALL to exporter span-type mappings and duration metrics so provider-executed tool spans are classified as tool spans across all observability platforms. (#19261)
@mastra/clickhouse@1.12.0
Minor Changes
- Added legacy-to-VNext span migration for ClickHouse observability storage. Customers using
ObservabilityStorageClickhouse(legacy) can now runnpx mastra migrateto copy historical spans frommastra_ai_spansto the VNextmastra_span_eventsschema. The migration handles column mapping, batches by day for memory safety, deduplicates legacy rows, converts empty-string parentSpanId to NULL for correct root span detection, and supports both old (Nullable(String)JSON) and new (Array(String)) tags schemas. The legacy table is preserved as a backup after migration. (#19050)
Patch Changes
@mastra/client-js@1.32.0
Minor Changes
-
Added an optional session scope to the agent controller API so clients can address independent sessions that share one resource (for example one session per git worktree). (#19357)
Session routes now accept a
sessionScopequery parameter, andAgentController.session()in the client accepts a scope that travels on every request:const controller = client.getAgentController('code'); // Address the worktree's own session instead of the shared one: const session = controller.session('repo-123', '/worktrees/feature-a'); await session.create({ tags: { projectPath: '/worktrees/feature-a' } }); await session.sendMessage('hello');
Requests without a scope behave exactly as before.
-
Added image attachment support to agent controller chat. You can now send images (and other files) with a message, and the Mastra Code web chat lets you attach, paste, or drag-and-drop images which render inline in the transcript. (#19368)
await session.sendMessage({ content: 'What is in this screenshot?', files: [{ data: base64Png, mediaType: 'image/png', filename: 'screenshot.png' }], });
-
Added caller-defined dataset item identities for safe retries across all dataset storage adapters. (#19384)
Dataset items can now include an
externalIdwhen callingaddItemoraddItems:await dataset.addItem({ externalId: 'source-item-123', input: { prompt: 'Hello' }, });
Retrying with the same identity and payload returns the existing item. Reusing an identity with different content returns a typed conflict, including during concurrent writes. Updates and deletes preserve the identity, Spanner retries transactions without changing the outcome, and MySQL batch writes now preserve every supported dataset item field.
-
Added run activity reporting to agent controller sessions.
session.state()responses include arunningflag so UIs can show a working indicator immediately when attaching to a session that is already mid-run, and each thread returned bysession.listThreads()carries astateof'active'or'idle'(backed by the same per-thread run tracking that signalifIdledelivery uses), so one listing can power activity indicators across every worktree or scope sharing a resource instead of polling each session: (#19357)const session = client.getAgentController('code').session(resourceId); const { running } = await session.state(); // is the session mid-run? const threads = await session.listThreads({ tags: { projectPath } }); const busy = threads.filter(thread => thread.state === 'active');
Patch Changes
-
Added HTTP and client bindings for recovering an interrupted durable agent run. (#19191)
What changed
@mastra/server: newPOST /agents/:agentId/recoverroute. Given the id of a durable agent run that was interrupted by a deploy or crash, the server picks the run back up from where it left off and streams the rest of the response to the caller. Non-durable agents are rejected, and callers can only recover runs that belong to them (same permission and ownership rules as resuming a suspended run).@mastra/client-js: newagent.recover({ runId })method that reads that stream from the browser or Node. It behaves the same asagent.resumeStream()— you get back a readable stream of the agent's remaining response.
Why
The underlying core API for recovering an interrupted durable agent run could previously only be called from server-side code. This adds the standard HTTP + client surface so operators can reattach to an interrupted run from a dashboard, an admin tool, or any other client, using the same auth and ownership rules as the rest of the agents API.
Usage
const stream = await mastraClient.getAgent('support').recover({ runId: 'run-abc123' }); for await (const chunk of stream) { // render or forward the remaining agent output }
-
Type structured Agent Controller notification message content so Web clients can render notification provenance from live and persisted transcript messages. (#19446)
if (part.type === 'notification') { renderNotification(part.message, part.source); }
@mastra/code-sdk@0.1.0
Minor Changes
-
Added support for async
extraToolsproviders inMastraCodeConfig. TheextraToolsoption now accepts an async function that receives the request context, so tools can be resolved per session (for example, only exposing an integration tool when the current project has that integration connected). (#19369)const mastraCode = await createMastraCode({ extraTools: async ({ requestContext }) => { const controller = requestContext.get('controller'); if (!(await hasLinearConnection(controller?.resourceId))) return {}; return { linear_get_issue: linearGetIssueTool }; }, });
-
Added a post-tool observer for custom Mastra Code integrations to react to completed tool calls without replacing built-in tools. (#19446)
await mountAgentControllerOnMastra({ postToolObserver: ({ toolName, output }) => logToolResult(toolName, output), });
-
Renamed the Gateway constants exported from
@mastra/code-sdk/onboarding/settingsand addedMastraCodeGateway.getMastraGatewayApiKey()so they match the Gateway product name. The old constant and method names keep working as deprecated aliases, and the stored values are unchanged. (#18691)// Before import { MEMORY_GATEWAY_PROVIDER, MEMORY_GATEWAY_DEFAULT_URL } from '@mastra/code-sdk/onboarding/settings'; // After import { MASTRA_GATEWAY_PROVIDER, MASTRA_GATEWAY_DEFAULT_URL } from '@mastra/code-sdk/onboarding/settings';
-
Publish the Mastra Code agent core as
@mastra/code-sdk(previously the internal@internal/mastracodepackage), so third parties can build their own UIs and surfaces on top of the Mastra Code coding agent. ThemastracodeCLI now consumes it as a regular runtime dependency instead of bundling it into its published output. (#18986) -
Improved GitHub plugin dependency installs by requiring exact pnpm versions and running them through Corepack, with an actionable setup error when Corepack is unavailable. (#19288)
Patch Changes
-
Fixed the server-owned Mastra instance created by prepareAgentControllerMount ignoring a configured PubSub. When you pass a distributed pubsub (for example Redis Streams) to the agent controller, the mounted Mastra now runs its event bus on the same transport, so streams, workflows, and signals work across multiple server processes. (#19431)
-
Fixed secure discovery of symlinked custom commands and skills. (#19279)
-
Removed invalid CommonJS export entries from @mastra/code-sdk so package resolution matches the published ESM output. (#19127)
-
Added the authoritative session scope to agent controller request context for scoped session integrations. (#19446)
const controllerContext = requestContext.get('controller'); console.log(controllerContext?.scope);
@mastra/datadog@1.3.4
Patch Changes
- Added PROVIDER_TOOL_CALL to exporter span-type mappings and duration metrics so provider-executed tool spans are classified as tool spans across all observability platforms. (#19261)
@mastra/deployer@1.51.0
Minor Changes
- Added anonymous feature usage telemetry for server startup surface counts and a
trackFeatureUsage()API. (#19159)
Patch Changes
-
Fixed a false-positive LOCAL_STORAGE_PATH preflight error that flagged storage paths like
file:./data.dbthat don't exist in your project. The deploy bundler's local-storage detector now excludes everything under.mastra/.build/(deployer-generated intermediate chunks), not just@mastra__*shim files. Those chunks can carry JSDoc examples from library code (for exampleLibSQLStore({ url: 'file:./data.db' })from@mastra/core), which previously blockedmastra server deployand forced--skip-preflighteven though the user's code had no local storage paths. Local storage paths in your own source files are still detected. (#19071) -
Added durable-agent recovery for orphaned RUNNING runs after process restart. (#19191)
What changed
DurableAgent.listActiveRuns()— discover in-flight durable runs for an agent from persistent storage, filtered by agentId, threadId, and resourceId (mirrorslistSuspendedRunsbut forrunningstatus).DurableAgent.recover(runId, options?)— rehydrate a single orphaned run's non-serializable state (MessageList, model, tools, memory,SaveQueueManager, processors, request context, agent span,BackgroundTaskManager+ agent background-tasks config) from its persisted workflow snapshot, re-subscribe to the pubsub topic, and re-drive the workflow in the background. Returns the same{ output, fullStream, runId, threadId, resourceId, cleanup, abort }shape asstream()/resume(), so callers can stream the recovered response or attach later viaobserve(runId). Memory writes flow through the rebuiltSaveQueueManagerexactly like a fresh run.DurableAgent.recoverActiveRuns(options?)— bulk recovery hook that delegates torecover(runId)for each in-flight run discovered vialistActiveRuns()(or a caller-suppliedrunId), awaits each workflow settlement, and returns{ recovered, succeeded, failed }counts. Use this on boot to drain the backlog; userecover()when you want to stream a specific run.- The default workflow engine now persists
runningsnapshots for the durable agentic loop, with a guard that prevents arunningwrite from overwriting an already-suspendedsnapshot for the same run. Without this,listActiveRuns()would never see a live durable run in storage. - Background-task state is re-wired on recovery so the
bg-task-checkstep waits for pre-crash in-flight tasks (still tracked inBackgroundTaskManagerstorage), thetool-callstep can still dispatch new tools as background tasks, andllm-executionstill injects the background-task system prompt. Without this, the recovered segment would silently run with background-tasks disabled and could end before storage-backed tasks delivered their tool-result chunks. - Snapshot rows are now deleted after a durable run reaches any non-suspended terminal status — this applies to
stream/generate(the initialrun.start()),resume(),recover(), andrecoverActiveRuns(). Suspended terminals still keep their snapshots so a later resume/recover can find them. Mirrors the existing loop-stream cleanup so snapshot storage doesn't grow one stale row per completed durable run. Mastra.recoverAllDurableAgents()— new server-level fan-out that walks every registered agent, filters those exposingrecoverActiveRuns()(default-engineDurableAgentonly — Inngest and other externally-executed durable wrappers run their own recovery), and aggregates{ agents, recovered, succeeded, failed }counts. A per-agent failure is logged and skipped so one bad agent doesn't stop the rest.- New
MastraConfig.recoveryoption:recovery?: { durableAgents?: 'auto' | 'off' }(default'off'). When set to'auto', the deployer's/__restart-active-workflow-runsboot handler will invokemastra.recoverAllDurableAgents()right afterrestartAllActiveWorkflowRuns(), so both user workflows and durable-agent runs are re-driven from the same boot hook the CLI/deployer already call. Also exposed asmastra.recoveryConfigfor callers who want to gate their own recovery pipelines on it. - Recovery is opt-in on purpose. Auto-recovery re-runs the agentic loop from the last persisted snapshot, so it re-issues LLM calls (real cost) and re-executes tool calls (must be idempotent); in multi-instance deploys every replica will race to recover the same runs until a lease/lock is added. Leave
'off'and callmastra.recoverAllDurableAgents()(or the per-agent APIs) from a cron, a leader-elected worker, or an admin endpoint if you need finer control.
Why
Previously, the durable agent's agentic loop was an awaited in-process Promise and
globalRunRegistrywas an in-memory TTLCache, so any RUNNING run silently died on process restart with no boot-time recovery or re-drive API (see issue #19056). Suspended runs already hadprepare/resume/listSuspendedRuns; RUNNING runs now have the equivalent discover-and-recover pair.Usage
// Opt into boot-time recovery for every durable agent on this Mastra instance. // The deployer will call `mastra.recoverAllDurableAgents()` automatically // after restarting active workflow runs. const mastra = new Mastra({ agents: { support: supportDurableAgent }, storage, recovery: { durableAgents: 'auto' }, }); // Or drive it yourself (cron, leader election, admin endpoint, etc.): const { agents, recovered, succeeded, failed } = await mastra.recoverAllDurableAgents(); // Per-agent: drain all orphaned RUNNING runs on one agent. const agent = mastra.getAgent('support') as DurableAgent; await agent.recoverActiveRuns(); // Per-run: recover a specific run and stream its output to the caller. const { fullStream, runId, cleanup } = await agent.recover('run-abc123'); for await (const chunk of fullStream) { // forward chunks to the client, log them, etc. } cleanup();
-
Fixed deploy preflight false positives for env-guarded storage fallbacks. The build now records when a local storage path like
file:./.mastra-demo.dbis only used as a fallback behind an environment variable (for exampleprocess.env.TURSO_DATABASE_URL || "file:./.mastra-demo.db"), and which environment variables your own code reads, so the deploy preflight can tell dead fallbacks and library-internal variables apart from real problems. (#19071)Also fixed another source of false
LOCAL_STORAGE_PATHerrors: dependencies installed via symlinks (pnpmlink:/file:) resolve to paths outsidenode_modulesand are no longer treated as your code. -
Added file-system routing for a Mastra logger and per-agent scorers. (#19262)
Define a logger in
src/mastra/logger.ts(default export) and it is auto-registered as the Mastra logger, just likestorage.tsandobservability.ts. A code-registered logger still wins.Register scorers per agent by adding an
agents/<name>/scorers/folder. Each module's default export (aMastraScorer, or a{ scorer, sampling }entry) is wired into that agent, keyed by filename.config.scorerswins on collision.src/mastra/ logger.ts # export default new PinoLogger({ name: 'App' }) agents/weather/ config.ts scorers/ relevance.ts # export default myRelevanceScorer
@mastra/e2b@0.6.0
Minor Changes
-
Add
E2BCodeModeTransportfor running Code Mode in anE2BSandbox. (#19372)The default
StdioCodeModeTransportwrites the runner/program to the host tmpdir and spawnsnode <hostPath>, which only works when the sandbox shares the host filesystem (e.g.LocalSandbox). E2B runs the program in a remote micro-VM, so those host paths don't exist there.E2BCodeModeTransportwrites the program and runner into the sandbox via the E2B files API, strips TypeScript on the host with esbuild (no--experimental-strip-types, no Node-version dependency), auto-starts the sandbox when needed, surfaces captured stderr in timeout/no-result errors, and cleans up the sandbox directory afterwards.import { createCodeMode } from '@mastra/core/tools'; import { E2BSandbox, E2BCodeModeTransport } from '@mastra/e2b'; const { tool, instructions } = createCodeMode({ tools, sandbox: new E2BSandbox() }, new E2BCodeModeTransport());
Patch Changes
@mastra/editor@0.13.7
Patch Changes
-
Fixed multi-tenant tool connections for Composio-backed agents. (#19144)
Multi-tenant credential auto-resolve
Agents that use a
caller-suppliedconnection scope now let Composio pick the connected account within each tenant's user bucket, instead of pinning one shared account for every caller. This removes the need to track per-tenant account IDs yourself when building multi-tenant SaaS on top of the Agent Builder.Fixed account authorization
Connecting a new account now uses Composio's supported managed-OAuth link flow, replacing a deprecated endpoint that stopped working for managed OAuth. Connections that collect custom fields (such as a Confluence subdomain) continue to work unchanged.
Config-level connection scope
ComposioToolProvidernow forwardsdefaultScopeto the provider, so you can set a provider's connection scope once at construction (for exampledefaultScope: 'caller-supplied') instead of relying on per-request scope. Every connection authorized against the provider is then bucketed accordingly.
@mastra/inngest@1.8.2
Patch Changes
-
Fix
ToolNotFoundErrorfor workspace/skill tools (skill,skill_read,skill_search,mastra_workspace_*) when a durable agent's steps execute on a cross-process engine (e.g. the@mastra/inngestconnect()worker). (#19331)The durable tool-call step resolved tools only from the per-process
globalRunRegistryplus Mastra-instance-level tools, while the sibling LLM-execution step already rebuilds the full toolset from the agent viaresolveRuntimeDependencies/getToolsForExecution. On a worker process the registry is empty, so the model could callskill(the LLM step saw it) but the tool-call step rejected it withToolNotFoundError. The tool-call step now falls back to rebuilding the toolset from the agent (rebuildRunToolsFromMastra) when the registry misses, resolving workspace/skill tools symmetrically cross-process.resolveRuntimeDependenciesalso now rebuildsinputProcessors/outputProcessors(and writes the rebuilt tools + processors back intoglobalRunRegistry) when the registry entry is a cross-process placeholder, so theSkillsProcessorandWorkspaceInstructionsProcessorrun cross-process too — restoring the available-skills list and workspace instructions in the system prompt on the worker.Placeholder registry entries are detected via a new explicit
RunRegistryEntry.isPlaceholderflag (set by@mastra/inngestwhen seeding resume-segment entries) or the absence of a live model instance — never by an emptytoolsmap, which is a legitimate state for agents configured without tools.Fixes #19330.
-
Fixed Inngest durable agents and workflows so request context values are preserved when runs start, resume, and invoke nested workflows. Previously the trigger and resume events sent an empty request context, so tools, processors, and dynamic resolvers inside a durable run could not see values the caller set (for example tenant or user IDs). (#19223)
-
Fixed durable agents on the Inngest engine ignoring
toolCallConcurrency— parallel tool calls always ran one at a time. Tool calls now run concurrently up to the run'stoolCallConcurrency(default 10). Concurrency is forced to 1 when the run requires tool approval or any tool in the step's effective active tool set requires approval or can suspend, so approval and suspend/resume flows keep working. The concurrency is resolved from the run's own state at execution time, so it stays correct across Inngest replays and concurrent runs. Fixes #19317. (#19329) -
Add MastraNonRetryableError for workflow steps to signal permanent failures and skip retries (#19321)
import { MastraNonRetryableError } from '@mastra/core/error'; throw new MastraNonRetryableError('Invalid template ID');
@mastra/laminar@1.3.4
Patch Changes
- Added PROVIDER_TOOL_CALL to exporter span-type mappings and duration metrics so provider-executed tool spans are classified as tool spans across all observability platforms. (#19261)
@mastra/langsmith@1.3.4
Patch Changes
- Added PROVIDER_TOOL_CALL to exporter span-type mappings and duration metrics so provider-executed tool spans are classified as tool spans across all observability platforms. (#19261)
@mastra/libsql@1.16.0
Minor Changes
-
Added atomic caller-defined dataset IDs for idempotent dataset creation across built-in storage adapters. Supplying
idtomastra.datasets.create()now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throwDATASET_ID_CONFLICT. (#19370) -
Added
LibSQLVector.close()to release the underlying libsql client. For local file databases it checkpoints the WAL and switches back tojournal_mode=DELETEbefore closing (mirroringLibSQLStore.close()), so the-wal/-shmsidecar files and OS handles are released promptly. Safe to call more than once. (#19059) -
Added caller-defined dataset item identities for safe retries across all dataset storage adapters. (#19384)
Dataset items can now include an
externalIdwhen callingaddItemoraddItems:await dataset.addItem({ externalId: 'source-item-123', input: { prompt: 'Hello' }, });
Retrying with the same identity and payload returns the existing item. Reusing an identity with different content returns a typed conflict, including during concurrent writes. Updates and deletes preserve the identity, Spanner retries transactions without changing the outcome, and MySQL batch writes now preserve every supported dataset item field.
Patch Changes
- Raised the
@mastra/corepeer dependency floor to>=1.51.0-0so the dataset item identity helpers used by the storage adapters are available at runtime. (#19384)
@mastra/livekit@0.3.0
Minor Changes
-
Added per-call speech-to-text and text-to-speech selection to
createLiveKitWorker. Set the newconfiguration.sttandconfiguration.ttsresolvers to pick the transcriber and voice for each call — one voice or language per tenant — keyed off the dispatch metadata and request context. Each resolver runs once per call and falls back to the top-levelstt/ttsoption when it returnsundefined. (#19136)export default createLiveKitWorker({ mastra, agent: 'support', stt: 'deepgram/nova-3', tts: 'cartesia/sonic-3', // fallback voice configuration: { // Give each tenant its own voice, resolved per call from the dispatch metadata. tts: ({ requestContext }) => tenantVoices[requestContext?.tenant as string], }, });
Previously the worker's speech pipeline was fixed at construction, so a multi-tenant worker could not vary voices or transcription per call. Customers who own their LiveKit session (the
MastraLLMplugin path) already choose STT/TTS per call by construction; this brings the same flexibility to the batteries-included worker. -
Added
MastraLLM, a standard LiveKit LLM plugin, on the new@mastra/livekit/pluginentry point. Build your ownvoice.AgentSessionand put a Mastra agent in thellmslot — the agent loop, tools, and memory run on a remote Mastra server reached over HTTP, so the worker process needs no Mastra app, database, or model provider keys. (#19136)Before, the worker wrapper always owned the LiveKit session:
import { createLiveKitWorker } from '@mastra/livekit/worker'; import { mastra } from './index'; export default createLiveKitWorker({ mastra, agent: 'support', stt: 'deepgram/nova-3', tts: 'cartesia/sonic-3', });
Now you can own the session and keep Mastra as the LLM component:
import { voice } from '@livekit/agents'; import { MastraLLM } from '@mastra/livekit/plugin'; const session = new voice.AgentSession({ llm: new MastraLLM({ remote: { baseUrl: process.env.MASTRA_URL!, agentId: 'support' }, memory: { thread: callId, resource: userId }, }), stt: 'deepgram/nova-3', tts: 'cartesia/sonic-3', // Required with `memory`: LiveKit enables preemptive generation by default. turnHandling: { preemptiveGeneration: { enabled: false } }, });
createLiveKitWorkerstays the batteries-included path; the plugin is the composable one. Tools keep running server-side on the Mastra agent, and interrupting the agent aborts the server-side generation.New transport and helpers
- Added
createRemoteAgentReplyGenerator(): streams replies from a remote Mastra server over HTTP with per-turn abort, LiveKit-typed errors, and a connect + first-token timeout. It also plugs intocreateLiveKitWorker'sgenerateoption to run the existing worker against a remote server. - Promoted
speakGreeting(),waitForAgentDoneSpeaking(), andrunEndCall()to public exports of@mastra/livekit/worker, so a worker that owns its session can rebuild the greeting and agent-initiated hang-up patterns in a few lines.
Improvements to the existing worker
- Interrupted turns now self-heal: when a caller interrupts a reply, nothing is persisted at that moment, and the part the caller actually heard is backfilled into the memory thread on the next turn — so saved transcripts match the call.
- Added an
onToolCallhook that fires as each tool call starts mid-reply, the building block for tool-driven side effects such as analytics or hang-up. onTurnCompletenow receives the turn's token usage asresult.usage.
- Added
-
Added a
configurationoption tocreateLiveKitWorker— one grouped home for conversation and compliance controls, so these don't each become a separate top-level worker option. It ships with greeting/AI-disclosure controls, a consent model, and agent-initiated hang-up, and is where further compliance controls will land. (#19136)Greeting and AI disclosure
configuration.greetingcontrols the opening line spoken at call start. SetallowInterruptions: falseso a legally-required AI disclosure plays through and can't be talked over (EU AI Act Art. 50),awaitPlayout: trueto hold post-greeting work until it finishes, andrepeatEveryto re-disclose periodically on long calls (spoken at the next turn boundary, never mid-sentence).createLiveKitWorker({ mastra, agent: 'support', configuration: { greeting: { text: 'You are speaking with an AI assistant. This call may be recorded. How can I help?', allowInterruptions: false, awaitPlayout: true, repeatEvery: 3 * 60_000, // re-disclose ~every 3 minutes }, }, });
Per-tenant greeting
greeting.textalso accepts a resolver, called once per call with the call context, so one multi-tenant agent can open differently per tenant based on the dispatch metadata:greeting: { text: ({ metadata }) => `Thanks for calling ${tenantName(metadata)}. You're speaking with an AI assistant.`, allowInterruptions: false, }
Consent
configuration.consentPolicydeclares which data-use consents a call needs, as a named, extensible set (starting withsummaryStorage) rather than one global flag. Declaring the policy enforces nothing by itself: the newcreateConsentToolcaptures the caller's decision at runtime — add it to your agent and it hands each decision to your own store — and your code enforces the requirement atonCallEnd(or before any consent-gated step).import { createConsentTool } from '@mastra/livekit'; // in your agent's tools: recordConsent: createConsentTool({ items: ['summaryStorage'], onGrant: async ({ item, granted, resourceId }) => { if (resourceId) await db.saveConsent(resourceId, item, granted); }, }),
Agent-initiated hang-up
configuration.endCalllets the agent end the call itself. Add the newcreateEndCallToolto your agent and instruct it to say goodbye and then call the tool; the worker waits for the closing words to finish playing, holds a short audio drain (drainMs, default 800ms) so the tail of the goodbye isn't clipped while it's still buffered at the caller, then hangs up — runningonCallEndon the way out, exactly as a caller hang-up does. It works on both the agent and workflow reply paths.import { createEndCallTool } from '@mastra/livekit'; // in your agent's tools: endCall: (createEndCallTool(), // on the worker: createLiveKitWorker({ mastra, agent: 'support', configuration: { endCall: {} } }));
Backwards compatible
The previous top-level
greeting(string) andpersistGreetingoptions still work as deprecated aliases forconfiguration.greeting.textandconfiguration.greeting.persist. When both are set,configuration.greetingwins field by field, so existing worker configs keep running unchanged.
Patch Changes
@mastra/mcp@1.14.0
Minor Changes
-
Add
serverlessStreamingoption toMCPServer.startHTTP()for request-scoped progress notifications in serverless mode. (#17927)Serverless mode (
serverless: true) buffers each request into a single JSON response, which silently drops anynotifications/progressa tool emits viaextra.sendNotification(). Settingoptions: { serverless: true, serverlessStreaming: true }now handles the request with request-scoped SSE streaming (enableJsonResponse: falseon the transient transport), so progress notifications reach the MCP client'sonprogresshandler before the final result. An explicitenableJsonResponseis also honored.await server.startHTTP({ url, httpPath: '/mcp', req, res, options: { serverless: true, serverlessStreaming: true }, });
This is still fully stateless — no
mcp-session-idis required or persisted — and the default behavior is unchanged (enableJsonResponse: true), so existing serverless JSON-response users are unaffected. It enables only notifications scoped to the current request, such as progress; elicitation, resource subscriptions, and out-of-request resource/list-change notifications still require session state. -
Fixed MCP server notifications (resource updates, resource/prompt list changes) not reaching clients connected over streamable HTTP. Notifications are now broadcast to every connected session across all transports. (#19193)
Fixed resource subscriptions being shared globally across all clients. Subscriptions are now tracked per session, so
resources.notifyUpdated()only notifies sessions that subscribed to the resource URI, and one client unsubscribing no longer removes another client's subscription. Clients that relied on receivingnotifications/resources/updatedwithout subscribing must now callresources/subscribefirst.Added support for the remaining MCP notification features:
Dynamic tools and tools/list_changed
Servers can now add and remove tools at runtime and notify clients via
notifications/tools/list_changed:// Server: manage tools at runtime await server.toolActions.add({ myNewTool }); await server.toolActions.remove(['myNewTool']); await server.toolActions.notifyListChanged();
When the server is registered with a Mastra instance, dynamic add/remove also keeps the Mastra instance's tool registry in sync.
// Client: react to tool list changes await mcp.tools.onListChanged('myServer', async () => { const tools = await mcp.listTools(); });
Server-side log messages
Servers can now emit
notifications/messagelog notifications. The minimum level a client sets vialogging/setLevelis honored per session:// Broadcast to all connected clients await server.sendLoggingMessage({ level: 'info', data: { message: 'Sync completed' } }); // From inside a tool, send to the calling client await context.mcp.log('debug', 'Fetching weather', { location });
Progress notifications from tools
Tools can now report progress to the calling client:
await context.mcp.progress({ progress: 1, total: 3, message: 'step 1' });
Patch Changes
-
Fixed MCP clients advertising elicitation support before a handler is registered. (#19192)
-
Fixed
MCPServerdropping a resource's_metafromresources/readresults. The read handler rebuilt each content item with only{ uri, mimeType, text | blob }, soappResources(MCP Apps / SEP-1865) metadata attached duringresources/listas_meta: { ui: meta }never reached the client. Hosts read the UI CSP fromcontents[]._meta.ui.csp, soconnectDomainswas silently ignored and widgetfetch/XHR calls failed withFailed to fetch. The resource's_metais now preserved on the read contents. (#19163)
@mastra/memory@1.23.0
Minor Changes
-
Added one-shot conversation summarization: a standalone
summarizeConversation()function and aMemory.summarizeThread()method. Both distill a whole conversation with the same Observer plumbing that powers Observational Memory — without Observational Memory attached to an agent, and without writing anything back to memory. The summary and extracted values are returned to you (and to each extractor'sonExtractedhook), so you decide where they go. (#19135)import { Extractor } from '@mastra/memory'; import { z } from 'zod'; // e.g. in an end-of-call or end-of-session hook const result = await memory.summarizeThread({ model: 'openai/gpt-5-mini', threadId, resourceId, instructions: 'Summarize this voicemail call for the business owner.', extract: [ new Extractor({ name: 'call-summary', instructions: 'Return a concise summary of the call.', schema: z.object({ summary: z.string(), sentiment: z.enum(['positive', 'neutral', 'negative']), }), metadataKeyPath: false, onExtracted: async ({ current, threadId, resourceId }) => { await callRecords.upsert({ callId: threadId, callerId: resourceId, record: current }); }, }), ], });
summarizeConversation({ model, messages, instructions, extract })takes the same options with messages you already have in hand instead of athreadId.summarizeThreadloads the thread's messages page-by-page starting from the newest, and acceptslastMessages(only summarize the last N messages) andmaxInputTokens(stop loading older messages past this estimated token count, default 1,000,000) to bound how much history is read from storage.Why: session-based agents (for example voice calls) often need a summary or structured extraction of the finished conversation — sentiment, requested services, follow-ups — stored in the application's own database. Observational Memory's observer and extractors are built for exactly this distillation, but attaching OM to an agent just to summarize at session end forces the whole observe/activate lifecycle onto a use case that doesn't need it. These APIs expose the summarization/extraction logic directly.
Patch Changes
-
Fixed a "Converting circular structure to JSON" crash when Observational Memory was used with an input or output processor composed as a workflow (the "run guardrails in parallel" pattern). (#17949)
The circular data reached the processor workflow's snapshot and broke storage: PostgreSQL threw, and LibSQL saved a corrupted snapshot. Observational Memory now keeps its runtime data in a serializable form, so workflow-composed processors work with it and snapshots persist correctly.
-
Improved memory integration coverage for AI SDK v6 models and stabilized replayed semantic recall tests. (#19133)
@mastra/mongodb@1.13.0
Minor Changes
-
Added atomic caller-defined dataset IDs for idempotent dataset creation across built-in storage adapters. Supplying
idtomastra.datasets.create()now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throwDATASET_ID_CONFLICT. (#19370) -
Added caller-defined dataset item identities for safe retries across all dataset storage adapters. (#19384)
Dataset items can now include an
externalIdwhen callingaddItemoraddItems:await dataset.addItem({ externalId: 'source-item-123', input: { prompt: 'Hello' }, });
Retrying with the same identity and payload returns the existing item. Reusing an identity with different content returns a typed conflict, including during concurrent writes. Updates and deletes preserve the identity, Spanner retries transactions without changing the outcome, and MySQL batch writes now preserve every supported dataset item field.
Patch Changes
- Raised the
@mastra/corepeer dependency floor to>=1.51.0-0so the dataset item identity helpers used by the storage adapters are available at runtime. (#19384)
@mastra/mysql@0.4.0
Minor Changes
-
Added atomic caller-defined dataset IDs for idempotent dataset creation across built-in storage adapters. Supplying
idtomastra.datasets.create()now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throwDATASET_ID_CONFLICT. (#19370) -
Added caller-defined dataset item identities for safe retries across all dataset storage adapters. (#19384)
Dataset items can now include an
externalIdwhen callingaddItemoraddItems:await dataset.addItem({ externalId: 'source-item-123', input: { prompt: 'Hello' }, });
Retrying with the same identity and payload returns the existing item. Reusing an identity with different content returns a typed conflict, including during concurrent writes. Updates and deletes preserve the identity, Spanner retries transactions without changing the outcome, and MySQL batch writes now preserve every supported dataset item field.
Patch Changes
- Raised the
@mastra/corepeer dependency floor to>=1.51.0-0so the dataset item identity helpers used by the storage adapters are available at runtime. (#19384)
@mastra/observability@1.16.1
Patch Changes
-
Fixed storage exporters to report persistence failures immediately and retry failed batches automatically with the configured exponential backoff. (#19259)
-
Fixed cost estimation for Amazon Bedrock inference profiles, including geographic prefixes and versioned model IDs. (#19360)
-
Added PROVIDER_TOOL_CALL to exporter span-type mappings and duration metrics so provider-executed tool spans are classified as tool spans across all observability platforms. (#19261)
@mastra/otel-exporter@1.3.4
Patch Changes
- Added PROVIDER_TOOL_CALL to exporter span-type mappings and duration metrics so provider-executed tool spans are classified as tool spans across all observability platforms. (#19261)
@mastra/pg@1.16.0
Minor Changes
-
Added atomic caller-defined dataset IDs for idempotent dataset creation across built-in storage adapters. Supplying
idtomastra.datasets.create()now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throwDATASET_ID_CONFLICT. (#19370) -
Added caller-defined dataset item identities for safe retries across all dataset storage adapters. (#19384)
Dataset items can now include an
externalIdwhen callingaddItemoraddItems:await dataset.addItem({ externalId: 'source-item-123', input: { prompt: 'Hello' }, });
Retrying with the same identity and payload returns the existing item. Reusing an identity with different content returns a typed conflict, including during concurrent writes. Updates and deletes preserve the identity, Spanner retries transactions without changing the outcome, and MySQL batch writes now preserve every supported dataset item field.
Patch Changes
- Raised the
@mastra/corepeer dependency floor to>=1.51.0-0so the dataset item identity helpers used by the storage adapters are available at runtime. (#19384)
@mastra/platform-workspace@0.1.0
Minor Changes
-
Added Mastra Platform workspace providers for connecting agents to Platform sandboxes and bucket-backed filesystems. (#18908)
PlatformFilesystemandPlatformSandboxextendMastraFilesystem/MastraSandboxfrom@mastra/core/workspaceand speak the workspace-proxy wire format (Authorization: Bearer sk_*, project-scoped routes at/v1/projects/:projectId/...). Both accept config directly and fall back to env vars (MASTRA_PLATFORM_ACCESS_TOKEN,MASTRA_PROJECT_ID,MASTRA_ENVIRONMENT_ID,MASTRA_PLATFORM_BUCKET_NAME,MASTRA_WORKSPACE_PROXY_URL).import { Workspace } from '@mastra/core/workspace'; import { PlatformFilesystem, PlatformSandbox } from '@mastra/platform-workspace'; const workspace = new Workspace({ filesystem: new PlatformFilesystem({ bucketName: 'dev-bucket' }), sandbox: new PlatformSandbox({ environmentId: 'env_123', idleTimeoutMinutes: 30, networkIsolation: 'ISOLATED', }), });
Also exports
platformFilesystemProviderandplatformSandboxProviderdescriptors for hosts that register providers dynamically through the editor'sFilesystemProvider/SandboxProviderregistries:import { platformFilesystemProvider, platformSandboxProvider } from '@mastra/platform-workspace'; registry.registerFilesystem(platformFilesystemProvider); registry.registerSandbox(platformSandboxProvider);
Patch Changes
@mastra/playground-ui@41.0.0
Minor Changes
-
Added a shared composable
Plancomponent for rendering markdown plan previews with status, copy, action slots, and collapsed-content controls. (#19155)import { Plan, PlanBody, PlanContent, PlanControls, PlanCopyButton, PlanHeader, PlanHeaderActions, PlanIntro, PlanLabel, PlanMain, PlanPath, PlanTitle, } from '@mastra/playground-ui/components/ai/plan'; <Plan> <PlanHeader> <PlanLabel /> <PlanHeaderActions> <PlanCopyButton content={planMarkdown} /> </PlanHeaderActions> </PlanHeader> <PlanBody> <PlanIntro> <PlanTitle>Review migration plan</PlanTitle> <PlanPath>/workspace/.mastracode/plans/migration.md</PlanPath> </PlanIntro> <PlanMain> <PlanContent>{planMarkdown}</PlanContent> <PlanControls /> </PlanMain> </PlanBody> </Plan>;
Patch Changes
-
Add a paste hint to
EnvironmentVariablesEditorletting users know they can paste a whole.envinto any field. The hint renders bottom-right by default (hidden in read-only mode, opt out viahidePasteHint) and is also exposed as the composableEnvironmentVariablesEditor.PasteHintpart for custom placement/copy. (#19061) -
Improved Tailwind utility consistency across Playground UI components. (#19318)
-
Added PROVIDER_TOOL_CALL span type rendering so provider-executed tool spans display with a distinct label and color in Studio traces. (#19261)
-
Improved command dialogs with accessible labels and keyboard wrapping. (#19229)
-
Fixed scorer data table usability: Score column is now always visible without horizontal scrolling, columns can be shown or hidden via a Columns toggle, and a scrollbar now appears at both the top and bottom of the table (#19055)
-
Improved side panel resizing so programmatic layout changes animate smoothly and respect reduced-motion preferences. (#19337)
-
Fixed data panel content so it scrolls correctly inside constrained layouts. (#19419)
@mastra/rag@2.4.1
Patch Changes
-
Fixed token chunking to reject overlaps that cannot advance. (#19213)
-
Fixed RAG rerank observability spans to record scoring failures before rethrowing. (#19246)
-
Fixed RAG filter parsing to reject JSON string filters that parse to non-object values. (#19244)
-
Fixed Mastra agent relevance scoring to reject invalid model score output. (#19251)
-
Fixed character chunking to preserve content with custom length functions. (#19214)
-
Fixed Cohere relevance scoring to use COHERE_API_KEY by default and accept zero relevance scores. (#19238)
@mastra/react@1.2.5
Patch Changes
- Fixed attachment rendering crash in
useChatwhenenableThreadSignalsis enabled. File attachments now display correctly during live streaming without requiring a page refresh. (#19362)
@mastra/redis-streams@0.3.0
Minor Changes
-
Make
RedisStreamsPubSubsurvive Redis connection drops, and add topic cleanup to bound memory. (#19138)Fixed — a dropped Redis connection (restart, failover, idle reset) no longer wedges the client. The clients were missing the
'error'listener node-redis requires, so a socket drop threw an uncaughtException and left the client unable to reconnect, hanging every later publish. It now reconnects on its own. The read loop also recovers when a stream's consumer group disappears, instead of retrying forever.Added —
clearTopic(topic)deletes a topic's stream so finished runs release their memory instead of accumulating. The durable-agent runtime calls it during cleanup.Added — an opt-in
streamIdleTtlMsoption puts a rolling time-to-live on each stream. Active streams keep refreshing it and never expire mid-flight; abandoned ones (e.g. a crashed run) are removed after they go quiet.const pubsub = new RedisStreamsPubSub({ url: 'redis://localhost:6379', streamIdleTtlMs: 24 * 60 * 60 * 1000, // remove streams idle for 24h }); // free a finished topic's stream immediately await pubsub.clearTopic('workflow.events.run-123');
Patch Changes
@mastra/schema-compat@1.3.4
Patch Changes
-
test(schema-compat): fix 'successful' typo in provider e2e test descriptions (#19167)
-
Fixed a crash when converting Zod v4 schemas containing
z.record(...)throughapplyCompatLayerwith any provider compat layer attached (e.g.GoogleSchemaCompatLayer,OpenAISchemaCompatLayer). (#17052)The record patch is now applied in
toStandardSchemabefore theStandardSchemaWithJSONshort-circuit, so it covers Zod >= 4.2 (which natively exposes~standard.jsonSchemaand bypasses the Zod v4 adapter) as well as older Zod v4 versions that go through the adapter. Affects Zod 4.0.0–4.3.x; the underlyingz.record()bug is fixed upstream in Zod 4.4.0.Fixes #17051.
-
Fixed Meta (Llama) and DeepSeek schemas leaking raw number bounds to the model. A field like
z.number().int()was sent to the model with bogusminimum: -9007199254740991/maximum: 9007199254740991values, andz.number().min(1).max(50)leakedminimum/maximumkeywords, even though OpenAI, Google, and Anthropic already strip these. Numeric constraints are now moved into the field description for Meta and DeepSeek too, matching the other providers. (#19073)Before (Meta/DeepSeek,
z.object({ age: z.number().min(0).max(120) })):{ "age": { "type": "number", "minimum": 0, "maximum": 120 } }After:
{ "age": { "type": "number", "description": "constraints: greater than or equal to 0, lower than or equal to 120" } }Closes #19072.
@mastra/sentry@1.2.4
Patch Changes
- Added PROVIDER_TOOL_CALL to exporter span-type mappings and duration metrics so provider-executed tool spans are classified as tool spans across all observability platforms. (#19261)
@mastra/server@1.51.0
Minor Changes
-
Added an optional session scope to the agent controller API so clients can address independent sessions that share one resource (for example one session per git worktree). (#19357)
Session routes now accept a
sessionScopequery parameter, andAgentController.session()in the client accepts a scope that travels on every request:const controller = client.getAgentController('code'); // Address the worktree's own session instead of the shared one: const session = controller.session('repo-123', '/worktrees/feature-a'); await session.create({ tags: { projectPath: '/worktrees/feature-a' } }); await session.sendMessage('hello');
Requests without a scope behave exactly as before.
-
Added image attachment support to agent controller chat. You can now send images (and other files) with a message, and the Mastra Code web chat lets you attach, paste, or drag-and-drop images which render inline in the transcript. (#19368)
await session.sendMessage({ content: 'What is in this screenshot?', files: [{ data: base64Png, mediaType: 'image/png', filename: 'screenshot.png' }], });
-
Added caller-defined dataset item identities for safe retries across all dataset storage adapters. (#19384)
Dataset items can now include an
externalIdwhen callingaddItemoraddItems:await dataset.addItem({ externalId: 'source-item-123', input: { prompt: 'Hello' }, });
Retrying with the same identity and payload returns the existing item. Reusing an identity with different content returns a typed conflict, including during concurrent writes. Updates and deletes preserve the identity, Spanner retries transactions without changing the outcome, and MySQL batch writes now preserve every supported dataset item field.
-
Added run activity reporting to agent controller sessions.
session.state()responses include arunningflag so UIs can show a working indicator immediately when attaching to a session that is already mid-run, and each thread returned bysession.listThreads()carries astateof'active'or'idle'(backed by the same per-thread run tracking that signalifIdledelivery uses), so one listing can power activity indicators across every worktree or scope sharing a resource instead of polling each session: (#19357)const session = client.getAgentController('code').session(resourceId); const { running } = await session.state(); // is the session mid-run? const threads = await session.listThreads({ tags: { projectPath } }); const busy = threads.filter(thread => thread.state === 'active');
Patch Changes
-
Added HTTP and client bindings for recovering an interrupted durable agent run. (#19191)
What changed
@mastra/server: newPOST /agents/:agentId/recoverroute. Given the id of a durable agent run that was interrupted by a deploy or crash, the server picks the run back up from where it left off and streams the rest of the response to the caller. Non-durable agents are rejected, and callers can only recover runs that belong to them (same permission and ownership rules as resuming a suspended run).@mastra/client-js: newagent.recover({ runId })method that reads that stream from the browser or Node. It behaves the same asagent.resumeStream()— you get back a readable stream of the agent's remaining response.
Why
The underlying core API for recovering an interrupted durable agent run could previously only be called from server-side code. This adds the standard HTTP + client surface so operators can reattach to an interrupted run from a dashboard, an admin tool, or any other client, using the same auth and ownership rules as the rest of the agents API.
Usage
const stream = await mastraClient.getAgent('support').recover({ runId: 'run-abc123' }); for await (const chunk of stream) { // render or forward the remaining agent output }
-
Fixed streamed agent-controller error events losing their message: Error instances are now flattened before serialization so clients see the real failure reason instead of a generic "Error". (#19258)
-
The tool provider authorize handler now falls back to the provider's
defaultScopewhen a request does not specify a connectionscope. Precedence is: explicit requestscope, then the provider'sdefaultScope, then'per-author'. This lets a provider configured withdefaultScope: 'caller-supplied'produce per-tenant connections through the Agent Builder connect flow, which does not send a scope. (#19144)
@mastra/spanner@1.3.0
Minor Changes
-
Added atomic caller-defined dataset IDs for idempotent dataset creation across built-in storage adapters. Supplying
idtomastra.datasets.create()now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throwDATASET_ID_CONFLICT. (#19370) -
Added caller-defined dataset item identities for safe retries across all dataset storage adapters. (#19384)
Dataset items can now include an
externalIdwhen callingaddItemoraddItems:await dataset.addItem({ externalId: 'source-item-123', input: { prompt: 'Hello' }, });
Retrying with the same identity and payload returns the existing item. Reusing an identity with different content returns a typed conflict, including during concurrent writes. Updates and deletes preserve the identity, Spanner retries transactions without changing the outcome, and MySQL batch writes now preserve every supported dataset item field.
Patch Changes
-
resolve silent exception swallowing in rollback handlers (#18606)
-
Raised the
@mastra/corepeer dependency floor to>=1.51.0-0so the dataset item identity helpers used by the storage adapters are available at runtime. (#19384)
@mastra/temporal@0.2.7
Patch Changes
-
Fixed Temporal workflows to apply the configured activity start-to-close timeout in generated worker bundles. (#19347)
-
Added support for
foreachconcurrency resolver functions in the Temporal workflow runtime, matching the new@mastra/corebehavior: (#19329)workflow .foreach(step, { concurrency: ({ inputData, getInitData }) => (getInitData().fast ? 10 : 1), }) .commit();
@mastra/voice-google@0.14.0
Minor Changes
-
Added support for SSML, custom pronunciations, multi-speaker markup, and Gemini-TTS model selection in
GoogleVoice.speak(). Existing text-only usage is unchanged. (#19425)New
options.inputfields: passssml,markup,customPronunciations,multiSpeakerMarkup, orprompt(for Gemini-TTS style steering) directly to the Google Cloud TTS API.New
options.voicefields: setmodelName(e.g.gemini-2.5-flash-preview-tts) ormultiSpeakerVoiceConfigalongside the defaultnameandlanguageCode.// SSML with custom pronunciations await voice.speak('Give Metacam to the patient.', { input: { ssml: '<speak>Give <phoneme alphabet="ipa" ph="mɛtəˈkæm">Metacam</phoneme>.</speak>', }, }); // Gemini-TTS with prompt-driven styling await voice.speak('Hello!', { voice: { name: 'Kore', modelName: 'gemini-2.5-flash-preview-tts' }, input: { prompt: 'Warm, calm tone.' }, });
-
Added Cloud Speech-to-Text v2 support to
GoogleVoice.listen(). Pass{ v2: true }in options to use the v2 API, which supports additional audio formats like AAC-in-MP4 (iOS Safari) viaautoDecodingConfigorexplicitDecodingConfig. The v1 path remains the default — no breaking changes. (#19422)
@mastra/voice-speechify@0.14.0
Minor Changes
-
Added support for Speechify's Simba 3.2 and Simba 3.0 text-to-speech models. Simba 3.2 is Speechify's latest streaming model with lower latency and richer expressivity, and is the recommended model for English speech. (#19411)
import { SpeechifyVoice } from '@mastra/voice-speechify'; // Set as the default model const voice = new SpeechifyVoice({ speechModel: { name: 'simba-3.2' }, }); // Or override per request const stream = await voice.speak('Hello world', { model: 'simba-3.2' });
Note:
simba-3.2andsimba-3.0are currently English only. The default model remainssimba-english.
Patch Changes
-
Fixed voice selection for the Simba 3 models. Speechify's
simba-3.2andsimba-3.0serve a curated voice set only (beatrice_32,dominic_32,edmund_32,geffen_32,harper_32,hugh_32,imogen_32,wyatt_32), so pairing them with a classic catalog voice likegeorgefailed with an API error. (#19415)- Added the curated Simba 3 voices to the voice list, so they type-check as
speakerand appear ingetSpeakers() - The default speaker now follows the configured model:
harper_32for Simba 3 models,georgeotherwise
import { SpeechifyVoice } from '@mastra/voice-speechify'; // Works out of the box now — defaults to the harper_32 voice const voice = new SpeechifyVoice({ speechModel: { name: 'simba-3.2' }, }); // Or pick a curated voice explicitly new SpeechifyVoice({ speechModel: { name: 'simba-3.2' }, speaker: 'imogen_32', });
When overriding the model per request, pass a matching speaker too:
voice.speak('Hi', { model: 'simba-3.2', speaker: 'harper_32' }). - Added the curated Simba 3 voices to the voice list, so they type-check as
Other updated packages
The following packages were updated with dependency changes only:
- @mastra/agent-builder@1.1.6
- @mastra/arize@1.3.4
- @mastra/arthur@0.4.4
- @mastra/deployer-cloud@1.51.0
- @mastra/deployer-cloudflare@1.2.7
- @mastra/deployer-netlify@1.2.7
- @mastra/deployer-vercel@1.2.7
- @mastra/express@1.4.7
- @mastra/fastify@1.4.7
- @mastra/gcs@0.3.1
- @mastra/google-cloud-pubsub@1.1.2
- @mastra/hono@1.5.7
- @mastra/koa@1.6.7
- @mastra/langfuse@1.4.4
- @mastra/longmemeval@1.1.7
- @mastra/mcp-docs-server@1.2.7
- @mastra/nestjs@0.2.7
- @mastra/next@0.2.6
- @mastra/opencode@0.1.7
- @mastra/otel-bridge@1.4.2
- @mastra/posthog@1.1.4
- @mastra/railway@0.3.1
- @mastra/tanstack-start@0.2.6
- @mastra/voice-google-gemini-live@0.14.4
- @mastra/voice-openai-realtime@0.13.4
- @mastra/voice-xai-realtime@0.2.4