github mastra-ai/mastra @mastra/core@1.76.0
October 11, 2026

3 hours ago

Highlights

Durable agent run ownership + storage-level run fencing

Crash recovery is now safe across multiple instances: runs are claimed by the execution driving them, recovery skips live-claimed runs (or can force take over), and storage adapters can enforce “run fences” to reject writes from an execution that lost ownership—preventing message/thread/workflow state corruption.

Atomic working-memory merges on PostgreSQL (and explicit merge mode over HTTP)

Concurrent schema-based working memory updates no longer stomp each other in Postgres: with scope: 'resource', partial updates are deep-merged inside a row-locked transaction. You can also explicitly mode: 'merge' via the server/client APIs, and unsupported adapters now fail with a clear 400 instead of silently replacing records.

File-handling resilience + sandbox-first file workflows

Agents no longer get “stuck” when a provider rejects an attached file: the new default UnsupportedFileHandler retries with the file removed (or replaced by a sandbox path when available). For file-centric tool workflows, the new FileUploadProcessor can upload eligible user files into the workspace sandbox and pass the model a sandbox path instead of raw file content.

Observational Memory now supports native model: 'auto' (new default)

Observer/Reflector model selection can follow the active main model/provider automatically per call, while explicit model IDs stay pinned. This is now the default for OM (model, observation.model, reflection.model), with hooks (autoModels, resolveModel, resolveAutoModelId) to control how “auto” resolves in your environment.

New observability nesting + richer trace search

tracingOptions.nestUnderParent (and nestUnderRun()) lets you attach a run as a true child span under an earlier run in the same trace (e.g., LLM judges), preserving the parent run’s trace-level identity. Trace querying also gains word/phrase matches / notMatches operators (across stores/types), and metrics add a usageId to group token/cost rows per model call.

Breaking Changes

  • @mastra/playground-ui: old Metrics card views/components were removed in favor of the new self-contained Metrics cards/dashboard building blocks.
  • @mastra/playground-ui: MetricsCardGroup no longer accepts variant; MetricsLineChart dropped xAxisInterval/xAxisMinTickGap.
  • @mastra/memory Observational Memory defaults changed: OM now defaults to model: 'auto' (instead of google/gemini-2.5-flash), which may select a different provider/model depending on available credentials.

Changelog

@mastra/core@1.76.0

