github mastra-ai/mastra @mastra/core@1.70.0
September 24, 2026

latest release: @mastra/core@1.71.0
5 hours ago

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: ErrorState removed (use EmptyState tone="error").
  • @mastra/playground-ui: PermissionDenied and SessionExpired moved to auth domain with simplified props (custom copy/actions require rendering EmptyState directly).
  • @mastra/playground-ui: Button outline variant removed (use default/primary).
  • @mastra/playground-ui: TraceKeysAndValues removed; 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 Classifier you already have, pass it as classifier and map its typed answers to a model with select.

    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 minProbability set, the configured model is used when the confidence is too low or missing.
  • Added a closeOnSuspend option to durable agent stream() and resume(), so callers can end the stream when a tool suspends. (#24894)

    Previously, the stream returned by DurableAgent.stream() (and createInngestAgent().stream()) stayed open after a tool suspended for approval or user input, and there was no public way to change that. Loops over fullStream hung, so integrations like AG-UI could not emit RUN_FINISHED.

    Pass closeOnSuspend: true to close the stream at the suspension boundary, matching non-durable Agent.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 status or userId, or by top-level metadata.<key> paths.
    • Supported measures: count, duration.avg/min/max/p50/p90/p95/p99, errorCount, errorRate, and countDistinct.<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)
  • 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 createdAt to reorder it. Use memory.saveMessages or the memory store's updateMessages to change stored history.

    To opt out, set the new retainFullInput memory option, per call on memory.options or 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. The useAgent structured 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 describeProcessorPipeline for 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 configured Classifier as 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 includes and notIncludes on the tags field, and exists / notExists on tags mean "at least one tag" / "no tags". Missing and empty tag lists are treated alike. tags also supports value discovery. (#24554)

    Example

    { op: 'includes', path: 'tags', value: 'manual-review' }
  • Add an optional title to createTool() 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-start and tool-call stream 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? } to planTraceQuery, 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 with TRACE_QUERY_CURSOR_CONFLICT before storage runs. Callers still can't name organizationId or projectId in 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 durationMs predicates 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. Added clearPendingSignals to 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 queueOwnerId selector 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 new approvalButtons adapter 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 ask policy). 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 clearTopic as 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, where clearTopic is a no-op. (#24580)

    Full cleanup on Redis Streams requires the matching @mastra/redis-streams release. With an older one, unsubscribe() does nothing while a subscribe is still in flight, and that late subscribe recreates the stream key after clearTopic deletes it.

  • Fixed TokenLimiterProcessor counting history that later prompt processors remove from the request. In the default best-fit and contiguous trim modes, the input budget is now enforced on the provider prompt in processLLMRequest, after earlier prompt processors such as ToolCallFilter have 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 processLLMRequest doesn't run, such as generateLegacy(), streamLegacy() and limiters inside a processor workflow, processInputStep still 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 properties as own keys, so a field named constructor or toString no 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, and parseTraceAggregateRequest(). 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, and notExists on tags, 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 runs ALTER TABLE … UPDATE on mastra_feedback_events, so the runtime database user needs ALTER UPDATE(reviewStatus) on that table, and INSERT on mastra_feedback_events_delta if it does not already have it. A user limited to SELECT and INSERT, which was enough for review updates before this release, now fails with Not 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: true and run init() 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-scope feature 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. Added clearPendingSignals to abortThread() and subscription abort() 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 includes and notIncludes trace-query operators and the array value 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 title on the tool endpoints and in GetToolResponse, and copy it from tool-call and tool-call-input-streaming-start chunks onto the tool-invocation message part in the useChat accumulator. 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 prepareWakeRequestContext option to createMastraCode(). 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_send so senders put content where the peer can see it. The payload parameter was removed because peers never received it, and the summary parameter was renamed to message to 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 /think thinking 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 high
    

    Gemini and OpenAI API-key models map the level to what each model supports. Custom OpenAI-compatible providers receive the selected level unchanged, including xhigh and max. With thinking off (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, and notExists on tags, 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)

    waitForPendingFilesystemCapture is 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_transition rejections. 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/inngest stack 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 with Cannot 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 retries is set. Inngest applied retries to every failing step on top of the step's own retries, so a step with no retries ran three times with retries: 2. Errors marked non-retryable were retried too. Failing steps now only use their own retry settings. retries still 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 title through MCPClient and MCPServer. Closes #20249. (#24117)

    • MCPServer publishes a tool's title in tools/list. Tools that only set mcp.annotations.title are unchanged.
    • Tools returned by MCPClient carry tool.title, taken from the server's tool title and falling back to annotations.title, the same precedence MCP clients use for display names.
    • listToolDefinitions() keeps the title and toolFromDefinition() 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/mcp 2.x and serves the MCP 2026-07-28 revision over stdio. Editors that still open with the pre-2026 initialize handshake (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 an initialize, serves the connection with the published @mastra/mcp 1.x implementation instead. The same tools and prompts are registered either way, so npx -y @mastra/mcp-docs-server@latest needs 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-level notifications/message logging 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 removed version field, and the package's server export is replaced by createDocsServer(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-reminder or notification) 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. Added getObservationContextPrompt(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, and notExists on tags, and value discovery returns each observed tag with the number of traces that carry it. Tag membership uses the existing GIN index on tags. (#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

  • EmptyState is now the design system's only status block. A new tone prop colors its icon, and tone="error" defaults to the red circle-x icon ErrorState used to render. (#24799)

    Breaking

    • ErrorState is removed. Use EmptyState with tone="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}
      />;
    • PermissionDenied and SessionExpired hold 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';
    • PermissionDenied now takes only resource (required) and variant, and SessionExpired only variant. The title, description, actionSlot and className overrides 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 EmptyState directly.

    • EmptyState renders every icon at 32px, whatever size the icon sets itself, so status blocks stay consistent across apps.

    Improved

    • PermissionDenied shows a lock icon and SessionExpired a timer-off icon, so neither reads as an empty list anymore.
    • The Log in button on SessionExpired now sends the client's custom headers, like Studio's own login does, and shows an error toast when the login cannot start.
    • EmptyState icons without their own color now render muted by default.
  • Added a narrow variant and an optional header slot to PageLayout. The narrow variant centers the page body in a wide max-width column; header renders a page-level header (such as PageHeader) 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 --dialog surface that is off-white in light mode. Nothing changes at the call site.

    Error outline

    Passing error to 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, and iconOnlyValue to Combobox so triggers can show a custom selected label, hide the chevron, or center an icon-only value. The popup list now scrolls in ScrollArea, with an overlay scrollbar and edge fades. (#24728)

    <Combobox options={countries} value={country} showChevron={false} iconOnlyValue />
  • Removed the outline variant from Button, and from the triggers built on it (SelectTrigger, Combobox, DropdownMenu.Trigger, PopoverTrigger). The default variant 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 TraceDataPanelView shows that summary on the trace page too, not only in the side panel. This replaces TraceKeysAndValues, which is removed. (#24803)

    Breaking changes:

    • TraceKeysAndValues and TraceKeysAndValuesProps are removed. TraceDataPanelView with placement="trace-page" now renders entity, status, start time, duration and usage itself, so drop it from headerSlot:

      // Before
      <TraceDataPanelView
        placement="trace-page"
        headerSlot={<TraceKeysAndValues rootSpan={rootSpan} numOfCol={3} />}
        {...props}
      />
      
      // After
      <TraceDataPanelView placement="trace-page" {...props} />
    • DataKeysAndValues no longer takes numOfCol and 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 SettingsLayout into the settings family, so a settings page is built from one import: SettingsLayout frames the page, SettingsGroup / SettingsContainer / SettingsRow fill it. The Storybook New/Settings page now shows the full page, not just the groups. (#24801)

    Removed exports

    • @mastra/playground-ui/components/SettingsLayout is gone. Import it from @mastra/playground-ui/new/settings instead:
    // 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 with Command and Dialog. (#24854)

Patch Changes

  • Card now uses the same corner radius as DataList, and CardHeader uses the same vertical padding as the DataList column 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 PageLayout with variant="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 inside AppShell and holds the page content. (#24806)

    import { AppShell, MainCard } from '@mastra/playground-ui/new/layout/app-shell';
    
    <AppShell sidebar={<Sidebar />}>
      <MainCard>
        <Outlet />
      </MainCard>
    </AppShell>;
  • PageLayout with variant="narrow" now fills the available height, so content such as EmptyState variant="fill" can center vertically below the page header. (#24838)

  • PageHeader.Action now 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 MultiColumn component and the withLeftSeparator / withRightSeparator props on Column. Nothing in Studio or Factory used them. (#24804)

  • Removed the unused SelectDataFilter component. 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 transform for 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 LogsLayout and replaced LogDetailsView with LogDataPanel. 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 default maxStackSizeBytes is now 256 KiB (was 1 MiB). (#24896)

@mastra/react@1.6.2

Patch Changes

  • Fixed useStreamWorkflow crashing with Cannot read properties of undefined (reading 'id') when a workflow step emits a custom event through writer.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-flight subscribe(). (#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() and close() now wait for an in-flight subscribe to finish and tear it down. Duplicate concurrent subscribe() 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 bounded reconnectStrategy via redisOptions if shutdown must not wait on a dead Redis.

@mastra/server@1.70.0

Minor Changes

  • Added POST /api/agents/:agentId/threads/signals/cancel to 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 accept clearPendingSignals: true to 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/core alongside @mastra/server on 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 organizationId request-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 with 501 when the installed @mastra/core or 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:

Don't miss a new mastra release

NewReleases is sending notifications on new releases.