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:MetricsCardGroupno longer acceptsvariant;MetricsLineChartdroppedxAxisInterval/xAxisMinTickGap.@mastra/memoryObservational Memory defaults changed: OM now defaults tomodel: 'auto'(instead ofgoogle/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
400from 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
-
processLLMRequestnow 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
threadMetadatato theonDelegationStartreturn 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
runningstate, 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(), orrecover(). 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 withstatus: 'skipped', areason, and, for a run live in another process, aretryAttime.mastra.recoverAllDurableAgents()counts skipped runs in a newskippedfield and checks each one again once its claim could have lapsed, so a run whose process crashed is still recovered.recover()throwsDURABLE_AGENT_RUN_ACTIVEfor a live run. Passforce: trueto take the run over. The execution that held it stops.recover()throwsDURABLE_AGENT_RECOVER_RUN_ACTIVE_LOCALLYfor a run the same process is still executing, even withforce: true. Previously it started a second execution of the run. Wait for the run to finish instead.- Starting a run with a
runIdthat another execution holds waits up to 5 seconds, then throwsDURABLE_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()orrecover(), and, withrecovery: { 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.nestUnderParentto 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 newnestUnderRun()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
parentSpanIdwithout the new option behave as before. -
Added
getModelReasoningOptionsto 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, andundefinedwhen 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, anderrorProcessorDefaults: falseturns 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_CALLfinish instead of failing the turn withAGENT_STREAM_ERROR. The defaultstream-error-retry-processorresends 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 customStreamErrorRetryProcessorcan 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
filterfunction 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_CODESlists 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
outputOptionstorun.restart(), matchingrun.start()andrun.resume(). SetincludeState: trueto 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(), andgetRunOwnership()methods. - Memory stores have new
raiseRunFence()andretireRunFence()methods. - Methods that write run data, such as
saveMessages(),saveThread(),persistWorkflowSnapshot(), andupdateWorkflowState(), accept an optionalfence. A write whose fence is no longer the run's current claim throwsRunFenceConflictError. 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 tofalse, 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
Memoryhas 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.runOwnershipandmemory.runFencesretention 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 arunIdafter 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. } }
- Workflows stores have new
-
Added
matchesandnotMatchesoperators to advanced trace queries. They match whole words or phrases, ignoring case, on spannameand feedbackcomment. (#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.workflowConcurrencysets the parallelism (Infinityfor no limit). workflow.restartAllActiveWorkflowRuns()also restarts runs concurrently, capped byrecovery.workflowConcurrency. Set it to1if 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
usageIdfield to exported metrics. Every token and cost metric produced from the same model usage shares oneusageId, 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/reflectorModelandsession.om.<role>.switchModel({ modelId })accept'auto'to follow the active main model, andresolveAutoModelIdlets a consumer decide which concrete model an automatic role uses.session.om.<role>.model()reports the role's configured intent ('auto', a model ID, orundefined) whilemodelId()keeps returning the effective concrete model.defaultObserverModelId/defaultReflectorModelIdremain 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 fullprovider/modelID. Resolved models expose that label as a read-onlyid, 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 toolsuspend()(for exampleask_user), the run's in-memory registration and its workflow snapshot rows are now released, soagent.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
isTaskCompleteverdict as a livetask_complete_evaluationevent, so web clients can show "the check failed, the agent is fixing it" right away.@mastra/client-jsrecognizes 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.
currentMessagecan 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
Agentand durable agents. The finaltextand the retry prompt no longer include it, and neither doresponse.messagesand stored messages. Steps that completed before the failed attempt are kept.Text chunks already streamed live from the failed attempt still reach
textStreamandfullStreamconsumers. The step that carries them finishes with reasonretry. -
Keep Studio and Factory sessions alive for the identity provider's full session length. (#26447)
MastraAuthStudiosession cookies now last 14 days by default (was a hardcoded 24 hours), configurable via the newsessionMaxAgeSecondsoption or theMASTRA_SESSION_MAX_AGEenvironment variable.- When the shared API renews the session during verification,
MastraAuthStudiore-issues the renewed cookie under the deployment's own cookie domain and exposes it through a new optionalconsumePendingResponseHeadersprovider hook. @mastra/server's auth middleware,CompositeAuth, the Factory auth gate, and Factory's per-routeensureFactoryAuthUser(used by routes declaredrequiresAuth: false, which skip the gate) forward those headers to the browser, as does the publicGET /auth/meroute. 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
setStateupdates 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: trueclosing 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
messageFilterandonDelegationStartdelegation 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 — andmessageFilterreturnsModelMessage[]. Previously they were typed as stored database messages, so filters reading fields likecontent.partsoridcould silently let parent context through. Update filters to readroleandcontent. Fixes #25983. (#26161) -
Fixed
UnixSocketPubSubleaving lease files behind when a lease release was followed immediately byclose(). 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 beforeclose()now completes, andclose()waits for it, for up to one second if another process holds the lease's lock. New lease operations afterclose()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)
-
Classifiernow supports AI SDK decision models. Pass a model from a provider'sdecisionModel()factory and Mastra callsdoDecide(). Models that only implement the deprecateddoEvaluate()contract still work.ClassifierProcessor,ModelSelectionProcessor, and classifier scorers accept the same models. (#26279)To match the AI SDK naming,
Classifier.decide()replacesClassifier.evaluate()andMastraDecisionModelreplacesMastraEvaluationModel. The old names are deprecated aliases and will be removed in a future release.EvaluationModelResult,MastraEvaluationModelInterface,ConfiguredClassifierEvaluateOptions, andPerCallClassifierEvaluateOptionsare likewise deprecated in favor of theirDecision/Decidecounterparts.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
nullfor optional fields, or usequery,message, orinputinstead ofpromptnow 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'sabort()now removes it fromlistSuspendedRuns(). 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
finishevents are sent without the step history, and oversizederrorevents keep the error name and a shortened message soonErrorstill fires. Oversized non-terminal events are replaced with adata-oversized-eventchunk so clients know an event was reduced, and a warning is logged through the Mastra logger.
Also in: @mastra/inngest@1.10.4
- Aborting a suspended run with
-
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 whenmastra.shutdown()starts now persists arunningcheckpoint at each step boundary it reaches during the drain. If it reaches one before the drain ends, it can be resumed later withrecover()orrecoverActiveRuns()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();
- Smaller checkpoints: a durable run's checkpoints no longer repeat messages the run has already saved to memory (for example with
-
Fixed a durable agent thread staying busy forever when
cleanup()was called before the run finished. Cleanup now waits for the run to settle, soonFinish/onErrorfire and the thread is released for the next request. Fixes #25974. (#26587) -
Fixed
sendSignal(..., { ifIdle: { behavior: 'wake' } })on a DurableAgent so the accepted result'soutput.textnow resolves to the generated text, the same as with a regular Agent. Previously it resolved toundefined. (#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'sprocessOutputResult. The run now reports the tripwire withfinishReason: 'other', as a regularAgentdoes. 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
processOutputStreamchanged 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
lengthorcontent-filter) and responses that fail validation do not savemetadata.structuredOutput. The response message itself is still saved. - With
errorStrategy: 'fallback', thefallbackValueis saved instead.
This does not yet apply when
structuredOutput.modelis set to a separate structuring model (see #26431).Fixes #26432.
- Truncated responses (finish reason
-
Fixed durable agents ignoring
structuredOutput.model. The separate structuring model now runs when the turn finishes, sooutput.objectresolves andmetadata.structuredOutputis saved on the assistant message, matchingAgent. (#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-suspendedandtool-call-approvalchunks 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
AgentControllersession 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 thatToolSearchProcessorloaded 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 withToolNotFoundErrorand being retried under a new call id. (#26592) -
Fixed durable agents (such as Inngest agents) silently dropping call-time
toolsetswhen 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 viarequestContext). (#25860) -
Fixed
DurableAgentabort()not stopping in-flight tool or model calls whenmodelSettings.timeout.totalMsis 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
payloadandoutput— including the whole, monotonically growing iteration state — because its step-result merge writes bypassed the workflow'spruneSnapshothook, 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-streamsand@mastra/valkey-streams. Awaitedstart(),resume(),restart()and time-travel calls now only read finish events published after the call, soresume()returns the resumed result instead of the earliersuspendedone, and waiting no longer re-reads the wholeworkflows-finishstream. (#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_commandno 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
ServerConfigfrom@mastra/core/serverandOPENSEARCH_PROMPTfrom@mastra/opensearchso the documented imports resolve. Fixed JSDoc import paths in@mastra/evalsto use@mastra/evals/scorers/prebuiltand@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),toAISdkStreamnow setstoolMetadata.app(resourceUri,mimeType: 'text/html;profile=mcp-app', andserverIdwhen available) ontool-input-startandtool-input-availablechunks, 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
channelsconfigured exported the live Telegram/Slack adapter on every span'srequestContext, 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 useserializeForSpan()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.
- Reserved Mastra request-context keys (those starting with
-
Corrected the documented defaults for
SerializationOptions.maxStringLength(131072) andmaxDepth(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
maxStepsor 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.
- The restarted request keeps the same step: it doesn't use up
-
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.pruneAgentLoopSnapshotis 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
runIdinonStepFinishandonFinish, matching the regular Agent. UserunIdto tell which run an event came from when callbacks are shared across runs. (#26531)onFinishalso now includesmodel,messages,object,errorandusedFallbackValue. 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,reasoningTextandgetFullOutput()exclude discarded reasoning, and tracers can close the cancelled request's inference span through the optionalinterruptInference()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, sosetStateupdates 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
approveToolCallorrecover) 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/coreversion. -
Fixed
PIIDetectorsending a message's original text to the model when redaction left nothing usable. Withstrategy: '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 withredactionMethod: 'remove'), or when PII is flagged with no usable spans. This applies toprocessInputandprocessOutputResult. 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/coreversion. (#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 andsubAgentToolResults: []with no indication why. Now the delegation result includessubAgentPendingToolCallslisting 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
autoResumeSuspendedToolskept growing the heap on every tool conversion (eachgenerate/streamcall 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 whenzod@3.25.xis 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
updateWorkingMemorywhenworkingMemory.agentManagedisfalse. The tool is not available in that mode, so agents now receive the read-only working memory instruction instead (#25896). (#26198) -
Fixed
resumeStreamignoring therequestContextanddelegationyou pass when a supervisor run is resumed in the same process that suspended it. The resumed delegation now calls theonDelegationStarthook from the resume call, and sub-agent tools see the resume call'srequestContext.DurableAgenttools approved withapproveToolCallin 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)
listMessagesnow reportshasMorecorrectly whenincludeis combined with pagination or several threads. Before, it could report no more pages while filtered messages remained, or report more pages whenincludehad already returned every message.- Saving an existing channel installation again no longer resets its
createdAt. batchDeleteTraceswithorganizationIdorresourceIdnow deletes only that tenant's spans. Before, it removed the whole trace or nothing, depending on which root span was written last.
-
Added
closeOnSuspendtoDurableAgent.recover(), matchingstream()andresume(). When a recovered run suspends (for example, waiting on tool approval), the returnedfullStreamnow ends, so callers can hand off toresume()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
processOutputStepwas still saved to the memory thread and returned asresult.textwhen the run ended on the tripwire (aftermaxProcessorRetriesran 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, orresumeStream, each saved step now contains only its own content. Later steps no longer repeat earlier tool calls, so snapshots stay small and processors andprepareStepsee 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:readwhen inspecting an existing live session. If the in-memory session is missing after a restart, they preserve recovery behavior by recreating it through theagent-controller:execute-authorized path. Other operations that create, resume, or mutate sessions also requireagent-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
setStateupdates 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 forstart()andresume(). (#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
usageIdshared by all rows from the same model call. Usage rolled up from hidden model calls onto a visible span gets a separateusageIdper 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-registeredevent 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). sendSignalnow recognizes an active run owned by another instance while the thread is observed, soifActivebehavior (for examplediscard) applies instead of waking a new run.
- A replayed
-
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
ToolCallFilterremoving 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/serverand@modelcontextprotocol/coremove from 2.0.0 to 2.2.0, and@modelcontextprotocol/sdkfrom ^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:readandmemory:writefor 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
suspendSchemaorresumeSchema, sotoolCallConcurrencyruns it one call at a time, the same as a regular tool with asuspendSchema. Steps that callsuspend()should declare asuspendSchemaorresumeSchemaso the workflow tool is detected as suspendable. (#26608) -
Preserve the original
TypeErrorwhen plain agents reject invalidmodelSettings.timeoutstream 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
messageMetadatacallback inchatRoute,handleChatStream, andtoAISdkStreamnow receives the run'straceId. Return it as message metadata souseChatclients 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) });
traceIdis undefined when tracing is disabled or with@mastra/coreversions before 1.75.0.
Patch Changes
-
Restored live sub-agent progress in
handleChatStream()andchatRoute(). Both helpers now streamdata-tool-agentanddata-tool-agent-stepparts 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. PassincludeSubAgentMetadata: falseto 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 andcloseStdin()sends EOF, so you can run interactive or stdio-based programs (such as MCP servers or CLI agents) in a Blaxel sandbox. Processes spawned withstdinMode: '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 theonStdout/onStderrcallbacks 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.nestUnderParentappearing 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
matchesandnotMatchestrace-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 theuseExperiment()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
matchesandnotMatchestrace-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 Controllersubscribe({ reconnect })reports it throughonError, instead of retrying.408timeouts,429rate 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
processDataStreamrejection or inonError: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 Conflictinstead of500. 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.nestUnderParentbeing dropped from agent and workflow requests sent over HTTP. A run started through@mastra/client-jswith 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 forwardsoptions(includingspeaker) to the agent's voice provider, so the requested voice is used instead of the provider default. The top-levelspeakerIdfield still works; if both are sent,options.speakertakes 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
listMessagesByIdin 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
CloudflareSandboxsilently 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
runThinkingLevelto@mastra/code-sdk/thinking. It returns the thinking level a request actually sends for a model, using the reasoning options models.dev publishes.getAvailableThinkingLevelsForModeltakes 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 sendhigh.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 threadactiveModelPackId. An explicit--modelstill 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
previewLinespreference, since compact rendering is now the only Mastra Code display mode. (#26576)GlobalSettingsno longer haspreferences.quietMode,preferences.quietModeMaxToolPreviewLinesoronboarding.quietModePreferenceSelected. It addspreferences.previewLines(0–8, default 2). When settings load andpreviewLinesisn't set yet, it is filled in fromquietModeMaxToolPreviewLinesand 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_sendreporting 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 withexpectsReply: truestill 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_REGIONthere. 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-lunainstead ofopenai/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 newreleaseThreadClaims()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/modelacceptsmodelId: '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 asnulland 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-defaultsroute 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.inputProcessorsalso 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) alongsidemessage, 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.performUpdatefollows 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, andstripe_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
reposoption tocreateDockerRepoTemplateto build one template from several repositories. (#26039)- Each entry has its own
getRepositoryAccess,refandsetupCommand, and is cloned to<workingDirectory>/<repo>. workspaceSetupCommandruns once at the working directory after every repository.continueOnSetupFailurerecords a failing repository setup in.mastra-sandbox/setup-failedinstead of failing the build.- Writes a marker at
.mastra-sandbox/repos/<repo>after each repository setup and.mastra-sandbox/workspace-setupafter 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" });
- Each entry has its own
Patch Changes
-
Added an
owneroption tocreateDockerRepoTemplateandrunWithSecretsso checkouts work on base images that run as a non-rootUSER. Previously, the checkout was copied as root, sogitreported "dubious ownership" and writes failed withEACCES. (#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
matchesandnotMatchestrace-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: falseon aNamedTemplateSpecto makestart()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
reposoption tocreateRepoTemplateto build one template from several repositories. (#26039)- Each entry has its own
getRepositoryAccessandsetupCommand, and is cloned to<workingDirectory>/<repo>. workspaceSetupCommandruns once at the working directory after every repository.continueOnSetupFailurerecords a failing repository setup in.mastra-sandbox/setup-failedinstead of failing the build.- Writes a marker at
.mastra-sandbox/repos/<repo>after each repository setup and.mastra-sandbox/workspace-setupafter 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" });
- Each entry has its own
-
Added a
reattachoption toRailwaySandbox,PlatformSandbox, andE2BSandboxto control what happens when a persistedsandboxIdcan't be reattached onstart(). (#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 logicalidand only creates a new one if none is found.'required': throw aSandboxErrorwith codeSANDBOX_REATTACH_FAILEDinstead, 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
reattachsetting.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, andnpm install -gfailing with "Class extends value undefined" in sandboxes built fromcreateDefaultMountableTemplate(). 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
reasoningOptionsto the modelsGET /web/config/modelsreturns 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
eventstodefineBoard, keyed by integration (github,gitlab,linear,jira,incidentio) and event name. A board handler runs for events bound to a card on that board;nullswitches 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
phaseto 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'srulesstill 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
sourceClosedindefineBoard(): (#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
sourceClosedstill leave their cards alone. Custom rule overrides can read the targets fromcontext.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_IDto 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/namestored 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 /decisionsnow reportscanRetry: falsefor decisions that have not failed, instead oftrue. The Supervisorfactory_retry_decisiontool now refuses failures a rerun cannot fix, such asrun_overdueorskill_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-triagerun 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: falsewere 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 foundon 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
createInngestAgentignoring lifecycle callbacks set in the agent'sdefaultOptions(such asonStepFinishandonFinish). Inngest agents now fire these defaults fromstream(),generate(),resume(), andresumeGenerate()just like plain and durable agents, and callbacks passed at call time still take precedence. Fixes #26527. (#26533) -
Fixed
createInngestAgentignoring an externalabortSignalwhen the Inngest function runs in a different process. Aborting the signal passed tostream()orresumeStream()now stops the run on the step worker, the same asresult.abort(), so the stream ends withfinishReason: 'aborted'andonAbortfires. (#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
runIdon theonStepFinishpayload forcreateInngestAgentstream, resume and observe calls. You can now readpayload.runIdwithout a cast. Fixes #26524. (#26531) -
Fixed
createInngestAgentignoringstopWhen. The agentic loop now evaluatesstopWhenconditions 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 tomaxSteps. (#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: trueand 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.LibSQLStorenow reopens its connections after a busy refusal, including when it is under constant load. (#26261) -
Fixed
SQLITE_BUSY: database is lockederrors when more than one LibSQL client in the same process uses the same local database file, for example aMemorywith its ownLibSQLStorepointing 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_ownersandmastra_memory_run_fences, which are created on init. If you setdisableInit: true, create them before upgrading. Durable agent runs fail to start until the tables exist.To prune them, set
retention.workflows.runOwnershipandretention.memory.runFencesand callstorage.prune().Requires
@mastra/core1.76.0 or a later 1.x release. -
Fixed memory writes failing with
SQLITE_BUSYwhen 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 existingmaxRetriesandinitialBackoffMsoptions. (#26143)
@mastra/loggers@1.3.6
Patch Changes
-
UpstashTransportnow keeps a limited number of records in memory while Upstash is unreachable, instead of growing without limit. (#26605)- The new
maxBufferSizeoption limits how many unsent records are kept in memory. It must be a positive integer. It defaults tomaxListLength, or 10000 whenmaxListLengthturns off trimming. - When the buffer is full, the oldest records are dropped. The new
droppedRecordsfield 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);
- The new
-
Fixed Upstash logger shutdown to send every buffered batch. (#26174)
-
FileTransport.listLogsandlistLogsByRunIdnow reject with aMastraErrorwhen 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
FileTransportaccepting a directory as its log path. It now throws a clear error at construction instead of failing later with anEISDIRstream error. (#26330) -
Fixed
FileTransportsilently 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
HttpTransportlog queries andUpstashTransporterror results returningpage: 0when called withpage: 0. They now report page1, consistent with the other transports. (#26678) -
Fixed HTTP and Upstash log transports replacing a supplied
time: 0with the current time. Epoch-zero timestamps are now preserved; logs without atimestill 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
clientInfooption toMCPClientso you can set the client name and version that MCP servers receive on connect and with each request, instead of the server key and1.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
MCPClientpreventing reuse of the client that replaced it when both share anid.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 formodel,observation.model, andreflection.model, which previously defaulted togoogle/gemini-2.5-flash. Auto picksgoogle/gemini-2.5-flashwhenGOOGLE_GENERATIVE_AI_API_KEYis set, otherwise the low-cost model for the main model's provider (for exampleanthropic/claude-sonnet-4-6→anthropic/claude-haiku-4-5, keeping amastra/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:
autoModelsoverrides 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/mesabeing published without TypeScript type declarations. The package now shipsdist/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, atraceId, and aparentSpanIdis exported withnestedUnderParent: 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 thenshutdown()right away, as in serverless handlers.flush()andshutdown()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_INFERENCEspan, ended with finish reasoninterrupted. The replacement request opens a new inference span under the sameMODEL_STEP, so its latency and chunks are no longer merged with the cancelled request. (#25861)
@mastra/pg@1.31.0
Minor Changes
- Added the
matchesandnotMatchestrace-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_ownersandmastra_memory_run_fences, which are created on init. If you setdisableInit: true, create them before upgrading.exportSchemas()includes them. Durable agent runs fail to start until the tables exist.To prune them, set
retention.workflows.runOwnershipandretention.memory.runFencesand callstorage.prune().Requires
@mastra/core1.76.0 or a later 1.x release. -
Fixed
PostgresStore.init()blocking writes tomastra_workflow_snapshotwhile 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 withCREATE 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 withCREATE INDEX CONCURRENTLYand setskipDefaultIndexes: true. -
Fixed caller-driven experiments failing with
null value in column "targetType" ... violates not-null constrainton databases created beforetargetTypeandtargetIdbecame 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
reposoption tocreateRepoTemplateto build one template from several repositories. (#26039)- Each entry has its own
getRepositoryAccessandsetupCommand, and is cloned to<workingDirectory>/<repo>. workspaceSetupCommandruns once at the working directory after every repository.continueOnSetupFailurerecords a failing repository setup in.mastra-sandbox/setup-failedinstead of failing the build.- Writes a marker at
.mastra-sandbox/repos/<repo>after each repository setup and.mastra-sandbox/workspace-setupafter 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" });
- Each entry has its own
Patch Changes
-
Fixed
PlatformSandbox.getInfo()asking the workspace proxy about asandboxIdhint beforestart()had confirmed it, which failed the first tool call of a resumed session with404 not_found.getInfo()now answers from local state until the sandbox has started in the current process.start()also treats a410 sandbox_destroyedanswer 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'sDELETE /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 nextstart()reattaches;destroy()is the only call that releases the VM and its checkpoint.Added a
sandboxIdproperty 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_toolsin PostHog listing tools the model did not receive. Each call to the model provider is now exported as its own$ai_generationevent 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_generationevent per call, where it sent one per run before. The number of$ai_generationevents 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()andstreamLegacy()methods no longer send$ai_generationevents, so PostHog shows no tokens or cost for them. Usegenerate()andstream()instead. - With
@mastra/coreor@mastra/observabilityversions that do not record a span per model call, events are exported as before.
- A run that calls the model several times now sends one
Patch Changes
- Fixed
$ai_inputin PostHog missing the results of provider-executed tools (for example a provider's web search). Each later call's$ai_inputnow includes those results, and failed ones are marked as errors. (#26234)
@mastra/rag@2.6.7
Patch Changes
- Fixed
rag chunktracing spans so they recordchunkSizefrom themaxSizechunk option. Previously the attribute was always empty. (#26290)
@mastra/react@1.9.0
Minor Changes
-
Added
useMcpAppHtmlto@mastra/react/hooks/mcps. It loads the HTML of an MCP tool'sui://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.useTraceMetadataFilterFieldsnow also returnscanonicalFields, 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)
-
useModelUsageCostMetricsnow groups usage by model and provider, and each row includes an optionalproviderfield. A model served by more than one provider (for examplegpt-4othrough 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: falsereturning 401 when a protected pattern such as/*matched them. Public custom routes now bypass auth as declared, while routes withrequiresAuth: truestill 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/threadsandPOST /api/v1/conversationsnow 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.SpritesSandboxgets 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, andsnapshot()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_ownersandmastra_memory_run_fences, which are created on init. If you setdisableInit: true, create them before upgrading. Durable agent runs fail to start until the tables exist.To prune them, set
retention.workflows.runOwnershipandretention.memory.runFencesand callstorage.prune().Requires
@mastra/core1.76.0 or a later 1.x release.
@mastra/valkey-streams@0.5.5
Patch Changes
- Fixed
ValkeyStreamsPubSubconnecting without TLS when given arediss://orvalkeys://URL. TLS-only endpoints such as AWS ElastiCache with in-transit encryption now connect from the URL alone; an explicitvalkeyOptions.useTLSstill 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 emitsinterrupt; ordinary text now emitswritingas 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 openspeakerstream for the current reply never ended, leaving anything piping or awaiting it hung, and themaxDurationtimer (up to 24h) kept the process alive and later calleddisconnect()on whatever session was current. The close and error handlers now end speaker streams and clear the session timers, including thesessionExpiringwarning 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 asnullinstead of being dropped, so the item always carries the requiredoutputstring, matching the xAI realtime provider. The same fix is applied to the Inworld realtime provider, which uses the samefunction_call_outputshape. Fixes #26505. (#26624)Also in: @mastra/voice-openai-realtime@0.14.4
@mastra/voice-openai-realtime@0.14.4
Patch Changes
-
Fixed the
speakerstream 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 gotoutput_audio.done, so its stream stayed open. Anything piping it or waiting forendhung, and repeated tool turns piled up pipes until Node printed aMaxListenersExceededWarning. The stream now ends onresponse.done(before tools run), and all open streams end when the socket closes or onclose()/disconnect(). (#26697)Fixes #26693.
Other updated packages
The following packages were updated with dependency changes only:
- @mastra/agent-builder@1.1.27
- @mastra/arize@1.3.23
- @mastra/arthur@0.4.23
- @mastra/datadog@1.4.15
- @mastra/deepeval@0.1.17
- @mastra/deployer-cloud@1.76.0
- @mastra/deployer-cloudflare@1.2.35
- @mastra/deployer-netlify@1.2.35
- @mastra/deployer-sandbox@0.3.20
- @mastra/deployer-vercel@1.2.35
- @mastra/e2b-desktop@0.1.6
- @mastra/editor@0.16.1
- @mastra/laminar@1.3.25
- @mastra/langsmith@1.3.25
- @mastra/longmemeval@1.1.35
- @mastra/next@0.2.34
- @mastra/opencode@0.1.35
- @mastra/otel-bridge@1.5.15
- @mastra/otel-exporter@1.4.6
- @mastra/sentry@1.2.25
- @mastra/tanstack-start@0.2.34