Minor Changes

  • Concurrent schema-based working memory updates no longer lose each other's fields when using PostgreSQL. With scope: 'resource', the update tool now deep-merges partial updates inside a row-locked transaction, so writers in separate processes that change different fields all keep their changes. Other storage adapters keep the existing in-process behavior. (#25848)

    You can also merge from your own code or over HTTP:

    await memory.mergeWorkingMemory({ threadId, resourceId, workingMemory: { city: "Berlin" } });
    
    await client.updateWorkingMemory({
      agentId,
      threadId,
      resourceId,
      workingMemory: JSON.stringify({ city: "Berlin" }),
      mode: "merge"
    });

    Merges are rejected (HTTP 400 from the server) when the storage adapter doesn't support atomic merges, instead of silently replacing the record.

    Also in: @mastra/client-js@1.53.0, @mastra/memory@1.37.0, @mastra/pg@1.31.0, @mastra/server@1.76.0

  • processLLMRequest now receives the workspace of the step, the one the agent's tools use, so a processor can work with the agent's sandbox or filesystem before a model call. (#25814)

    const processor: Processor = {
      id: "my-processor",
      async processLLMRequest({ prompt, workspace }) {
        if (workspace?.hasSandboxConfig()) {
          // ...
        }
      }
    };
  • Added threadMetadata to the onDelegationStart return value so delegated sub-agent threads can carry metadata such as a tenant ID. The metadata is applied when the sub-agent thread is created, which makes those threads findable with metadata-filtered queries. (#25200)

    await supervisor.generate("Summarize the report", {
      memory: { thread: "parent-thread", resource: "user-1" },
      delegation: {
        onDelegationStart: () => ({ threadMetadata: { tenantId: "acme" } })
      }
    });
    
    await memory.listThreads({ filter: { metadata: { tenantId: "acme" } } });
  • Fixed durable agent crash recovery re-running runs that were still executing. Recovery restarted every run left in the running state, even when another process was still driving it. Both executions then wrote to the same thread, and the original one could overwrite the recovered assistant messages (#23734). (#26418)

    Each run is now claimed by the execution driving it: stream(), generate(), resume(), or recover(). The claim is renewed while the run executes and lapses about 30 seconds after its process dies.

    • recoverActiveRuns() skips runs whose claim is still live. Skipped runs are returned with status: 'skipped', a reason, and, for a run live in another process, a retryAt time.
    • mastra.recoverAllDurableAgents() counts skipped runs in a new skipped field and checks each one again once its claim could have lapsed, so a run whose process crashed is still recovered.
    • recover() throws DURABLE_AGENT_RUN_ACTIVE for a live run. Pass force: true to take the run over. The execution that held it stops.
    • recover() throws DURABLE_AGENT_RECOVER_RUN_ACTIVE_LOCALLY for a run the same process is still executing, even with force: true. Previously it started a second execution of the run. Wait for the run to finish instead.
    • Starting a run with a runId that another execution holds waits up to 5 seconds, then throws DURABLE_AGENT_EXECUTION_CONFLICT.
    • mastra.shutdown() releases runs still executing after the drain window, so the next boot recovers them right away.
    • Run streams drop events from an execution that lost its run. With a storage adapter that supports run fencing, that execution's writes are also rejected, so it can't overwrite messages, threads, or workflow state. A warning is logged when the workflows or memory store doesn't support run fencing: whenever runs are recovered, through recoverActiveRuns() or recover(), and, with recovery: { durableAgents: 'auto' }, when a run starts.
    const { recovered } = await durableAgent.recoverActiveRuns();
    
    for (const run of recovered) {
      if (run.status === "skipped" && run.reason === "run-active") {
        console.log(`${run.runId} is still running. Check again at ${new Date(run.retryAt!)}`);
      }
    }
    
    // Take a run over even though another execution still holds it
    const stream = await durableAgent.recover(runId, { force: true });

    See Run ownership for how claims are enforced and how recovery works across multiple instances.

  • Added tracingOptions.nestUnderParent to attach a run as a child of an earlier run in the same trace, for example an LLM judge under the run it scores. The trace then keeps the name, input, output, and tags of the run that started it. The new nestUnderRun() helper builds these options from the earlier run's result. (#25691)

    import { nestUnderRun } from "@mastra/core/observability";
    
    const turn = await assistant.generate("When does my order ship?");
    
    await judge.generate("Grade this answer", {
      tracingOptions: nestUnderRun(turn)
      // same as: { traceId: turn.traceId, parentSpanId: turn.spanId, nestUnderParent: true }
    });

    Runs that pass parentSpanId without the new option behave as before.

  • Added getModelReasoningOptions to read which reasoning controls a model accepts, as published by models.dev: named effort levels, a thinking-token budget, or an on/off toggle. Returns [] when models.dev lists the model without reasoning controls, and undefined when no data describes the model. (#26225)

    import { getModelReasoningOptions } from "@mastra/core/llm";
    
    getModelReasoningOptions("anthropic/claude-sonnet-4-6");
    // [{ type: 'effort', values: ['low', 'medium', 'high', 'max'] }, { type: 'budget_tokens', min: 1024 }]
    
    getModelReasoningOptions("anthropic/claude-haiku-4-5");
    // [{ type: 'budget_tokens', min: 1024 }]

    Use it to offer only the reasoning levels a model supports instead of hard-coding them per model.

  • Agents now keep working when the model rejects a file the user sent. Before, the model call failed, and since the file stayed in the thread, every later turn failed the same way, even a text-only one. (#25814)

    A new default error processor, UnsupportedFileHandler, takes the rejected file out of the prompt and calls the model again. When the agent's workspace has a sandbox, the file is uploaded there and the model gets its path, so the agent can still work on it with its tools. Otherwise, the model gets the [Attachment unavailable: <name>] placeholder Mastra already uses for attachments it can't use. Only the prompt changes: the stored message keeps the file, so a thread that already holds such a file works again on its next turn.

    It handles the refusals that provider SDKs, such as OpenAI's and Anthropic's, raise before the request goes out, and the errors a provider returns over HTTP for a request that holds such a file, like Gemini's 502. A rate limit or another error that asks to try again later keeps the file: the request is sent again unchanged. Every agent gets it, alongside the other default error processors, and errorProcessorDefaults: false turns it off with them. Uploaded files can be read by other threads that share the same sandbox.

  • Rearchitected an experimental memory feature. (#26024)

    Also in: @mastra/clickhouse@1.24.0, @mastra/cloudflare@1.7.2, @mastra/code-sdk@1.12.0, @mastra/factory@0.21.0, @mastra/libsql@1.26.0, @mastra/memory@1.37.0, @mastra/mongodb@1.23.0, @mastra/mysql@0.13.0, @mastra/pg@1.31.0

  • Agents using the default error processors now recover from Gemini's intermittent MALFORMED_FUNCTION_CALL finish instead of failing the turn with AGENT_STREAM_ERROR. The default stream-error-retry-processor resends the failed step unchanged up to 2 times with a short backoff. No tool runs on that step, so the retry cannot repeat side effects. Durable agents are not covered yet. (#26612)

    The matcher is exported as isMalformedFunctionCallError, so a custom StreamErrorRetryProcessor can reuse it:

    import { StreamErrorRetryProcessor, isMalformedFunctionCallError } from "@mastra/core/processors";
    
    const agent = new Agent({
      // ...
      errorProcessors: [new StreamErrorRetryProcessor({ matchers: [isMalformedFunctionCallError], maxRetries: 2 })]
    });
  • Added FileUploadProcessor, an input processor that uploads the files a user sends to the workspace sandbox and gives the model the sandbox path instead of the file content. (#25814)

    Use it when an agent works on user files with its sandbox tools. A filter function decides, file by file, what is uploaded; the files it rejects, such as images, keep going to the model. Only the prompt sent to the model changes: the stored messages keep their files, so the thread and clients still have the original. Memory is optional.

    Files are named after a hash of their content, so a file is written once and the next turns only check that it's still in the sandbox. Files of the thread history and files sent with signals are handled too. The processor never downloads anything itself: a link the model fetches, or a provider file ID, is left to the model.

    Pass the same workspace to the agent and to the processor:

    import { FileUploadProcessor, FILE_UPLOAD_ERROR_CODES } from "@mastra/core/processors";
    import type { FileUploadTripwireMetadata } from "@mastra/core/processors";
    
    const agent = new Agent({
      // ...
      workspace,
      inputProcessors: [
        new FileUploadProcessor({
          workspace,
          filter: ({ mimeType, extension }) => mimeType === "application/pdf" || extension === "xlsx",
          maxFileSize: () => 25 * 1024 * 1024
        })
      ]
    });
    
    const result = await agent.generate(messages);
    
    if (result.tripwire?.processorId === "file-upload") {
      const { code } = result.tripwire.metadata as FileUploadTripwireMetadata;
    
      if (code === FILE_UPLOAD_ERROR_CODES.FILE_TOO_LARGE) {
        // ...
      }
    }

    When a file of the current turn can't be uploaded, the processor stops the turn and reports why in the tripwire metadata. FILE_UPLOAD_ERROR_CODES lists every code. A file of the thread history that can't be uploaded is replaced by a note in the prompt instead, so it never blocks the thread.

  • Added outputOptions to run.restart(), matching run.start() and run.resume(). Set includeState: true to get the final workflow state in the result. (#26221)

    const result = await run.restart({ outputOptions: { includeState: true } });
    console.log(result.state);
  • Added run fencing to the storage API, so storage adapters can reject writes from a durable agent execution that lost its run to crash recovery. (#26418)

    • Workflows stores have new claimRunOwnership(), renewRunOwnership(), releaseRunOwnership(), and getRunOwnership() methods.
    • Memory stores have new raiseRunFence() and retireRunFence() methods.
    • Methods that write run data, such as saveMessages(), saveThread(), persistWorkflowSnapshot(), and updateWorkflowState(), accept an optional fence. A write whose fence is no longer the run's current claim throws RunFenceConflictError.
    • supportsRunFencing() reports whether a store implements this. It can return a promise, for stores that must probe the backend first, and should reject when the store can't tell yet. It defaults to false, so custom storage adapters keep working without changes, and durable agents fall back to the PubSub lease for them.
    • A run is fully fenced only when both the workflows store and the memory store it writes to support fencing. If only the workflows store does, for example because a composite store takes memory from another adapter or the agent's Memory has its own storage, an execution that lost the run can't write workflow state anymore, but its messages, threads, and working memory still land. If only the memory store does, nothing is fenced.
    • Stores that support fencing keep one ownership record per run, plus one memory fence record per run with memory. Stores with retention support prune them with the new workflows.runOwnership and memory.runFences retention keys. Ownership records age out by inactivity, and fence records once the execution holding them settles. A run's first generation is taken from the store's clock, so a run that reuses a runId after its records were pruned still claims a higher generation than its earlier executions.

    Pruning run fencing records

    const storage = new LibSQLStore({
      id: "mastra-storage",
      url: "file:./mastra.db",
      retention: {
        workflows: { runOwnership: { maxAge: "1d" } },
        memory: { runFences: { maxAge: "1d" } }
      }
    });
    
    // From your own scheduler
    await storage.prune();

    Writing with a fence

    import { RunFenceConflictError } from "@mastra/core/storage";
    
    const workflows = await storage.getStore("workflows");
    const claim = await workflows.claimRunOwnership({ runId, ownerId: "worker-a", leaseMs: 30_000 });
    
    if (claim.acquired) {
      const fence = { runId, ownerId: "worker-a", generation: claim.record.generation };
      try {
        await workflows.persistWorkflowSnapshot({ workflowName, runId, snapshot, fence });
      } catch (error) {
        if (error instanceof RunFenceConflictError) {
          // Another execution claimed the run since. Nothing was written.
        }
      }
    }

    Opting in from a custom adapter

    import { RunFenceConflictError, resolveRunFence, WorkflowsStorage } from "@mastra/core/storage";
    
    class MyWorkflowsStorage extends WorkflowsStorage {
      supportsRunFencing() {
        return true;
      }
    
      // Also implement claimRunOwnership(), renewRunOwnership(), releaseRunOwnership() and getRunOwnership().
    
      async persistWorkflowSnapshot({ workflowName, runId, snapshot, fence }) {
        // The fence passed in, or the one of the durable agent execution making this write
        const current = resolveRunFence(this, fence, runId);
        // In one transaction: if `current` is set and is no longer the run's claim,
        // throw new RunFenceConflictError(current, 'persistWorkflowSnapshot'). Otherwise write.
      }
    }
  • Added matches and notMatches operators to advanced trace queries. They match whole words or phrases, ignoring case, on span name and feedback comment. (#25111)

    { feedback: { some: { op: 'matches', left: { path: 'comment' }, right: { literal: 'incorrect dosage' } } } }
  • Faster, configurable workflow recovery on server boot. (#26607)

    • Active workflow runs now restart in parallel (up to 5 at a time by default) instead of one by one, and durable agent recovery no longer waits for the workflow sweep.
    • New recovery.workflows: 'off' turns off boot-time workflow restarts for the whole instance; recovery.workflowConcurrency sets the parallelism (Infinity for no limit).
    • workflow.restartAllActiveWorkflowRuns() also restarts runs concurrently, capped by recovery.workflowConcurrency. Set it to 1 if runs share side effects that must not overlap.
    new Mastra({
      recovery: { workflows: "auto", workflowConcurrency: 10 }
    });

    Also in: @mastra/deployer@1.76.0

  • Added an optional usageId field to exported metrics. Every token and cost metric produced from the same model usage shares one usageId, so storage can group a model call's metric rows without relying on the span. Other metrics leave it unset. (#26461)

    Also in: @mastra/observability@1.19.0

  • AgentController sessions now support automatic observational memory model selection per role. AgentControllerOMConfig.observerModel/reflectorModel and session.om.<role>.switchModel({ modelId }) accept 'auto' to follow the active main model, and resolveAutoModelId lets a consumer decide which concrete model an automatic role uses. session.om.<role>.model() reports the role's configured intent ('auto', a model ID, or undefined) while modelId() keeps returning the effective concrete model. defaultObserverModelId/defaultReflectorModelId remain the concrete fallback, and persisted explicit selections are unchanged. (#24508)

    Dynamic model functions can now return a labeled model, { model, id }, so code that only sees the resolved model object still knows its full provider/model ID. Resolved models expose that label as a read-only id, and models created from a plain router ID set it automatically.

    const agent = new Agent({
      model: ({ requestContext }) => ({ model: createMyModel(requestContext), id: "openai/gpt-5.6-sol" })
    });

Patch Changes

  • Fixed aborted suspended runs staying listed as suspended. When session.abort() cancels a run that is parked in a tool suspend() (for example ask_user), the run's in-memory registration and its workflow snapshot rows are now released, so agent.listSuspendedRuns() no longer returns it and memory no longer grows with each aborted run. This applies to both regular and durable agents. (#26181)

  • AgentController sessions now forward the agent's isTaskComplete verdict as a live task_complete_evaluation event, so web clients can show "the check failed, the agent is fixing it" right away. @mastra/client-js recognizes the new event type. (#26185)

    The session stream also sends the current display state, including the message being streamed, as its first event. A page that opens or reloads mid-run now sees the in-flight assistant text instead of waiting until it is stored. currentMessage can hold a message that is already stored (for example between steps or after a run), so clients should merge it by message id rather than append it.

    Also in: @mastra/client-js@1.53.0, @mastra/server@1.76.0

  • Fixed retried API errors leaking the failed attempt's partial text. (#26654)

    A provider could stream some text, fail mid-stream, and then succeed when an error processor retried. The final answer came out as the failed prefix followed by the recovered answer, for example "The answer is The answer is 42.". The retry request also sent the failed prefix back to the model.

    The failed attempt's partial text is now discarded, for both Agent and durable agents. The final text and the retry prompt no longer include it, and neither do response.messages and stored messages. Steps that completed before the failed attempt are kept.

    Text chunks already streamed live from the failed attempt still reach textStream and fullStream consumers. The step that carries them finishes with reason retry.

  • Keep Studio and Factory sessions alive for the identity provider's full session length. (#26447)

    • MastraAuthStudio session cookies now last 14 days by default (was a hardcoded 24 hours), configurable via the new sessionMaxAgeSeconds option or the MASTRA_SESSION_MAX_AGE environment variable.
    • When the shared API renews the session during verification, MastraAuthStudio re-issues the renewed cookie under the deployment's own cookie domain and exposes it through a new optional consumePendingResponseHeaders provider hook.
    • @mastra/server's auth middleware, CompositeAuth, the Factory auth gate, and Factory's per-route ensureFactoryAuthUser (used by routes declared requiresAuth: false, which skip the gate) forward those headers to the browser, as does the public GET /auth/me route. Forwarding is best-effort and never fails a request.
    import { MastraAuthStudio } from "@mastra/auth-studio";
    
    // Defaults to 14 days. Override per deployment, or set MASTRA_SESSION_MAX_AGE (seconds).
    const auth = new MastraAuthStudio({ sessionMaxAgeSeconds: 7 * 24 * 60 * 60 });

    Also in: @mastra/auth-studio@1.3.9, @mastra/factory@0.21.0, @mastra/server@1.76.0

  • Update provider registry and model documentation with latest models and providers (2233844)

  • Fixed concurrent durable agent runs saving each other's conversation. When runs of the same durable agent (or parent workflows sharing a nested workflow) started at the same time, a nested run's first saved snapshot could hold another run's input and state. Listing runs for one resource could return another run's conversation while it was in progress, and recovering a run that crashed before its first step continued as the other run: it sent that conversation to the model and saved the reply to the other run's thread. Each nested run now saves its own input and state. (#26221)

  • Fixed evented workflows losing state changes after a restart. When a workflow run on the evented engine was restarted after a crash, steps resumed with the state from the run's start (or last suspension) instead of the state produced by the steps that already finished, so any setState updates made since then were lost. The workflow state is now recorded together with each step's result, and restarts resume from it. (#26221)

  • Fixed DurableAgent streams with closeOnSuspend: true closing before the suspended run was saved. Approving a tool call right after the stream ends now works from any process, instead of sometimes failing with "This workflow run was not suspended". Fixes #26454. (#26556)

  • Faster startup when restarting active workflow runs. Mastra no longer scans storage for workflows that set autoRestartActiveRuns: false. It also skips the internal workflows that run agent processors, since those only run during an agent call. (#26588)

  • Fixed the messageFilter and onDelegationStart delegation types to match the messages they actually receive. Both now receive AI SDK model messages (ModelMessage[]) — the processor-adjusted prompt the supervisor model saw, including system messages, with tool calls removed — and messageFilter returns ModelMessage[]. Previously they were typed as stored database messages, so filters reading fields like content.parts or id could silently let parent context through. Update filters to read role and content. Fixes #25983. (#26161)

  • Fixed UnixSocketPubSub leaving lease files behind when a lease release was followed immediately by close(). The release was aborted mid-way, so a thread claim released during shutdown could stay on disk until another process noticed the holder had exited. A release that starts before close() now completes, and close() waits for it, for up to one second if another process holds the lease's lock. New lease operations after close() still fail. (#26292)

  • Fixed output-stream processing for durable tool results after a restart or cleanup, including Inngest resumes. Restored processors receive the request context, and concurrent cold calls share the published processor pipeline and state. When the agent resolves but its processor pipeline is missing, processor reconstruction or cold-worker dependency-resolution failures (tools, memory, and workspace) stop the step instead of exposing unprocessed output. Unregistered agents, persistence-only callers, and complete live pipelines retain their dependency-resolution fallback. Fixes #26148. (#26367)

  • Fixed Inngest tool resumes after a worker restart or cleanup to emit tool-call-resumed before tool-result, preserving configured display transforms. (#26370)

  • Classifier now supports AI SDK decision models. Pass a model from a provider's decisionModel() factory and Mastra calls doDecide(). Models that only implement the deprecated doEvaluate() contract still work. ClassifierProcessor, ModelSelectionProcessor, and classifier scorers accept the same models. (#26279)

    To match the AI SDK naming, Classifier.decide() replaces Classifier.evaluate() and MastraDecisionModel replaces MastraEvaluationModel. The old names are deprecated aliases and will be removed in a future release. EvaluationModelResult, MastraEvaluationModelInterface, ConfiguredClassifierEvaluateOptions, and PerCallClassifierEvaluateOptions are likewise deprecated in favor of their Decision/Decide counterparts.

    import { openai } from "@ai-sdk/openai";
    import { Classifier } from "@mastra/core/classifier";
    
    const classifier = new Classifier({
      id: "support-router",
      model: openai.decisionModel("gpt-6-luna"),
      questions: {
        urgent: { type: "boolean", instructions: "Is this request urgent?" }
      }
    });
    
    const result = await classifier.decide({ state: "My order never arrived" });
  • Fixed tool calls failing validation when the model's arguments needed more than one automatic fix. Tool calls that send lists or objects as text, send null for optional fields, or use query, message, or input instead of prompt now validate even when several of these happen in the same call. For example, { args: '["a.py"]', note: null } is now accepted. (#25859)

  • Fixed plain agents emitting awaited background tool results twice. (#26344)

  • Fixed durable and evented agents so they no longer execute tools excluded by prepareStep. (#26339)

  • Fixed three durable agent bugs: (#26583)

    • Aborting a suspended run with abortThreadStream(), abortRunStream() or the stream result's abort() now removes it from listSuspendedRuns(). Previously the run stayed listed as suspended after the abort. This also applies to runs waiting for tool approval: aborting them now releases the run instead of leaving it for a later approve or decline.
    • Client tool spans on Inngest agents are now parented to the exported agent run span instead of a span that was never exported.
    • Inngest agent streams no longer hang when an event exceeds the Realtime message size limit. Oversized finish events are sent without the step history, and oversized error events keep the error name and a shortened message so onError still fires. Oversized non-terminal events are replaced with a data-oversized-event chunk so clients know an event was reduced, and a warning is logged through the Mastra logger.

    Also in: @mastra/inngest@1.10.4

  • Reduced how much a long durable agent run writes to storage, and made runs in flight at shutdown recoverable (#26600). (#26642)

    • Smaller checkpoints: a durable run's checkpoints no longer repeat messages the run has already saved to memory (for example with savePerStep: true). They store a small reference to the memory row instead, as recalled messages already did. This covers every copy of the conversation in a checkpoint, including the run input and the final step results. Messages that change after being saved stay inline. This applies to agents with a fixed memory instance.
    • Checkpoint on shutdown: with the default snapshot policy and recovery.durableAgents: 'off', a durable run still in flight when mastra.shutdown() starts now persists a running checkpoint at each step boundary it reaches during the drain. If it reaches one before the drain ends, it can be resumed later with recover() or recoverActiveRuns() instead of being lost.
    // Process A: a run is still in flight when the process shuts down.
    await mastra.shutdown({ drainTimeout: 5_000 });
    
    // Process B: resume every run checkpointed during the drain.
    await mastra.recoverAllDurableAgents();
    // or per agent: await durableAgent.recoverActiveRuns();
  • Fixed a durable agent thread staying busy forever when cleanup() was called before the run finished. Cleanup now waits for the run to settle, so onFinish/onError fire and the thread is released for the next request. Fixes #25974. (#26587)

  • Fixed sendSignal(..., { ifIdle: { behavior: 'wake' } }) on a DurableAgent so the accepted result's output.text now resolves to the generated text, the same as with a regular Agent. Previously it resolved to undefined. (#26564)

    const { accepted } = durableAgent.sendSignal(signal, { resourceId, threadId, ifIdle: { behavior: "wake" } });
    const result = await accepted;
    await result.output?.text; // now the generated text
  • Fixed durable agents ending a turn without an answer when Gemini returns a malformed tool call (MALFORMED_FUNCTION_CALL). Durable agents now retry the request like regular agents do, and report an error if the retries fail. (#26636)

  • Fixed durable agents running the model call outside the model step span context. getCurrentSpan() inside AI SDK model middleware and span-correlated logs now resolve to the model span for durable agents, matching regular agents. (#26184)

  • Fixed durable agents saving each step of a turn as a separate message. A multi-step turn is now saved as one message, as with Agent. Conversation history now holds more turns, and the streamed message ID matches the saved message. (#26586)

  • Fixed durable agents, including Inngest agents, ignoring an abort() from an output processor's processOutputResult. The run now reports the tripwire with finishReason: 'other', as a regular Agent does. The rejected answer is no longer saved to memory. (#26178)

    Also in: @mastra/inngest@1.10.4

  • Fixed durable agents saving raw model output after processOutputStream changed or removed streamed text. Stored text now matches the stream, as it does for regular agents. Redacted values no longer reappear on reload or reach the model on a later turn. (#26559)

  • Fixed DurableAgent.recover() dropping the agent's tools when a run was interrupted during a model request. The recovered model call now gets the same tools as the original run, so it can still call them instead of only replying with text. Fixes #25890. (#26535)

  • Fixed two durable agent bugs: (#26585)

    • Mastra.shutdown() now closes the pubsub, so open Redis connections no longer keep the process running after shutdown.
    • Read-only memory is now respected when a durable run finishes from a saved snapshot, so read-only turns are no longer saved.
  • Fixed durable and evented agents not saving the structured output object to memory. The saved assistant message now includes metadata.structuredOutput, matching regular agents, so reconnecting clients and recovered runs can read the object. (#26452)

    As with regular agents:

    • Truncated responses (finish reason length or content-filter) and responses that fail validation do not save metadata.structuredOutput. The response message itself is still saved.
    • With errorStrategy: 'fallback', the fallbackValue is saved instead.

    This does not yet apply when structuredOutput.model is set to a separate structuring model (see #26431).

    Fixes #26432.

  • Fixed durable agents ignoring structuredOutput.model. The separate structuring model now runs when the turn finishes, so output.object resolves and metadata.structuredOutput is saved on the assistant message, matching Agent. (#26571)

    Also in: @mastra/inngest@1.10.4

  • Fixed durable agent runs that could not be resumed after a crash right after a tool suspended or asked for approval (#26435). The tool-call-suspended and tool-call-approval chunks are now published only after the suspended snapshot is saved, so a client that sees the question can always resume the run. (#26589)

    Detaching an AgentController session from a thread no longer declines and deletes a parked durable run, so another session or process can still resume it.

  • Fixed DurableAgent.recover() losing tools that ToolSearchProcessor loaded during the run. After a crash, recovery in a fresh process now reads the run's latest transcript and restores those tools, so the model's pending tool call runs once instead of failing with ToolNotFoundError and being retried under a new call id. (#26592)

  • Fixed durable agents (such as Inngest agents) silently dropping call-time toolsets when the worker runs in a separate process. Toolset tools contain server-side code that can't be serialized, so the worker now throws an error naming the missing tools instead of running without them. To use these tools on a separate worker, register them on the agent (statically or via requestContext). (#25860)

  • Fixed DurableAgent abort() not stopping in-flight tool or model calls when modelSettings.timeout.totalMs is set and the run's registry entry had been evicted under heavy concurrency. (#26628)

  • Fixed agent signal delivery errors being cut off at the resource id. Messages such as "No claimed thread owner responded" now name the thread and resource in full, so the whole error is stored and shown. (#26292)

  • Fixed evented workflows and evented agents storing the caller's auth token (mastra__authToken) in workflow snapshots. The token is now stripped before every snapshot write, matching the default workflow engine; steps still receive the live token while the run executes, and resumed runs use the token from the resuming request instead of any token left in older snapshots. Fixes #26217. (#26638)

  • Fixed durable agents on the evented engine running the process out of memory on long runs. The engine persisted every completed step's full payload and output — including the whole, monotonically growing iteration state — because its step-result merge writes bypassed the workflow's pruneSnapshot hook, so snapshots and per-write heap cost grew without bound. The engine now prunes after each non-parallel step, and the durable loop no longer retains all completed-step history now that its same-iteration reads are declared. (#26553)

  • Fixed evented workflows returning a stale result from run.resume() on persistent pubsubs such as @mastra/redis-streams and @mastra/valkey-streams. Awaited start(), resume(), restart() and time-travel calls now only read finish events published after the call, so resume() returns the resumed result instead of the earlier suspended one, and waiting no longer re-reads the whole workflows-finish stream. (#26300)

  • Fixed messages queued during an active agent run being dropped when that run is aborted. Queued messages now start their own run after the abort instead of inheriting the aborted run's cancellation. (#26280)

  • execute_command no longer tells the agent an aborted command was killed unless the sandbox confirms it (killed: true). When the provider omits kill metadata or throws after the run is cancelled, the tool now notes that the run was cancelled and keeps the provider's error, so agents are not falsely assured that remote work stopped. (#26606)

  • Exported ServerConfig from @mastra/core/server and OPENSEARCH_PROMPT from @mastra/opensearch so the documented imports resolve. Fixed JSDoc import paths in @mastra/evals to use @mastra/evals/scorers/prebuilt and @mastra/evals/scorers/utils. (#26440)

    Also in: @mastra/evals@1.10.6, @mastra/opensearch@1.1.2

  • Fixed MCP App tools not being detectable by AI SDK UI hosts. For tools whose MCP server exposes an app UI (mcp._meta.ui.resourceUri), toAISdkStream now sets toolMetadata.app (resourceUri, mimeType: 'text/html;profile=mcp-app', and serverId when available) on tool-input-start and tool-input-available chunks, so hosts can render MCP Apps without a custom stream transform. Fixes #25892. (#26352)

    Also in: @mastra/ai-sdk@1.11.0

  • Fixed foreign durable run lookups mutating the local registry and breaking shutdown. (#26189)

  • Fixed channel bot tokens and other internal Mastra state leaking into traces and score records (#25893). (#26243)

    Agents with channels configured exported the live Telegram/Slack adapter on every span's requestContext, including bot and app tokens, and added hundreds of KB to each span. Delegated runs exported the parent agent's memory instance, and agent-controller runs walked the whole workspace.

    What changed

    • Reserved Mastra request-context keys (those starting with mastra__ or __mastra_) are no longer exported on spans or saved on score records. The auth token is now omitted instead of shown as [REDACTED]. Thread and resource IDs are still recorded as span metadata.
    • Workspaces now show as { id, name, status } in traces and score records instead of exposing their configuration and providers. Score records also use serializeForSpan() for any other object that defines it.
    • Request-context keys without a reserved prefix keep their existing representation. If you set your own keys starting with mastra__ or __mastra_, rename them to keep them in traces.
  • Corrected the documented defaults for SerializationOptions.maxStringLength (131072) and maxDepth (8) to match the runtime defaults. (#26425)

  • Fixed tools running with empty input when using agent.stream(). Some providers, like OpenAI Responses and Anthropic programmatic tool calling, send tool arguments only at the end of a tool call. Tools now wait for the complete arguments before they run. Tools with required inputs no longer fail validation in this case. (#26251)

  • Experimental durable and evented agents now restart a model request that has only produced reasoning when a new message or signal arrives, so the agent answers with the new input instead of finishing a stale response. This matches the default agent. (#25878)

    • The restarted request keeps the same step: it doesn't use up maxSteps or add token usage, and the discarded reasoning stays out of history.
    • Input processors always finish; only the model request is cancelled.
    • In traces, the cancelled request's step and inference spans end with finish reason interrupted.
  • Reduced storage used by agent runs waiting on tool approval or a suspended tool. Each suspended snapshot now stores the conversation one fewer time, cutting snapshot size by about 29% in a 12-approval run. Suspended runs resume exactly as before. (#26221)

  • Fixed follow-up signals being silently dropped when the process that forwarded them to another thread owner later took over that thread. Previously, the retained signal was treated as the process's own echo and ignored. Signals the process queued locally are still deduplicated. (#25847)

  • Fixed Inngest durable agents persisting the agent's system prompt in every step result of workflow snapshots. Inngest agent workflows now apply the same snapshot pruning as the core DurableAgent, so instructions are only kept on the run input. pruneAgentLoopSnapshot is now exported from @mastra/core/agent/durable. (#26152)

    import { pruneAgentLoopSnapshot } from "@mastra/core/agent/durable";
    import { createWorkflow } from "@mastra/core/workflows";
    
    const workflow = createWorkflow({
      id: "my-agent-loop",
      inputSchema,
      outputSchema,
      options: { pruneSnapshot: pruneAgentLoopSnapshot }
    });

    Also in: @mastra/inngest@1.10.4

  • Fixed cross-process events going missing after a process restarted. Processes that share a socket, such as Mastra Code sessions, could split into separate groups that never received each other's events. A message sent from one session to another could then fail to arrive until every process restarted. A restart, a busy machine briefly refusing connections, or a deleted socket file could cause the split. (#26292)

    • Refused connections are retried before a process takes over the socket.
    • A broker removes the socket path only while it still points at its own socket.
    • A broker that loses its socket path rejoins the shared group automatically.
  • Fixed durable and evented runs to filter internal messages before saving them to memory. (#26186)

  • Durable and evented agents now include runId in onStepFinish and onFinish, matching the regular Agent. Use runId to tell which run an event came from when callbacks are shared across runs. (#26531)

    onFinish also now includes model, messages, object, error and usedFallbackValue. You can read the structured output and response messages directly in the callback. Fixes #26524.

  • Fixed agent runs that never finished when a processor tripwire stopped the stream. The stream output now reports status tripwire, and Agent Controller sessions end the run with an error that includes the processor's reason instead of staying busy forever. (#26142)

  • Fixed queued messages and signals arriving too late during reasoning in the default agent loop. Reasoning-only model requests are cancelled and restarted with pending input in the same run, without keeping discarded reasoning in model history or interrupting text and tool output. Interrupted requests reuse the same logical step without consuming maxSteps, advancing stepNumber, or contributing token usage. The run's reasoning, reasoningText and getFullOutput() exclude discarded reasoning, and tracers can close the cancelled request's inference span through the optional interruptInference() tracker method. (#25861)

  • Fixed evented workflows dropping state changes made inside a nested workflow that runs as a .dowhile() or .dountil() loop body. Each iteration started from the state the loop had before the nested workflow ran, so setState updates from the loop body were lost and later steps never saw them. The nested workflow's final state now carries into the next iteration and the steps after the loop, matching the default engine. (#26221)

  • Reduced storage used by agent runs that recall conversation history from memory. Saved run state now stores recalled messages as references to the stored messages instead of full copies. When the run continues, the messages are loaded back from memory. (#26222)

    In a 12-step run that recalled 20 messages, the saved run state was about 25% smaller for durable agents and about 21% smaller for agents waiting on tool approval.

    This applies to agents configured with a memory instance. Agents whose memory is a function, or that use memory inherited from a supervisor agent, keep full copies of recalled messages, because a resumed run may not pass the request context that memory was chosen from.

    Loading recalled messages back. If a recalled message is edited while a run is suspended, the resumed run uses the stored version. If it is deleted, the resumed run drops it. Both are logged at debug level. When a run is resumed or recovered, the messages are loaded before anything runs, so if memory storage can't load them, the call (for example approveToolCall or recover) throws and the run stays as it was, so you can retry it.

    A durable agent also loads them in the middle of a run when its process has no cached copy: when a step runs on a different worker (evented engine with a distributed pubsub), after more than 10 minutes between steps (for example, a slow tool call), or when more than 1,000 durable runs are in progress in one process. A memory storage failure at that point is retried for about 10 seconds, with each retry logged as a warning. If memory storage is still failing after that, the run fails.

    Storage reads. Runs that store recalled messages as references look them up in memory storage by id, once when first storing them and again whenever they are loaded back as described above. Cloudflare KV storage finds a message by id by checking every thread, so on KV each lookup reads every thread once per recalled message.

    Upgrading and downgrading. Runs started on an earlier version still resume and finish normally. Runs started on this version can't be resumed after downgrading to an earlier @mastra/core version.

  • Fixed PIIDetector sending a message's original text to the model when redaction left nothing usable. With strategy: 'redact', a message is now dropped instead of passed through unredacted when redaction leaves no text or only whitespace (for example an SSN-only message with redactionMethod: 'remove'), or when PII is flagged with no usable spans. This applies to processInput and processOutputResult. Fixes #25553. (#26209)

  • Reduced storage used by durable agent runs. The conversation transcript is now saved once per workflow snapshot instead of being copied into every step's input and output. In a 12-iteration tool-calling run this cut persisted snapshot size by about 31% on the default engine and 63% on the evented engine. Runs started on an earlier version still resume and finish normally. Runs started on this version can't be resumed after downgrading to an earlier @mastra/core version. (#26221)

  • Fixed subagents silently returning an empty result when they call a client tool (a tool without execute). Subagents can't run client tools, so their run stops at that tool call. Previously the supervisor received empty text and subAgentToolResults: [] with no indication why. Now the delegation result includes subAgentPendingToolCalls listing the unresolved calls, the supervisor model is told which tools were never run, and a warning is logged. (#26257)

    const supervisor = new Agent({
      // ...
      agents: { shopper },
      defaultOptions: {
        delegation: {
          onDelegationComplete: ({ result }) => {
            if (result.subAgentPendingToolCalls) {
              // e.g. [{ toolName: 'add-to-cart', toolCallId: '...', args: { productId: 'sku-1' } }]
            }
          }
        }
      }
    });

    Give client tools to the supervisor, or use suspend() in the subagent's tool when it needs input from the user.

  • Fixed a memory leak where agents with sub-agents, workflows, or autoResumeSuspendedTools kept growing the heap on every tool conversion (each generate/stream call and agent listing). Resume fields injected into tool input schemas are now created once instead of per conversion, so they no longer pile up in Zod's global schema registry when zod@3.25.x is installed. Fixes #26160. (#26167)

  • Fixed duplicated assistant messages after a suspended tool or sub-agent call resumes. Each earlier reply and tool call now appears only once in saved conversation history. Fixes #26150. (#26163)

  • Agent runs whose model stream stops without a finish reason now end with an error instead of being reported as complete. (#26226)

  • Fixed agents being told to call updateWorkingMemory when workingMemory.agentManaged is false. The tool is not available in that mode, so agents now receive the read-only working memory instruction instead (#25896). (#26198)

  • Fixed resumeStream ignoring the requestContext and delegation you pass when a supervisor run is resumed in the same process that suspended it. The resumed delegation now calls the onDelegationStart hook from the resume call, and sub-agent tools see the resume call's requestContext. DurableAgent tools approved with approveToolCall in the same process now also run with the request context passed to that call. Fixes #25960. (#26686)

  • Setting toolChoice: 'none' now keeps earlier tool calls and results in the conversation, so the model can give a final text answer based on them. Amazon Bedrock no longer drops that tool history or rejects extended-thinking blocks. (#26232)

    prepareStep: ({ stepNumber }) => (stepNumber >= 3 ? { toolChoice: "none" } : undefined);

    Tools are still left out when the step sends a structured-output schema as the response format, because some providers (such as Gemini) reject tools combined with it.

  • Fixed three bugs in the in-memory storage adapter so it behaves like the database adapters: (#26418)

    • listMessages now reports hasMore correctly when include is combined with pagination or several threads. Before, it could report no more pages while filtered messages remained, or report more pages when include had already returned every message.
    • Saving an existing channel installation again no longer resets its createdAt.
    • batchDeleteTraces with organizationId or resourceId now deletes only that tenant's spans. Before, it removed the whole trace or nothing, depending on which root span was written last.
  • Added closeOnSuspend to DurableAgent.recover(), matching stream() and resume(). When a recovered run suspends (for example, waiting on tool approval), the returned fullStream now ends, so callers can hand off to resume() instead of hanging. (#26555)

    const recovered = await agent.recover(runId, { closeOnSuspend: true });
    for await (const chunk of recovered.fullStream) {
      // ends after `tool-call-approval`
    }
  • Fixed thread subscribers missing approval requests and completion events after a durable agent run recovers from a crash. Recovered runs that pause for tool approval can now be approved and finish normally, including when multiple server instances share Redis. (#26554)

  • Fixed a guardrail leak where a reply rejected by an output processor's processOutputStep was still saved to the memory thread and returned as result.text when the run ended on the tripwire (after maxProcessorRetries ran out, or on an abort without retry). The rejected reply is now dropped; the user message and any earlier accepted steps, such as tool calls and results, are still kept. Fixes #26048. (#26168)

  • Fixed saved agent steps after tool approval or stream resumption. After approveToolCall, declineToolCall, or resumeStream, each saved step now contains only its own content. Later steps no longer repeat earlier tool calls, so snapshots stay small and processors and prepareStep see the right content. Fixes #26475. (#26646)

  • Fixed abortRunStream so runs without a thread can be stopped by run ID. These runs now pass their run-level abort signal to tools and sub-agents, matching threaded streams. After a run settles, aborting the caller signal no longer reaches tools or sub-agents that retained the run signal. (#26188)

  • Fixed internal Agent Controller operations so configured authorization providers enforce read and execute permissions. Read routes authorize agent-controller:read when inspecting an existing live session. If the in-memory session is missing after a restart, they preserve recovery behavior by recreating it through the agent-controller:execute-authorized path. Other operations that create, resume, or mutate sessions also require agent-controller:execute. Applications without an authorization provider are unchanged. (#26092)

    Also in: @mastra/server@1.76.0

  • Fixed durable agents forwarding the parent conversation to delegated agents and delegation hooks, including transient request-processor context. Ordinary tools now receive input-only messages consistently with non-durable agents. (#26365)

  • Fixed restarted workflows losing state changes made inside a nested workflow. When a run restarted after a crash while a nested workflow was still running, the nested workflow finished but its setState updates never reached the parent, so later steps saw the old state. The nested workflow's final state now carries back to the parent, as it already did for start() and resume(). (#26221)

  • Reduced durable agent snapshot size when crash recovery is on. Running snapshots no longer save a copy of the full provider request (prompt and tool schemas) for each tool-calling step. Resume and recovery behave the same as before. Fixes #26462. (#26568)

  • Fixed durable, evented, and Inngest agents to stop before model execution when processor resolution or input processing throws. (#26347)

  • Fixed cross-process durable run observers to return parsed structured output objects. (#26195)

  • Token and cost metrics now carry a usageId shared by all rows from the same model call. Usage rolled up from hidden model calls onto a visible span gets a separate usageId per call, so each call stays distinguishable even though the rows share a span. (#26461)

    Also in: @mastra/observability@1.19.0

  • Fixed evented approval runs recreating deleted durable run registry keys during finalization. (#26315)

  • Fixed evented workflows to recover sleeps after a process restart by replaying the sleeping path without rerunning completed steps. (#26268)

  • Fixed stored signals rendering different markup after they are reloaded from storage. Signal attributes now render in a stable order, so the same message produces the same prompt text on every turn. (#25957)

  • Fixed approved tool suspensions reappearing as pending after a restart. When a new session subscribed to a thread with initial history, suspensions that had already been answered could be registered again, showing stale approval prompts and making auto-resume code fail completed runs. Only suspensions on the latest assistant message are now treated as pending. (#26165)

  • Agents now receive file and image contents from AI SDK v7 message input. (#26177)

    Unsupported file data now throws an error instead of sending an empty file.

  • Fixed durable and evented step-finish events to report whether the agent loop actually continued. (#26348)

  • Fixed EventEmitterPubSub so publish() no longer rejects when a subscriber throws synchronously; the error is logged and delivery continues. (#26187)

  • Fixed nested workflows losing the parent's state when a run restarted before the nested workflow's first step finished. After a crash in that window, the nested workflow restarted with empty state instead of the state the parent passed in, so steps reading that state failed. The nested workflow now restarts with the parent's state. (#26221)

  • Fixed two thread ownership bugs in durable agents running across multiple instances: (#25080)

    • A replayed run-registered event from a finished run no longer takes over as the thread's active run, whether or not its record is still cached locally (for example when Redis redelivers the event after the run finished).
    • sendSignal now recognizes an active run owned by another instance while the thread is observed, so ifActive behavior (for example discard) applies instead of waking a new run.
  • Fixed agent runs stopping mid-task after a tool approval. Approving or declining a tool call now resumes the run with the controller's full step budget instead of the agent's default of 5 steps, so the run no longer ends as "complete" a few steps later with work still pending. (#26493)

  • Fixed ToolCallFilter removing tool results from the current run after Observational Memory pruned messages mid-run or a suspended run was resumed. The filter now keeps those results so the model can still use them. (#26350)

  • Fixed agent steps that start directly with a tool call being merged into the previous step in the conversation history sent to the model. Calls that ran one after another, where the second used the first's result, no longer look like parallel calls. Parallel tool calls within a single step still stay together. (#26685)

  • Fixed durable, evented, and Inngest agents leaking one-step prepareStep system message overrides into later model steps. (#26343)

    Also in: @mastra/inngest@1.10.4

  • Bumped the MCP SDK dependencies for GHSA-6qxp-vccf-f47h (high). @modelcontextprotocol/client, @modelcontextprotocol/server and @modelcontextprotocol/core move from 2.0.0 to 2.2.0, and @modelcontextprotocol/sdk from ^1.29.0 to ^1.31.0. No API changes. (#26260)

    Also in: @mastra/mcp@2.3.0, @mastra/mcp-docs-server@1.3.6, @mastra/mcp-registry-registry@1.1.4, @mastra/playground-ui@62.0.0

  • Fixed internal agent memory reads and writes so configured authorization providers enforce permissions with actor and acting-agent attribution across standard, durable, and delegated execution paths. Applications without an authorization provider are unchanged. Applications with an authorization provider must grant memory:read and memory:write for the affected memory threads to users and system actors that run agents in process; otherwise those operations now fail with a 403 response. (#26092)

  • Fixed resumed durable and evented agent outputs losing tool calls made before suspension or approval, even after a restart or repeated approvals (COR-1398). (#26364)

  • Fixed workflows exposed to agents as tools running in parallel with other tool calls even when they can suspend. A workflow tool now reports that it can suspend when any of its steps (including nested workflows) declares a suspendSchema or resumeSchema, so toolCallConcurrency runs it one call at a time, the same as a regular tool with a suspendSchema. Steps that call suspend() should declare a suspendSchema or resumeSchema so the workflow tool is detected as suspendable. (#26608)

  • Preserve the original TypeError when plain agents reject invalid modelSettings.timeout stream options, matching durable and evented agents (#26340)

  • Fixed aborted agent streams reporting the wrong finish reason and emitting completed tool results more than once. (#26345)

@mastra/ai-sdk@1.11.0

Minor Changes

  • The messageMetadata callback in chatRoute, handleChatStream, and toAISdkStream now receives the run's traceId. Return it as message metadata so useChat clients have the trace ID on a live answer, for example to attach feedback, without a server lookup. Fixes #26140. (#26233)

    chatRoute({
      path: "/chat/:agentId",
      messageMetadata: ({ part, traceId }) => (part.type === "start" ? { traceId } : undefined)
    });

    traceId is undefined when tracing is disabled or with @mastra/core versions before 1.75.0.

Patch Changes

  • Restored live sub-agent progress in handleChatStream() and chatRoute(). Both helpers now stream data-tool-agent and data-tool-agent-step parts by default when an agent delegates to sub-agents, including on the AI SDK v6/v7 approval-resume path. These parts can contain sub-agent reasoning and intermediate tool calls and results. Pass includeSubAgentMetadata: false to turn them off. (#26224)

    chatRoute({
      path: "/chat/:agentId",
      includeSubAgentMetadata: false
    });

@mastra/blaxel@0.10.0

Minor Changes

  • Blaxel background processes now support stdin. sendStdin() writes to the running process and closeStdin() sends EOF, so you can run interactive or stdio-based programs (such as MCP servers or CLI agents) in a Blaxel sandbox. Processes spawned with stdinMode: 'ignore' still start with stdin closed. (#26626)

    const handle = await sandbox.processes.spawn("node server.js");
    await handle.sendStdin('{"jsonrpc":"2.0","method":"ping","id":1}\n');
    await handle.closeStdin();

    Process output from handle.stdout, handle.stderr, and the onStdout/onStderr callbacks now keeps line breaks. Before, "a\nb\n" came through as "ab", which broke line-delimited protocols.

@mastra/braintrust@1.3.20

Patch Changes

  • Fixed a run started with tracingOptions.nestUnderParent appearing as a second top-level span in Braintrust. It now appears as a child of its parent span, also when another process exported the parent. (#25691)

@mastra/clickhouse@1.24.0

Minor Changes

  • Added the matches and notMatches trace-query operators to the ClickHouse observability store, so traces can be found by the words in a span name or feedback comment. (#25111)

@mastra/client-js@1.53.0

Minor Changes

  • Fixed the Studio experiment page showing "Experiment not found" for experiments older than the 10 most recent. The page and its breadcrumb now load the experiment by ID instead of searching the first page of the experiments list. (#26633)

    Added GET /api/experiments/:experimentId, client.getExperiment() and the useExperiment() hook to get an experiment by ID without knowing its dataset.

    const experiment = await client.getExperiment("experiment-id");
    console.log(experiment.datasetId);

    Also in: @mastra/react@1.9.0, @mastra/server@1.76.0

  • Added the matches and notMatches trace-query operators to the generated trace-query types. (#25111)

Patch Changes

  • Fixed thread and Agent Controller subscriptions retrying forever when reconnecting is denied. If a resubscribe request fails with a 4xx error (such as 401 or 403) or 501 Not Implemented, processDataStream({ reconnect }) now rejects with the original error and Agent Controller subscribe({ reconnect }) reports it through onError, instead of retrying. 408 timeouts, 429 rate limits, network failures and other server errors still retry within the reconnect limit. (#26611)

    If you relied on reconnect retrying 4xx responses, handle them by catching the processDataStream rejection or in onError:

    try {
      await subscription.processDataStream({ onChunk, reconnect: true });
    } catch (error) {
      if (error instanceof MastraClientError && error.status === 401) {
        // refresh credentials, then subscribe again
      }
    }
  • Durable agent requests that conflict with a run's state now return 409 Conflict instead of 500. This covers recovering a run that another execution still drives, that is already being recovered, or that is suspended, and starting a run that another execution holds. (#26418)

    Observing a durable agent run no longer streams events from an execution that lost the run to recovery.

    Also in: @mastra/server@1.76.0

  • Fixed tracingOptions.nestUnderParent being dropped from agent and workflow requests sent over HTTP. A run started through @mastra/client-js with this option now stays a child of its parent run in the trace. (#25691)

    Also in: @mastra/server@1.76.0

  • Fixed voice.speak() in the Client SDK ignoring the selected speaker and other options. The speak endpoint now forwards options (including speaker) to the agent's voice provider, so the requested voice is used instead of the provider default. The top-level speakerId field still works; if both are sent, options.speaker takes precedence. (#26298)

    await client.getAgent("my-agent").voice.speak("Hello", { speaker: "nova", speed: 1.2 });

    Also in: @mastra/server@1.76.0

@mastra/cloudflare@1.7.2

Patch Changes

  • Fixed listMessagesById in Cloudflare KV storage returning an empty list when a lookup fails. It now throws, like the other storage adapters, so a temporary KV outage is no longer mistaken for the messages having been deleted. (#26222)

@mastra/cloudflare-sandbox@0.5.2

Patch Changes

  • Fixed CloudflareSandbox silently running commands and file operations on the container's temporary disk when a storage mount could not be verified or restored. Operations now wait for in-flight mounts and fail with a clear error if a mount check fails, a re-mount fails, or the initial mount failed; they recover automatically once the mount succeeds again. A missing or malformed bridge exit status is now treated as a failure instead of success. (#26180)

@mastra/code-sdk@1.12.0

Minor Changes

  • Added runThinkingLevel to @mastra/code-sdk/thinking. It returns the thinking level a request actually sends for a model, using the reasoning options models.dev publishes. getAvailableThinkingLevelsForModel takes the same options and lists only levels that send a different request. (#26229)

    import { getModelReasoningOptions } from "@mastra/core/llm";
    import { getAvailableThinkingLevelsForModel, runThinkingLevel } from "@mastra/code-sdk/thinking";
    
    const options = getModelReasoningOptions("openai/gpt-5");
    runThinkingLevel("openai/gpt-5", "max", options); // 'high'
    getAvailableThinkingLevelsForModel("openai/gpt-5", options); // ['off', 'low', 'medium', 'high']

    Fixed GPT-5 requests at Extra high or Max thinking. They sent xhigh, an effort models.dev does not list for the model, and now send high.

    Models that models.dev lists without reasoning controls, such as GPT-4o, now run every level as off, so GPT-4o requests no longer carry a reasoning effort. Claude and Gemini models keep the levels their thinking settings send.

  • Added a --pack <id> flag to headless mode (mastracode -p ... --pack anthropic). It resolves builtin pack ids and saved custom packs (custom:<name> or bare name), exits with the list of valid packs on unknown ids, and applies the pack like /models → Activate: mode models, subagent models, model route, and thread activeModelPackId. An explicit --model still wins over the pack for the active mode. (#26691)

    Example

    mastracode -p "summarize this repo" --pack anthropic
    mastracode -p "fix the tests" --pack Kimi --model openai/gpt-5.5
  • Replaced the quiet mode settings with a single previewLines preference, since compact rendering is now the only Mastra Code display mode. (#26576)

    GlobalSettings no longer has preferences.quietMode, preferences.quietModeMaxToolPreviewLines or onboarding.quietModePreferenceSelected. It adds preferences.previewLines (0–8, default 2). When settings load and previewLines isn't set yet, it is filled in from quietModeMaxToolPreviewLines and saved. The legacy keys stay in the file unchanged.

    // Before
    const lines = settings.preferences.quietModeMaxToolPreviewLines;
    
    // After
    const lines = settings.preferences.previewLines;

Patch Changes

  • Fixed agent_signal_send reporting a signal as failed when it had been saved to the target agent's inbox. For signals that don't require a reply, when direct delivery fails but the notification is still pending, the tool now says the signal was queued, includes the delivery error, and suggests retrying with the same messageId. Signals sent with expectsReply: true still return an error in this case, because the reply obligation was not established. (#26292)

  • Fixed DeepSeek models ignoring the thinking level. Max on DeepSeek V4 Pro now asks for maximum effort, and Low asks for low effort. Off still leaves DeepSeek on its own default. (#26407)

  • Fixed the update check treating a prerelease, such as 1.2.0-alpha.1, as already up to date when its stable release (1.2.0) is out. /update, mastracode update, and the startup update notice now offer the stable release. (#26455)

  • Factory deployments with sign-in enabled can now use Amazon Bedrock. Opt in on the server, then provide AWS credentials and AWS_REGION there. Every signed-in account can then pick Bedrock models without saving a per-account API key. (#26249)

    new MastraFactory({
      // ...
      deploymentModelProviders: ["amazon-bedrock"]
    });

    The web deployment reads the same list from FACTORY_DEPLOYMENT_MODEL_PROVIDERS=amazon-bedrock.

    Without the opt-in, Bedrock stays hidden from signed-in accounts and runs fail with a ProviderAuthRequiredError. Before this change, a run could silently fall back to whatever AWS credentials the server had.

    Also in: @mastra/factory@0.21.0

  • Improved Mastra Code observational memory so automatic Observer and Reflector roles follow the active main model provider while explicit role choices stay pinned. (#24508)

  • The built-in OpenAI Observational Memory pack now uses openai/gpt-6-luna instead of openai/gpt-5.4-mini. (#24508)

  • Fixed per-agent-type subagent model selections (subagentModelId_explore/plan/execute) being stripped from session state by schema validation. Pack activation (/models → Activate or --pack) assigned subagent models, but the next session state update silently dropped them, so subagents fell back to their default models. (#26691)

  • Fixed restarted Mastra Code sessions staying unreachable to other agents for up to about 13 seconds. A session that was shutting down kept its threads claimed until it fully exited, and the restarted session waited up to 5 seconds between attempts to claim them. Shutdown in the TUI, headless mode and ACP now releases thread claims first, so a restarted session is reachable again within about a second. Retries are also capped at 1 second. (#26292)

    Embedders that shut down a createMastraCode() result themselves can call the new releaseThreadClaims() at the start of their teardown:

    const mastraCode = await createMastraCode(config);
    
    // On shutdown, before stopping workers and closing storage:
    mastraCode.releaseThreadClaims();
  • Removed the "Starting on " notice that was emitted at the start of each turn when using subscription accounts. Preferred-account routing is unchanged. (#25937)

  • Factory observational memory now supports automatic model selection per role. PUT /web/config/om/:role/model accepts modelId: 'auto' to clear that role back to automatic selection, and any other value to pin it. Previously 'auto' was stored as if it were a model name, which pinned the role to a model that does not exist; automatic roles are now stored as null and follow the active main model on every run. The request body is unchanged, so existing callers keep working. (#24508)

    Connecting a model provider or signing in over ACP no longer writes observer or reflector selections, and the unused POST /web/config/om/provider-defaults route was removed. Stored settings are applied per run instead of being copied into session state. Settings responses now report each role's intent, its effective model, and whether that model's provider is currently available; mastra/ models count as available when the Mastra gateway key is configured. If Factory cannot load the saved settings for a run, memory falls back to Auto models and default thresholds, and the thread shows an error explaining that saved choices were not applied.

    MastraCodeConfig.inputProcessors also accepts a function of { requestContext }, so hosts can choose input processors per request:

    await createMastraCodeAgentController({
      inputProcessors: ({ requestContext }) => (requestContext.get("channel") ? [channelProcessor] : [])
    });

    The server now prevents request payloads from overriding Factory's internal memory-settings context.

    Also in: @mastra/factory@0.21.0, @mastra/server@1.76.0

  • Update results now include their parts as separate fields (via, command, details, installDir, runningVersion, ranWith, managedBy) alongside message, so callers can lay them out. (#26455)

    Added planUpdate(pm, version), which decides how an update will run without changing anything: an install (with the tool and command), or no install with the result to show instead, for example when Homebrew owns the install. performUpdate follows the same plan, and accepts one you already showed the user:

    const plan = await planUpdate(pm, latestVersion);
    if (plan.willInstall) {
      console.log(`Installing with ${plan.via}: ${plan.command}`);
      const outcome = await performUpdate(pm, latestVersion, plan);
    }

@mastra/connect@1.0.1

Patch Changes

  • Added search tools so agents can find records directly instead of paging through full listings: slack_search_channels (find channels by name), github_search_issues, github_search_repositories, github_search_code, hubspot_search_contacts, discord_search_members, and stripe_search_customers. (#26271)

    const tools = await mastra.tools();
    // Find a channel without paging conversations.list manually
    await tools.slack_search_channels.execute({ query: "eng-platform" }, { requestContext });
    // Search issues and PRs with GitHub's query syntax
    await tools.github_search_issues.execute({ q: "repo:mastra-ai/mastra is:open label:bug" }, { requestContext });

@mastra/convex@1.7.2

Patch Changes

  • Workflow step results can now be recorded together with the workflow state in a single write, so evented workflows resume with the correct state after a restart. (#26221)

    Also in: @mastra/dsql@1.5.1, @mastra/dynamodb@1.4.1, @mastra/elasticsearch@1.4.4, @mastra/libsql@1.26.0, @mastra/mongodb@1.23.0, @mastra/mssql@1.10.2, @mastra/mysql@0.13.0, @mastra/oracledb@0.5.1, @mastra/pg@1.31.0, @mastra/redis@1.4.9, @mastra/spanner@1.9.3, @mastra/upstash@1.5.2, @mastra/valkey@0.2.5

@mastra/docker@0.10.0

Minor Changes

  • Added a repos option to createDockerRepoTemplate to build one template from several repositories. (#26039)

    • Each entry has its own getRepositoryAccess, ref and setupCommand, and is cloned to <workingDirectory>/<repo>.
    • workspaceSetupCommand runs once at the working directory after every repository.
    • continueOnSetupFailure records a failing repository setup in .mastra-sandbox/setup-failed instead of failing the build.
    • Writes a marker at .mastra-sandbox/repos/<repo> after each repository setup and .mastra-sandbox/workspace-setup after the workspace step.
    const template = createDockerRepoTemplate({
      repos: [
        {
          getRepositoryAccess: async () => ({ cloneUrl: "https://github.com/acme/app.git" }),
          setupCommand: "pnpm install --frozen-lockfile"
        },
        {
          getRepositoryAccess: async () => ({
            cloneUrl: "https://github.com/acme/shared-ui.git",
            authorization: { scheme: "bearer", token: process.env.GITHUB_TOKEN! }
          }),
          setupCommand: "pnpm install --frozen-lockfile"
        }
      ],
      workspaceSetupCommand: "cd app && pnpm link ../shared-ui"
    });

Patch Changes

  • Added an owner option to createDockerRepoTemplate and runWithSecrets so checkouts work on base images that run as a non-root USER. Previously, the checkout was copied as root, so git reported "dubious ownership" and writes failed with EACCES. (#26291)

    createDockerRepoTemplate({
      getRepositoryAccess,
      baseImage: "node:22",
      owner: "node"
    });

@mastra/dsql@1.5.1

Patch Changes

  • Fixed concurrent create() calls deleting another caller's draft. When two requests created the same prompt block, agent, MCP client, MCP server, scorer definition, skill, or workspace at the same time, the request that lost with a duplicate-key error could delete the winner's unpublished draft. Cleanup now only removes a draft the failing call inserted itself. (#26663)

    Also fixed failed creates leaving an orphaned draft behind. If saving the initial version failed, the draft row was sometimes kept because the cleanup step was skipped. It is now removed and the original error is still thrown.

    Also in: @mastra/pg@1.31.0

@mastra/duckdb@1.14.0

Minor Changes

  • Added the matches and notMatches trace-query operators to the DuckDB observability store, so traces can be found by the words in a span name or feedback comment. (#25111)

@mastra/e2b@0.13.0

Minor Changes

  • Added a fail-closed option for named E2B templates. Set fallbackTemplate: false on a NamedTemplateSpec to make start() reject with the original error when the named build fails or the resolved template can't create a sandbox, instead of silently booting the default template without your toolchain. Default behavior is unchanged. (#26634)

    const sandbox = new E2BSandbox({
      template: { ref: "my-tools:v1", template: myTemplate, fallbackTemplate: false }
    });
  • Added a repos option to createRepoTemplate to build one template from several repositories. (#26039)

    • Each entry has its own getRepositoryAccess and setupCommand, and is cloned to <workingDirectory>/<repo>.
    • workspaceSetupCommand runs once at the working directory after every repository.
    • continueOnSetupFailure records a failing repository setup in .mastra-sandbox/setup-failed instead of failing the build.
    • Writes a marker at .mastra-sandbox/repos/<repo> after each repository setup and .mastra-sandbox/workspace-setup after the workspace step.
    const template = createRepoTemplate({
      repos: [
        {
          getRepositoryAccess: async () => ({ cloneUrl: "https://github.com/acme/app.git" }),
          setupCommand: "pnpm install --frozen-lockfile"
        },
        {
          getRepositoryAccess: async () => ({
            cloneUrl: "https://github.com/acme/shared-ui.git",
            authorization: { scheme: "bearer", token: process.env.GITHUB_TOKEN! }
          }),
          setupCommand: "pnpm install --frozen-lockfile"
        }
      ],
      workspaceSetupCommand: "cd app && pnpm link ../shared-ui"
    });
  • Added a reattach option to RailwaySandbox, PlatformSandbox, and E2BSandbox to control what happens when a persisted sandboxId can't be reattached on start(). (#26675)

    • 'best-effort' (default, unchanged behavior): carry on without the lost sandbox. Railway and Platform provision a replacement; E2B first looks for another sandbox with the same logical id and only creates a new one if none is found.
    • 'required': throw a SandboxError with code SANDBOX_REATTACH_FAILED instead, so the caller can tell the user their previous sandbox (and its state) is gone rather than silently starting a fresh one.

    Transient errors such as auth, rate limit, or network failures still propagate unchanged. Clones inherit the parent's reattach setting.

    const sandbox = new RailwaySandbox({ sandboxId: session.sandboxId, reattach: "required" });
    
    try {
      await sandbox.start();
    } catch (error) {
      if (error instanceof SandboxError && error.code === "SANDBOX_REATTACH_FAILED") {
        // Previous sandbox is gone: ask the user to start fresh, then clear the persisted sandboxId.
      }
    }

    Also in: @mastra/platform-workspace@1.7.0, @mastra/railway@0.10.0

Patch Changes

  • Fixed npm, npx, and npm install -g failing with "Class extends value undefined" in sandboxes built from createDefaultMountableTemplate(). The template now removes the base image's old npm and corepack before installing Node. Existing cached default templates rebuild once. (#26329)

@mastra/elysia@0.1.16

Patch Changes

  • Fixed multipart form parsing to safely handle field names that collide with built-in object properties (#26683)

    Also in: @mastra/express@1.5.20, @mastra/fastify@1.5.20, @mastra/hono@1.7.18, @mastra/koa@1.7.20, @mastra/nestjs@0.2.35

@mastra/factory@0.21.0

Minor Changes

  • Added reasoningOptions to the models GET /web/config/models returns when models.dev describes them, so the Factory UI can offer only the thinking levels a model runs. An empty list means models.dev lists the model without reasoning controls. (#26229)

    {
      "models": [
        {
          "id": "openai/gpt-5",
          "provider": "openai",
          "modelName": "gpt-5",
          "hasApiKey": true,
          "reasoningOptions": [{ "type": "effort", "values": ["minimal", "low", "medium", "high"] }]
        }
      ]
    }
  • Added board-owned integration event rules: pass events to defineBoard, keyed by integration (github, gitlab, linear, jira, incidentio) and event name. A board handler runs for events bound to a card on that board; null switches the event off for that board only, so Work and Review keep their behavior. (#26627)

    defineBoard({
      id: "delivery",
      // ...
      events: {
        github: { pullRequestMerged: verifyCompletion, pullRequestCommentCreated: null },
        linear: { issueClosed: moveToCanceled }
      }
    });

    Added phase to integration rule contexts bound to a card: phase: { id, kind, role?, outcomes }, so handlers can act on what the card's phase means without checking board ids. Handlers set explicitly in an integration's rules still take precedence, so existing deployments behave as before.

  • Closing a tracked issue now closes its card on custom boards too. Boards declare where closed sources go with sourceClosed in defineBoard(): (#26621)

    defineBoard({
      id: "release",
      title: "Release",
      initialPhase: "queued",
      sourceClosed: { completed: "shipped", canceled: "dropped" },
      phases: {
        /* ... */
      }
    });

    The stock close rules for GitHub, GitLab, Linear, Jira, and incident.io move the card to that phase on its own board. Before, they only acted on Work cards and silently left cards on other boards in place. Work behaves as before, and boards that don't declare sourceClosed still leave their cards alone. Custom rule overrides can read the targets from context.boardClosure.

Patch Changes

  • Improved maintainability of Factory repository resolution while preserving provider ID matching and repository name fallbacks. (#26250)

  • Avoid invalid Work-board transitions when pull requests merge from custom boards. (#26604)

  • Fixed hosted Linear project filters hiding matching Factory cards. Preserve workspace-scoped project identity across issue reads and updates, and reconcile stored cards when project membership changes. (#26427)

  • Added the ability for one organization's admins to edit deployment thinking defaults from Settings when authentication is enabled. Set MASTRACODE_DEPLOYMENT_ORGANIZATION_ID to the organization the deployment serves. Only admins of that organization can save the defaults; everyone else sees them read-only. Without the variable, the defaults stay read-only while authentication is enabled. (#25199)

  • Open board tabs on large Factory projects no longer reload every card from the database on each poll. Card changes, including new comment counts, still appear on the next poll from any server replica. (#26647)

  • Factory now reads far fewer rule decisions in the background. The health check only loads decisions still in flight, and the supervisor's card and failure views load only the decisions they show, instead of every decision the Factory ever made. Finding the decisions produced by a rule evaluation is also much faster on large projects. (#26648)

  • Fixed the Factory board only picking up issues and pull requests from a project's first linked GitHub repository. Previously, repositories you linked later stayed empty until someone opened their issue lists directly. (#26631)

    • Linking a GitHub repository now brings its open issues and pull requests onto the board right away (up to five pages of each). Older items show up as cards but are never auto-started.
    • The background issue reconciler for the direct GitHub integration now finds open issues that were missed while webhooks were down, in every linked repository.
    • The Intake lane, global search, and the Sessions sidebar now cover every linked repository. Card details load from the repository the card came from, and one repository failing to load no longer blanks the others.
  • Work-item feeds handed to Factory role sessions now say when text is missing. A comment or reply quote cut at 2,000 characters ends with a marker like [truncated: 2,000 of 2,842 characters; comment <id>]. When older comments don't fit, the block opens with [N older comments omitted] (N+ when the comment cap was reached). (#26194)

  • Supervisor health now flags a card that is stuck in a working phase, such as a Planning card whose plan run failed, even when leftover triage work is still attached to it. Previously a parked triage proposal or an old triage session hid the stuck card, so nothing reported that it needed a new run. (#26649)

  • Fixed the Factory review verdict badge so it no longer shows an old approval as current after new commits are pushed. When a pull request or merge request gets a new head, its Review card and the Work card that opened it now show the previous verdict as outdated (for example Approved · be10bfa (outdated)) until the next review pass records a new one. The stored verdict is kept, so the next pass still runs as a re-review. (#26609)

  • Factory no longer aborts the running agent, posts the stage notice, or sends a skill kickoff when the card leaves its phase while the work-item feed is loading. The decision now settles as superseded instead of succeeded. The same check runs after the stage notice is delivered. (#26632)

  • Fixed runs failing with "Repository … is not linked to this Factory" after a linked GitHub or GitLab repository was renamed. Cards now resolve their repository by its stable provider ID first, so a stale owner/name stored on the card no longer blocks runs. (#26162)

  • Repository skills no longer collide with skills your app ships in its own Factory skills directory. Previously only Factory's bundled skill names were protected, so a repository skill with the same name as an app-defined skill made that skill unresolvable and broke any board phase that invoked it. In Factory sessions, repository skills that share a name with any Factory-served skill (bundled or app-defined) are now hidden. (#26614)

  • Fixed Factory disagreeing about which failed runs can be retried. GET /decisions now reports canRetry: false for decisions that have not failed, instead of true. The Supervisor factory_retry_decision tool now refuses failures a rerun cannot fix, such as run_overdue or skill_delivery_ambiguous, which the retry route and inbox already treated as not retryable. (#26659)

  • Fixed Factory reviews being withheld for pull requests opened by GitHub Apps. Re-reviews now find Factory's earlier review. Reviews still stop when the pull request author cannot be verified or does not match the recorded author. (#26658)

  • Faster rule processing on large Factory projects. Rule evaluations are now indexed by their ingress event, so looking up whether an event was already evaluated no longer scans every evaluation ever recorded. (#26645)

  • GitHub issues now land on the board set by their repository's intake source binding when no label route matches, instead of always arriving on Work. Label routes still take precedence, and when an issue's last routed label is removed — or its label route is cleared — its card returns to the binding's board. (#26616)

  • Fixed issue edits and comments queuing a factory-triage run for cards that shouldn't be re-triaged. The default GitHub and GitLab re-triage rules now only fire for cards on the Work board, so cards on custom boards (including ones in working or terminal phases) are left alone. GitLab cards in Execute, Review, Done, or Canceled are also no longer re-triaged, matching GitHub. Custom boards that want re-triage can add their own rule. (#26656)

  • Fixed label routing reopening done or canceled cards when their board is not installed. For example, Work cards in an app that sets includeDefaultBoards: false were moved onto a custom board when a label route changed. Cards on an uninstalled board now stay where they are. (#26351)

  • Fixed session resume after a host restart failing with Sandbox not found on the platform sandbox provider. Factory no longer persists the session's own id as the sandbox's reattach id when the provider exposes no physical id; the provider resumes through its own recovery key instead. (#26488)

  • Fixed webhook events being silently lost when a work item changed while a rule was being evaluated. Losing that race no longer records the event as a rejected, decision-less ingress. Factory now re-reads the work item and re-runs the rule (up to three attempts). If the item keeps changing, the delivery fails visibly so a redelivery is processed instead of being ignored as a duplicate. (#26547)

@mastra/inngest@1.10.4

Patch Changes

  • Fixed Inngest durable agents to emit step-finish events after resolving whether the loop continues. (#26348)

  • Fixed createInngestAgent ignoring lifecycle callbacks set in the agent's defaultOptions (such as onStepFinish and onFinish). Inngest agents now fire these defaults from stream(), generate(), resume(), and resumeGenerate() just like plain and durable agents, and callbacks passed at call time still take precedence. Fixes #26527. (#26533)

  • Fixed createInngestAgent ignoring an external abortSignal when the Inngest function runs in a different process. Aborting the signal passed to stream() or resumeStream() now stops the run on the step worker, the same as result.abort(), so the stream ends with finishReason: 'aborted' and onAbort fires. (#26548)

    const controller = new AbortController();
    const result = await agent.stream("...", { abortSignal: controller.signal });
    controller.abort(); // now stops the run on the remote worker
  • Inngest durable agents now switch to fallback models when the primary model fails. (#26585)

  • Typed runId on the onStepFinish payload for createInngestAgent stream, resume and observe calls. You can now read payload.runId without a cast. Fixes #26524. (#26531)

  • Fixed createInngestAgent ignoring stopWhen. The agentic loop now evaluates stopWhen conditions after each step when the run executes in the process that started it, matching the core durable agent. On a cross-worker resume it still falls back to maxSteps. (#25858)

@mastra/langfuse@1.5.14

Patch Changes

  • Fixed a run nested under another run replacing the Langfuse trace name, input, output, metadata, user, and session. Start the nested run with tracingOptions.nestUnderParent: true and the trace keeps the values of the run that started it. (#25691)

    await judge.generate("Grade this answer", {
      tracingOptions: { traceId: turn.traceId, parentSpanId: turn.spanId, nestUnderParent: true }
    });

@mastra/libsql@1.26.0

Patch Changes

  • Fixed local libSQL file databases staying blocked for seconds after a write was refused with SQLITE_BUSY. Later writes failed with "SQL statements in progress" until the stuck connection was garbage collected. LibSQLStore now reopens its connections after a busy refusal, including when it is under constant load. (#26261)

  • Fixed SQLITE_BUSY: database is locked errors when more than one LibSQL client in the same process uses the same local database file, for example a Memory with its own LibSQLStore pointing at the file Mastra's storage uses. A write from one client no longer blocks the process while another client's transaction is open; it waits for that transaction to finish instead. (#26420)

  • Added run fencing for durable agents. libSQL stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26420)

    Claims are kept in two new tables, mastra_workflow_run_owners and mastra_memory_run_fences, which are created on init. If you set disableInit: true, create them before upgrading. Durable agent runs fail to start until the tables exist.

    To prune them, set retention.workflows.runOwnership and retention.memory.runFences and call storage.prune().

    Requires @mastra/core 1.76.0 or a later 1.x release.

  • Fixed memory writes failing with SQLITE_BUSY when another process briefly holds the database lock. Saving messages, updating or deleting threads, and observational memory writes now retry with backoff, like other LibSQL storage writes. This is controlled by the existing maxRetries and initialBackoffMs options. (#26143)

@mastra/loggers@1.3.6

Patch Changes

  • UpstashTransport now keeps a limited number of records in memory while Upstash is unreachable, instead of growing without limit. (#26605)

    • The new maxBufferSize option limits how many unsent records are kept in memory. It must be a positive integer. It defaults to maxListLength, or 10000 when maxListLength turns off trimming.
    • When the buffer is full, the oldest records are dropped. The new droppedRecords field counts them.
    • Only one request is sent to Upstash at a time, so many writes during an outage no longer open hundreds of requests.
    const transport = new UpstashTransport({
      upstashUrl: process.env.UPSTASH_URL!,
      upstashToken: process.env.UPSTASH_TOKEN!,
      maxBufferSize: 5000
    });
    
    console.log(transport.droppedRecords);
  • Fixed Upstash logger shutdown to send every buffered batch. (#26174)

  • FileTransport.listLogs and listLogsByRunId now reject with a MastraError when the log file cannot be read (for example, it was deleted or is unreadable), instead of logging to the console and returning an empty result that looked like "no logs". Individual malformed lines are still skipped. (#26371)

  • Fixed FileTransport accepting a directory as its log path. It now throws a clear error at construction instead of failing later with an EISDIR stream error. (#26330)

  • Fixed FileTransport silently treating failed log writes as successful. Write errors (for example, a full disk) are now reported instead of logs being lost without notice. (#26331)

  • Fixed HttpTransport log queries and UpstashTransport error results returning page: 0 when called with page: 0. They now report page 1, consistent with the other transports. (#26678)

  • Fixed HTTP and Upstash log transports replacing a supplied time: 0 with the current time. Epoch-zero timestamps are now preserved; logs without a time still get the current time. (#26679)

  • Improved Upstash log queries to retrieve records in bounded pages. (#26173)

@mastra/mcp@2.3.0

Minor Changes

  • Added a clientInfo option to MCPClient so you can set the client name and version that MCP servers receive on connect and with each request, instead of the server key and 1.0.0. Set it once for all servers or override it per server. Tool name prefixes are unchanged. Fixes #25473. (#26328)

    Also fixed disconnecting a replaced MCPClient preventing reuse of the client that replaced it when both share an id.

    const mcp = new MCPClient({
      clientInfo: { name: "my-app", version: "2.3.4" },
      servers: {
        weather: { url: new URL("http://localhost:8080/mcp") }
      }
    });

@mastra/memory@1.37.0

Minor Changes

  • Added native model: 'auto' support for Observational Memory. Observer and Reflector models resolve independently from the agent's main model on every call, while explicit model IDs and advanced model configurations stay pinned. (#24508)

    'auto' is also the new default for model, observation.model, and reflection.model, which previously defaulted to google/gemini-2.5-flash. Auto picks google/gemini-2.5-flash when GOOGLE_GENERATIVE_AI_API_KEY is set, otherwise the low-cost model for the main model's provider (for example anthropic/claude-sonnet-4-6 → anthropic/claude-haiku-4-5, keeping a mastra/ gateway route), otherwise the main model itself. Set a concrete model ID to keep a fixed model; note that auto can fail where the old default worked if the chosen provider has no reachable credential.

    Applications can shape auto without reimplementing it:

    • autoModels overrides the model picked for a provider, for example { openai: 'openai/gpt-5-nano' }.
    • resolveModel(modelId, { requestContext }) turns the picked ID into a model, so auto can reuse your own credentials and routing.
    • resolveAutoModelId(mainModelId, { autoModels }) previews the model auto would pick, for display in settings UIs.
    new Memory({
      options: {
        observationalMemory: {
          autoModels: { openai: "openai/gpt-5-nano" },
          resolveModel: (modelId, { requestContext }) => myRouter(modelId, requestContext)
        }
      }
    });

Patch Changes

  • Improved observational memory finalization defensively: persistence and idle buffering use the terminal message list accepted by the processor pipeline rather than the list captured by the observation turn. Invariant tests cover distinct-list ownership and removed output. This is hardening, not a confirmed fix for #25023; the reported message loss remains unreproduced and the issue stays open. (#26362)

  • Fixed observational memory losing finished tool calls from a thread when an observational memory error stopped the run. The user message and tool results from steps that already completed are now saved before the run stops. (#26145)

@mastra/mesa@0.3.1

Patch Changes

  • Fixed @mastra/mesa being published without TypeScript type declarations. The package now ships dist/index.d.ts, so TypeScript projects no longer get "Could not find a declaration file for module '@mastra/mesa'". Fixes #26377. (#26610)

@mastra/observability@1.19.0

Minor Changes

  • Added support for tracingOptions.nestUnderParent. Every span of a run started with this option, a traceId, and a parentSpanId is exported with nestedUnderParent: true, and the run's tags are no longer applied to the trace. Custom exporters can read this field to skip trace-level data for such runs. (#25691)

    await judge.generate("Grade this answer", {
      tracingOptions: { traceId: turn.traceId, parentSpanId: turn.spanId, nestUnderParent: true }
    });

Patch Changes

  • Fixed cost estimates leaving out reasoning tokens. Reasoning tokens now use the output price when a model has no separate reasoning price, so estimated costs for reasoning models go up to match what providers bill. (#26521)

  • Fixed Datadog APM dropping the whole trace when a span type is excluded with excludeSpanTypes. The bridge no longer starts a dd-trace span for excluded types, so the remaining spans finish and the trace is sent. Fixes #25888. (#26044)

  • Fixed time to first token on model inference spans. Each inference span now records when its own request produced its first content, instead of reusing the time from the run's first request. This was most visible when a signal interrupted a request and the replacement reported a first-token time from before it started. (#25878)

  • Fixed spans being reported as aborted with "Observability is shutting down." in Braintrust, LangSmith and PostHog when calling flush() and then shutdown() right away, as in serverless handlers. flush() and shutdown() now finish processing queued span events first, so only spans that are really still open get aborted. Fixes #26137. (#26253)

  • Model requests cancelled by a queued signal now get their own MODEL_INFERENCE span, ended with finish reason interrupted. The replacement request opens a new inference span under the same MODEL_STEP, so its latency and chunks are no longer merged with the cancelled request. (#25861)

@mastra/pg@1.31.0

Minor Changes

  • Added the matches and notMatches trace-query operators to the PostgreSQL observability store, so traces can be found by the words in a span name or feedback comment. (#25111)

Patch Changes

  • Added run fencing for durable agents. Postgres stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26420)

    Claims are kept in two new tables, mastra_workflow_run_owners and mastra_memory_run_fences, which are created on init. If you set disableInit: true, create them before upgrading. exportSchemas() includes them. Durable agent runs fail to start until the tables exist.

    To prune them, set retention.workflows.runOwnership and retention.memory.runFences and call storage.prune().

    Requires @mastra/core 1.76.0 or a later 1.x release.

  • Fixed PostgresStore.init() blocking writes to mastra_workflow_snapshot while it builds the workflow run status and thread id expression indexes. On a table that already holds many runs, the build could keep other processes from saving workflow snapshots until it finished. init() now builds both indexes with CREATE INDEX CONCURRENTLY, like the other default indexes. The exported schema DDL is unchanged. (#26299)

    Concurrent index builds no longer silently retry as a blocking CREATE INDEX. Invalid indexes left behind by an interrupted concurrent build (pg_index.indisvalid = false) are now dropped and rebuilt instead of being treated as present, and every index build logs when it starts and how long it took. To build indexes outside of deploys, pre-create them with CREATE INDEX CONCURRENTLY and set skipDefaultIndexes: true.

  • Fixed caller-driven experiments failing with null value in column "targetType" ... violates not-null constraint on databases created before targetType and targetId became optional. The store now removes the old NOT NULL constraint from both columns at startup, so experiments without a target can be created again after upgrading. (#26297)

@mastra/platform-workspace@1.7.0

Minor Changes

  • Added a repos option to createRepoTemplate to build one template from several repositories. (#26039)

    • Each entry has its own getRepositoryAccess and setupCommand, and is cloned to <workingDirectory>/<repo>.
    • workspaceSetupCommand runs once at the working directory after every repository.
    • continueOnSetupFailure records a failing repository setup in .mastra-sandbox/setup-failed instead of failing the build.
    • Writes a marker at .mastra-sandbox/repos/<repo> after each repository setup and .mastra-sandbox/workspace-setup after the workspace step.
    const template = createRepoTemplate({
      repos: [
        {
          getRepositoryAccess: async () => ({ cloneUrl: "https://github.com/acme/app.git" }),
          setupCommand: "pnpm install --frozen-lockfile"
        },
        {
          getRepositoryAccess: async () => ({
            cloneUrl: "https://github.com/acme/shared-ui.git",
            authorization: { scheme: "bearer", token: process.env.GITHUB_TOKEN! }
          }),
          setupCommand: "pnpm install --frozen-lockfile"
        }
      ],
      workspaceSetupCommand: "cd app && pnpm link ../shared-ui",
      workingDirectory: "/home/user/repos"
    });

Patch Changes

  • Fixed PlatformSandbox.getInfo() asking the workspace proxy about a sandboxId hint before start() had confirmed it, which failed the first tool call of a resumed session with 404 not_found. getInfo() now answers from local state until the sandbox has started in the current process. start() also treats a 410 sandbox_destroyed answer for the reattach id like a 404 and provisions a fresh sandbox instead of failing the resumed session. (#26488)

    Fixed PlatformSandbox.stop() releasing the sandbox through the proxy's DELETE /sandbox/:id, which kills the VM on E2B. Mastra.shutdown() stops every registered workspace, so a host restart destroyed every live session. stop() now sends nothing and keeps the sandbox id, so the next start() reattaches; destroy() is the only call that releases the VM and its checkpoint.

    Added a sandboxId property that exposes the platform's id for the running sandbox, so callers persist an id the proxy recognises and reattach to the same sandbox later:

    const sandbox = new PlatformSandbox({ id: "session-1", environmentId: "env_123" });
    await sandbox.start();
    const persisted = sandbox.sandboxId; // the platform's id, undefined before start()
    
    const resumed = new PlatformSandbox({ id: "session-1", environmentId: "env_123", sandboxId: persisted });
    await resumed.start(); // { outcome: 'connected' }

@mastra/playground-ui@62.0.0

This release adds self-contained Metrics cards and dashboard building blocks, new Traces filters (Duration, Tags, and text matching on span name and feedback comment), reorderable DataList columns, and custom ask_user answers. See the full changelog for every change, including the breaking Metrics changes listed under Highlights.

@mastra/posthog@1.4.0

Minor Changes

  • Fixed $ai_tools in PostHog listing tools the model did not receive. Each call to the model provider is now exported as its own $ai_generation event with the model, token usage, output, the tools sent on that call, and the conversation that call received as $ai_input (including full tool arguments and results from earlier calls). (#25919)

    What changes in PostHog

    • A run that calls the model several times now sends one $ai_generation event per call, where it sent one per run before. The number of $ai_generation events goes up for multi-step runs.
    • The span that wraps those calls is now sent as $ai_span. Tokens and cost are counted once, so total cost per trace does not change.
    • Runs through the deprecated generateLegacy() and streamLegacy() methods no longer send $ai_generation events, so PostHog shows no tokens or cost for them. Use generate() and stream() instead.
    • With @mastra/core or @mastra/observability versions that do not record a span per model call, events are exported as before.

Patch Changes

  • Fixed $ai_input in PostHog missing the results of provider-executed tools (for example a provider's web search). Each later call's $ai_input now includes those results, and failed ones are marked as errors. (#26234)

@mastra/rag@2.6.7

Patch Changes

  • Fixed rag chunk tracing spans so they record chunkSize from the maxSize chunk option. Previously the attribute was always empty. (#26290)

@mastra/react@1.9.0

Minor Changes

  • Added useMcpAppHtml to @mastra/react/hooks/mcps. It loads the HTML of an MCP tool's ui:// app resource so you can render the tool's own UI, and skips the request when the tool has no app. (#25621)

    import { useMcpAppHtml } from "@mastra/react/hooks/mcps";
    
    const { data: html } = useMcpAppHtml({ serverId, appResourceUri: "ui://weather/map" });
  • Added useTraceQueryDiscoveryAvailable, which reports whether the observability store supports trace query field and value discovery. useTraceMetadataFilterFields now also returns canonicalFields, the store's built-in trace fields with their supported operators. (#26212)

    import { useTraceQueryAvailable, useTraceQueryDiscoveryAvailable } from "@mastra/react/hooks/capabilities";
    import { useTraceMetadataFilterFields } from "@mastra/react/hooks/traces";
    
    function TraceFields({ timeRange }) {
      const { enabled: withQueryTrace } = useTraceQueryAvailable();
      const { enabled: withDiscovery } = useTraceQueryDiscoveryAvailable();
    
      // Only call the discovery endpoint when the store supports both.
      const { fields, canonicalFields } = useTraceMetadataFilterFields({
        timeRange,
        queryOptions: { enabled: withQueryTrace && withDiscovery }
      });
    
      const tags = canonicalFields.find((field) => field.path === "tags");
      // tags?.operators → ['includes', 'notIncludes', 'exists', 'notExists']
    }

Patch Changes

  • Fixed streamed assistant messages using a temporary ID after a user signal instead of their persisted message ID. (#25770)

  • useModelUsageCostMetrics now groups usage by model and provider, and each row includes an optional provider field. A model served by more than one provider (for example gpt-4o through OpenAI and Azure) now shows up as separate rows instead of being merged. (#26241)

@mastra/redis-streams@0.6.3

Patch Changes

  • Fixed close() hanging forever when a Redis connection was reconnecting at shutdown. A brief Redis outage just before shutdown could keep the process from exiting, even after Redis came back. (#26256)

@mastra/server@1.76.0

Patch Changes

  • Fixed query parameter parsing to safely handle parameter names that collide with built-in object properties (#26683)

  • Fixed custom routes registered with requiresAuth: false returning 401 when a protected pattern such as /* matched them. Public custom routes now bypass auth as declared, while routes with requiresAuth: true still require authentication. (#26546)

  • Fixed a security issue where a signed-in user could take over another user's thread by creating a thread or conversation that reused its id. POST /api/memory/threads and POST /api/v1/conversations now return 403 when the id belongs to a thread owned by a different resource. The original owner keeps the thread, its title, metadata and messages. Re-creating your own thread with the same id still works. (#26254)

@mastra/server-adapters-test-suite@0.1.2

Patch Changes

  • Added coverage for reserved field and parameter names in shared adapter request parsing tests (#26683)

@mastra/spanner@1.9.3

Patch Changes

  • Fixed listMessages() on Spanner always failing with a SQL syntax error, which kept memory from loading message history. (#26262)

  • Fixed concurrent resumes of the same suspended workflow run on Spanner all succeeding. When a transaction retried after an abort, a resume that lost the race could still report a win, so the run resumed more than once. (#26262)

@mastra/sprites@0.1.0

Minor Changes

  • Added @mastra/sprites, a workspace sandbox provider for Fly.io Sprites. SpritesSandbox gets or creates a Sprite by name, so files persist across sessions, and runs commands with streaming output, timeouts, cancellation, and background processes with stdin. stop() keeps the Sprite, destroy() deletes it, and snapshot() captures a checkpoint. Requires Node.js 24 or later. (#26650)

    import { Workspace } from "@mastra/core/workspace";
    import { SpritesSandbox } from "@mastra/sprites";
    
    const workspace = new Workspace({
      sandbox: new SpritesSandbox({ id: "mastra-project", token: process.env.SPRITES_TOKEN })
    });

@mastra/temporal@0.4.14

Patch Changes

  • Added support for configuring Temporal activity retries with init({ retry }), allowing failing activities to stop after a specified number of attempts while preserving Temporal defaults when omitted. (#26366)

    Previously, activity retries couldn't be configured:

    const { createWorkflow, createStep } = init({ client, taskQueue: "mastra" });

    You can now limit generated activities to five total attempts:

    const { createWorkflow, createStep } = init({
      client,
      taskQueue: "mastra",
      retry: { maximumAttempts: 5, initialInterval: "5 seconds" }
    });

@mastra/turso@0.1.14

Patch Changes

  • Added run fencing for durable agents. Turso stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26420)

    Turso uses the run fencing in @mastra/libsql. Claims are kept in two new tables, mastra_workflow_run_owners and mastra_memory_run_fences, which are created on init. If you set disableInit: true, create them before upgrading. Durable agent runs fail to start until the tables exist.

    To prune them, set retention.workflows.runOwnership and retention.memory.runFences and call storage.prune().

    Requires @mastra/core 1.76.0 or a later 1.x release.

@mastra/valkey-streams@0.5.5

Patch Changes

  • Fixed ValkeyStreamsPubSub connecting without TLS when given a rediss:// or valkeys:// URL. TLS-only endpoints such as AWS ElastiCache with in-transit encryption now connect from the URL alone; an explicit valkeyOptions.useTLS still takes precedence. (#26630)

@mastra/voice-aws-nova-sonic@0.2.4

Patch Changes

  • Transcripts that contain the word "interrupted" are no longer swallowed as a barge-in. Only Nova Sonic's JSON { "interrupted": true } marker emits interrupt; ordinary text now emits writing as usual. Fixes #26504. (#26623)

@mastra/voice-google-gemini-live@0.14.14

Patch Changes

  • Fixed cleanup when the Gemini Live server closes the socket. A close from the server side (1011, goAway, a network drop) only changed state and emitted events, and the app's later disconnect() returned early with "Already disconnected". So the open speaker stream for the current reply never ended, leaving anything piping or awaiting it hung, and the maxDuration timer (up to 24h) kept the process alive and later called disconnect() on whatever session was current. The close and error handlers now end speaker streams and clear the session timers, including the sessionExpiring warning timer, which was never stored and so could not be cleared before. The events emitted on close are unchanged. (#26698)

    Fixes #26694.

@mastra/voice-inworld@0.4.4

Patch Changes

  • Tools that return nothing no longer produce an invalid function_call_output. The result is now serialised as null instead of being dropped, so the item always carries the required output string, matching the xAI realtime provider. The same fix is applied to the Inworld realtime provider, which uses the same function_call_output shape. Fixes #26505. (#26624)

    Also in: @mastra/voice-openai-realtime@0.14.4

@mastra/voice-openai-realtime@0.14.4

Patch Changes

  • Fixed the speaker stream never ending for responses that have no audio. A response that only calls a tool, a text-only response, or a response cut off by a dropped socket never got output_audio.done, so its stream stayed open. Anything piping it or waiting for end hung, and repeated tool turns piled up pipes until Node printed a MaxListenersExceededWarning. The stream now ends on response.done (before tools run), and all open streams end when the socket closes or on close()/disconnect(). (#26697)

    Fixes #26693.

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.