github mastra-ai/mastra @mastra/core@1.51.0
July 15, 2026

4 hours ago

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 AgentController so independent sessions can run in parallel over the same resource (for example one session per git worktree). (#19357)

    Previously createSession() was get-or-create by resourceId alone, 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 new scope option 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 resourceId and scope still resume the same session (get-or-create), and unscoped sessions behave exactly as before.

  • Added support for custom processors on createScorer judges. You can now pass inputProcessors, outputProcessors, errorProcessors, and maxProcessorRetries in a scorer's judge config (or per-step judge config) to apply processors to the internal judge agent — for example wiring up StreamErrorRetryProcessor to 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 foreach concurrency at execution time. concurrency can 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. scorers is optional when at least one gate is provided. (#19348)

  • Added maxRetryAfterMs to StreamErrorRetryProcessor, with a default of 30 seconds, so provider Retry-After waits 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)

    clearTopic is now part of the PubSub base 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 — CachingPubSub and the durable-agent runtime now forward clearTopic unconditionally.

  • 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 id to mastra.datasets.create() now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throw DATASET_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 MastraCompositeStore by setting it to false in the domains config. A disabled domain no longer falls back to the editor or default store, 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-call retention option 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 externalId when calling addItem or addItems:

    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 log and progress functions 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 to mastra.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 like storage.ts and observability.ts. A code-registered logger still wins.

    Register scorers per agent by adding an agents/<name>/scorers/ folder. Each module's default export (a MastraScorer, or a { scorer, sampling } entry) is wired into that agent, keyed by filename. config.scorers wins 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 scope field to ResolveToolsOpts so tool providers can see a connection's identity bucketing (per-author, shared, or caller-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 defaultScope to BaseToolProviderOptions (surfaced on the ToolProvider interface as defaultScope). This lets an app author set a tool provider's connection scope at config time — for example defaultScope: '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.
        }
        // ...
      }
    }
  • FixedCachingPubSub.clearTopic now forwards to the wrapped transport. Because the durable-agent runtime wraps every pubsub in CachingPubSub, 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 calls clearTopic on the inner transport when the inner implements it. (#19138)

  • Fixed Memory generateTitle never firing for durable agents created with createEventedAgent (including Inngest). Thread titles now generate and persist on the durable path when generateTitle is configured, including when Observational Memory is enabled. Existing thread titles are preserved. (#19315)

  • Fix durable agents orphaning per-step output processor spans. In createDurableLLMExecutionStep, the runProcessOutputStep(...) call omitted tracingContext (unlike the sibling runProcessInputStep/runProcessLLMRequest calls), so output step processor processor_run spans were created without a parent and appeared as their own root traces — one per LLM step — instead of nesting under MODEL_STEPAGENT_RUN. Passing tracingContext: modelSpanTracker?.getTracingContext() ?? tracingContext restores parity with the non-durable path. Fixes #19312. (#19313)

  • Fix ToolNotFoundError for 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/inngest connect() worker). (#19331)

    The durable tool-call step resolved tools only from the per-process globalRunRegistry plus Mastra-instance-level tools, while the sibling LLM-execution step already rebuilds the full toolset from the agent via resolveRuntimeDependencies/getToolsForExecution. On a worker process the registry is empty, so the model could call skill (the LLM step saw it) but the tool-call step rejected it with ToolNotFoundError. 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.

    resolveRuntimeDependencies also now rebuilds inputProcessors/outputProcessors (and writes the rebuilt tools + processors back into globalRunRegistry) when the registry entry is a cross-process placeholder, so the SkillsProcessor and WorkspaceInstructionsProcessor run 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.isPlaceholder flag (set by @mastra/inngest when seeding resume-segment entries) or the absence of a live model instance — never by an empty tools map, 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 iterationCount always being 1 for dountil and dowhile loops on the evented workflow engine. The count was never carried forward between iterations, so any loop whose condition depended on iterationCount never 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 createHandler option type on registerApiRoute. 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 internal ApiRoute type. (#19320)

  • Fixed DurableAgent losing reasoning items on turns that include tool calls, which caused OpenAI reasoning models like gpt-5-mini to 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 live Workflow instance by reference and JSON.stringify'd it into the map step's mapConfig, 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. The initData mapping is now serialized as a slim { initData: <id>, path } reference. Runtime behavior is unchanged (the execute path only reads initData for 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 abort finish reason instead of crashing. The sendMessage and sendSignal wake 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.text so 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 through fullStream, and per-step text remains available on steps. 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 transform policy, 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 Agent used directly without being registered on a Mastra instance) 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 entityId now uses the durable agent's ID instead of the wrapped agent's ID
    • Fixed dropped background-task-completed events in streamUntilIdle caused by the same agent ID mismatch
    • \_\_setMemory, \_\_setPubSub, and \_\_setWorkspace now 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 (mirrors listSuspendedRuns but for running status).
    • 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 as stream()/resume(), so callers can stream the recovered response or attach later via observe(runId). Memory writes flow through the rebuilt SaveQueueManager exactly like a fresh run.
    • DurableAgent.recoverActiveRuns(options?) — bulk recovery hook that delegates to recover(runId) for each in-flight run discovered via listActiveRuns() (or a caller-supplied runId), awaits each workflow settlement, and returns { recovered, succeeded, failed } counts. Use this on boot to drain the backlog; use recover() when you want to stream a specific run.
    • The default workflow engine now persists running snapshots for the durable agentic loop, with a guard that prevents a running write from overwriting an already-suspended snapshot 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-check step waits for pre-crash in-flight tasks (still tracked in BackgroundTaskManager storage), the tool-call step can still dispatch new tools as background tasks, and llm-execution still 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 initial run.start()), resume(), recover(), and recoverActiveRuns(). 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 exposing recoverActiveRuns() (default-engine DurableAgent only — 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.recovery option: recovery?: { durableAgents?: 'auto' | 'off' } (default 'off'). When set to 'auto', the deployer's /__restart-active-workflow-runs boot handler will invoke mastra.recoverAllDurableAgents() right after restartAllActiveWorkflowRuns(), so both user workflows and durable-agent runs are re-driven from the same boot hook the CLI/deployer already call. Also exposed as mastra.recoveryConfig for 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 call mastra.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 globalRunRegistry was 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 had prepare/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 mapUserToResourceId to MastraAuthWorkosOptions so it can be set directly in the MastraAuthWorkos constructor. This maps an authenticated user to a resource id that multi-tenant tool providers use to bucket connected accounts (for example, Composio's caller-supplied scope). 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_tool in the Braintrust Messages tab. Tool spans (TOOL_CALL, MCP_TOOL_CALL, and PROVIDER_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 run npx mastra migrate to copy historical spans from mastra_ai_spans to the VNext mastra_span_events schema. 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 sessionScope query parameter, and AgentController.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 externalId when calling addItem or addItems:

    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 a running flag so UIs can show a working indicator immediately when attaching to a session that is already mid-run, and each thread returned by session.listThreads() carries a state of 'active' or 'idle' (backed by the same per-thread run tracking that signal ifIdle delivery 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: new POST /agents/:agentId/recover route. 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: new agent.recover({ runId }) method that reads that stream from the browser or Node. It behaves the same as agent.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 extraTools providers in MastraCodeConfig. The extraTools option 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/settings and added MastraCodeGateway.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/mastracode package), so third parties can build their own UIs and surfaces on top of the Mastra Code coding agent. The mastracode CLI 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.db that 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 example LibSQLStore({ url: 'file:./data.db' }) from @mastra/core), which previously blocked mastra server deploy and forced --skip-preflight even 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 (mirrors listSuspendedRuns but for running status).
    • 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 as stream()/resume(), so callers can stream the recovered response or attach later via observe(runId). Memory writes flow through the rebuilt SaveQueueManager exactly like a fresh run.
    • DurableAgent.recoverActiveRuns(options?) — bulk recovery hook that delegates to recover(runId) for each in-flight run discovered via listActiveRuns() (or a caller-supplied runId), awaits each workflow settlement, and returns { recovered, succeeded, failed } counts. Use this on boot to drain the backlog; use recover() when you want to stream a specific run.
    • The default workflow engine now persists running snapshots for the durable agentic loop, with a guard that prevents a running write from overwriting an already-suspended snapshot 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-check step waits for pre-crash in-flight tasks (still tracked in BackgroundTaskManager storage), the tool-call step can still dispatch new tools as background tasks, and llm-execution still 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 initial run.start()), resume(), recover(), and recoverActiveRuns(). 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 exposing recoverActiveRuns() (default-engine DurableAgent only — 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.recovery option: recovery?: { durableAgents?: 'auto' | 'off' } (default 'off'). When set to 'auto', the deployer's /__restart-active-workflow-runs boot handler will invoke mastra.recoverAllDurableAgents() right after restartAllActiveWorkflowRuns(), so both user workflows and durable-agent runs are re-driven from the same boot hook the CLI/deployer already call. Also exposed as mastra.recoveryConfig for 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 call mastra.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 globalRunRegistry was 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 had prepare/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.db is only used as a fallback behind an environment variable (for example process.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_PATH errors: dependencies installed via symlinks (pnpm link:/file:) resolve to paths outside node_modules and 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 like storage.ts and observability.ts. A code-registered logger still wins.

    Register scorers per agent by adding an agents/<name>/scorers/ folder. Each module's default export (a MastraScorer, or a { scorer, sampling } entry) is wired into that agent, keyed by filename. config.scorers wins 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 E2BCodeModeTransport for running Code Mode in an E2BSandbox. (#19372)

    The default StdioCodeModeTransport writes the runner/program to the host tmpdir and spawns node <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. E2BCodeModeTransport writes 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-supplied connection 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

    ComposioToolProvider now forwards defaultScope to the provider, so you can set a provider's connection scope once at construction (for example defaultScope: '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 ToolNotFoundError for 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/inngest connect() worker). (#19331)

    The durable tool-call step resolved tools only from the per-process globalRunRegistry plus Mastra-instance-level tools, while the sibling LLM-execution step already rebuilds the full toolset from the agent via resolveRuntimeDependencies/getToolsForExecution. On a worker process the registry is empty, so the model could call skill (the LLM step saw it) but the tool-call step rejected it with ToolNotFoundError. 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.

    resolveRuntimeDependencies also now rebuilds inputProcessors/outputProcessors (and writes the rebuilt tools + processors back into globalRunRegistry) when the registry entry is a cross-process placeholder, so the SkillsProcessor and WorkspaceInstructionsProcessor run 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.isPlaceholder flag (set by @mastra/inngest when seeding resume-segment entries) or the absence of a live model instance — never by an empty tools map, 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's toolCallConcurrency (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 id to mastra.datasets.create() now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throw DATASET_ID_CONFLICT. (#19370)

  • Added LibSQLVector.close() to release the underlying libsql client. For local file databases it checkpoints the WAL and switches back to journal_mode=DELETE before closing (mirroring LibSQLStore.close()), so the -wal/-shm sidecar 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 externalId when calling addItem or addItems:

    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/core peer dependency floor to >=1.51.0-0 so 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 new configuration.stt and configuration.tts resolvers 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-level stt / tts option when it returns undefined. (#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 MastraLLM plugin 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/plugin entry point. Build your own voice.AgentSession and put a Mastra agent in the llm slot — 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 } },
    });

    createLiveKitWorker stays 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 into createLiveKitWorker's generate option to run the existing worker against a remote server.
    • Promoted speakGreeting(), waitForAgentDoneSpeaking(), and runEndCall() 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 onToolCall hook that fires as each tool call starts mid-reply, the building block for tool-driven side effects such as analytics or hang-up.
    • onTurnComplete now receives the turn's token usage as result.usage.
  • Added a configuration option to createLiveKitWorker — 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.greeting controls the opening line spoken at call start. Set allowInterruptions: false so a legally-required AI disclosure plays through and can't be talked over (EU AI Act Art. 50), awaitPlayout: true to hold post-greeting work until it finishes, and repeatEvery to 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.text also 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.consentPolicy declares which data-use consents a call needs, as a named, extensible set (starting with summaryStorage) rather than one global flag. Declaring the policy enforces nothing by itself: the new createConsentTool captures 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 at onCallEnd (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.endCall lets the agent end the call itself. Add the new createEndCallTool to 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 — running onCallEnd on 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) and persistGreeting options still work as deprecated aliases for configuration.greeting.text and configuration.greeting.persist. When both are set, configuration.greeting wins field by field, so existing worker configs keep running unchanged.

Patch Changes

@mastra/mcp@1.14.0

Minor Changes

  • Add serverlessStreaming option to MCPServer.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 any notifications/progress a tool emits via extra.sendNotification(). Setting options: { serverless: true, serverlessStreaming: true } now handles the request with request-scoped SSE streaming (enableJsonResponse: false on the transient transport), so progress notifications reach the MCP client's onprogress handler before the final result. An explicit enableJsonResponse is also honored.

    await server.startHTTP({
      url,
      httpPath: '/mcp',
      req,
      res,
      options: { serverless: true, serverlessStreaming: true },
    });

    This is still fully stateless — no mcp-session-id is 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 receiving notifications/resources/updated without subscribing must now call resources/subscribe first.

    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/message log notifications. The minimum level a client sets via logging/setLevel is 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 MCPServer dropping a resource's _meta from resources/read results. The read handler rebuilt each content item with only { uri, mimeType, text | blob }, so appResources (MCP Apps / SEP-1865) metadata attached during resources/list as _meta: { ui: meta } never reached the client. Hosts read the UI CSP from contents[]._meta.ui.csp, so connectDomains was silently ignored and widget fetch/XHR calls failed with Failed to fetch. The resource's _meta is now preserved on the read contents. (#19163)

@mastra/memory@1.23.0

Minor Changes

  • Added one-shot conversation summarization: a standalone summarizeConversation() function and a Memory.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's onExtracted hook), 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 a threadId.

    summarizeThread loads the thread's messages page-by-page starting from the newest, and accepts lastMessages (only summarize the last N messages) and maxInputTokens (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 id to mastra.datasets.create() now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throw DATASET_ID_CONFLICT. (#19370)

  • Added caller-defined dataset item identities for safe retries across all dataset storage adapters. (#19384)

    Dataset items can now include an externalId when calling addItem or addItems:

    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/core peer dependency floor to >=1.51.0-0 so 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 id to mastra.datasets.create() now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throw DATASET_ID_CONFLICT. (#19370)

  • Added caller-defined dataset item identities for safe retries across all dataset storage adapters. (#19384)

    Dataset items can now include an externalId when calling addItem or addItems:

    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/core peer dependency floor to >=1.51.0-0 so 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 id to mastra.datasets.create() now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throw DATASET_ID_CONFLICT. (#19370)

  • Added caller-defined dataset item identities for safe retries across all dataset storage adapters. (#19384)

    Dataset items can now include an externalId when calling addItem or addItems:

    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/core peer dependency floor to >=1.51.0-0 so 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)

    PlatformFilesystem and PlatformSandbox extend MastraFilesystem / MastraSandbox from @mastra/core/workspace and 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 platformFilesystemProvider and platformSandboxProvider descriptors for hosts that register providers dynamically through the editor's FilesystemProvider / SandboxProvider registries:

    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 Plan component 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 EnvironmentVariablesEditor letting users know they can paste a whole .env into any field. The hint renders bottom-right by default (hidden in read-only mode, opt out via hidePasteHint) and is also exposed as the composable EnvironmentVariablesEditor.PasteHint part 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 useChat when enableThreadSignals is enabled. File attachments now display correctly during live streaming without requiring a page refresh. (#19362)

@mastra/redis-streams@0.3.0

Minor Changes

  • Make RedisStreamsPubSub survive 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.

    AddedclearTopic(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 streamIdleTtlMs option 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(...) through applyCompatLayer with any provider compat layer attached (e.g. GoogleSchemaCompatLayer, OpenAISchemaCompatLayer). (#17052)

    The record patch is now applied in toStandardSchema before the StandardSchemaWithJSON short-circuit, so it covers Zod >= 4.2 (which natively exposes ~standard.jsonSchema and 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 underlying z.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 bogus minimum: -9007199254740991 / maximum: 9007199254740991 values, and z.number().min(1).max(50) leaked minimum/maximum keywords, 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 sessionScope query parameter, and AgentController.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 externalId when calling addItem or addItems:

    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 a running flag so UIs can show a working indicator immediately when attaching to a session that is already mid-run, and each thread returned by session.listThreads() carries a state of 'active' or 'idle' (backed by the same per-thread run tracking that signal ifIdle delivery 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: new POST /agents/:agentId/recover route. 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: new agent.recover({ runId }) method that reads that stream from the browser or Node. It behaves the same as agent.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 defaultScope when a request does not specify a connection scope. Precedence is: explicit request scope, then the provider's defaultScope, then 'per-author'. This lets a provider configured with defaultScope: '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 id to mastra.datasets.create() now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throw DATASET_ID_CONFLICT. (#19370)

  • Added caller-defined dataset item identities for safe retries across all dataset storage adapters. (#19384)

    Dataset items can now include an externalId when calling addItem or addItems:

    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/core peer dependency floor to >=1.51.0-0 so 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 foreach concurrency resolver functions in the Temporal workflow runtime, matching the new @mastra/core behavior: (#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.input fields: pass ssml, markup, customPronunciations, multiSpeakerMarkup, or prompt (for Gemini-TTS style steering) directly to the Google Cloud TTS API.

    New options.voice fields: set modelName (e.g. gemini-2.5-flash-preview-tts) or multiSpeakerVoiceConfig alongside the default name and languageCode.

    // 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) via autoDecodingConfig or explicitDecodingConfig. 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.2 and simba-3.0 are currently English only. The default model remains simba-english.

Patch Changes

  • Fixed voice selection for the Simba 3 models. Speechify's simba-3.2 and simba-3.0 serve 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 like george failed with an API error. (#19415)

    • Added the curated Simba 3 voices to the voice list, so they type-check as speaker and appear in getSpeakers()
    • The default speaker now follows the configured model: harper_32 for Simba 3 models, george otherwise
    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' }).

Other updated packages

The following packages were updated with dependency changes only:

Don't miss a new mastra release

NewReleases is sending notifications on new releases.