Highlights
Model Routing per Request (ModelSelectionProcessor)
ModelSelectionProcessor lets an agent dynamically choose the best/cheapest model for each run via a built-in classifier, with sensible fallbacks and options like scope: 'first-step' and minProbability to control when routing applies.
Cross-Process Cancellation of Queued Thread Input
cancelQueuedMessages({ signalIds }) now cancels selected pending input across all Agents sharing the same runtime + memory thread (not just the calling Agent), with clearPendingSignals available on thread aborts (core, server route, and client-js all add support).
Multi-Tenant Safe Observability Queries (Trusted Tenant Scope)
Advanced trace/thread queries can now be planned with a trusted { organizationId, resourceId? } scope that is enforced by stores and bound into cursors (conflicts fail fast), and @mastra/server can derive this automatically from a reserved organizationId request-context key.
More Powerful Trace Querying + Foundations for Trace Aggregation
Trace queries gain tag predicates (includes/notIncludes/exists/notExists) and top-level durationMs predicates for filtering completed root traces; plus new helper APIs and Zod schemas/types that lay the groundwork for the upcoming aggregateTraces() operation.
Better Durable Streaming + Inngest Efficiency
Durable agent stream()/resume() add closeOnSuspend to end streams at tool-suspension boundaries (so UIs can reliably emit “finished”), and durable turns use dramatically fewer Inngest steps for the same work.
Breaking Changes
@mastra/playground-ui:ErrorStateremoved (useEmptyState tone="error").@mastra/playground-ui:PermissionDeniedandSessionExpiredmoved to auth domain with simplified props (custom copy/actions require renderingEmptyStatedirectly).@mastra/playground-ui:Buttonoutlinevariant removed (use default/primary).@mastra/playground-ui:TraceKeysAndValuesremoved;TraceDataPanelView placement="trace-page"now renders the summary itself.
Changelog
@mastra/core@1.70.0
Minor Changes
-
Added
ModelSelectionProcessor, which picks the model for each request with a classifier. Keep a capable model as the agent's default and let simple requests run on a cheaper one. (#24865)Describe each model and the requests it should handle. The processor builds the classifier for you:
import { Agent } from '@mastra/core/agent'; import { ModelSelectionProcessor } from '@mastra/core/processors'; // `model` is the evaluation model that makes the decision (`EvaluationModelV4 | MastraEvaluationModel`, the same type Classifier accepts). new Agent({ name: 'support-agent', model: 'openai/gpt-5.6-sol', inputProcessors: [ new ModelSelectionProcessor({ model, choices: [ { model: 'openai/gpt-5-mini', criteria: 'Answerable in one or two sentences with no reasoning steps' }, { model: 'openai/gpt-5.6-sol', criteria: 'Requires multi-step reasoning or careful judgment' }, ], onDecision: decision => console.log('model selection', decision), }), ], });
To use a
Classifieryou already have, pass it asclassifierand map its typed answers to a model withselect.Routing doesn't always save money. Models don't share prompt caches, and a cheaper model can take more steps. Measure cost and quality on your own traffic first.
Behavior
- The chosen model serves the whole run. Set
scope: 'first-step'to change only the first call. - If the classifier fails, the agent's configured model is used.
- If the chosen model fails, the agent's fallback models take over.
- With
minProbabilityset, the configured model is used when the confidence is too low or missing.
- The chosen model serves the whole run. Set
-
Added a
closeOnSuspendoption to durable agentstream()andresume(), so callers can end the stream when a tool suspends. (#24894)Previously, the stream returned by
DurableAgent.stream()(andcreateInngestAgent().stream()) stayed open after a tool suspended for approval or user input, and there was no public way to change that. Loops overfullStreamhung, so integrations like AG-UI could not emitRUN_FINISHED.Pass
closeOnSuspend: trueto close the stream at the suspension boundary, matching non-durableAgent.stream():const result = await durableAgent.stream('hi', { closeOnSuspend: true }); for await (const chunk of result.fullStream) { // loop ends after the tool-call-suspended chunk }
The default is unchanged (
false): the stream stays open across suspension.Also in: @mastra/inngest@1.10.0
-
Added helpers to check which trace fields you can group by and which measures you can request for the upcoming
aggregateTraces()API. (#24847)- Group by trace fields such as
statusoruserId, or by top-levelmetadata.<key>paths. - Supported measures:
count,duration.avg/min/max/p50/p90/p95/p99,errorCount,errorRate, andcountDistinct.<field>. Percentile values can be approximate.
import { getTraceAggregateDimensionDescriptors, isTraceAggregateDimension, parseTraceAggregateMeasure, } from '@mastra/core/storage'; isTraceAggregateDimension('metadata.tenant'); // true — top-level metadata keys are groupable isTraceAggregateDimension('metadata.customer.id'); // false — nested paths are not isTraceAggregateDimension('traceId'); // false — identity fields are not dimensions parseTraceAggregateMeasure('duration.p95'); // { type: 'canonical', measure: 'duration.p95', rule: { approximate: true, ... } } parseTraceAggregateMeasure('countDistinct.traceId'); // { type: 'countDistinct', field: 'traceId' } const dimensions = getTraceAggregateDimensionDescriptors(); // canonical trace-field descriptors (metadata.<key> is checked via isTraceAggregateDimension)
- Group by trace fields such as
-
Filter client-echoed history before memory processors load stored messages. (#24076)
On a thread that already has stored messages, memory now keeps only the new part of the request input: the trailing user messages, plus any tool outcomes the client sends for calls the stored conversation still has pending (results, errors, denials, and approval answers). This works whether the outcome arrives on its own or together with the next user message. An empty thread is still seeded with the full input, with assistant provider metadata stripped.
When an input message has the same ID as a stored message, the stored message remains the base so its reasoning, provider metadata, ordering, and timestamp are retained. Client tool outcomes only fill in calls that are still pending, so an echo can't overwrite a stored tool result.
This prevents lossy client echoes from orphaning OpenAI reasoning items, re-persisting user messages with client timestamps, or duplicating assistant text during history replay. Observational Memory uses the same stored-base layering behavior.
This is a behavior change: on an existing thread, any input message before the last assistant message that isn't stored is removed. That includes few-shot examples and caller-assembled message arrays, not only assistant messages sent to modify the thread or user messages re-sent with a changed
createdAtto reorder it. Usememory.saveMessagesor the memory store'supdateMessagesto change stored history.To opt out, set the new
retainFullInputmemory option, per call onmemory.optionsor agent-wide in the memory constructor options. The request input is then processed exactly as supplied, history still loads, and every input message that isn't already stored is saved to the thread. TheuseAgentstructured output path uses it so its replayed request keeps the parent's message prefix.Fixes #24052.
Also in: @mastra/memory@1.32.0
-
Added typed descriptions for processor span payloads and pipeline attributes. Processor spans now record their exact pipeline phase, so consumers can narrow supported payloads without guessing their shape. (#24672)
import { describeSpanOutput } from '@mastra/core/observability'; const output = describeSpanOutput(span); if (output?.type === 'processor' && output.value.phase === 'outputStream') { output.value.data.totalChunks; // number }
Added
describeProcessorPipelinefor executor, pipeline position, hook duration, mutations, and tripwire details. Unknown attributes remain separate from the known fields, so no value is rendered twice. Spans recorded before the phase existed keep their untyped shape and fall back to JSON.Fixed missing message-list mutation logs in workflow processor executions.
-
Added
createClassifierScorer()for using a configuredClassifieras a typed Mastra scorer. Select one classifier question, get a score between 0 and 1, and retain the classifier evidence in the scorer result. (#24792)import { Classifier } from '@mastra/core/classifier'; import { createClassifierScorer } from '@mastra/core/evals'; import { getAssistantMessageFromRunOutput } from '@mastra/evals/scorers/utils'; const classifier = new Classifier({ id: 'response-quality', model, questions: { quality: { type: 'score', criteria: ['Incorrect', 'Partially correct', 'Correct'], }, }, }); const scorer = createClassifierScorer({ id: 'response-quality-scorer', classifier, question: 'quality', type: 'agent', state: ({ run }) => ({ output: getAssistantMessageFromRunOutput(run.output) ?? '' }), });
-
Added tag predicates to advanced trace queries. Trace predicates accept
includesandnotIncludeson thetagsfield, andexists/notExistsontagsmean "at least one tag" / "no tags". Missing and empty tag lists are treated alike.tagsalso supports value discovery. (#24554)Example
{ op: 'includes', path: 'tags', value: 'manual-review' }
-
Add an optional
titletocreateTool()so a tool call can carry a human-readable display name. Closes #20249. (#24117)The title is never sent to the model. It is stamped on the
tool-call-input-streaming-startandtool-callstream chunks and persisted on the stored tool-invocation part, so a chat UI can label the call after a reload without a client-side name catalog.const weatherTool = createTool({ id: 'get_weather_by_coordinates', title: 'Weather Lookup', description: 'Fetches the current weather for a latitude/longitude pair', inputSchema: z.object({ lat: z.number(), lon: z.number() }), execute: async ({ lat, lon }) => fetchWeather(lat, lon), });
-
Added a trusted tenant scope to advanced trace queries. Hosts pass
{ organizationId, resourceId? }toplanTraceQuery,planThreadQuery, and the discovery planners; the scope is carried on the trusted plan, ANDed into every root and related-signal scan by stores, and bound into keyset cursors so a cursor reused under another scope fails withTRACE_QUERY_CURSOR_CONFLICTbefore storage runs. Callers still can't nameorganizationIdorprojectIdin predicates. No scope means no filter, so self-hosted behavior is unchanged. (#24566)Example
const plan = planTraceQuery(parseTraceQueryRequest(request), { scope: { organizationId: 'org_123', resourceId: 'project_456' }, });
-
Added trace-level
durationMspredicates to advanced trace queries. (#24635)Previously, duration filtering required a span relation, which can match a child span:
where: { spans: { some: { op: "gt", left: { path: "durationMs" }, right: { literal: 5000 } } } }
Use the top-level field to evaluate only the selected completed root:
where: { op: "gt", left: { path: "durationMs" }, right: { literal: 5000 } }
-
Expanded
cancelQueuedMessages({ signalIds })to cancel pending input across all Agents sharing the runtime and memory thread. AddedclearPendingSignalsto thread abort options. (#23942)Changed behavior
- Signal-ID cancellation previously matched only the calling Agent's queued messages. It now also covers other Agents' messages, pre-run signals, and signals pending in an active run.
- The
queueOwnerIdselector still cancels only the calling Agent's queued messages in that owner group. - Cancellation remains effective during lease handoffs. Clear-on-abort prevents queued input from returning after a preparation failure.
- Running Agents listen for remote signals, cancellation, and abort requests without requiring
subscribeToThread(). Disconnecting the last thread observer no longer disables these controls. - Remote thread aborts also stop durable runs through their existing abort transport, including when clearing pending input without an observer.
- Remote input already in transit survives run handoffs. Observer-only signal copies don't keep execution listeners alive or become new input when a thread is reused.
- Input queued on a claimed thread remains remotely cancellable after the claim is released, even without an open thread subscription.
- Cancellation results exclude observer history and report only pending work removed locally. Delayed enqueue retries can't restore cancelled input while the execution listener remains active.
const thread = { resourceId: 'user-123', threadId: 'thread-abc' }; // Selected pending input across Agents sharing the thread. agent.cancelQueuedMessages({ ...thread, signalIds: ['signal-123'] }); // Existing Agent-scoped owner-group behavior. agent.cancelQueuedMessages({ ...thread, queueOwnerId: 'session-123' }); agent.abortThreadStream({ ...thread, clearPendingSignals: true }); // Existing behavior: abort without clearing pending input. agent.abortThreadStream(thread);
Selected-ID cancellation publishes all requested IDs through PubSub, even when none are pending locally, so other processes subscribed to the thread can remove matching pending input. Propagation is asynchronous and best-effort. The result reports only local cancellations, without remote acknowledgements. Clear-on-abort forwards the clear flag to the active owner, but doesn't clear every process's queues. Neither operation cancels
continueWithMessages()continuations or undoes persisted effects.
Patch Changes
-
Update provider registry and model documentation with latest models and providers (
bfde500) -
Fixed channel threads getting permanently stuck after a tool call suspended on adapters that cannot show approval buttons. With
toolDisplay: 'hidden', runs now auto-resume suspended tools on platforms without interactive buttons (for example SMS, iMessage or custom gateways). Set the newapprovalButtonsadapter option to override the detection. Inbound messages that don't start a run are now logged at warn level instead of being dropped silently. (#24913)channels: { adapters: { imessage: { adapter: imessageAdapter, toolDisplay: 'hidden' }, // now auto-resumes custom: { adapter: customAdapter, toolDisplay: 'hidden', approvalButtons: true }, }, }
-
Fixed client-sent copies of stored messages changing what's stored. When a request includes a message with the same ID as a stored one: (#24895)
- The stored message is kept as is. Client text, reasoning, and metadata no longer replace or add to it.
- For assistant messages, the client copy can only fill in a tool outcome for a call the stored message still has pending.
- To change a stored message, update it in storage.
- This also applies with
retainFullInput, so the client's rendered copy can't undo an output processor's rewrite of a saved message, such as a redacted card number.
Fixes #20836.
-
Channel approval cards (Slack, Telegram, etc.) now show the sub-agent tool awaiting approval and its arguments, instead of the supervisor's
agent-<name>delegation call. (#24907) -
Fixed in-memory dataset configuration reads to return undefined for unset or cleared tags, target type, target IDs, and scorer IDs, matching LibSQL. Serialized responses omit these properties instead of returning null. Consumers should use nullish checks rather than require explicit null properties. Empty arrays remain distinct from cleared settings. (#23937)
-
Fixed agent controller chat channels showing Approve/Deny buttons for tools that run or are blocked automatically. Channels now only show approval buttons when a person actually needs to decide (tools with an
askpolicy). Fixes #22379. (#24914) -
Fixed per-request reply topics leaking streams on persistent pub/sub backends. The agent runtime's cross-process flows (thread owner discovery, peer discovery, and idle-signal acceptance) create a unique reply topic per request; on backends like Redis Streams, subscribing creates a real stream key that previously outlived the request forever. Reply topics are now deleted via
clearTopicas soon as their request settles, and a request whose timeout fires before its subscribe finishes no longer publishes at all. Requests also carry the caller's absolute deadline: a responder that receives one late (backlog replay on a fresh subscription, redelivery) drops it instead of replying into the released reply topic — or, for idle signals, starting a run the caller already reported as timed out. A clock-skew grace keeps responders from dropping live requests when process clocks drift. No change for the in-memory pub/sub, whereclearTopicis a no-op. (#24580)Full cleanup on Redis Streams requires the matching
@mastra/redis-streamsrelease. With an older one,unsubscribe()does nothing while a subscribe is still in flight, and that late subscribe recreates the stream key afterclearTopicdeletes it. -
Fixed
TokenLimiterProcessorcounting history that later prompt processors remove from the request. In the defaultbest-fitandcontiguoustrim modes, the input budget is now enforced on the provider prompt inprocessLLMRequest, after earlier prompt processors such asToolCallFilterhave run, so only tokens that reach the model are counted. Tool calls and their results are kept or removed together, and stored messages are no longer changed. (#24679)Where
processLLMRequestdoesn't run, such asgenerateLegacy(),streamLegacy()and limiters inside a processor workflow,processInputStepstill trims stored messages as before. -
Durable agent turns now use far fewer Inngest steps, including when observability is not configured. A 20-step agent turn previously used 133–158 Inngest steps. (#24782)
Also in: @mastra/inngest@1.10.0
-
Fixed images and files returned by tools going missing when an agent uses a router model such as
anthropic/*. The selected model now receives the complete tool result, including images and files. (#24910) -
Dynamic workflow validation now reads JSON-Schema
propertiesas own keys, so a field namedconstructorortoStringno longer looks present on every schema (#24900) -
Reduced stored durable agent iteration data for large prompts and tool sets. (#24946)
-
Fixed Bedrock-hosted OpenAI models sending unsupported temperature and top-p settings. Fixes #24815. (#24886)
-
Added public Zod request and response schemas and types for the upcoming
aggregateTraces()observability operation:traceAggregateRequestSchema,traceAggregateResponseSchema, andparseTraceAggregateRequest(). Selection (timeRange,where) reuses the existing trace-query schemas, so aggregate and list queries validate the same population. No runtime operation ships yet; the storage method and HTTP route follow in later releases. (#24834)import { parseTraceAggregateRequest } from '@mastra/core/storage'; const request = parseTraceAggregateRequest({ timeRange: { from: '2026-09-01T00:00:00Z', to: '2026-09-08T00:00:00Z' }, groupBy: ['entityName'], measures: ['count', 'duration.p95'], }); // request.limit === 100, request.orderBy === { field: 'count', direction: 'desc' }
@mastra/clickhouse@1.21.0
Minor Changes
-
Added trace-query tag predicates for ClickHouse. Trace queries can use
includes,notIncludes,exists, andnotExistsontags, and value discovery returns each observed tag with the number of traces that carry it. Missing and empty tag lists behave the same. (#24554)Example
const result = await clickhouseObservability.queryTraces( planTraceQuery( parseTraceQueryRequest({ timeRange: { from: '2026-09-01T00:00:00.000Z', to: '2026-09-21T00:00:00.000Z' }, where: { op: 'notIncludes', path: 'tags', value: 'archived' }, }), ), );
-
Fixed ClickHouse deletion requests blocking review updates on feedback that was never deleted. A request is now marked applied only after its delete succeeds, and review updates ignore unapplied requests. If a delete fails, the feedback stays editable; call
deleteFeedback()again to retry. (#24033)Review-status updates now change the feedback row in place instead of inserting a copy, so a concurrent update can no longer bring deleted feedback back. If a newer version of the same feedback event is ingested during an update, the update is re-applied to that version and throws a conflict error after repeated conflicts.
Upgrade note:
updateFeedbackReviewStatus()now runsALTER TABLE … UPDATEonmastra_feedback_events, so the runtime database user needsALTER UPDATE(reviewStatus)on that table, andINSERTonmastra_feedback_events_deltaif it does not already have it. A user limited toSELECTandINSERT, which was enough for review updates before this release, now fails withNot enough privileges:GRANT ALTER UPDATE(reviewStatus) ON <database>.mastra_feedback_events TO <runtime_user>; GRANT INSERT ON <database>.mastra_feedback_events_delta TO <runtime_user>;
Add the grants before you deploy this version. No schema migration is required. If you set
disableInit: trueand runinit()with separate migration credentials, grant these to the runtime user, not only to the migration user. -
Added ClickHouse support for filtering completed root traces by elapsed duration. (#24635)
Previously, duration filtering required a span relation, which can match a child span:
where: { spans: { some: { op: "gt", left: { path: "durationMs" }, right: { literal: 5000 } } } }
Use the top-level field to evaluate only the selected completed root:
where: { op: "gt", left: { path: "durationMs" }, right: { literal: 5000 } }
-
Applied the trusted tenant scope of advanced trace queries to root spans, related spans, scores, feedback, and discovery scans. The store advertises the
trace-query-tenant-scopefeature so the server can reject scoped requests against older stores. (#24566)Also in: @mastra/duckdb@1.11.0, @mastra/pg@1.27.0
@mastra/client-js@1.49.0
Minor Changes
-
Added
cancelQueuedMessages()to cancel selected pending input without stopping the active run. The server propagates all requested signal IDs through shared PubSub, even when none are pending locally, to other processes subscribed to the thread. The response reports only local cancellations, without remote acknowledgements. AddedclearPendingSignalstoabortThread()and subscriptionabort()options. (#23942)const thread = { resourceId: 'user-123', threadId: 'thread-abc' }; await agent.cancelQueuedMessages({ ...thread, signalIds: ['signal-123'] }); await agent.abortThread({ ...thread, clearPendingSignals: true }); // Existing behavior: abort without clearing pending input. await agent.abortThread(thread);
-
Added the
includesandnotIncludestrace-query operators and thearrayvalue kind to the generated trace-query types. (#24554)Example
const result = await mastraClient.queryTraces({ timeRange: { from: '2026-09-01T00:00:00.000Z', to: '2026-09-21T00:00:00.000Z' }, where: { op: 'includes', path: 'tags', value: 'manual-review' }, });
Patch Changes
-
Expose the optional tool
titleon the tool endpoints and inGetToolResponse, and copy it fromtool-callandtool-call-input-streaming-startchunks onto the tool-invocation message part in theuseChataccumulator. Part of #20249. (#24117)const tool = await client.getTool('get_weather_by_coordinates').details(); tool.title; // 'Weather Lookup'
Also in: @mastra/react@1.6.2, @mastra/server@1.70.0
@mastra/code-sdk@1.8.2
Patch Changes
-
Added a
prepareWakeRequestContextoption tocreateMastraCode(). A wake (a notification or cross-agent signal that starts a run on an idle thread) has no inbound request, so hosts that resolve credentials per tenant can use this option to attach the owning identity before the run starts. It is called only when a session owns the target resource. (#24909)const mastraCode = await createMastraCode({ prepareWakeRequestContext: async ({ requestContext, resourceId }) => { const owner = await lookUpOwner(resourceId); if (owner) requestContext.set('user', owner); }, });
-
Fixed
agent_signal_sendso senders put content where the peer can see it. Thepayloadparameter was removed because peers never received it, and thesummaryparameter was renamed tomessageto make clear it is the full message delivered to the peer. (#24823)Before:
agent_signal_send({ targetId: 'peer-id', summary: 'Review this', expectsReply: false, });
After:
agent_signal_send({ targetId: 'peer-id', message: 'Review this', expectsReply: false, });
The tool result now reports only the routing outcome instead of echoing the whole message back to the sender. Mastra Code still shows the target, routing options, full message, and outcome in the standard tool display, with a truncated message preview in quiet mode.
-
Fixed plugin updates occasionally keeping stale code loaded. When an updated plugin file had the same size and modification timestamp as the previous version, the reload could reuse the old module; plugin reloads now detect changes by file content, so an update always runs the new code. (#24890)
-
The
/thinkthinking level now applies to Gemini, custom OpenAI-compatible providers, and OpenAI API-key models, not just Anthropic and OpenAI Codex. Previously these models silently ignored it. (#24899)/think highGemini and OpenAI API-key models map the level to what each model supports. Custom OpenAI-compatible providers receive the selected level unchanged, including
xhighandmax. With thinkingoff(the default), requests are unchanged.
@mastra/duckdb@1.11.0
Minor Changes
-
Added DuckDB support for filtering completed root traces by elapsed duration. (#24635)
Previously, duration filtering required a span relation, which can match a child span:
where: { spans: { some: { op: "gt", left: { path: "durationMs" }, right: { literal: 5000 } } } }
Use the top-level field to evaluate only the selected completed root:
where: { op: "gt", left: { path: "durationMs" }, right: { literal: 5000 } }
-
Added trace-query tag predicates for DuckDB. Trace queries can use
includes,notIncludes,exists, andnotExistsontags, and value discovery returns each observed tag with the number of traces that carry it. Missing and empty tag lists behave the same. Span tags are now trimmed, deduplicated, and stripped of blank entries on write, matching the PostgreSQL and ClickHouse stores. (#24554)Example
const observability = await duckdbStore.getStore('observability'); const result = await observability.queryTraces( planTraceQuery( parseTraceQueryRequest({ timeRange: { from: '2026-09-01T00:00:00.000Z', to: '2026-09-21T00:00:00.000Z' }, where: { op: 'exists', path: 'tags' }, }), ), );
Patch Changes
- Trimmed, deduplicated, and dropped blank tags when writing spans, matching the PostgreSQL and ClickHouse stores so tag predicates and tag value discovery see the same values. (#24554)
@mastra/factory@0.17.1
Patch Changes
-
Fixed Factory Work filters to include every GitHub issue assignee instead of only the first assignee. (#24839)
-
Filesystem snapshots are no longer lost with "No active thread on this session" when a session is deleted right after a turn. Deleting a Factory session now waits up to 10 seconds for the last turn's snapshot to finish before tearing the session down; a snapshot that takes longer can still be skipped. (#24905)
waitForPendingFilesystemCaptureis now exported so custom hosts can do the same:import { waitForPendingFilesystemCapture } from '@mastra/factory'; await waitForPendingFilesystemCapture(resourceId); await controller.deleteSession({ resourceId });
-
Secondary buttons and select menus in Factory now use the default button style instead of the removed outline style. (#24818)
-
Factory empty states now use the design system's icon color and 32px icon size, so every empty state matches instead of mixing 20px and 40px icons. (#24799)
-
Improved
invalid_transitionrejections. They now name the next stages declared from the current phase. An agent that requests a mistyped stage id can correct it in the same run. (#24906)Example:
The Delivery board does not allow moving from planning to plan_review. Next stages declared from planning: plan-review, canceled. -
Fixed Factory runs woken by a notification or cross-agent signal on an idle thread failing with "No usable anthropic credential is configured". The run now resolves credentials as the user who owns the Factory session, in that session's organization, including sessions opened from the browser or Slack. (#24909)
-
Fixed the project feed event stream leaving every delivered event unacknowledged. On durable pub/sub backends such as Redis Streams, unacknowledged deliveries pile up in the broker's pending list and the client's in-flight tracking for as long as a feed connection stays open, growing memory with every feed update on long-lived dashboard tabs. Feed deliveries are now acknowledged as soon as they are handled. (#24580)
-
Slack sessions now start on the model the linked sender picked in their own model pack, instead of always using the factory project's default model. When the sender has no active pack, the factory project default still applies, and when neither exists the session keeps the built-in default. (#24816)
The model a conversation starts on is now recorded on that conversation, so every later message and every restart keeps using it rather than re-checking preferences that may have changed since. Conversations that already have a model are left alone.
Slack sessions also observe with the linked sender's own observational-memory settings — observer and reflector models, thresholds, and attachment handling — instead of the factory project's shared settings. Anything the sender has not configured themselves keeps following the project.
-
Fixed Factory work item conversations: board links now open the item's lifecycle conversation, and the transcript no longer crashes on malformed message parts. (#24866)
@mastra/inngest@1.10.0
Patch Changes
-
Fixed a type error where Inngest workflows created with init() rejected a first step that shares the workflow's input schema when that schema uses .default() or coercion. The workflow's .then() now compares the step against the parsed input type (defaults applied), while run.start() and cron inputs keep accepting the raw caller input where defaulted fields may be omitted. Fixes #24409 (#24732)
-
Fixed durable step failures in the Inngest dashboard and logs showing only an internal
@mastra/inngeststack frame. The reported error now keeps the original stack, including the error type and the line that threw, while custom error properties are still preserved. Fixes #24748. (#24771) -
Fixed a crash when resuming a durable agent run immediately after a tool suspends.
InngestAgent.resume()now waits for the run to finish suspending before resuming it, instead of failing withCannot read properties of undefined (reading 'threadId'). Fixes #24749. (#24770) -
Fixed durable agent streams to publish through configured transports without duplicating Inngest Realtime events. (#24814)
-
Fixed failing steps in Inngest workflows running extra times when
retriesis set. Inngest appliedretriesto every failing step on top of the step's own retries, so a step with no retries ran three times withretries: 2. Errors marked non-retryable were retried too. Failing steps now only use their own retry settings.retriesstill re-runs a workflow when a request to your app fails, such as during a process restart. (#24842) -
Fixed resumed Inngest agents returning chunks from the original suspended run. (#24813)
-
Fixed serve and connect registration for Inngest durable agents and corrected the required @mastra/core version. (#24812)
@mastra/libsql@1.23.2
Patch Changes
- Fixed LibSQL dataset writes to preserve JSON null in input, groundTruth, and expectedTrajectory across insertion, updates, and version history. Omitted dataset descriptions now read back as undefined, matching their declared type. Existing SQL NULL values are unchanged because previously lost distinctions cannot be recovered. (#23937)
@mastra/mcp@2.1.0
Minor Changes
-
Carry the MCP tool
titlethroughMCPClientandMCPServer. Closes #20249. (#24117)MCPServerpublishes a tool'stitleintools/list. Tools that only setmcp.annotations.titleare unchanged.- Tools returned by
MCPClientcarrytool.title, taken from the server's tooltitleand falling back toannotations.title, the same precedence MCP clients use for display names. listToolDefinitions()keeps the title andtoolFromDefinition()restores it.
const tools = await mcp.listTools(); tools.github_create_issue.title; // 'Create Issue'
@mastra/mcp-docs-server@1.3.0
Minor Changes
-
The docs server now runs on
@mastra/mcp2.x and serves the MCP2026-07-28revision over stdio. Editors that still open with the pre-2026initializehandshake (Cursor, Codex CLI, VS Code and Claude Code at the time of writing) keep working: the server reads the first request on stdin and, when it is aninitialize, serves the connection with the published@mastra/mcp1.x implementation instead. The same tools and prompts are registered either way, sonpx -y @mastra/mcp-docs-server@latestneeds no configuration change. (#24707){ "mcpServers": { "mastra": { "command": "npx", "args": ["-y", "@mastra/mcp-docs-server@latest"] } } }The startup log line on stderr now reports which protocol was selected, for example
{"level":"info","message":"Started Mastra Docs MCP Server","data":{"protocol":"legacy"}}. Server-levelnotifications/messagelogging is gone with the 2.x protocol; the server's own log output goes to stderr, filtered by--log-level, and error logs are still written to~/.cache/mastra/mcp-docs-server-logs. The migration prompts no longer carry the removedversionfield, and the package'sserverexport is replaced bycreateDocsServer(era).
@mastra/memory@1.32.0
Patch Changes
-
Fixed Observational Memory saving its own instructions and system reminders as things the user said. The observer could record lines like "User's current priority is to extract new observations" as the thread's current task, which then misled the agent on later turns. The observer now receives its instructions after the conversation instead of as a separate message before it, and system reminders and signals in the conversation are labeled by their tag (for example
system-reminderornotification) instead of as the user. Fixes #22195. (#24908) -
Thread-scoped Observational Memory now describes observations as memory of the current conversation instead of "past conversations with this user". Agents using the default
scope: 'thread'will reuse IDs, artifacts, and tool results recorded in observations instead of treating them as coming from a different session. Resource scope keeps its existing wording. AddedgetObservationContextPrompt(scope)for integrations that build the observations context themselves. (#24889)import { getObservationContextPrompt } from '@mastra/memory/processors'; const preamble = getObservationContextPrompt('thread'); // "The following observations block contains your memory of earlier parts of this current conversation. ..."
Also in: @mastra/opencode@0.1.29
@mastra/mongodb@1.18.10
Patch Changes
- Fixed dataset storage to preserve authored strings and distinguish omitted item fields from explicit null through updates and version history. Cleared dataset schemas now read as undefined. Previously lost values cannot be recovered automatically. (#23937)
@mastra/mysql@0.10.1
Patch Changes
- Fixed dataset storage to preserve JSON null, JSON-looking strings, and Unicode text through reads, updates, and version history. Cleared optional dataset configuration now reads as undefined. Previously lost null distinctions cannot be recovered automatically. (#23937)
@mastra/pg@1.27.0
Minor Changes
-
Added PostgreSQL support for filtering completed root traces by elapsed duration with exact millisecond comparisons. (#24635)
Previously, duration filtering required a span relation, which can match a child span:
where: { spans: { some: { op: "gt", left: { path: "durationMs" }, right: { literal: 5000 } } } }
Use the top-level field to evaluate only the selected completed root:
where: { op: "gt", left: { path: "durationMs" }, right: { literal: 5000 } }
-
Added trace-query tag predicates for PostgreSQL. Trace queries can use
includes,notIncludes,exists, andnotExistsontags, and value discovery returns each observed tag with the number of traces that carry it. Tag membership uses the existing GIN index ontags. (#24554)Example
const observability = await pgStore.getStore('observability'); const result = await observability.queryTraces( planTraceQuery( parseTraceQueryRequest({ timeRange: { from: '2026-09-01T00:00:00.000Z', to: '2026-09-21T00:00:00.000Z' }, where: { op: 'includes', path: 'tags', value: 'manual-review' }, }), ), );
Patch Changes
-
Fixed PostgreSQL dataset reads and writes to preserve JSON null and JSON-looking strings in item input, groundTruth, and expectedTrajectory across insertion, updates, and version history. Existing externalId retry equivalence is unchanged: omitted and null payload fields compare equally, and a retry returns the original stored representation. Previously lost SQL NULL distinctions cannot be recovered. (#23937)
Unset or cleared target type, target IDs, and scorer IDs now return undefined, matching in-memory and LibSQL storage. Serialized responses omit these properties instead of returning null. Consumers should use nullish checks rather than require explicit null properties.
-
Fix PostgreSQL saves failing on NUL characters or unpaired surrogates while preserving literal Unicode escape text. Fixes #24873. (#24887)
@mastra/playground-ui@58.0.0
Minor Changes
-
EmptyStateis now the design system's only status block. A newtoneprop colors its icon, andtone="error"defaults to the red circle-x iconErrorStateused to render. (#24799)Breaking
-
ErrorStateis removed. UseEmptyStatewithtone="error":// Before import { ErrorState } from '@mastra/playground-ui/components/ErrorState'; <ErrorState title="Failed to load tools" message={error.message} action={retryButton} />; // After import { EmptyState } from '@mastra/playground-ui/components/EmptyState'; <EmptyState tone="error" titleSlot="Failed to load tools" descriptionSlot={error.message} actionSlot={retryButton} />;
-
PermissionDeniedandSessionExpiredhold Studio's permission copy and SSO login flow, so they moved out of the design system into the auth domain:// Before import { PermissionDenied } from '@mastra/playground-ui/components/PermissionDenied'; import { SessionExpired } from '@mastra/playground-ui/components/SessionExpired'; // After import { PermissionDenied } from '@mastra/playground-ui/domains/auth/components/permission-denied'; import { SessionExpired } from '@mastra/playground-ui/domains/auth/components/session-expired';
-
PermissionDeniednow takes onlyresource(required) andvariant, andSessionExpiredonlyvariant. Thetitle,description,actionSlotandclassNameoverrides are removed:// Before <PermissionDenied title="Access required" description="Ask an admin." actionSlot={requestButton} /> <SessionExpired title="Sign in to continue" className="py-12" /> // After <PermissionDenied resource="workflows" /> <SessionExpired variant="fill" />
For custom copy or actions, render
EmptyStatedirectly. -
EmptyStaterenders every icon at 32px, whatever size the icon sets itself, so status blocks stay consistent across apps.
Improved
PermissionDeniedshows a lock icon andSessionExpireda timer-off icon, so neither reads as an empty list anymore.- The Log in button on
SessionExpirednow sends the client's custom headers, like Studio's own login does, and shows an error toast when the login cannot start. EmptyStateicons without their own color now render muted by default.
-
-
Added a
narrowvariant and an optionalheaderslot toPageLayout. Thenarrowvariant centers the page body in a wide max-width column;headerrenders a page-level header (such asPageHeader) inside the body container, above the content. (#24817)<PageLayout variant="narrow" breadcrumbs={crumbs} header={<PageHeader>…</PageHeader>}> {content} </PageLayout>
-
Fields now stand out from the card, dialog, or drawer they sit in, and fields with errors show a red outline. (#24843)
Fields on surfaces
Text fields, textareas, input groups, and the default Select, Combobox, and DateTimePicker triggers pick their fill and outline from the surface around them. Inside a card, dialog, or drawer they're one step lighter in dark mode and get a stronger outline in light mode. Dialogs, drawers, and alert dialogs use a new
--dialogsurface that is off-white in light mode. Nothing changes at the call site.Error outline
Passing
errorto Input, Textarea, InputGroup, Select, Combobox, or CodeEditor now shows a red outline on every surface. Before, the red border was hidden behind the field's shadow.New tokens:
--dialog,--field,--field-on-surface,--field-disabled,--field-rim,--field-rim-focus. -
Added
displayLabel,showChevron, andiconOnlyValuetoComboboxso triggers can show a custom selected label, hide the chevron, or center an icon-only value. The popup list now scrolls inScrollArea, with an overlay scrollbar and edge fades. (#24728)<Combobox options={countries} value={country} showChevron={false} iconOnlyValue />
-
Removed the
outlinevariant fromButton, and from the triggers built on it (SelectTrigger,Combobox,DropdownMenu.Trigger,PopoverTrigger). Thedefaultvariant covers the same neutral role, so there is one look for secondary actions and form triggers instead of two that sat side by side. (#24818)If you passed
variant="outline", remove it to get the default look:Before
<Button variant="outline">Cancel</Button> <SelectTrigger variant="outline" size="sm" /> <Button variant={active ? 'primary' : 'outline'}>List</Button>
After
<Button>Cancel</Button> <SelectTrigger size="sm" /> <Button variant={active ? 'primary' : 'default'}>List</Button>
-
The trace summary now shows the trace status (Success, Running or Error), and
TraceDataPanelViewshows that summary on the trace page too, not only in the side panel. This replacesTraceKeysAndValues, which is removed. (#24803)Breaking changes:
-
TraceKeysAndValuesandTraceKeysAndValuesPropsare removed.TraceDataPanelViewwithplacement="trace-page"now renders entity, status, start time, duration and usage itself, so drop it fromheaderSlot:// Before <TraceDataPanelView placement="trace-page" headerSlot={<TraceKeysAndValues rootSpan={rootSpan} numOfCol={3} />} {...props} /> // After <TraceDataPanelView placement="trace-page" {...props} />
-
DataKeysAndValuesno longer takesnumOfColand always renders a single key/value column. For side-by-side groups, render several lists in your own grid:// Before <DataKeysAndValues numOfCol={2}>{rows}</DataKeysAndValues> // After <div className="grid grid-cols-2 gap-x-4"> <DataKeysAndValues>{firstRows}</DataKeysAndValues> <DataKeysAndValues>{secondRows}</DataKeysAndValues> </div>
-
-
Moved
SettingsLayoutinto the settings family, so a settings page is built from one import:SettingsLayoutframes the page,SettingsGroup/SettingsContainer/SettingsRowfill it. The StorybookNew/Settingspage now shows the full page, not just the groups. (#24801)Removed exports
@mastra/playground-ui/components/SettingsLayoutis gone. Import it from@mastra/playground-ui/new/settingsinstead:
// Before import { SettingsLayout } from '@mastra/playground-ui/components/SettingsLayout'; // After import { SettingsLayout } from '@mastra/playground-ui/new/settings';
Sections(@mastra/playground-ui/components/Sections) is gone. It only stacked its children with a gap; use a plain element instead:
// Before <Sections>…</Sections> // After <div className="grid gap-6">…</div>
-
Removed
IntegrationDialog. Mastra Platform now owns its connection picker, and nothing else in Mastra imported it. If you used it, copy the component from a previous release or build the picker withCommandandDialog. (#24854)
Patch Changes
-
Cardnow uses the same corner radius asDataList, andCardHeaderuses the same vertical padding as theDataListcolumn header. (#24817) -
Processor spans in Studio traces now open with a readable Preview instead of JSON only. The preview shows the messages a processor received, the messages and system messages it changed, and tool, step and chunk details where the phase records them. (#24672)
The Attributes section also gains a Preview for processor spans: processor name, pipeline phase, executor, pipeline position, hook duration, message-list changes as readable actions (added, removed, cleared), and a tripwire notice with its reason and retry state. Attributes the preview does not explain stay in JSON, so no value is shown twice.
The Preview / JSON toggle still keeps the exact stored payload one click away, and processor spans recorded before the phase was tracked keep their JSON view. Both the full span panel and the compact span details use the same presentation.
With tracing enabled, an agent using this processor now shows the added system message in its processor span Preview:
import { Agent } from '@mastra/core/agent'; const agent = new Agent({ id: 'assistant', name: 'Assistant', instructions: 'Help the user.', model: 'openai/gpt-5-mini', inputProcessors: [ { id: 'brief-answers', processInput: async ({ messageList }) => { messageList.addSystem('Answer briefly.'); return messageList; }, }, ], });
-
Fixed
PageLayoutwithvariant="narrow"overflowing the page when content is wider than the column, such as a list with a long unbroken name. The column now stays at its width and wide content scrolls inside its own container. (#24877) -
Error rows in data lists now stay red when pressed instead of flashing grey. (#24945)
-
Added bottom padding and default spacing between items to the SidebarNew footer, so the last item no longer sits against the bottom edge of the sidebar. (#24820)
-
Added
MainCard, the rounded, raised surface that sits insideAppShelland holds the page content. (#24806)import { AppShell, MainCard } from '@mastra/playground-ui/new/layout/app-shell'; <AppShell sidebar={<Sidebar />}> <MainCard> <Outlet /> </MainCard> </AppShell>;
-
PageLayoutwithvariant="narrow"now fills the available height, so content such asEmptyState variant="fill"can center vertically below the page header. (#24838) -
PageHeader.Actionnow sits outside the title grid and aligns to the top of the header, so the action no longer shares the title row alignment. (#24817) -
Fixed clicks on a toggle placed inside a menu activating the menu's last highlighted row. Clicking a theme toggle between DropdownMenu rows used to open whichever row was highlighted last, often a link; radios, checkboxes, switches, sliders, tabs and their groups now keep their own clicks. (#24802)
-
Removed the unused
MultiColumncomponent and thewithLeftSeparator/withRightSeparatorprops onColumn. Nothing in Studio or Factory used them. (#24804) -
Removed the unused
SelectDataFiltercomponent. Nothing in Studio or Factory used it. (#24822) -
Code blocks in side panels (logs, traces, scores, experiments, dataset items) now use the same syntax highlighting as span payloads, and look the same in light and dark mode. (#24945)
-
Smoothed the FilterBar chip entrance: segment text now fades in once the segment has nearly finished growing, so it no longer looks squeezed or clipped. The chip shine sweep now animates with
transformfor smoother rendering. (#24942) -
The logs page now opens log details in a side drawer, like the traces page. Clicking Trace or Span in a log opens the trace drawer on top. Long log messages and data no longer make the logs list scroll sideways. Removed
LogsLayoutand replacedLogDetailsViewwithLogDataPanel. The log drawer now has a readable timestamp heading, and the message sits in its own "Message" code section. (#24945)
@mastra/quickjs@0.1.3
Patch Changes
- Fixed guest code being able to crash the host Node process. Running out of memory while
external_*calls are in flight now returns an "out of memory" error instead of aborting, and deep recursion now returns a "stack overflow" error instead of killing Node on arm64. The defaultmaxStackSizeBytesis now 256 KiB (was 1 MiB). (#24896)
@mastra/react@1.6.2
Patch Changes
- Fixed
useStreamWorkflowcrashing withCannot read properties of undefined (reading 'id')when a workflow step emits a custom event throughwriter.custom(). Running, observing, resuming and time-travelling such workflows now keep updating step results, and custom events are skipped. Fixes #17111. (#24807)
@mastra/redis-streams@0.5.1
Patch Changes
-
Fixed a connection and memory leak when
unsubscribe()raced an in-flightsubscribe(). (#24580)Subscribing takes several Redis round trips. An unsubscribe issued during that window used to return without doing anything, and the subscription then finished registering afterward, leaking its dedicated reader connection and read loop with no way to ever stop them. Short-timeout request/reply flows (such as the agent runtime's cross-process discovery) hit this window regularly against remote Redis.
unsubscribe()andclose()now wait for an in-flight subscribe to finish and tear it down. Duplicate concurrentsubscribe()calls for the same topic and callback share one setup instead of orphaning the first.Because both calls now wait for in-flight subscribes to settle, they inherit the client's connection behavior: with Redis unreachable and node-redis's default reconnect strategy (retry forever), a
close()issued mid-subscribe blocks until Redis is reachable again. Pass a boundedreconnectStrategyviaredisOptionsif shutdown must not wait on a dead Redis.
@mastra/server@1.70.0
Minor Changes
-
Added
POST /api/agents/:agentId/threads/signals/cancelto cancel selected pending input across Agents sharing a memory thread. The route checks thread ownership and accepts 1–1,000 signal IDs: (#23942){ "resourceId": "user-123", "threadId": "thread-abc", "signalIds": ["signal-123"] }The response contains
cancelledSignalIds, listing only IDs cancelled on the receiving process. Those IDs are published through shared PubSub so other subscribed processes can remove matching pending copies. Propagation is asynchronous and best-effort, without remote acknowledgements. Thread abort requests also acceptclearPendingSignals: trueto clear pending input before aborting. Omitting the flag preserves existing behavior.Both cancellation routes enforce thread write access when fine-grained authorization is configured, even before a thread is saved. Thread-wide cancellation and clear-on-abort return HTTP 501 when the Agent's core version doesn't support them. Upgrade
@mastra/corealongside@mastra/serveron every worker. -
Added capability-aware routing for trace-level root duration predicates. (#24635)
Previously, duration filtering used the existing span relation, which remains available on older adapters and can match a child span:
where: { spans: { some: { op: "gt", left: { path: "durationMs" }, right: { literal: 5000 } } } }
Stores that advertise root duration support now accept the top-level field, while older stores return a structured unsupported response and omit the field from trace discovery:
where: { op: "gt", left: { path: "durationMs" }, right: { literal: 5000 } }
-
Trace query, thread query, and trace-query discovery routes now resolve a trusted tenant scope from the reserved
organizationIdrequest-context key and pass it to the planner. Requests from hosts that set that key server-side only see their own organization's traces, related spans, scores, feedback, and discovery values, and cursors are bound to that scope. Without the key the routes behave as before. Scoped requests are rejected with501when the installed@mastra/coreor observability store predates tenant scope, so a scope can never be silently dropped. (#24566)Set the key from server-side authentication, for example in server middleware:
const mastra = new Mastra({ server: { middleware: [ async (c, next) => { c.get('requestContext').set('organizationId', getSession(c).organizationId); await next(); }, ], }, });
Patch Changes
- Return HTTP 409 when a feedback review-status update conflicts with a newer version of the feedback, so clients can retry instead of treating it as a server error. (#24033)
@mastra/spanner@1.8.1
Patch Changes
- Fixed dataset storage to preserve JSON null and JSON-looking strings through reads, updates, and version history. Cleared optional dataset configuration now reads as undefined. Previously lost values cannot be recovered automatically. (#23937)
Other updated packages
The following packages were updated with dependency changes only:
- @mastra/agent-builder@1.1.21
- @mastra/connect@0.3.1
- @mastra/deployer@1.70.0
- @mastra/deployer-cloud@1.70.0
- @mastra/deployer-cloudflare@1.2.29
- @mastra/deployer-netlify@1.2.29
- @mastra/deployer-sandbox@0.3.14
- @mastra/deployer-vercel@1.2.29
- @mastra/editor@0.15.2
- @mastra/elysia@0.1.10
- @mastra/express@1.5.14
- @mastra/fastify@1.5.14
- @mastra/hono@1.7.12
- @mastra/koa@1.7.14
- @mastra/longmemeval@1.1.29
- @mastra/nestjs@0.2.29
- @mastra/next@0.2.28
- @mastra/tanstack-start@0.2.28
- @mastra/temporal@0.4.8
- @mastra/turso@0.1.8